Browse Source

feat: create reset password backend service and route

Nicolas Meienberger 2 years ago
parent
commit
a4571bc27c

+ 68 - 0
src/server/routers/auth/auth.router.test.ts

@@ -142,3 +142,71 @@ describe('Test: disableTotp', () => {
     expect(error?.code).not.toBe('UNAUTHORIZED');
     expect(error?.code).not.toBe('UNAUTHORIZED');
   });
   });
 });
 });
+
+describe('Test: changeOperatorPassword', () => {
+  it('should be accessible without an account', async () => {
+    // arrange
+    const caller = authRouter.createCaller({ session: null });
+    let error;
+
+    // act
+    try {
+      await caller.changeOperatorPassword({ newPassword: '222' });
+    } catch (e) {
+      error = e as { code: string };
+    }
+
+    // assert
+    expect(error?.code).not.toBe('UNAUTHORIZED');
+  });
+
+  it('should be accessible with an account', async () => {
+    // arrange
+    const caller = authRouter.createCaller({ session: { userId: 122 } });
+    let error;
+
+    // act
+    try {
+      await caller.changeOperatorPassword({ newPassword: '222' });
+    } catch (e) {
+      error = e as { code: string };
+    }
+
+    // assert
+    expect(error?.code).not.toBe('UNAUTHORIZED');
+  });
+});
+
+describe('Test: resetPassword', () => {
+  it('should not be accessible without an account', async () => {
+    // arrange
+    const caller = authRouter.createCaller({ session: null });
+    let error;
+
+    // act
+    try {
+      await caller.changePassword({ currentPassword: '111', newPassword: '222' });
+    } catch (e) {
+      error = e as { code: string };
+    }
+
+    // assert
+    expect(error?.code).toBe('UNAUTHORIZED');
+  });
+
+  it('should be accessible with an account', async () => {
+    // arrange
+    const caller = authRouter.createCaller({ session: { userId: 122 } });
+    let error;
+
+    // act
+    try {
+      await caller.changePassword({ currentPassword: '111', newPassword: '222' });
+    } catch (e) {
+      error = e as { code: string };
+    }
+
+    // assert
+    expect(error?.code).not.toBe('UNAUTHORIZED');
+  });
+});

+ 4 - 1
src/server/routers/auth/auth.router.ts

@@ -14,8 +14,11 @@ export const authRouter = router({
   isConfigured: publicProcedure.query(async () => AuthService.isConfigured()),
   isConfigured: publicProcedure.query(async () => AuthService.isConfigured()),
   // Password
   // Password
   checkPasswordChangeRequest: publicProcedure.query(AuthServiceClass.checkPasswordChangeRequest),
   checkPasswordChangeRequest: publicProcedure.query(AuthServiceClass.checkPasswordChangeRequest),
-  resetPassword: publicProcedure.input(z.object({ newPassword: z.string() })).mutation(({ input }) => AuthService.changeOperatorPassword({ newPassword: input.newPassword })),
+  changeOperatorPassword: publicProcedure.input(z.object({ newPassword: z.string() })).mutation(({ input }) => AuthService.changeOperatorPassword({ newPassword: input.newPassword })),
   cancelPasswordChangeRequest: publicProcedure.mutation(AuthServiceClass.cancelPasswordChangeRequest),
   cancelPasswordChangeRequest: publicProcedure.mutation(AuthServiceClass.cancelPasswordChangeRequest),
+  changePassword: protectedProcedure
+    .input(z.object({ currentPassword: z.string(), newPassword: z.string() }))
+    .mutation(({ input, ctx }) => AuthService.changePassword({ userId: Number(ctx.session.userId), ...input })),
   // Totp
   // Totp
   verifyTotp: publicProcedure.input(z.object({ totpSessionId: z.string(), totpCode: z.string() })).mutation(({ input }) => AuthService.verifyTotp(input)),
   verifyTotp: publicProcedure.input(z.object({ totpSessionId: z.string(), totpCode: z.string() })).mutation(({ input }) => AuthService.verifyTotp(input)),
   getTotpUri: protectedProcedure.input(z.object({ password: z.string() })).mutation(({ input, ctx }) => AuthService.getTotpUri({ userId: Number(ctx.session.userId), password: input.password })),
   getTotpUri: protectedProcedure.input(z.object({ password: z.string() })).mutation(({ input, ctx }) => AuthService.getTotpUri({ userId: Number(ctx.session.userId), password: input.password })),

+ 44 - 0
src/server/services/auth/auth.service.test.ts

@@ -640,3 +640,47 @@ describe('Test: cancelPasswordChangeRequest', () => {
     expect(fs.existsSync('/runtipi/state/password-change-request')).toBe(false);
     expect(fs.existsSync('/runtipi/state/password-change-request')).toBe(false);
   });
   });
 });
 });
+
+describe('Test: changePassword', () => {
+  it('should change the password of the user', async () => {
+    // arrange
+    const email = faker.internet.email();
+    const user = await createUser({ email }, db);
+    const newPassword = faker.internet.password();
+
+    // act
+    await AuthService.changePassword({ userId: user.id, newPassword, currentPassword: 'password' });
+
+    // assert
+    const updatedUser = await db.user.findUnique({ where: { id: user.id } });
+    expect(updatedUser?.password).not.toBe(user.password);
+  });
+
+  it('should throw if the user does not exist', async () => {
+    // arrange
+    const newPassword = faker.internet.password();
+
+    // act & assert
+    await expect(AuthService.changePassword({ userId: 1, newPassword, currentPassword: 'password' })).rejects.toThrowError('User not found');
+  });
+
+  it('should throw if the password is incorrect', async () => {
+    // arrange
+    const email = faker.internet.email();
+    const user = await createUser({ email }, db);
+    const newPassword = faker.internet.password();
+
+    // act & assert
+    await expect(AuthService.changePassword({ userId: user.id, newPassword, currentPassword: 'wrongpassword' })).rejects.toThrowError('Current password is invalid');
+  });
+
+  it('should throw if password is less than 8 characters', async () => {
+    // arrange
+    const email = faker.internet.email();
+    const user = await createUser({ email }, db);
+    const newPassword = faker.internet.password(7);
+
+    // act & assert
+    await expect(AuthService.changePassword({ userId: user.id, newPassword, currentPassword: 'password' })).rejects.toThrowError('Password must be at least 8 characters');
+  });
+});

+ 25 - 0
src/server/services/auth/auth.service.ts

@@ -368,4 +368,29 @@ export class AuthServiceClass {
 
 
     return true;
     return true;
   };
   };
+
+  public changePassword = async (params: { currentPassword: string; newPassword: string; userId: number }) => {
+    const { currentPassword, newPassword, userId } = params;
+
+    const user = await this.prisma.user.findUnique({ where: { id: userId } });
+
+    if (!user) {
+      throw new Error('User not found');
+    }
+
+    const valid = await argon2.verify(user.password, currentPassword);
+
+    if (!valid) {
+      throw new Error('Current password is invalid');
+    }
+
+    if (newPassword.length < 8) {
+      throw new Error('Password must be at least 8 characters long');
+    }
+
+    const hash = await argon2.hash(newPassword);
+    await this.prisma.user.update({ where: { id: user.id }, data: { password: hash, totp_enabled: false, totp_secret: null } });
+
+    return true;
+  };
 }
 }