refactor: remove prisma from context and use client directly in service

This commit is contained in:
Nicolas Meienberger 2023-01-25 16:06:07 +01:00 committed by Nicolas Meienberger
parent 71d106b39a
commit 3e67758d86
4 changed files with 65 additions and 34 deletions

View file

@ -1,7 +1,6 @@
import { inferAsyncReturnType } from '@trpc/server';
import { CreateNextContextOptions } from '@trpc/server/adapters/next';
import { getServerAuthSession } from './common/get-server-auth-session';
import { prisma } from './db/client';
type Session = {
userId?: number;
@ -19,7 +18,6 @@ type CreateContextOptions = {
* */
export const createContextInner = async (opts: CreateContextOptions) => ({
session: opts.session,
prisma,
});
/**

View file

@ -3,10 +3,10 @@ import AuthService from '../../services/auth/auth.service';
import { router, publicProcedure, protectedProcedure } from '../../trpc';
export const authRouter = router({
login: publicProcedure.input(z.object({ username: z.string(), password: z.string() })).mutation(async ({ ctx, input }) => AuthService.login({ ...input }, ctx)),
login: publicProcedure.input(z.object({ username: z.string(), password: z.string() })).mutation(async ({ input }) => AuthService.login({ ...input })),
logout: protectedProcedure.mutation(async ({ ctx }) => AuthService.logout(ctx.session.id)),
register: publicProcedure.input(z.object({ username: z.string(), password: z.string() })).mutation(async ({ ctx, input }) => AuthService.register({ ...input }, ctx)),
register: publicProcedure.input(z.object({ username: z.string(), password: z.string() })).mutation(async ({ input }) => AuthService.register({ ...input })),
refreshToken: protectedProcedure.mutation(async ({ ctx }) => AuthService.refreshToken(ctx.session.id)),
me: publicProcedure.query(async ({ ctx }) => AuthService.me(ctx)),
isConfigured: publicProcedure.query(async ({ ctx }) => AuthService.isConfigured(ctx)),
me: publicProcedure.query(async ({ ctx }) => AuthService.me(ctx.session?.userId)),
isConfigured: publicProcedure.query(async () => AuthService.isConfigured()),
});

View file

@ -5,7 +5,6 @@ import { setConfig } from '../../core/TipiConfig';
import { createUser } from '../../tests/user.factory';
import AuthService from './auth.service';
import { prisma } from '../../db/client';
import { Context } from '../../context';
import TipiCache from '../../core/TipiCache';
jest.mock('redis');
@ -23,8 +22,6 @@ afterAll(async () => {
await prisma.$disconnect();
});
const ctx = { prisma } as Context;
describe('Login', () => {
it('Should return a valid jsonwebtoken containing a user id', async () => {
// Arrange
@ -32,7 +29,7 @@ describe('Login', () => {
const user = await createUser(email);
// Act
const { token } = await AuthService.login({ username: email, password: 'password' }, ctx);
const { token } = await AuthService.login({ username: email, password: 'password' });
const decoded = jwt.verify(token, 'test') as jwt.JwtPayload;
// Assert
@ -47,13 +44,13 @@ describe('Login', () => {
});
it('Should throw if user does not exist', async () => {
await expect(AuthService.login({ username: 'test', password: 'test' }, ctx)).rejects.toThrowError('User not found');
await expect(AuthService.login({ username: 'test', password: 'test' })).rejects.toThrowError('User not found');
});
it('Should throw if password is incorrect', async () => {
const email = faker.internet.email();
await createUser(email);
await expect(AuthService.login({ username: email, password: 'wrong' }, ctx)).rejects.toThrowError('Wrong password');
await expect(AuthService.login({ username: email, password: 'wrong' })).rejects.toThrowError('Wrong password');
});
});
@ -63,7 +60,7 @@ describe('Register', () => {
const email = faker.internet.email();
// Act
const { token } = await AuthService.register({ username: email, password: 'password' }, ctx);
const { token } = await AuthService.register({ username: email, password: 'password' });
const decoded = jwt.verify(token, 'test') as jwt.JwtPayload;
// Assert
@ -80,7 +77,7 @@ describe('Register', () => {
const email = faker.internet.email();
// Act
await AuthService.register({ username: email, password: 'test' }, ctx);
await AuthService.register({ username: email, password: 'test' });
const user = await prisma.user.findFirst({ where: { username: email.toLowerCase().trim() } });
// Assert
@ -94,15 +91,15 @@ describe('Register', () => {
// Act & Assert
await createUser(email);
await expect(AuthService.register({ username: email, password: 'test' }, ctx)).rejects.toThrowError('User already exists');
await expect(AuthService.register({ username: email, password: 'test' })).rejects.toThrowError('User already exists');
});
it('Should throw if email is not provided', async () => {
await expect(AuthService.register({ username: '', password: 'test' }, ctx)).rejects.toThrowError('Missing email or password');
await expect(AuthService.register({ username: '', password: 'test' })).rejects.toThrowError('Missing email or password');
});
it('Should throw if password is not provided', async () => {
await expect(AuthService.register({ username: faker.internet.email(), password: '' }, ctx)).rejects.toThrowError('Missing email or password');
await expect(AuthService.register({ username: faker.internet.email(), password: '' })).rejects.toThrowError('Missing email or password');
});
it('Password is correctly hashed', async () => {
@ -110,7 +107,7 @@ describe('Register', () => {
const email = faker.internet.email().toLowerCase().trim();
// Act
await AuthService.register({ username: email, password: 'test' }, ctx);
await AuthService.register({ username: email, password: 'test' });
const user = await prisma.user.findUnique({ where: { username: email } });
const isPasswordValid = await argon2.verify(user?.password || '', 'test');
@ -119,7 +116,7 @@ describe('Register', () => {
});
it('Should throw if email is invalid', async () => {
await expect(AuthService.register({ username: 'test', password: 'test' }, ctx)).rejects.toThrowError('Invalid username');
await expect(AuthService.register({ username: 'test', password: 'test' })).rejects.toThrowError('Invalid username');
});
});
@ -198,7 +195,8 @@ describe('Test: refreshToken', () => {
describe('Test: me', () => {
it('Should return null if userId is not provided', async () => {
// Act
const result = await AuthService.me(ctx);
// @ts-expect-error - ctx is missing session
const result = await AuthService.me();
// Assert
expect(result).toBeNull();
@ -206,7 +204,7 @@ describe('Test: me', () => {
it('Should return null if user does not exist', async () => {
// Act
const result = await AuthService.me({ ...ctx, session: { userId: 1 } });
const result = await AuthService.me(1);
// Assert
expect(result).toBeNull();
@ -218,7 +216,7 @@ describe('Test: me', () => {
const user = await createUser(email);
// Act
const result = await AuthService.me({ ...ctx, session: { userId: user.id } });
const result = await AuthService.me(user.id);
// Assert
expect(result).not.toBeNull();

View file

@ -2,10 +2,9 @@ import * as argon2 from 'argon2';
import { v4 } from 'uuid';
import jwt from 'jsonwebtoken';
import validator from 'validator';
import { User } from '@prisma/client';
import { getConfig } from '../../core/TipiConfig';
import TipiCache from '../../core/TipiCache';
import { Context } from '../../context';
import { prisma } from '../../db/client';
type UsernamePasswordInput = {
username: string;
@ -16,10 +15,16 @@ type TokenResponse = {
token: string;
};
const login = async (input: UsernamePasswordInput, ctx: Context): Promise<TokenResponse> => {
/**
* Authenticate user with given username and password
*
* @param {UsernamePasswordInput} input - An object containing the user's username and password
* @return {Promise<{token:string}>} - A promise that resolves to an object containing the JWT token
*/
const login = async (input: UsernamePasswordInput) => {
const { password, username } = input;
const user = await ctx.prisma.user.findUnique({ where: { username: username.trim().toLowerCase() } });
const user = await prisma.user.findUnique({ where: { username: username.trim().toLowerCase() } });
if (!user) {
throw new Error('User not found');
@ -39,7 +44,14 @@ const login = async (input: UsernamePasswordInput, ctx: Context): Promise<TokenR
return { token };
};
const register = async (input: UsernamePasswordInput, ctx: Context): Promise<TokenResponse> => {
/**
* Creates a new user with the provided email and password and returns a session token
*
* @param {UsernamePasswordInput} input - An object containing the email and password fields
* @returns {Promise<{token: string}>} - An object containing the session token
* @throws {Error} - If the email or password is missing, the email is invalid or the user already exists
*/
const register = async (input: UsernamePasswordInput) => {
const { password, username } = input;
const email = username.trim().toLowerCase();
@ -51,14 +63,14 @@ const register = async (input: UsernamePasswordInput, ctx: Context): Promise<Tok
throw new Error('Invalid username');
}
const user = await ctx.prisma.user.findUnique({ where: { username: email } });
const user = await prisma.user.findUnique({ where: { username: email } });
if (user) {
throw new Error('User already exists');
}
const hash = await argon2.hash(password);
const newUser = await ctx.prisma.user.create({ data: { username: email, password: hash } });
const newUser = await prisma.user.create({ data: { username: email, password: hash } });
const session = v4();
const token = jwt.sign({ id: newUser.id, session }, getConfig().jwtSecret, { expiresIn: '1d' });
@ -68,16 +80,28 @@ const register = async (input: UsernamePasswordInput, ctx: Context): Promise<Tok
return { token };
};
const me = async (ctx: Context): Promise<Pick<User, 'id' | 'username'> | null> => {
if (!ctx.session?.userId) return null;
/**
* Retrieves the user with the provided ID
*
* @param {number|undefined} userId - The user ID to retrieve
* @returns {Promise<{id: number, username: string} | null>} - An object containing the user's id and email, or null if the user is not found
*/
const me = async (userId: number | undefined) => {
if (!userId) return null;
const user = await ctx.prisma.user.findUnique({ where: { id: Number(ctx.session?.userId) }, select: { id: true, username: true } });
const user = await prisma.user.findUnique({ where: { id: Number(userId) }, select: { id: true, username: true } });
if (!user) return null;
return user;
};
/**
* Logs out the current user by removing the session token
*
* @param {string} [session] - The session token to log out
* @returns {Promise<boolean>} - Returns true if the session token is removed successfully
*/
const logout = async (session?: string): Promise<boolean> => {
if (session) {
await TipiCache.del(session);
@ -86,6 +110,12 @@ const logout = async (session?: string): Promise<boolean> => {
return true;
};
/**
* Refreshes a user's session token
*
* @param {string} [session] - The current session token
* @returns {Promise<{token: string} | null>} - An object containing the new session token, or null if the session is invalid
*/
const refreshToken = async (session?: string): Promise<TokenResponse | null> => {
if (!session) return null;
@ -102,8 +132,13 @@ const refreshToken = async (session?: string): Promise<TokenResponse | null> =>
return { token };
};
const isConfigured = async (ctx: Context): Promise<boolean> => {
const count = await ctx.prisma.user.count();
/**
* Check if the system is configured and has at least one user
*
* @returns {Promise<boolean>} - A boolean indicating if the system is configured or not
*/
const isConfigured = async (): Promise<boolean> => {
const count = await prisma.user.count();
return count > 0;
};