updated codegen, fixed tsconfig, added prisma

added login, registration and session check
This commit is contained in:
lnn0q 2025-03-23 21:06:15 +02:00
parent 9518787206
commit 04b606b7fa
24 changed files with 740 additions and 1747 deletions

View file

@ -1,15 +1,20 @@
/* gql imports */
import { QueryResolvers } from "@generated/graphql";
import { QueryResolvers } from '@generated/types';
/* Query */
import { BooksManyResolver } from "./resolvers/booksManyResolver";
import { BookOneResolver } from "./resolvers/bookOneResolver";
import { BooksManyResolver } from './resolvers/booksManyResolver';
import { BookOneResolver } from './resolvers/bookOneResolver';
/* Mutation */
import { LoginResolver } from './resolvers/auth/login';
export const Query: QueryResolvers = {
booksMany: BooksManyResolver,
bookOne: BookOneResolver,
bookOne: BookOneResolver
};
export const Mutation = {};
export const Mutation = {
login: LoginResolver
};
export const Subscription = {};

View file

@ -1,25 +1,44 @@
import { prisma } from '@app/server/prisma/client';
import { MutationLoginArgs, Session } from '@generated/types';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
const usersDB = {
users: require('../model/users.json'),
setUsers: function (data) {
this.users = data;
}
};
export async function LoginResolver(_: any, { input }: MutationLoginArgs): Promise<Session> {
const error = 'Invalid username or password';
const handleLogin = async (req, res) => {
const { user, pwd } = req.body;
if (!user || !pwd) res.status(400).json({ message: 'Username and password are required.' });
const foundUser = usersDB.users.find(person => person.username === user);
if (!foundUser) return res.sendStatus(401); // unauthorized
// evaluate password
const match = await bcrypt.compare(pwd, foundUser.password);
if (match) {
// create JWTs
res.json({ success: `User ${user} is logged in.` });
} else {
res.sendStatus(401);
}
};
const user = await prisma.user.findUnique({
where: { email: input.email }
});
if (!user) throw new Error(error);
module.exports = { handleLogin };
const match = await bcrypt.compare(input.password, user.password);
if (!match) throw new Error(error);
const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET;
const refreshTokenSecret = process.env.REFRESH_TOKEN_SECRET;
if (accessTokenSecret == null || refreshTokenSecret == null) throw new Error('No secrets found');
const accessToken = jwt.sign({ userId: user.id }, accessTokenSecret, {
expiresIn: '30s'
});
const refreshToken = jwt.sign({ userId: user.id }, refreshTokenSecret, {
expiresIn: '1d'
});
await prisma.user.update({
where: { id: user.id },
data: { refreshToken }
});
return {
accessToken,
refreshToken: {
token: refreshToken,
httpOnly: true,
secure: true,
maxAge: 24 * 60 * 60 * 1000,
sameSite: 'None'
}
};
}

View file

