Skip to content

Commit

Permalink
refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeh committed Sep 26, 2024
1 parent dfafdc0 commit 768b5e7
Show file tree
Hide file tree
Showing 14 changed files with 90 additions and 32 deletions.
7 changes: 6 additions & 1 deletion api/src/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import { AuthMailer } from '@api/modules/auth/services/auth.mailer';
import { NotificationsModule } from '@api/modules/notifications/notifications.module';
import { AuthenticationController } from '@api/modules/auth/authentication.controller';
import { AuthenticationModule } from '@api/modules/auth/authentication.module';
import { RequestPasswordRecoveryHandler } from '@api/modules/auth/commands/request-password-recovery.handler';

@Module({
imports: [AuthenticationModule, NotificationsModule],
controllers: [AuthenticationController],
providers: [PasswordRecoveryService, AuthMailer],
providers: [
PasswordRecoveryService,
AuthMailer,
RequestPasswordRecoveryHandler,
],
exports: [AuthenticationModule, AuthMailer],
})
export class AuthModule {}
14 changes: 8 additions & 6 deletions api/src/modules/auth/authentication.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ import { authContract } from '@shared/contracts/auth.contract';
import { AuthenticationService } from '@api/modules/auth/authentication.service';
import { JwtAuthGuard } from '@api/modules/auth/guards/jwt-auth.guard';
import { SignUp } from '@api/modules/auth/strategies/sign-up.strategy';
import { CommandBus } from '@nestjs/cqrs';
import { RequestPasswordRecoveryCommand } from '@api/modules/auth/commands/request-password-recovery.command';

