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.title": "Welcome to Cowsi!",
|
||||||
"mail.create-user.body-html": "<h1>Welcome to Cowsi<h1>"
|
"mail.create-user.body-title": "Welcome to Cowsi {{name}}!"
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"mail.create-user.title": "Bienvenue sur Cowsi",
|
"mail.create-user.title": "Bienvenue sur Cowsi!",
|
||||||
"mail.create-user.body-html": "<h1>Bienvenue sur Cowsi<h1>"
|
"mail.create-user.body-title": "Bienvenue sur Cowsi {{name}}!"
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,9 @@
|
|||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
|
"ejs": "^3.1.10",
|
||||||
"i18next": "^25.3.2",
|
"i18next": "^25.3.2",
|
||||||
"i18next-fs-backend": "^2.6.0",
|
"i18next-fs-backend": "^2.6.0",
|
||||||
"kysely": "^0.28.3",
|
"kysely": "^0.28.3",
|
||||||
@@ -44,6 +46,7 @@
|
|||||||
"@nestjs/testing": "^11.0.1",
|
"@nestjs/testing": "^11.0.1",
|
||||||
"@swc/cli": "^0.6.0",
|
"@swc/cli": "^0.6.0",
|
||||||
"@swc/core": "^1.10.7",
|
"@swc/core": "^1.10.7",
|
||||||
|
"@types/ejs": "^3.1.5",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { EnvService } from './services/env/env.service';
|
|||||||
import { UsersRegistry } from './app/users/users-registry';
|
import { UsersRegistry } from './app/users/users-registry';
|
||||||
import { UsersController } from './app/users/users.controller';
|
import { UsersController } from './app/users/users.controller';
|
||||||
import { MailerService } from './services/mailer/mailer.service';
|
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({
|
@Module({
|
||||||
imports: [],
|
imports: [],
|
||||||
@@ -19,7 +20,8 @@ import { localizationServiceProvider } from './services/localization/localizatio
|
|||||||
EnvService,
|
EnvService,
|
||||||
UsersRegistry,
|
UsersRegistry,
|
||||||
MailerService,
|
MailerService,
|
||||||
localizationServiceProvider,
|
LOCALIZATION_SERVICE_PROVIDER,
|
||||||
|
TemplateRendererService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
LOCALIZATION_SERVICE,
|
LOCALIZATION_SERVICE,
|
||||||
LocalizationService,
|
LocalizationService,
|
||||||
} from '../../services/localization/localization.service';
|
} from '../../services/localization/localization.service';
|
||||||
|
import { TemplateRendererService } from '../../services/template-renderer/template-renderer.service';
|
||||||
|
import { MailTemplateEnum } from '../../services/template-renderer/mail-template-data';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
@@ -14,18 +16,31 @@ export class UsersService {
|
|||||||
private mailerService: MailerService,
|
private mailerService: MailerService,
|
||||||
@Inject(LOCALIZATION_SERVICE)
|
@Inject(LOCALIZATION_SERVICE)
|
||||||
private localizationService: LocalizationService,
|
private localizationService: LocalizationService,
|
||||||
|
private templateRendererService: TemplateRendererService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createUser(newUser: CreateUser): Promise<void> {
|
async createUser(newUser: CreateUser): Promise<void> {
|
||||||
// FIXME : return result once auth is done
|
// FIXME : return result once auth is done
|
||||||
await this.userRegistry.addUser(newUser);
|
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({
|
void this.mailerService.sendEmail({
|
||||||
toAdresses: [newUser.email],
|
toAdresses: [newUser.email],
|
||||||
subject: 'Your account has been created!',
|
subject: mailTitle,
|
||||||
html: `<h1>Welcome to COWSI</h1>
|
html: mailHtml,
|
||||||
<p>Your account has been successfully created</p>
|
|
||||||
`,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import * as i18nextBackend from 'i18next-fs-backend';
|
|||||||
|
|
||||||
export const LOCALIZATION_SERVICE = 'LOCALIZATION_SERVICE';
|
export const LOCALIZATION_SERVICE = 'LOCALIZATION_SERVICE';
|
||||||
|
|
||||||
export const localizationServiceProvider: FactoryProvider<LocalizationService> =
|
export const LOCALIZATION_SERVICE_PROVIDER: Readonly<
|
||||||
{
|
FactoryProvider<LocalizationService>
|
||||||
|
> = {
|
||||||
provide: LOCALIZATION_SERVICE,
|
provide: LOCALIZATION_SERVICE,
|
||||||
scope: Scope.REQUEST,
|
scope: Scope.REQUEST,
|
||||||
useFactory: async (req: Request) => {
|
useFactory: async (req: Request) => {
|
||||||
@@ -31,7 +32,7 @@ export const localizationServiceProvider: FactoryProvider<LocalizationService> =
|
|||||||
export class LocalizationService {
|
export class LocalizationService {
|
||||||
constructor(private readonly i18n: i18next.i18n) {}
|
constructor(private readonly i18n: i18next.i18n) {}
|
||||||
|
|
||||||
translate(key: string) {
|
translate(key: string, args?: Record<string, unknown>): string {
|
||||||
this.i18n.t(key);
|
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':
|
'@nestjs/platform-express':
|
||||||
specifier: ^11.0.1
|
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)
|
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:
|
dotenv:
|
||||||
specifier: ^17.2.1
|
specifier: ^17.2.1
|
||||||
version: 17.2.1
|
version: 17.2.1
|
||||||
|
ejs:
|
||||||
|
specifier: ^3.1.10
|
||||||
|
version: 3.1.10
|
||||||
i18next:
|
i18next:
|
||||||
specifier: ^25.3.2
|
specifier: ^25.3.2
|
||||||
version: 25.3.2(typescript@5.7.3)
|
version: 25.3.2(typescript@5.7.3)
|
||||||
@@ -71,6 +77,9 @@ importers:
|
|||||||
'@swc/core':
|
'@swc/core':
|
||||||
specifier: ^1.10.7
|
specifier: ^1.10.7
|
||||||
version: 1.12.11
|
version: 1.12.11
|
||||||
|
'@types/ejs':
|
||||||
|
specifier: ^3.1.5
|
||||||
|
version: 3.1.5
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.3
|
version: 5.0.3
|
||||||
@@ -1002,6 +1011,9 @@ packages:
|
|||||||
'@types/cookiejar@2.1.5':
|
'@types/cookiejar@2.1.5':
|
||||||
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
|
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':
|
'@types/eslint-scope@3.7.7':
|
||||||
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
|
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
|
||||||
|
|
||||||
@@ -1646,6 +1658,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
dayjs@1.11.13:
|
||||||
|
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||||
|
|
||||||
debug@4.4.1:
|
debug@4.4.1:
|
||||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@@ -4721,6 +4736,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/cookiejar@2.1.5': {}
|
'@types/cookiejar@2.1.5': {}
|
||||||
|
|
||||||
|
'@types/ejs@3.1.5': {}
|
||||||
|
|
||||||
'@types/eslint-scope@3.7.7':
|
'@types/eslint-scope@3.7.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/eslint': 9.6.1
|
'@types/eslint': 9.6.1
|
||||||
@@ -5530,6 +5547,8 @@ snapshots:
|
|||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
|
dayjs@1.11.13: {}
|
||||||
|
|
||||||
debug@4.4.1:
|
debug@4.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|||||||
Reference in New Issue
Block a user