Skip to content

Commit b374b28

Browse files
davwysMarinolino
andauthored
feat: E-mail Module (#336)
* Folder structure * Setup flox helpers * Add flox config * Basic module helpers * Add frontend structure * Update user, baseentity * Add module status checker * Module options helper * Cleanup * Cleanup * Clean up i18n * Even more cleanup * Structure, cleanup * Minor folder changes * Auth/role/copypasta setup * update PR template * Cleanup, routes * Router fix * Add helper * Module structure * more structure * Helpers * Cleanup, rm shared * Cleanup * Structure, module separation * More cleanup, roles * auth module cleanup * More cleanup¨ * add helpers * User & role module * Backend stuff * Add FloxWrapper * Make wrapper nice * Restrict module components * Fix errors * Test module config, merge * role cleanup * Add index for each module * Cleanup * Module docs, even more cleanup * Add CognitoUuid * Module config cleanup, logging * Config changes * Add generic i18n * Auth cleanup * Config * Fix auth dialogs, adapt to configuration * Add flox demo data * Signup demo flow, cleanup * Password cleanup * Fix auth * Add nice label to QR code * Auth cleanup * DB User creation * Add email verification code * Notification added * Auth flow changes, jwt not working yet * Fix JWT * Update router service * Cleanup, entity helpers * More resolver stuff * Fix userPool * Finally fix auth * switch anyRole for loggedIn * Cleanup * Add role * Role mgmt * User role checking * Improved testing setup, still has issues * Rename files, testing setup * More test changes * Handle password reset force * Error handling, general cleanup * Auto-login after confirming e-mail * Cleanup * Auto-relogin * Fix auth flow, cleanup * Cleanup * remove sample form * Add user mapper sample * Cleanup * SampleForm added * Folder structure * Add module readme, structure2 * Remove unused test * Import cleanup * Fix frontend floxconfig * More cleanup * Remove readme, moved to wiki * BaseEntity in frontend * PR changes * More cleanup * Basic module & helper setup * More setup * E-Mail sending test setup * Cleanup * More cleanup * Cleanup, remove keys * Credentials cleanup, split * More key changes * Cleanup * Cleanup * Update backend/src/flox/modules/email/helpers/email-helpers.ts Co-authored-by: Marinolino <[email protected]> Co-authored-by: Marinolino <[email protected]>
1 parent b353bde commit b374b28

16 files changed

+809
-88
lines changed

backend/flox.config.js

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module.exports = {
55
roles: true,
66
file: true,
77
sharing: false,
8+
email: true,
89
},
910
moduleOptions: {
1011
auth: {
@@ -13,5 +14,8 @@ module.exports = {
1314
roles: {
1415
roles: ['ADMIN', 'SUPERUSER', 'USER'],
1516
},
17+
email: {
18+
emailSender: '[email protected]',
19+
},
1620
},
1721
};

backend/jest.config.js

-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
module.exports = {
22
moduleFileExtensions: ['vue', 'js', 'json', 'ts'],
33
moduleDirectories: ['node_modules'],
4-
// moduleNameMapper: {
5-
// '^src/(.*)$': '<rootDir>/../src/$1',
6-
// },
74
rootDir: 'src',
85
testRegex: '.*\\.spec\\.ts$',
96
transform: {

backend/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
},
2525
"dependencies": {
2626
"@aws-sdk/client-s3": "^3.41.0",
27+
"@aws-sdk/client-ses": "^3.112.0",
2728
"@aws-sdk/s3-request-presigner": "^3.42.0",
2829
"@nestjs/common": "^8.0.0",
2930
"@nestjs/config": "^1.0.2",
@@ -44,6 +45,7 @@
4445
"jest-sonar": "^0.2.12",
4546
"joi": "^17.4.2",
4647
"jwks-rsa": "^2.0.5",
48+
"nodemailer": "^6.7.5",
4749
"passport": "^0.5.0",
4850
"passport-jwt": "^4.0.0",
4951
"pg": "^8.7.1",

backend/src/app.module.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ import { floxModules, floxProviders } from './flox/flox';
3232

3333
// AWS
3434
AWS_REGION: Joi.string().required(),
35-
AWS_ACCESS_KEY_ID: Joi.string().required(),
36-
AWS_SECRET_ACCESS_KEY: Joi.string().required(),
35+
AWS_S3_ACCESS_KEY_ID: Joi.string().required(),
36+
AWS_S3_SECRET_ACCESS_KEY: Joi.string().required(),
37+
AWS_SES_ACCESS_KEY_ID: Joi.string().required(),
38+
AWS_SES_SECRET_ACCESS_KEY: Joi.string().required(),
3739
AWS_PUBLIC_BUCKET_NAME: Joi.string().required(),
3840
AWS_PRIVATE_BUCKET_NAME: Joi.string().required(),
3941
}),

backend/src/flox/MODULES.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export enum MODULES {
44
ROLES = 'roles',
55
FILE = 'file',
66
SHARING = 'sharing',
7+
EMAIL = 'email',
78
}

backend/src/flox/flox.ts

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { MODULES } from './MODULES';
88
import PublicFile from './modules/file/entities/public_file.entity';
99
import PrivateFile from './modules/file/entities/private_file.entity';
1010
import { User } from './modules/auth/entities/user.entity';
11+
import { EmailModule } from './modules/email/email.module';
1112
import { getActiveFloxModuleNames } from './core/flox-helpers';
1213

1314
/**
@@ -28,6 +29,9 @@ export function floxModules() {
2829
case MODULES.AUTH:
2930
modules.push(UserModule);
3031
break;
32+
case MODULES.EMAIL:
33+
modules.push(EmailModule);
34+
break;
3135
// Some modules don't have to be added (e.g. 'roles')
3236
default:
3337
break;
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { mergeConfigurations } from '../../core/flox-helpers';
2+
import { floxModuleOptions } from '../../flox';
3+
import { MODULES } from '../../MODULES';
4+
5+
/**
6+
* The file module handles file up/download using a database table each for private and public files, as well as storing
7+
* the files in S3 and requesting corresponding URLs.
8+
*/
9+
10+
type EmailModuleConfig = {
11+
emailSender: string;
12+
};
13+
14+
// Default configuration set; will get merged with custom config from flox.config.js
15+
const defaultConfig: EmailModuleConfig = {
16+
emailSender: '[email protected]',
17+
};
18+
19+
/**
20+
* Gets the module's actual configuration
21+
* @returns {FileModuleConfig} - configuration
22+
*/
23+
export function moduleConfig() {
24+
return mergeConfigurations(
25+
defaultConfig,
26+
floxModuleOptions(MODULES.EMAIL),
27+
) as EmailModuleConfig;
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Controller, Post, Query, Req, Res } from '@nestjs/common';
2+
import { EmailService } from './email.service';
3+
import { ConfigService } from '@nestjs/config';
4+
import { Credentials } from './helpers/email-helpers';
5+
import { FastifyReply, FastifyRequest } from 'fastify';
6+
7+
@Controller()
8+
export class EmailController {
9+
constructor(
10+
private readonly emailService: EmailService,
11+
private readonly configService: ConfigService,
12+
) {}
13+
14+
// SES credentials
15+
private readonly credentials: Credentials = {
16+
region: this.configService.get('AWS_REGION'),
17+
accessKeyId: this.configService.get('AWS_SES_ACCESS_KEY_ID'),
18+
secretAccessKey: this.configService.get('AWS_SES_SECRET_ACCESS_KEY'),
19+
};
20+
21+
/**
22+
* Sends a test e-mail to the given address (in 'recipient' param of query)
23+
* NOTE: This is just an example endpoint. Since it is not marked @Public / @LoggedIn, it will not be accessible by default.
24+
* @param {FastifyRequest} req - the request
25+
* @param {FastifyReply} res - reply to send on
26+
* @param {Record<string, unknown>} query - request query
27+
* @returns {Promise<void>} - done
28+
*/
29+
@Post('/sendTestEmail')
30+
async sendTestEmail(
31+
@Req() req: FastifyRequest,
32+
@Res() res: FastifyReply<any>,
33+
@Query() query: Record<string, unknown>,
34+
): Promise<void> {
35+
// Access to triggering user's Cognito UUID (if needed)
36+
//const triggeredBy = req['user'].userId;
37+
38+
const recipient = query.recipient as string;
39+
40+
if (!recipient) {
41+
throw new Error('No recipient given!');
42+
}
43+
44+
// Send e-mail
45+
try {
46+
await this.emailService.sendTestEmail(recipient, this.credentials);
47+
res.code(200);
48+
res.send();
49+
} catch (e) {
50+
res.code(500);
51+
res.send(`Error occurred while sending e-mail: ${e.message}`);
52+
}
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Module } from '@nestjs/common';
2+
import { EmailService } from './email.service';
3+
import { EmailController } from './email.controller';
4+
import { ConfigService } from '@nestjs/config';
5+
6+
@Module({
7+
providers: [EmailService, ConfigService],
8+
controllers: [EmailController],
9+
})
10+
export class EmailModule {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { Credentials, sendEmail } from './helpers/email-helpers';
3+
import { moduleConfig } from './config';
4+
5+
@Injectable()
6+
export class EmailService {
7+
/**
8+
* Sends a test e-mail
9+
* @param {string} recipient - e-mail recipient
10+
* @param {Credentials} credentials - SES auth credentials
11+
* @returns {Promise<void>} - done
12+
*/
13+
async sendTestEmail(
14+
recipient: string,
15+
credentials: Credentials,
16+
): Promise<void> {
17+
// Get sender from module configuration
18+
const sender = moduleConfig().emailSender;
19+
20+
// Send actual e-mail
21+
await sendEmail(
22+
credentials,
23+
sender,
24+
recipient,
25+
'Test Message from Flox',
26+
'This is a test.',
27+
);
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Data Type for a file attachment to be used sent via e-mail using Nodemailer
3+
*/
4+
5+
export type AttachmentFile = {
6+
filename: string;
7+
content: Buffer;
8+
contentType: string; // MIME content type (e.g. 'application/pdf')
9+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { SES, SendRawEmailCommand } from '@aws-sdk/client-ses';
2+
import * as nodemailer from 'nodemailer';
3+
import { AttachmentFile } from './AttachmentFile';
4+
5+
export type Credentials = {
6+
accessKeyId: string;
7+
secretAccessKey: string;
8+
region: string;
9+
};
10+
11+
/**
12+
* Sends an e-mail, optionally with attachment(s) using SES and Nodemailer
13+
* @param {Record<string, string>} credentials - SES credentials object
14+
* @param {string} from - the sender's e-mail address
15+
* @param {string|string[]} to - list of recipient's e-mail addresses
16+
* @param {string} subject - E-mail subject
17+
* @param {string} body - E-mail's HTML body
18+
* @param {AttachmentFile[]} attachments - file attachments
19+
* @returns {Promise<void>} - done
20+
*/
21+
export async function sendEmail(
22+
credentials: Credentials,
23+
from: string,
24+
to: string | string[],
25+
subject: string,
26+
body: string,
27+
attachments?: AttachmentFile[],
28+
): Promise<void> {
29+
// Create SES service object
30+
const sesClient = new SES({
31+
region: process.env.AWS_REGION,
32+
credentials: credentials,
33+
});
34+
35+
// Create Nodemailer SES transporter
36+
const transporter = nodemailer.createTransport({
37+
secure: true,
38+
requireTLS: true,
39+
secured: true,
40+
SES: {
41+
ses: sesClient,
42+
aws: { SendRawEmailCommand },
43+
},
44+
});
45+
46+
const emailParams = {
47+
from,
48+
to,
49+
subject,
50+
html: body,
51+
attachments: attachments ?? [],
52+
};
53+
54+
try {
55+
await transporter.sendMail(emailParams);
56+
} catch (e) {
57+
throw new Error(`Error while sending e-mail: ${e.name}: ${e.message}`);
58+
}
59+
}

backend/src/flox/modules/file/file.controller.ts

+13-13
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {
66
Res,
77
} from '@nestjs/common';
88
import { FileService } from './file.service';
9-
import fastify = require('fastify');
109
import { LoggedIn, Public } from '../auth/authentication.decorator';
10+
import { FastifyReply, FastifyRequest } from 'fastify';
1111

1212
@Controller()
1313
export class FileController {
@@ -16,28 +16,28 @@ export class FileController {
1616
@Public()
1717
@Post('/uploadPublicFile')
1818
async uploadPublicFile(
19-
@Req() req: fastify.FastifyRequest,
20-
@Res() res: fastify.FastifyReply<any>,
19+
@Req() req: FastifyRequest,
20+
@Res() res: FastifyReply<any>,
2121
): Promise<any> {
2222
// Verify that request is multipart
2323
if (!req.isMultipart()) {
2424
res.send(new BadRequestException('File expected on this endpoint'));
2525
return;
2626
}
2727
const file = await req.file();
28-
const file_buffer = await file.toBuffer();
29-
const new_file = await this.taskService.uploadPublicFile(
30-
file_buffer,
28+
const fileBuffer = await file.toBuffer();
29+
const newFile = await this.taskService.uploadPublicFile(
30+
fileBuffer,
3131
file.filename,
3232
);
33-
res.send(new_file);
33+
res.send(newFile);
3434
}
3535

3636
@Post('/uploadPrivateFile')
3737
@LoggedIn()
3838
async uploadPrivateFile(
39-
@Req() req: fastify.FastifyRequest,
40-
@Res() res: fastify.FastifyReply<any>,
39+
@Req() req: FastifyRequest,
40+
@Res() res: FastifyReply<any>,
4141
): Promise<any> {
4242
// Verify that request is multipart
4343
if (!req.isMultipart()) {
@@ -49,12 +49,12 @@ export class FileController {
4949
const owner = req['user'].userId;
5050

5151
const file = await req.file();
52-
const file_buffer = await file.toBuffer();
53-
const new_file = await this.taskService.uploadPrivateFile(
54-
file_buffer,
52+
const fileBuffer = await file.toBuffer();
53+
const newFile = await this.taskService.uploadPrivateFile(
54+
fileBuffer,
5555
file.filename,
5656
owner,
5757
);
58-
res.send(new_file);
58+
res.send(newFile);
5959
}
6060
}

backend/src/flox/modules/file/file.service.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export class FileService {
1515
// S3 credentials
1616
private readonly credentials = {
1717
region: this.configService.get('AWS_REGION'),
18-
accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID'),
19-
secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY'),
18+
accessKeyId: this.configService.get('AWS_S3_ACCESS_KEY_ID'),
19+
secretAccessKey: this.configService.get('AWS_S3_SECRET_ACCESS_KEY'),
2020
};
2121

2222
// AWS S3 instance

0 commit comments

Comments
 (0)