-
Notifications
You must be signed in to change notification settings - Fork 0
feat: send email confirmation #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
e0778e5
feat: add send email confirmation feature
daniloamsilva 67dbfb8
test: add send email queue tests
daniloamsilva 41182cb
feat: create token entity
daniloamsilva d5bb9c1
feat: attach tokens user aggregate root
daniloamsilva ab2c615
feat: create migration for tokens table
daniloamsilva 246100e
feat: enhance token management with validity checks and email confirm…
daniloamsilva bb3a323
feat: add user existence check in email confirmation process
daniloamsilva 5b6bc35
feat: optimize token saving logic with batch insert and early return …
daniloamsilva 5a1448d
refact: remove unused mailer module
daniloamsilva 11e61b6
refact: update variable name
daniloamsilva 384bd9f
feat: enhance token saving logic to include deleted_at field
daniloamsilva c92ae29
feat: refactor email confirmation token creation logic
daniloamsilva 0a051e0
feat: replace queues.model with queues.module and update imports
daniloamsilva e1cb4ce
feat: enhance error logging in MailerService
daniloamsilva 3a688d6
feat: export ToPersist type from token.mapper for improved type usage…
daniloamsilva bb437fd
feat: improve null handling for used_at and deleted_at fields in Toke…
daniloamsilva 5bd53b3
feat: simplify token persistence logic in UserMapper
daniloamsilva 8d86fd3
feat: update job addition logic in QueuesService for improved jobId f…
daniloamsilva 3e3f102
feat: refactor MailerService to use a class-level transporter for sen…
daniloamsilva e336164
feat: optimize token invalidation logic in createToken method
daniloamsilva c441ff3
feat: correct type definition for mailOptions in MailerService
daniloamsilva f32aad6
feat: ensure error is thrown after logging in EmailConfirmationConsumer
daniloamsilva aa47ebc
feat: refactor AggregateRoot to extend Aggregate and remove unnecessa…
daniloamsilva fe9951d
feat: move user save operation after sending confirmation email
daniloamsilva 3d4fd70
feat: replace crypto.randomUUID with uuid package for token ID genera…
daniloamsilva File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import { addMinutes } from 'date-fns'; | ||
| import { v7 as uuid } from 'uuid'; | ||
|
|
||
| import { Aggregate } from '@src/shared/domain/Aggregate'; | ||
|
|
||
| interface TokenProps { | ||
| id: string; | ||
| type: TokenType; | ||
| expiresAt: Date; | ||
| usedAt?: Date | null; | ||
|
|
||
| createdAt: Date; | ||
| updatedAt: Date; | ||
| deletedAt?: Date; | ||
| } | ||
|
|
||
| export enum TokenType { | ||
| EMAIL_CONFIRMATION = 'EMAIL_CONFIRMATION', | ||
| } | ||
|
|
||
| const TOKEN_EXPIRATION_IN_MINUTES: Record<TokenType, number> = { | ||
| [TokenType.EMAIL_CONFIRMATION]: 7 * 24 * 60, // 7 days | ||
| }; | ||
|
|
||
| export class Token extends Aggregate<TokenProps> { | ||
| private constructor(props: TokenProps) { | ||
| super(props); | ||
| } | ||
|
|
||
| get value(): string { | ||
| return this.props.id; | ||
| } | ||
| get type(): TokenType { | ||
| return this.props.type; | ||
| } | ||
| get expiresAt(): Date { | ||
| return this.props.expiresAt; | ||
| } | ||
| get usedAt(): Date | undefined { | ||
| return this.props.usedAt; | ||
| } | ||
| get createdAt(): Date { | ||
| return this.props.createdAt; | ||
| } | ||
| get updatedAt(): Date { | ||
| return this.props.updatedAt; | ||
| } | ||
| get deletedAt(): Date | undefined { | ||
| return this.props.deletedAt; | ||
| } | ||
|
|
||
| public static create(type: TokenType): Token { | ||
| const expirationInMinutes = TOKEN_EXPIRATION_IN_MINUTES[type]; | ||
|
|
||
| const tokenProps: TokenProps = { | ||
| id: uuid(), | ||
| type: type, | ||
| expiresAt: addMinutes(new Date(), expirationInMinutes), | ||
| createdAt: new Date(), | ||
| updatedAt: new Date(), | ||
| }; | ||
|
|
||
| return new Token(tokenProps); | ||
| } | ||
|
|
||
| public static hydrate(props: TokenProps): Token { | ||
| return new Token(props); | ||
| } | ||
|
|
||
| get isUsed(): boolean { | ||
| return !!this.usedAt; | ||
| } | ||
|
|
||
| get isExpired(): boolean { | ||
| return this.expiresAt < new Date(); | ||
| } | ||
|
|
||
| get isValid(): boolean { | ||
| return !this.isUsed && !this.isExpired && !this.deletedAt; | ||
| } | ||
|
|
||
| public invalidate(): void { | ||
| if (this.isValid) { | ||
| this.props.deletedAt = new Date(); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
src/infrastructure/event-handlers/send-email-confirmation.handler.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { Injectable } from '@nestjs/common'; | ||
|
|
||
| import { IHandle } from '@src/shared/domain/IDomainEvent'; | ||
| import { UserCreatedEvent } from '@src/domain/users/events/user-created.event'; | ||
| import { QueuesService } from '@src/infrastructure/queues/queues.service'; | ||
| import { QueueType } from '@src/infrastructure/queues/queues.enum'; | ||
|
|
||
| export const SEND_EMAIL_CONFIRMATION_HANDLER_PROVIDER = | ||
| 'SendConfirmationEmailHandler'; | ||
|
|
||
| @Injectable() | ||
| export class SendEmailConfirmationHandler implements IHandle<UserCreatedEvent> { | ||
| constructor(private readonly queuesService: QueuesService) {} | ||
|
|
||
| async handle(event: UserCreatedEvent): Promise<void> { | ||
| await this.queuesService.execute({ | ||
| userId: event.aggregateId, | ||
| queueType: QueueType.EMAIL_CONFIRMATION, | ||
| }); | ||
| } | ||
| } | ||
|
daniloamsilva marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import { Injectable, Logger } from '@nestjs/common'; | ||
| import * as nodemailer from 'nodemailer'; | ||
| import { Address } from 'nodemailer/lib/mailer'; | ||
| import * as hbs from 'nodemailer-express-handlebars'; | ||
|
|
||
| type MailOptions = { | ||
| from?: Address; | ||
| recipients: Address[]; | ||
| subject: string; | ||
| template: string; | ||
| context?: Record<string, string>; | ||
| }; | ||
|
|
||
| @Injectable() | ||
| export class MailerService { | ||
| private readonly transporter: nodemailer.Transporter; | ||
|
|
||
| constructor() { | ||
| this.transporter = this.createMailTransport(); | ||
| } | ||
|
|
||
| async sendMail(mailOptions: MailOptions) { | ||
| const { from, recipients, subject, template, context } = mailOptions; | ||
|
|
||
| const options = { | ||
| from: from ?? { | ||
| name: process.env.MAIL_FROM_NAME, | ||
| address: process.env.MAIL_FROM_ADDRESS, | ||
| }, | ||
| to: recipients, | ||
| subject, | ||
| template, | ||
| context, | ||
| }; | ||
|
|
||
| try { | ||
| return this.transporter.sendMail(options); | ||
| } catch (error) { | ||
| Logger.error('Error while sending email', error, MailerService.name); | ||
| throw error; | ||
| } | ||
|
daniloamsilva marked this conversation as resolved.
|
||
| } | ||
|
|
||
| private createMailTransport() { | ||
| const transporter = nodemailer.createTransport({ | ||
| host: process.env.MAIL_HOST, | ||
| port: parseInt(process.env.MAIL_PORT), | ||
| secure: process.env.MAIL_SECURE.toLowerCase() === 'true', | ||
| auth: { | ||
| user: process.env.MAIL_USERNAME, | ||
| pass: process.env.MAIL_PASSWORD, | ||
| }, | ||
|
daniloamsilva marked this conversation as resolved.
|
||
| }); | ||
|
|
||
| transporter.use( | ||
| 'compile', | ||
| hbs({ | ||
| viewEngine: { defaultLayout: '' }, | ||
| viewPath: 'src/infrastructure/mailer/templates', | ||
| extName: '.hbs', | ||
| }), | ||
| ); | ||
|
|
||
| return transporter; | ||
| } | ||
| } | ||
10 changes: 10 additions & 0 deletions
10
src/infrastructure/mailer/templates/email-confirmation.hbs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <p>Olá {{name}}</p> | ||
| <p>Clique neste link para verificar seu endereço de e-mail.</p> | ||
| <p><a | ||
| href='{{confirmationLink}}' | ||
| target='_blank' | ||
| rel='noopener noreferrer' | ||
| >{{confirmationLink}}</a></p> | ||
|
daniloamsilva marked this conversation as resolved.
|
||
| <p>Se você não solicitou a verificação deste endereço, ignore este e-mail.</p> | ||
| <p>Obrigado,</p> | ||
| <p>Equipe da {{from}}</p> | ||
15 changes: 15 additions & 0 deletions
15
src/infrastructure/migrations/1734736313692_create-updated-at-function-trigger.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| exports.up = (pgm) => { | ||
| pgm.sql(` | ||
| CREATE OR REPLACE FUNCTION trigger_set_timestamp() | ||
| RETURNS TRIGGER AS $$ | ||
| BEGIN | ||
| NEW.updated_at = NOW(); | ||
| RETURN NEW; | ||
| END; | ||
| $$ LANGUAGE plpgsql; | ||
| `); | ||
| }; | ||
|
|
||
| exports.down = (pgm) => { | ||
| pgm.sql(`DROP FUNCTION trigger_set_timestamp();`); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
SendEmailConfirmationHandleris registered twice in the providers array (lines 40 and 41-48). This creates two instances of the handler, which is redundant. The handler should only be registered once using the factory provider that also handles the domain event registration. Remove line 40.