Skip to content

Commit

Permalink
reset password
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeh committed Sep 21, 2024
1 parent 77e59e1 commit 0a55535
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 12 deletions.
21 changes: 13 additions & 8 deletions api/src/modules/auth/authentication/authentication.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,19 @@ export class AuthenticationController {
@UseGuards(AuthGuard(ResetPassword))
@TsRestHandler(authContract.resetPassword)
async resetPassword(@GetUser() user: User): Promise<ControllerResponse> {
return tsRestHandler(authContract.resetPassword, async () => {
const userWithAccessToken =
await this.passwordRecovery.resetPassword(user);
return {
body: userWithAccessToken,
status: 201,
};
});
return tsRestHandler(
authContract.resetPassword,
async ({ body: { password } }) => {
const userWithAccessToken = await this.passwordRecovery.resetPassword(
user,
password,
);
return {
body: userWithAccessToken,
status: 201,
};
},
);
}

@TsRestHandler(authContract.requestPasswordRecovery)
Expand Down
8 changes: 5 additions & 3 deletions api/src/modules/auth/services/password-recovery.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, Logger, NotImplementedException } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { UsersService } from '@api/modules/users/users.service';
import { JwtService } from '@nestjs/jwt';
import { AuthMailer } from '@api/modules/auth/services/auth.mailer';
Expand All @@ -7,6 +7,7 @@ import { PasswordRecoveryRequestedEvent } from '@api/modules/events/user-events/
import { ApiConfigService } from '@api/modules/config/app-config.service';
import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema';
import { User } from '@shared/entities/users/user.entity';
import * as bcrypt from 'bcrypt';

@Injectable()
export class PasswordRecoveryService {
Expand Down Expand Up @@ -40,7 +41,8 @@ export class PasswordRecoveryService {
this.eventBus.publish(new PasswordRecoveryRequestedEvent(email, user.id));
}

async resetPassword(user: User): Promise<void> {
throw new NotImplementedException();
async resetPassword(user: User, newPassword: string): Promise<void> {
const newHashedPassword = await bcrypt.hash(newPassword, 10);
await this.users.updatePassword(user, newHashedPassword);
}
}
5 changes: 5 additions & 0 deletions api/src/modules/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,9 @@ export class UsersService {
}
return this.repo.save(createUserDto);
}

async updatePassword(user: User, newPassword: string) {
user.password = newPassword;
return this.repo.save(user);
}
}
12 changes: 12 additions & 0 deletions api/test/e2e/features/password-recovery-reset-password.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Feature: Reset Password

Scenario: Successfully resetting the password with a valid token
Given a user has a valid reset-password token
When the user attempts to reset their password with a new valid password
Then the user should receive a 201 status code
And the user can log in with the new password

Scenario: Attempting to reset the password with an expired token
Given a user has an expired reset-password token
When the user attempts to reset their password with a new valid password
Then the user should receive a 401 status code
129 changes: 129 additions & 0 deletions api/test/e2e/steps/password-recovery-reset-email.steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { defineFeature, loadFeature } from 'jest-cucumber';
import { Response } from 'supertest';
import { TestManager } from '../../utils/test-manager';
import { User } from '@shared/entities/users/user.entity';
import { JwtService } from '@nestjs/jwt';
import { ApiConfigService } from '@api/modules/config/app-config.service';
import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema';

const feature = loadFeature(
'./test/e2e/features/password-recovery-reset-password.feature',
);

describe('Reset Password', () => {
defineFeature(feature, (test) => {
let testManager: TestManager;
let jwtService: JwtService;
let resetPasswordSecret: string;
let apiConfigService: ApiConfigService;

beforeAll(async () => {
testManager = await TestManager.createTestManager();
jwtService = testManager.moduleFixture.get<JwtService>(JwtService);
apiConfigService =
testManager.moduleFixture.get<ApiConfigService>(ApiConfigService);
});

beforeEach(async () => {
resetPasswordSecret = apiConfigService.getJWTConfigByType(
TOKEN_TYPE_ENUM.RESET_PASSWORD,
).secret;
await testManager.clearDatabase();
});

afterAll(async () => {
await testManager.close();
});

// Scenario 1: Successfully resetting the password with a valid token
test('Successfully resetting the password with a valid token', ({
given,
when,
then,
and,
}) => {
let user: User;
let resetPasswordToken: string;
let response: Response;

given('a user has a valid reset-password token', async () => {
user = await testManager.mocks().createUser({
email: '[email protected]',
password: 'OldPassword123',
});
resetPasswordToken = jwtService.sign(
{ sub: user.id },
{ secret: resetPasswordSecret, expiresIn: '1h' },
);
});

when(
'the user attempts to reset their password with a new valid password',
async () => {
response = await testManager
.request()
.post('/authentication/reset-password')
.set('Authorization', `Bearer ${resetPasswordToken}`)
.send({ password: 'NewPassword123' });
},
);

then(
/the user should receive a (\d+) status code/,
(statusCode: string) => {
expect(response.status).toBe(Number.parseInt(statusCode));
},
);

and('the user can log in with the new password', async () => {
const loginResponse = await testManager
.request()
.post('/authentication/login')
.send({ email: user.email, password: 'NewPassword123' });

expect(loginResponse.status).toBe(201);
expect(loginResponse.body.accessToken).toBeDefined();
});
});

// Scenario 2: Attempting to reset the password with an expired token
test('Attempting to reset the password with an expired token', ({
given,
when,
then,
}) => {
let user: User;
let expiredResetPasswordToken: string;
let response: Response;

given('a user has an expired reset-password token', async () => {
user = await testManager.mocks().createUser({
email: '[email protected]',
password: 'OldPassword123',
});
expiredResetPasswordToken = jwtService.sign(
{ sub: user.id },
{ secret: resetPasswordSecret, expiresIn: '1ms' },
);
});

when(
'the user attempts to reset their password with a new valid password',
async () => {
response = await testManager
.request()
.post('/authentication/reset-password')
.set('Authorization', `Bearer ${expiredResetPasswordToken}`)
.send({ password: 'NewPassword123' });
},
);

then(
/the user should receive a (\d+) status code/,
(statusCode: string) => {
expect(response.status).toBe(Number.parseInt(statusCode));
},
);
});
});
});
2 changes: 1 addition & 1 deletion shared/contracts/auth/auth.contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const authContract = contract.router({
201: null,
401: null,
},
body: LogInSchema,
body: z.object({ password: z.string() }),
},
requestPasswordRecovery: {
method: "POST",
Expand Down

0 comments on commit 0a55535

Please sign in to comment.