@Controller()
@UseInterceptors(ClassSerializerInterceptor)
export class AuthenticationController {
constructor(
private authService: AuthenticationService,
private readonly passwordRecovery: PasswordRecoveryService,
private readonly commandBus: CommandBus,
) {}

@Public()
Expand Down Expand Up @@ -59,12 +62,9 @@ export class AuthenticationController {
return tsRestHandler(
authContract.resetPassword,
async ({ body: { password } }) => {
const userWithAccessToken = await this.passwordRecovery.resetPassword(
user,
password,
);
await this.authService.updatePassword(user, password);
return {
body: userWithAccessToken,
body: null,
status: 201,
};
},
Expand All @@ -78,7 +78,9 @@ export class AuthenticationController {
return tsRestHandler(
authContract.requestPasswordRecovery,
async ({ body: { email } }) => {
await this.passwordRecovery.requestPasswordRecovery(email, origin);
await this.commandBus.execute(
new RequestPasswordRecoveryCommand(email),
);
return {
body: null,
status: HttpStatus.CREATED,
Expand Down
4 changes: 4 additions & 0 deletions api/src/modules/auth/authentication.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,8 @@ export class AuthenticationService {
}
throw new UnauthorizedException();
}

async updatePassword(user: User, newPassword: string): Promise<void> {
await this.usersService.updatePassword(user, newPassword);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class RequestPasswordRecoveryCommand {
constructor(public readonly email: string) {}
}
32 changes: 32 additions & 0 deletions api/src/modules/auth/commands/request-password-recovery.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';
import { AuthMailer } from '@api/modules/auth/services/auth.mailer';
import { RequestPasswordRecoveryCommand } from '@api/modules/auth/commands/request-password-recovery.command';
import { UsersService } from '@api/modules/users/users.service';
import { PasswordRecoveryRequestedEvent } from '@api/modules/events/user-events/password-recovery-requested.event';
import { NotFoundException } from '@nestjs/common';

@CommandHandler(RequestPasswordRecoveryCommand)
export class RequestPasswordRecoveryHandler
implements ICommandHandler<RequestPasswordRecoveryCommand>
{
constructor(
private readonly users: UsersService,
private readonly authMailer: AuthMailer,
private readonly eventBus: EventBus,
) {}

async execute(command: RequestPasswordRecoveryCommand): Promise<void> {
const { email } = command;
const user = await this.users.findByEmail(email);
if (!user) {
this.eventBus.publish(new PasswordRecoveryRequestedEvent(email, null));
throw new NotFoundException(`Email ${email} not found`);
}
await this.authMailer.sendPasswordRecoveryEmail({
user,
// TODO: Origin must come from env vars
origin: 'http://localhost:3000',
});
this.eventBus.publish(new PasswordRecoveryRequestedEvent(email, user.id));
}
}
7 changes: 4 additions & 3 deletions api/src/modules/auth/services/auth.mailer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,13 @@ export class AuthMailer {
user: User;
defaultPassword: string;
}) {
const { emailConfirmationToken, expiresIn } =
await this.jwt.signEmailConfirmationToken(welcomeEmailDto.user.id);
const { signUpToken, expiresIn } = await this.jwt.signSignUpToken(
welcomeEmailDto.user.id,
);

// TODO: We need to know the URL to confirm the email, we could rely on origin but we would need to pass it through a lot of code.
// probably better to have a config value for this.
const resetPasswordUrl = `TODO/auth/sign-up/${emailConfirmationToken}`;
const resetPasswordUrl = `TODO/auth/sign-up/${signUpToken}`;

const htmlContent: string = `
<h1>Dear User,</h1>
Expand Down
8 changes: 4 additions & 4 deletions api/src/modules/auth/services/jwt.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@ export class JwtManager {
};
}

async signEmailConfirmationToken(
async signSignUpToken(
userId: string,
): Promise<{ emailConfirmationToken: string; expiresIn: string }> {
const { token: emailConfirmationToken, expiresIn } = await this.sign(
): Promise<{ signUpToken: string; expiresIn: string }> {
const { token: signUpToken, expiresIn } = await this.sign(
userId,
TOKEN_TYPE_ENUM.SIGN_UP,
);
return {
emailConfirmationToken,
signUpToken: signUpToken,
expiresIn,
};
}
Expand Down
3 changes: 2 additions & 1 deletion api/src/modules/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ConflictException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from '@shared/entities/users/user.entity';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UsersService {
Expand All @@ -26,7 +27,7 @@ export class UsersService {
}

async updatePassword(user: User, newPassword: string) {
user.password = newPassword;
user.password = await bcrypt.hash(newPassword, 10);
return this.repo.save(user);
}

Expand Down
2 changes: 1 addition & 1 deletion api/test/e2e/features/password-recovery-send-email.feature
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ Feature: Password Recovery

Scenario: No email should be sent if the user is not found
When the user requests password recovery with an invalid email
Then the user should receive a 201 status code
Then the user should receive a 404 status code
And no email should be sent
3 changes: 3 additions & 0 deletions api/test/e2e/steps/password-recovery-reset-email.steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ describe('Reset Password', () => {
resetPasswordSecret = apiConfigService.getJWTConfigByType(
TOKEN_TYPE_ENUM.RESET_PASSWORD,
).secret;
});

afterEach(async () => {
await testManager.clearDatabase();
});

Expand Down
9 changes: 5 additions & 4 deletions api/test/integration/auth/create-user.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ describe('Create Users', () => {
// Given a user exists with valid credentials
// But the user has the role partner

const user = await testManager
.mocks()
.createUser({ role: ROLES.PARTNER, email: '[email protected]' });
const user = await testManager.mocks().createUser({
role: ROLES.PARTNER,
email: '[email protected]',
});
const { jwtToken } = await testManager.logUserIn(user);

// When the user creates a new user
Expand Down Expand Up @@ -72,7 +73,7 @@ describe('Create Users', () => {
);
});

test('An Admin registers a new user', async () => {
test('An Admin registers a new user ', async () => {
// Given a admin user exists with valid credentials
// beforeAll
const newUser = {
Expand Down
24 changes: 13 additions & 11 deletions api/test/integration/auth/sign-up.spec.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { TestManager } from '../../utils/test-manager';
import { HttpStatus } from '@nestjs/common';
import { ApiConfigService } from '@api/modules/config/app-config.service';
import { JwtService } from '@nestjs/jwt';
import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema';
import { authContract } from '@shared/contracts/auth.contract';
import { ROLES } from '@api/modules/auth/roles.enum';
import { JwtManager } from '@api/modules/auth/services/jwt.manager';

//create-user.feature

describe('Create Users', () => {
let testManager: TestManager;
let apiConfig: ApiConfigService;
let jwtService: JwtService;
let jwtManager: JwtManager;

beforeAll(async () => {
testManager = await TestManager.createTestManager();
apiConfig = testManager.getModule<ApiConfigService>(ApiConfigService);
jwtService = testManager.getModule<JwtService>(JwtService);
jwtManager = testManager.getModule<JwtManager>(JwtManager);
});

afterEach(async () => {
Expand All @@ -30,17 +28,12 @@ describe('Create Users', () => {
test('A sign-up token should not be valid if the user bound to that token has already been activated', async () => {
// Given a user exists with valid credentials
// But the user has the role partner

const user = await testManager.mocks().createUser({
role: ROLES.PARTNER,
email: '[email protected]',
isActive: true,
});
const { secret, expiresIn } = apiConfig.getJWTConfigByType(
TOKEN_TYPE_ENUM.SIGN_UP,
);

const token = jwtService.sign({ id: user.id }, { secret, expiresIn });
const token = jwtManager.signSignUpToken(user.id);

// When the user creates a new user

Expand All @@ -52,4 +45,13 @@ describe('Create Users', () => {

expect(response.status).toBe(HttpStatus.UNAUTHORIZED);
});

test('Sign up should fail if the current password is incorrect', async () => {
const user = await testManager.mocks().createUser({
role: ROLES.PARTNER,
email: '[email protected]',
isActive: true,
});
const token = await jwtManager.signSignUpToken(user.id);
});
});
1 change: 1 addition & 0 deletions api/test/utils/mocks/entity-mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const createUser = async (
email: '[email protected]',
...additionalData,
password: await hash(usedPassword, salt),
isActive: true,
};

await dataSource.getRepository(User).save(user);
Expand Down
5 changes: 4 additions & 1 deletion api/test/utils/test-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ export class TestManager {
}

async setUpTestUser() {
const user = await createUser(this.getDataSource(), { role: ROLES.ADMIN });
const user = await createUser(this.getDataSource(), {
role: ROLES.ADMIN,
isActive: true,
});
return logUserIn(this, user);
}

Expand Down

0 comments on commit 768b5e7

Please sign in to comment.