Skip to content

Commit

Permalink
feat(shared): implementing aws ses as mail provider
Browse files Browse the repository at this point in the history
  • Loading branch information
leonardodimarchi committed Dec 26, 2023
1 parent 9468819 commit ba92581
Show file tree
Hide file tree
Showing 14 changed files with 2,224 additions and 50 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ DB_PASSWORD=password
DB_DATABASE=backend_db
JWT_SECRET=SECRET
JWT_EXPIRES_IN=7d
AWS_REGION=us-east-1
MAIL_FROM=[email protected]

2,182 changes: 2,144 additions & 38 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"cli": "node -r @swc/register src/cli/index.ts"
},
"dependencies": {
"@aws-sdk/client-ses": "^3.481.0",
"@nestjs/common": "^10.2.6",
"@nestjs/core": "^10.2.6",
"@nestjs/jwt": "^10.1.1",
Expand Down
2 changes: 1 addition & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import * as path from 'path';
'../src/generated/i18n.generated.ts',
),
}),
{ module: EnvModule, global: true },
DatabaseModule,
EnvModule,
AuthModule,
UserModule,
CourseModule,
Expand Down
3 changes: 0 additions & 3 deletions src/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { PasswordEncryptionService } from '@modules/user/domain/services/passwor
import { UserModule } from '@modules/user/user.module';
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { EnvModule } from '@shared/infra/env/env.module';
import { EnvVariableKeys } from '@shared/infra/env/interfaces/env-variables';
import { EnvService } from '@shared/infra/env/interfaces/env.service';
import { LoginUseCase } from './domain/usecases/login.usecase';
Expand All @@ -14,9 +13,7 @@ import { AuthController } from './presenter/controllers/auth.controller';
@Module({
imports: [
UserModule,
EnvModule,
JwtModule.registerAsync({
imports: [EnvModule],
inject: [EnvService],
useFactory: (env: EnvService) => {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ describe('RequestPasswordResetUseCase', () => {
expect(mailService.send).toHaveBeenCalledTimes(1);
expect(mailService.send).toHaveBeenCalledWith(
expect.objectContaining<Partial<SendMailOptions>>({
to: email,
to: [email],
bodyHtml: expect.stringContaining(repository.items[0].code),
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class RequestPasswordResetUseCase
}

await this.mailService.send({
to: email,
to: [email],
subject: 'Password Reset',
bodyHtml: `
<h1>Here is your password reset code</h1>
Expand Down
9 changes: 8 additions & 1 deletion src/modules/password-reset/password-reset.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@ import { ValidatePasswordResetUseCase } from './domain/usecases/validate/validat
import { PasswordResetDatabaseModule } from './infra/database/password-reset-database.module';
import { PasswordResetController } from './presenter/controllers/password-reset.controller';
import { UserDatabaseModule } from '@modules/user/infra/database/user-database.module';
import { MailModule } from '@shared/infra/services/mail/mail.module';
import { UserServiceModule } from '@modules/user/infra/services/user-services.module';

@Module({
imports: [PasswordResetDatabaseModule, UserDatabaseModule],
imports: [
PasswordResetDatabaseModule,
UserDatabaseModule,
UserServiceModule,
MailModule,
],
controllers: [PasswordResetController],
providers: [
RequestPasswordResetUseCase,
Expand Down
4 changes: 3 additions & 1 deletion src/shared/domain/services/mail.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export interface SendMailOptions {
to: string;
to: string[];
subject: string;
bodyHtml: string;
cc?: string[];
bcc?: string[];
}

export abstract class MailService {
Expand Down
3 changes: 0 additions & 3 deletions src/shared/infra/database/database.module.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { EnvModule } from '../env/env.module';
import { EnvService } from '../env/interfaces/env.service';
import { getDatasourceOptions } from './typeorm/datasource-options';

@Module({
imports: [
EnvModule,
TypeOrmModule.forRootAsync({
imports: [EnvModule],
inject: [EnvService],
useFactory: (env: EnvService) => {
return getDatasourceOptions(env.getAll());
Expand Down
4 changes: 4 additions & 0 deletions src/shared/infra/env/interfaces/env-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export enum EnvVariableKeys {
DB_DATABASE = 'DB_DATABASE',
JWT_SECRET = 'JWT_SECRET',
JWT_EXPIRES_IN = 'JWT_EXPIRES_IN',
MAIL_FROM = 'MAIL_FROM',
AWS_REGION = 'AWS_REGION',
}

export type EnvVariables = {
Expand All @@ -16,4 +18,6 @@ export type EnvVariables = {
[EnvVariableKeys.DB_DATABASE]: string;
[EnvVariableKeys.JWT_SECRET]: string;
[EnvVariableKeys.JWT_EXPIRES_IN]: string;
[EnvVariableKeys.MAIL_FROM]: string;
[EnvVariableKeys.AWS_REGION]: string;
};
4 changes: 3 additions & 1 deletion src/shared/infra/env/services/dot-env.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { config as initDotEnv } from 'dotenv';
import { ValidatorSpec, cleanEnv, port, str } from 'envalid';
import { ValidatorSpec, cleanEnv, email, port, str } from 'envalid';
import { EnvVariableKeys, EnvVariables } from '../interfaces/env-variables';
import { EnvService } from '../interfaces/env.service';

Expand All @@ -25,6 +25,8 @@ export class DotEnvService implements EnvService {
[EnvVariableKeys.DB_DATABASE]: str(),
[EnvVariableKeys.JWT_SECRET]: str(),
[EnvVariableKeys.JWT_EXPIRES_IN]: str(),
[EnvVariableKeys.MAIL_FROM]: email(),
[EnvVariableKeys.AWS_REGION]: str(),
};

private readonly env: EnvVariables;
Expand Down
14 changes: 14 additions & 0 deletions src/shared/infra/services/mail/mail.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { MailService } from '@shared/domain/services/mail.service';
import { SESMailService } from './ses-mail.service';

@Module({
exports: [MailService],
providers: [
{
provide: MailService,
useClass: SESMailService,
},
],
})
export class MailModule {}
42 changes: 42 additions & 0 deletions src/shared/infra/services/mail/ses-mail.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
import { Injectable } from '@nestjs/common';
import {
MailService,
SendMailOptions,
} from '@shared/domain/services/mail.service';
import { EnvVariableKeys } from '@shared/infra/env/interfaces/env-variables';
import { EnvService } from '@shared/infra/env/interfaces/env.service';

@Injectable()
export class SESMailService implements MailService {
constructor(private readonly env: EnvService) {
this.client = new SESClient({
region: this.env.get(EnvVariableKeys.AWS_REGION),
});
}

private readonly client: SESClient;

async send(options: SendMailOptions): Promise<void> {
const command = new SendEmailCommand({
Source: this.env.get(EnvVariableKeys.MAIL_FROM),
Message: {
Subject: {
Data: options.subject,
},
Body: {
Html: {
Data: options.bodyHtml,
},
},
},
Destination: {
ToAddresses: options.to,
...(options.cc && { CcAddresses: options.cc }),
...(options.bcc && { BccAddresses: options.bcc }),
},
});

this.client.send(command);
}
}

0 comments on commit ba92581

Please sign in to comment.