@ -1,49 +1,28 @@
import { prisma, uuid } from '@app/server/prisma/client';
import { RegisterInput } from '@generated/types';
import bcrypt from 'bcrypt';
export async function SignUpResolver(_: any, input: any): Promise<any> {
// validate
export async function SignUpResolver(_: any, input: RegisterInput): Promise<void> {
const existingUsername = await prisma.user.findUnique({
where: { username: input.username },
select: { id: true }
});
if (existingUsername != null) throw Error('User with this username already exists.');
// db
const users = [
{
id: '1',
email: 'joe@test.com',
username: 'Joe',
password: 'test'
const existingEmail = await prisma.user.findUnique({
where: { email: input.email },
select: { id: true }
});
if (existingEmail != null) throw Error('Email is already in use.');
const hashedPassword = await bcrypt.hash(input.password, 10);
await prisma.user.create({
data: {
id: uuid(),
email: input.email,
username: input.username,
password: hashedPassword
}
];
const usersDB = { users };
// check for duplicate email+username
// encrypt the password
const hashedPwd = await bcrypt.hash(input.pwd, 10);
// store user in the db, return new user
return user;
});
}
const handleNewUser = async (req, res) => {
const { user, pwd } = req.body;
if (!user || !pwd)
return res.status(400).json({ message: 'Username and password are required.' });
// check for duplicate usernames in the db.
const duplicate = usersDB.users.find(person => person.username === user);
if (duplicate) return res.status(409);
try {
// encrypt the password
const hashedPwd = await brcypt.hash(pwd, 10);
// store the new user
const newUser = { username: user, password: hashedPwd };
usersDB.setUsers([...usersDB.users, newUser]);
await fs_promises.writeFile(
path.join(__dirname, '..', 'model', 'users.json'),
JSON.stringify(usersDB.users)
);
console.log(usersDB.users);
res.status(201).json({ success: `New user ${user} created!` });
} catch (err) {
res.status(500).json({ message: err.message });
}
};

View file

@ -1,10 +1,8 @@
import { BookInput, Book } from "@generated/graphql";
import { books } from "./booksManyResolver";
import { Book, QueryBookOneArgs } from '@generated/types';
import { books } from './booksManyResolver';
export async function BookOneResolver(_: any, input: BookInput): Promise<Book> {
console.log("Called 'BookOneResolver' resolver...");
const book = books.filter((item) => item.id === input.id)[0];
export async function BookOneResolver(_: any, { id }: QueryBookOneArgs): Promise<Book> {
const book = books.filter(item => item.id === id)[0];
return book;
}

View file

@ -1,20 +1,18 @@
import { Book } from "@generated/graphql";
import { Book } from '@generated/types';
export const books = [
{
id: "1",
title: "some-title-1",
author: "someone",
id: '1',
title: 'some-title-1',
author: 'someone'
},
{
id: "2",
title: "some-title-1",
author: "someone",
},
id: '2',
title: 'some-title-1',
author: 'someone'
}
];
export async function BooksManyResolver(_: any): Promise<Book[]> {
console.log("Called 'BooksManyResolver' resolver...");
return books;
}

View file

@ -1,6 +1,37 @@
scalar Void
type Query {
booksMany: [Book]!
bookOne(input: BookInput!): [Book]!
booksMany: [Book!]!
bookOne(id: ID!): Book!
}
type Mutation {
login(input: LoginInput!): Session!
register(input: RegisterInput!): Void!
}
input LoginInput {
email: String!
password: String!
}
input RegisterInput {
email: String!
username: String!
password: String!
}
type Session {
accessToken: String!
refreshToken: RefreshToken!
}
type RefreshToken {
token: String!
maxAge: Int!
secure: Boolean!
httpOnly: Boolean!
sameSite: String!
}
type Book {
@ -8,7 +39,3 @@ type Book {
title: String!
author: String!
}
input BookInput {
id: ID!
}

View file

@ -1,32 +1,31 @@
import "dotenv/config";
import express from "express";
import cors from "cors";
import ViteExpress from "vite-express";
import { ApolloServer } from "@apollo/server";
import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";
import { expressMiddleware } from "@apollo/server/express4";
import http from "http";
import fs from "fs";
import path from "path";
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import cors from 'cors';
import 'dotenv/config';
import express, { Request, Response } from 'express';
import fs from 'fs';
import http from 'http';
import jwt from 'jsonwebtoken';
import path from 'path';
import ViteExpress from 'vite-express';
import { Query, Mutation, Subscription } from "./graphql/graphql";
import { Mutation, Query } from './graphql/graphql';
import { prisma } from './prisma/client';
const __dirname = import.meta.dirname;
const HOST = process.env.VITE_HOST || "localhost";
const PORT = parseInt(process.env.VITE_PORT || "3000");
const HOST = process.env.VITE_HOST || 'localhost';
const PORT = parseInt(process.env.VITE_PORT || '3000');
const MOTD = `\n\n\n\x1b[35m-server is listening at...-\x1b[0m\n\nhttp://${HOST}:${PORT}\nhttp://localhost:${PORT}\n\n\n`;
const app = express();
const server = http.createServer(app);
const typeDefs = fs.readFileSync(
path.join(__dirname, "graphql", "schema.graphql"),
"utf8",
);
const typeDefs = fs.readFileSync(path.join(__dirname, 'graphql', 'schema.graphql'), 'utf8');
const resolvers = {
Query,
// Mutation,
Mutation
// Subscription,
};
@ -37,31 +36,35 @@ interface IdentityContext {
const apollo = new ApolloServer<IdentityContext>({
typeDefs,
resolvers,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer: server })],
plugins: [ApolloServerPluginDrainHttpServer({ httpServer: server })]
});
await apollo.start();
app.use(
"/graphql",
'/graphql',
cors<cors.CorsRequest>(),
express.json(),
expressMiddleware(apollo, {
context: async ({ req, res }) => {
const token = req.headers.authorization || "";
// const user = await getUser(token);
const user = "user";
return { user };
},
}),
context: getUser
})
);
await new Promise<void>((resolve) =>
server.listen({ hostname: HOST, port: PORT }, resolve),
);
await new Promise<void>(resolve => server.listen({ hostname: HOST, port: PORT }, resolve));
console.log(MOTD);
process.stdout.write(MOTD);
ViteExpress.bind(app, server);
async function getUser({ req, res }: { req: Request; res: Response }) {
const authHeader = req.headers.authorization;
if (!authHeader) return { identity: null };
const token = authHeader.split(' ')[1];
const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET;
if (accessTokenSecret == null) throw Error('No `accessToken`');
const decoded = jwt.verify(token, accessTokenSecret) as { userId: string };
const identity = await prisma.user.findUnique({ where: { id: decoded.userId } });
return { identity };
}

View file

@ -0,0 +1,8 @@
import { PrismaClient } from '@prisma/client';
import { v4 } from 'uuid';
export const prisma = new PrismaClient();
export function uuid() {
return v4();
}

View file

@ -0,0 +1,21 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model User {
id String @id
email String @unique @db.VarChar(255)
username String @unique @db.VarChar(255)
password String
refreshToken String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}