feat: create reset password backend service and route

This commit is contained in:
Nicolas Meienberger 2023-04-07 21:47:02 +02:00
parent 5f32cb23fa
commit a4571bc27c
4 changed files with 141 additions and 1 deletions

View file

@ -142,3 +142,71 @@ describe('Test: disableTotp', () => {
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');
});
});

View file

@ -14,8 +14,11 @@ export const authRouter = router({
isConfigured: publicProcedure.query(async () => AuthService.isConfigured()),
// Password
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),
changePassword: protectedProcedure
.input(z.object({ currentPassword: z.string(), newPassword: z.string() }))
.mutation(({ input, ctx }) => AuthService.changePassword({ userId: Number(ctx.session.userId), ...input })),
// Totp
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 })),

View file

@ -640,3 +640,47 @@ describe('Test: cancelPasswordChangeRequest', () => {
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');
});
});

View file

@ -368,4 +368,29 @@ export class AuthServiceClass {
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;
};
}