Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
import { PaymentsModule } from './payments/payments.module';
import { DonationsModule } from './donations/donations.module';
import { EmailsModule } from './emails/emails.module';
import AppDataSource from './data-source';

@Module({
Expand All @@ -16,6 +17,7 @@ import AppDataSource from './data-source';
AuthModule,
PaymentsModule,
DonationsModule,
// EmailsModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
38 changes: 38 additions & 0 deletions apps/backend/src/emails/amazon-ses-client.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Provider } from '@nestjs/common';
import { SES as AmazonSESClient } from '@aws-sdk/client-ses';
import { assert } from 'console';
import * as dotenv from 'dotenv';
dotenv.config();

export const AMAZON_SES_CLIENT = 'AMAZON_SES_CLIENT';

/**
* Factory that produces a new instance of the Amazon SES client.
* Used to send emails via Amazon SES.
*/
export const amazonSESClientFactory: Provider<AmazonSESClient> = {
provide: AMAZON_SES_CLIENT,
useFactory: () => {
assert(
process.env.AWS_SES_ACCESS_KEY_ID !== undefined,
'AWS_SES_ACCESS_KEY_ID is not defined',
);
assert(
process.env.AWS_SES_SECRET_ACCESS_KEY !== undefined,
'AWS_SES_SECRET_ACCESS_KEY is not defined',
);
assert(
process.env.AWS_SES_REGION !== undefined,
'AWS_SES_REGION is not defined',
);
const SES_CONFIG = {
region: process.env.AWS_SES_REGION,
credentials: {
accessKeyId: process.env.AWS_SES_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SES_SECRET_ACCESS_KEY,
},
};

return new AmazonSESClient(SES_CONFIG);
},
};
54 changes: 54 additions & 0 deletions apps/backend/src/emails/amazon-ses.wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Inject, Injectable } from '@nestjs/common';
import {
SES as AmazonSESClient,
SendRawEmailCommandInput,
SendRawEmailCommand,
} from '@aws-sdk/client-ses';
import { AMAZON_SES_CLIENT } from './amazon-ses-client.factory';
import MailComposer = require('nodemailer/lib/mail-composer');
import * as dotenv from 'dotenv';
import Mail from 'nodemailer/lib/mailer';
dotenv.config();

@Injectable()
export class AmazonSESWrapper {
private client: AmazonSESClient;

/**
* @param client injected from `amazon-ses-client.factory.ts`
*/
constructor(@Inject(AMAZON_SES_CLIENT) client: AmazonSESClient) {
this.client = client;
}

/**
* Sends an email via Amazon SES.
*
* @param recipientEmails the email addresses of the recipients
* @param subject the subject of the email
* @param emailContent the HTML body of the email
* @resolves if the email was sent successfully
* @rejects if the email was not sent successfully
*/
async sendEmail(
recipientEmails: string[],
subject: string,
emailContent: string,
) {
const mailOptions: Mail.Options = {
from: process.env.AWS_SES_SENDER_EMAIL,
to: recipientEmails,
subject: subject,
html: emailContent,
};

const messageData = await new MailComposer(mailOptions).compile().build();

const params: SendRawEmailCommandInput = {
Source: process.env.AWS_SES_SENDER_EMAIL,
RawMessage: { Data: messageData },
Destinations: recipientEmails,
};
await this.client.send(new SendRawEmailCommand(params));
}
}
12 changes: 12 additions & 0 deletions apps/backend/src/emails/create-email.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IsEmail, IsString } from 'class-validator';

export class CreateEmailDto {
@IsEmail()
email: string;

@IsString()
emailSubject: string;

@IsString()
emailContent: string;
}
18 changes: 18 additions & 0 deletions apps/backend/src/emails/emails.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Controller, Post, Body } from '@nestjs/common';
import { EmailsService } from './emails.service';
import { CreateEmailDto } from './create-email.dto';

@Controller('emails')
export class EmailsController {
constructor(private readonly emailService: EmailsService) {}

@Post('send-email')
async sendVerificationEmail(@Body() body: CreateEmailDto) {
await this.emailService.sendEmail(
body.email,
body.emailSubject,
body.emailContent,
);
return { message: 'email sent' };
}
}
12 changes: 12 additions & 0 deletions apps/backend/src/emails/emails.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { EmailsController } from './emails.controller';
import { EmailsService } from './emails.service';
import { JwtStrategy } from '../auth/jwt.strategy';
import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor';
import { AuthService } from '../auth/auth.service';

@Module({
controllers: [EmailsController],
providers: [EmailsService, AuthService, JwtStrategy, CurrentUserInterceptor],
})
export class EmailsModule {}
1 change: 1 addition & 0 deletions apps/backend/src/emails/emails.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// will add later
34 changes: 34 additions & 0 deletions apps/backend/src/emails/emails.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Injectable, Logger } from '@nestjs/common';
import { AmazonSESWrapper } from './amazon-ses.wrapper';

@Injectable()
export class EmailsService {
private readonly logger = new Logger(EmailsService.name);
constructor(private amazonSESWrapper: AmazonSESWrapper) {}

/**
* Sends an email.
*
* @param recipientEmail the email address of the recipient
* @param subject the subject of the email
* @param bodyHtml the HTML body of the email
* @resolves if the email was sent successfully
* @rejects if the email was not sent successfully
*/
public async sendEmail(
recipientEmail: string,
subject: string,
bodyHTML: string,
): Promise<unknown> {
try {
return this.amazonSESWrapper.sendEmail(
[recipientEmail],
subject,
bodyHTML,
);
} catch (error) {
this.logger.error('Error sending email', error);
throw error;
}
}
}
2 changes: 1 addition & 1 deletion apps/backend/src/payments/payments.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { DonationsModule } from '../donations/donations.module';
provide: 'STRIPE_CLIENT',
useFactory: (configService: ConfigService) => {
return new Stripe(configService.get<string>('STRIPE_SECRET_KEY'), {
apiVersion: '2025-09-30.clover',
apiVersion: '2025-10-29.clover',
});
},
inject: [ConfigService],
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"private": true,
"dependencies": {
"@aws-sdk/client-cognito-identity-provider": "^3.410.0",
"@aws-sdk/client-ses": "^3.975.0",
"@nestjs/cli": "^10.1.17",
"@nestjs/common": "^10.0.2",
"@nestjs/config": "^4.0.2",
Expand All @@ -46,10 +47,12 @@
"@types/supertest": "^6.0.3",
"amazon-cognito-identity-js": "^6.3.5",
"axios": "^1.5.0",
"bottleneck": "^2.19.5",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"global": "^4.4.0",
"jwks-rsa": "^3.1.0",
"nodemailer": "^7.0.12",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
"pg": "^8.16.3",
Expand Down
Loading