瀏覽代碼

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

Nicolas Meienberger 2 年之前
父節點
當前提交
3e67758d86

+ 0 - 2
packages/dashboard/src/server/context.ts

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

+ 4 - 4
packages/dashboard/src/server/routers/auth/auth.router.ts

@@ -3,10 +3,10 @@ import AuthService from '../../services/auth/auth.service';
 import { router, publicProcedure, protectedProcedure } from '../../trpc';
 import { router, publicProcedure, protectedProcedure } from '../../trpc';
 
 
 export const authRouter = router({
 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)),
   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)),
   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()),
 });
 });

+ 14 - 16
packages/dashboard/src/server/services/auth/auth.service.test.ts

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

+ 47 - 12
packages/dashboard/src/server/services/auth/auth.service.ts

@@ -2,10 +2,9 @@ import * as argon2 from 'argon2';
 import { v4 } from 'uuid';
 import { v4 } from 'uuid';
 import jwt from 'jsonwebtoken';
 import jwt from 'jsonwebtoken';
 import validator from 'validator';
 import validator from 'validator';
-import { User } from '@prisma/client';
 import { getConfig } from '../../core/TipiConfig';
 import { getConfig } from '../../core/TipiConfig';
 import TipiCache from '../../core/TipiCache';
 import TipiCache from '../../core/TipiCache';
-import { Context } from '../../context';
+import { prisma } from '../../db/client';
 
 
 type UsernamePasswordInput = {
 type UsernamePasswordInput = {
   username: string;
   username: string;
@@ -16,10 +15,16 @@ type TokenResponse = {
   token: string;
   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 { 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) {
   if (!user) {
     throw new Error('User not found');
     throw new Error('User not found');
@@ -39,7 +44,14 @@ const login = async (input: UsernamePasswordInput, ctx: Context): Promise<TokenR
   return { token };
   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 { password, username } = input;
   const email = username.trim().toLowerCase();
   const email = username.trim().toLowerCase();
 
 
@@ -51,14 +63,14 @@ const register = async (input: UsernamePasswordInput, ctx: Context): Promise<Tok
     throw new Error('Invalid username');
     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) {
   if (user) {
     throw new Error('User already exists');
     throw new Error('User already exists');
   }
   }
 
 
   const hash = await argon2.hash(password);
   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 session = v4();
   const token = jwt.sign({ id: newUser.id, session }, getConfig().jwtSecret, { expiresIn: '1d' });
   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 };
   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;
   if (!user) return null;
 
 
   return user;
   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> => {
 const logout = async (session?: string): Promise<boolean> => {
   if (session) {
   if (session) {
     await TipiCache.del(session);
     await TipiCache.del(session);
@@ -86,6 +110,12 @@ const logout = async (session?: string): Promise<boolean> => {
   return true;
   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> => {
 const refreshToken = async (session?: string): Promise<TokenResponse | null> => {
   if (!session) return null;
   if (!session) return null;
 
 
@@ -102,8 +132,13 @@ const refreshToken = async (session?: string): Promise<TokenResponse | null> =>
   return { token };
   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;
   return count > 0;
 };
 };