email templating
This commit is contained in:
121
apps/backend/assets/ejs-templates/mail/account-created.ejs
Normal file
121
apps/backend/assets/ejs-templates/mail/account-created.ejs
Normal file
@@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title><%= title %></title>
|
||||
<style>
|
||||
/* General body styling for all clients */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
/* Container for the email content */
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background-color: #ffffff;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
/* Header section */
|
||||
.header {
|
||||
background-color: #007bff;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
/* Main content section */
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
/* Button styling */
|
||||
.button {
|
||||
display: inline-block;
|
||||
background-color: #007bff;
|
||||
color: #ffffff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
}
|
||||
/* List items styling */
|
||||
.feature-item {
|
||||
padding: 5px 0;
|
||||
color: #555555;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="background-color: #f4f4f4">
|
||||
<table
|
||||
class="container"
|
||||
role="presentation"
|
||||
cellspacing="0"
|
||||
cellpadding="0"
|
||||
border="0"
|
||||
>
|
||||
<tr>
|
||||
<td
|
||||
class="header"
|
||||
style="
|
||||
background-color: #007bff;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
"
|
||||
>
|
||||
<h1 style="margin: 0"><%= bodyTitle %></h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content" style="padding: 20px">
|
||||
<p>
|
||||
Thank you for signing up for our service. We're excited to have you
|
||||
on board!
|
||||
</p>
|
||||
|
||||
<p>Here are some of the key features you can now enjoy:</p>
|
||||
|
||||
<p style="text-align: center; margin: 30px 0">
|
||||
<a
|
||||
href="https://google.com"
|
||||
class="button"
|
||||
style="
|
||||
display: inline-block;
|
||||
background-color: #007bff;
|
||||
color: #ffffff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
"
|
||||
>
|
||||
Get Started
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you have any questions, feel free to reply to this email or visit
|
||||
our
|
||||
<a href="http://goolge.com" style="color: #007bff">support center</a
|
||||
>.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 12px;
|
||||
color: #888888;
|
||||
"
|
||||
>
|
||||
<p>© <%= currentYear %> Cowsi. All rights reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"mail.create-user.title": "Welcome to Cowsi",
|
||||
"mail.create-user.body-html": "<h1>Welcome to Cowsi<h1>"
|
||||
"mail.create-user.title": "Welcome to Cowsi!",
|
||||
"mail.create-user.body-title": "Welcome to Cowsi {{name}}!"
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"mail.create-user.title": "Bienvenue sur Cowsi",
|
||||
"mail.create-user.body-html": "<h1>Bienvenue sur Cowsi<h1>"
|
||||
"mail.create-user.title": "Bienvenue sur Cowsi!",
|
||||
"mail.create-user.body-title": "Bienvenue sur Cowsi {{name}}!"
|
||||
}
|
||||
@@ -26,7 +26,9 @@
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^17.2.1",
|
||||
"ejs": "^3.1.10",
|
||||
"i18next": "^25.3.2",
|
||||
"i18next-fs-backend": "^2.6.0",
|
||||
"kysely": "^0.28.3",
|
||||
@@ -44,6 +46,7 @@
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@swc/cli": "^0.6.0",
|
||||
"@swc/core": "^1.10.7",
|
||||
"@types/ejs": "^3.1.5",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.10.7",
|
||||
|
||||
@@ -7,7 +7,8 @@ import { EnvService } from './services/env/env.service';
|
||||
import { UsersRegistry } from './app/users/users-registry';
|
||||
import { UsersController } from './app/users/users.controller';
|
||||
import { MailerService } from './services/mailer/mailer.service';
|
||||
import { localizationServiceProvider } from './services/localization/localization.service';
|
||||
import { LOCALIZATION_SERVICE_PROVIDER } from './services/localization/localization.service';
|
||||
import { TemplateRendererService } from './services/template-renderer/template-renderer.service';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
@@ -19,7 +20,8 @@ import { localizationServiceProvider } from './services/localization/localizatio
|
||||
EnvService,
|
||||
UsersRegistry,
|
||||
MailerService,
|
||||
localizationServiceProvider,
|
||||
LOCALIZATION_SERVICE_PROVIDER,
|
||||
TemplateRendererService,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
LOCALIZATION_SERVICE,
|
||||
LocalizationService,
|
||||
} from '../../services/localization/localization.service';
|
||||
import { TemplateRendererService } from '../../services/template-renderer/template-renderer.service';
|
||||
import { MailTemplateEnum } from '../../services/template-renderer/mail-template-data';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
@@ -14,18 +16,31 @@ export class UsersService {
|
||||
private mailerService: MailerService,
|
||||
@Inject(LOCALIZATION_SERVICE)
|
||||
private localizationService: LocalizationService,
|
||||
private templateRendererService: TemplateRendererService,
|
||||
) {}
|
||||
|
||||
async createUser(newUser: CreateUser): Promise<void> {
|
||||
// FIXME : return result once auth is done
|
||||
await this.userRegistry.addUser(newUser);
|
||||
this.localizationService.translate('mail.create-user.title');
|
||||
|
||||
const mailTitle = this.localizationService.translate(
|
||||
'mail.create-user.title',
|
||||
);
|
||||
const mailBodyTitle = this.localizationService.translate(
|
||||
'mail.create-user.body-title',
|
||||
{ name: newUser.firstName },
|
||||
);
|
||||
const mailHtml = await this.templateRendererService.renderMailTemplate(
|
||||
MailTemplateEnum.CreateAccount,
|
||||
{
|
||||
title: mailTitle,
|
||||
bodyTitle: mailBodyTitle,
|
||||
},
|
||||
);
|
||||
void this.mailerService.sendEmail({
|
||||
toAdresses: [newUser.email],
|
||||
subject: 'Your account has been created!',
|
||||
html: `<h1>Welcome to COWSI</h1>
|
||||
<p>Your account has been successfully created</p>
|
||||
`,
|
||||
subject: mailTitle,
|
||||
html: mailHtml,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ import * as i18nextBackend from 'i18next-fs-backend';
|
||||
|
||||
export const LOCALIZATION_SERVICE = 'LOCALIZATION_SERVICE';
|
||||
|
||||
export const localizationServiceProvider: FactoryProvider<LocalizationService> =
|
||||
{
|
||||
export const LOCALIZATION_SERVICE_PROVIDER: Readonly<
|
||||
FactoryProvider<LocalizationService>
|
||||
> = {
|
||||
provide: LOCALIZATION_SERVICE,
|
||||
scope: Scope.REQUEST,
|
||||
useFactory: async (req: Request) => {
|
||||
@@ -31,7 +32,7 @@ export const localizationServiceProvider: FactoryProvider<LocalizationService> =
|
||||
export class LocalizationService {
|
||||
constructor(private readonly i18n: i18next.i18n) {}
|
||||
|
||||
translate(key: string) {
|
||||
this.i18n.t(key);
|
||||
translate(key: string, args?: Record<string, unknown>): string {
|
||||
return this.i18n.t(key, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
const MAIL_BASE = 'assets/ejs-templates/mail';
|
||||
|
||||
export interface BaseMailTempalteData {
|
||||
currentYear: number | string;
|
||||
}
|
||||
|
||||
export enum MailTemplateEnum {
|
||||
CreateAccount = 'create-account',
|
||||
}
|
||||
|
||||
export interface MailTemplateTypeMap extends Record<MailTemplateEnum, unknown> {
|
||||
[MailTemplateEnum.CreateAccount]: CreateAccountMailTemplateData;
|
||||
}
|
||||
|
||||
const MAIL_TEMPLATE_PATH_MAP = {
|
||||
[MailTemplateEnum.CreateAccount]: 'account-created.ejs',
|
||||
} as const satisfies Record<MailTemplateEnum, string>;
|
||||
|
||||
export interface CreateAccountMailTemplateData {
|
||||
title: string;
|
||||
bodyTitle: string;
|
||||
}
|
||||
|
||||
export function getMailTemplatePath(template: MailTemplateEnum): string {
|
||||
const suffix = MAIL_TEMPLATE_PATH_MAP[template];
|
||||
return `${MAIL_BASE}/${suffix}`;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { TemplateRendererService } from './template-renderer.service';
|
||||
|
||||
describe('TemplateRendererService', () => {
|
||||
let service: TemplateRendererService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [TemplateRendererService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<TemplateRendererService>(TemplateRendererService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as ejs from 'ejs';
|
||||
import {
|
||||
BaseMailTempalteData,
|
||||
getMailTemplatePath,
|
||||
MailTemplateEnum,
|
||||
MailTemplateTypeMap,
|
||||
} from './mail-template-data';
|
||||
import * as dayjs from 'dayjs';
|
||||
|
||||
@Injectable()
|
||||
export class TemplateRendererService {
|
||||
renderMailTemplate<T extends MailTemplateEnum>(
|
||||
template: T,
|
||||
data: MailTemplateTypeMap[T],
|
||||
): Promise<string> {
|
||||
return this.render(getMailTemplatePath(template), data);
|
||||
}
|
||||
|
||||
private render(file: string, data: ejs.Data): Promise<string> {
|
||||
const base: BaseMailTempalteData = {
|
||||
currentYear: dayjs().year(),
|
||||
};
|
||||
return ejs.renderFile(file, {
|
||||
...data,
|
||||
...base,
|
||||
});
|
||||
}
|
||||
}
|
||||
19
pnpm-lock.yaml
generated
19
pnpm-lock.yaml
generated
@@ -22,9 +22,15 @@ importers:
|
||||
'@nestjs/platform-express':
|
||||
specifier: ^11.0.1
|
||||
version: 11.1.3(@nestjs/common@11.1.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3)
|
||||
dayjs:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.13
|
||||
dotenv:
|
||||
specifier: ^17.2.1
|
||||
version: 17.2.1
|
||||
ejs:
|
||||
specifier: ^3.1.10
|
||||
version: 3.1.10
|
||||
i18next:
|
||||
specifier: ^25.3.2
|
||||
version: 25.3.2(typescript@5.7.3)
|
||||
@@ -71,6 +77,9 @@ importers:
|
||||
'@swc/core':
|
||||
specifier: ^1.10.7
|
||||
version: 1.12.11
|
||||
'@types/ejs':
|
||||
specifier: ^3.1.5
|
||||
version: 3.1.5
|
||||
'@types/express':
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.3
|
||||
@@ -1002,6 +1011,9 @@ packages:
|
||||
'@types/cookiejar@2.1.5':
|
||||
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
|
||||
|
||||
'@types/ejs@3.1.5':
|
||||
resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==}
|
||||
|
||||
'@types/eslint-scope@3.7.7':
|
||||
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
|
||||
|
||||
@@ -1646,6 +1658,9 @@ packages:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
dayjs@1.11.13:
|
||||
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||
|
||||
debug@4.4.1:
|
||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
@@ -4721,6 +4736,8 @@ snapshots:
|
||||
|
||||
'@types/cookiejar@2.1.5': {}
|
||||
|
||||
'@types/ejs@3.1.5': {}
|
||||
|
||||
'@types/eslint-scope@3.7.7':
|
||||
dependencies:
|
||||
'@types/eslint': 9.6.1
|
||||
@@ -5530,6 +5547,8 @@ snapshots:
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
dayjs@1.11.13: {}
|
||||
|
||||
debug@4.4.1:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
Reference in New Issue
Block a user