Skip to content

Commit

Permalink
send email update request
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeh committed Oct 14, 2024
1 parent 9c0283a commit 9d96aa2
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 109 deletions.
29 changes: 29 additions & 0 deletions api/src/modules/auth/authentication.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { JwtManager } from '@api/modules/auth/services/jwt.manager';
import { SignUpDto } from '@shared/schemas/auth/sign-up.schema';
import { UserSignedUpEvent } from '@api/modules/admin/events/user-signed-up.event';
import { UpdateUserPasswordDto } from '@shared/dtos/users/update-user-password.dto';
import { RequestEmailUpdateDto } from '@shared/dtos/users/request-email-update.dto';
import { SendEmailConfirmationEmailCommand } from '@api/modules/notifications/email/commands/send-email-confirmation-email.command';

@Injectable()
export class AuthenticationService {
Expand Down Expand Up @@ -116,4 +118,31 @@ export class AuthenticationService {
async hashPassword(password: string) {
return bcrypt.hash(password, 10);
}

async requestEmailUpdate(
user: User,
dto: RequestEmailUpdateDto,
origin: string,
) {
const { email, newEmail } = dto;
const existingUser = await this.usersService.findByEmail(newEmail);
if (existingUser) {
throw new ConflictException(`Email already in use`);
}
if (email === newEmail) {
throw new ConflictException(
'New email must be different from the current one',
);
}
if (user.email !== email) {
this.logger.warn(
`User ${user.id} tried to update email without providing the correct email`,
);
throw new UnauthorizedException('Invalid email provided');
}

await this.commandBus.execute(
new SendEmailConfirmationEmailCommand(user, newEmail, origin),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { SendEmailConfirmationEmailCommand } from '@api/modules/notifications/email/commands/send-email-confirmation-email.command';

@CommandHandler(SendEmailConfirmationEmailCommand)
export class SendWelcomeEmailHandler
export class SendEmailConfirmationHandler
implements ICommandHandler<SendEmailConfirmationEmailCommand>
{
constructor(private readonly authMailer: AuthMailer) {}
Expand Down
2 changes: 2 additions & 0 deletions api/src/modules/notifications/email/email.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { NodemailerEmailService } from '@api/modules/notifications/email/nodemai
import { AuthModule } from '@api/modules/auth/auth.module';
import { EmailFailedEventHandler } from '@api/modules/notifications/email/events/handlers/emai-failed-event.handler';
import { SendWelcomeEmailHandler } from '@api/modules/notifications/email/commands/handlers/send-welcome-email.handler';
import { SendEmailConfirmationHandler } from '@api/modules/notifications/email/commands/handlers/send-email-confirmation.handler';

@Module({
imports: [forwardRef(() => AuthModule)],
providers: [
{ provide: IEmailServiceToken, useClass: NodemailerEmailService },
SendEmailConfirmationHandler,
SendWelcomeEmailHandler,
EmailFailedEventHandler,
],
Expand Down
8 changes: 6 additions & 2 deletions api/src/modules/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ClassSerializerInterceptor,
Controller,
Headers,
HttpStatus,
UnauthorizedException,
UseGuards,
Expand Down Expand Up @@ -57,9 +58,12 @@ export class UsersController {
}

@TsRestHandler(usersContract.requestEmailUpdate)
async requestEmailUpdate(@GetUser() user: User): ControllerResponse {
async requestEmailUpdate(
@GetUser() user: User,
@Headers('origin') origin: string,
): ControllerResponse {
return tsRestHandler(usersContract.requestEmailUpdate, async ({ body }) => {
await this.usersService.remove(user.id);
await this.auth.requestEmailUpdate(user, body, origin);
return { body: null, status: HttpStatus.OK };
});
}
Expand Down
7 changes: 0 additions & 7 deletions api/src/modules/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,4 @@ export class UsersService extends AppBaseService<
const user = await this.userRepository.findOneBy({ id });
return user.isActive;
}

async requestEmailUpdate(user: User, dto: RequestEmailUpdateDto) {
const { email, newEmail } = dto;
if (user.email !== email) {
throw new UnauthorizedException();
}
}
}
4 changes: 1 addition & 3 deletions api/test/integration/auth/sign-up.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import { ROLES } from '@shared/entities/users/roles.enum';
import { JwtManager } from '@api/modules/auth/services/jwt.manager';
import { User } from '@shared/entities/users/user.entity';

//create-user.feature

describe('Create Users', () => {
describe('Sign Up', () => {
let testManager: TestManager;
let jwtManager: JwtManager;

Expand Down
139 changes: 139 additions & 0 deletions api/test/integration/users/users-email-update.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { createUser } from '@shared/lib/entity-mocks';
import { TestManager } from '../../utils/test-manager';
import { User } from '@shared/entities/users/user.entity';
import { HttpStatus } from '@nestjs/common';
import { usersContract } from '@shared/contracts/users.contract';
import { ROLES } from '@shared/entities/users/roles.enum';
import { MockEmailService } from '../../utils/mocks/mock-email.service';
import { IEmailServiceToken } from '@api/modules/notifications/email/email-service.interface';

describe('Users ME (e2e)', () => {
let testManager: TestManager;
let emailService: MockEmailService;

beforeAll(async () => {
testManager = await TestManager.createTestManager();
emailService = testManager.getModule<MockEmailService>(IEmailServiceToken);
});

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

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

describe('Send Email Update notification', () => {
it('Should fail if the current email sent does not match, and no email should be sent', async () => {
const user = await testManager
.mocks()
.createUser({ role: ROLES.PARTNER });
const { jwtToken } = await testManager.logUserIn(user);

const response = await testManager
.request()
.patch(usersContract.requestEmailUpdate.path)
.send({ email: '[email protected]', newEmail: '[email protected]' })
.set('Authorization', `Bearer ${jwtToken}`);

expect(response.status).toBe(HttpStatus.UNAUTHORIZED);
expect(response.body.errors[0].title).toBe('Invalid email provided');
expect(emailService.sendMail).toHaveBeenCalledTimes(0);
});
it('Should fail if the new email is already in use, and no email should be sent', async () => {
const previousUser = await testManager
.mocks()
.createUser({ role: ROLES.PARTNER });
const user = await testManager
.mocks()
.createUser({ email: '[email protected]', role: ROLES.PARTNER });

const { jwtToken } = await testManager.logUserIn(user);

const response = await testManager
.request()
.patch(usersContract.requestEmailUpdate.path)
.send({ email: user.email, newEmail: previousUser.email })
.set('Authorization', `Bearer ${jwtToken}`);

expect(response.status).toBe(HttpStatus.CONFLICT);
expect(response.body.errors[0].title).toBe('Email already in use');
});

it('Should send an email to the new email address', async () => {
const user = await testManager
.mocks()
.createUser({ role: ROLES.PARTNER });
const { jwtToken } = await testManager.logUserIn(user);

const newEmail = '[email protected]';
const response = await testManager
.request()
.patch(usersContract.requestEmailUpdate.path)
.send({ email: user.email, newEmail })
.set('Authorization', `Bearer ${jwtToken}`);

expect(response.status).toBe(HttpStatus.OK);
expect(emailService.sendMail).toHaveBeenCalledTimes(1);
});
});
describe('Confirm email update', () => {
it('should update my own password', async () => {
const user = await testManager
.mocks()
.createUser({ email: '[email protected]', role: ROLES.PARTNER });

const { jwtToken, password: oldPassword } =
await testManager.logUserIn(user);
const newPassword = 'newPassword';
const response = await testManager
.request()
.patch(usersContract.updatePassword.path)
.send({ password: oldPassword, newPassword })
.set('Authorization', `Bearer ${jwtToken}`);

expect(response.status).toBe(200);
expect(response.body.data.id).toEqual(user.id);

const { jwtToken: noToken } = await testManager.logUserIn({
...user,
password: oldPassword,
});
expect(noToken).toBeUndefined();

const { jwtToken: newToken } = await testManager.logUserIn({
...user,
password: newPassword,
});

expect(newToken).toBeDefined();
});
it('should update a user name', async () => {
const user = await createUser(testManager.getDataSource(), {
email: '[email protected]',
role: ROLES.PARTNER,
});

const { jwtToken } = await testManager.logUserIn(user);
const newName = 'newName';
const response = await testManager
.request()
.patch(usersContract.updateMe.path)
.send({ name: newName })
.set('Authorization', `Bearer ${jwtToken}`);
expect(response.status).toBe(201);
expect(response.body.data.id).toEqual(user.id);
expect(response.body.data.name).toEqual(newName);

// Previous token should work after updating the user's email
const userMeResponse = await testManager
.request()
.get('/users/me')
.set('Authorization', `Bearer ${jwtToken}`);

expect(userMeResponse.status).toBe(200);
expect(userMeResponse.body.data.name).toEqual(newName);
});
});
});
Loading

0 comments on commit 9d96aa2

Please sign in to comment.