refactor: remove prisma from context and use client directly in service
This commit is contained in:
parent
71d106b39a
commit
3e67758d86
4 changed files with 65 additions and 34 deletions
|
@ -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,
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -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()),
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue