feat: email templating
This commit is contained in:
parent
baefa80d2c
commit
3608f91381
21 changed files with 215 additions and 25 deletions
0
mail/html/password-changed.hbs
Normal file
0
mail/html/password-changed.hbs
Normal file
69
mail/html/verify-email.hbs
Normal file
69
mail/html/verify-email.hbs
Normal file
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Email Verification</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f3f4f6; /* Tailwind class 'bg-gray-100' */
|
||||
}
|
||||
.container {
|
||||
max-width: 28rem; /* Tailwind class 'max-w-md' */
|
||||
margin: 2rem auto; /* Tailwind classes 'mx-auto' 'mt-8' */
|
||||
padding: 1.5rem; /* Tailwind class 'p-6' */
|
||||
background-color: #ffffff; /* Tailwind class 'bg-white' */
|
||||
border-radius: 0.5rem; /* Tailwind class 'rounded-lg' */
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* Tailwind class 'shadow-md' */
|
||||
}
|
||||
.button {
|
||||
display: inline-block;
|
||||
background-color: #6366f1; /* Tailwind class 'bg-indigo-600' */
|
||||
color: #ffffff; /* Tailwind class 'text-white' */
|
||||
font-weight: 700; /* Tailwind class 'font-bold' */
|
||||
padding: 0.5rem 1rem; /* Tailwind classes 'py-2' and 'px-4' */
|
||||
border-radius: 0.375rem; /* Tailwind class 'rounded-md' */
|
||||
text-decoration: none; /* Remove underline for links */
|
||||
}
|
||||
.button:hover {
|
||||
background-color: #4f46e5; /* Tailwind class 'hover:bg-indigo-700' */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="text-2xl font-semibold text-gray-800">Verify Your Email Address</h1>
|
||||
<p class="mt-4 text-gray-600">Hi there,</p>
|
||||
<p class="mt-2 text-gray-600">
|
||||
Thanks for signing up! Please confirm your email address by clicking the button below.
|
||||
</p>
|
||||
<div class="mt-6 text-center">
|
||||
<a href="{{verificationUrl}}" class="button">Verify Email</a>
|
||||
</div>
|
||||
<p class="mt-6 text-gray-600">
|
||||
If you did not create an account, no further action is required.
|
||||
</p>
|
||||
<p class="mt-6 text-gray-600">
|
||||
Regards,<br>WaterWolf
|
||||
</p>
|
||||
<hr class="mt-6 border-gray-200">
|
||||
<div class="mt-6 flex items-center justify-center text-gray-400 text-xs">
|
||||
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 204.73 204.47">
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
stroke-width: 0px;
|
||||
}
|
||||
</style>
|
||||
<g id="svg">
|
||||
<g id="svgg">
|
||||
<path id="path0" class="cls-1" d="M87.69,1.22C25.89,11.33-13.11,71.52,4.06,130.27c2.91,9.95,2.81,9.92,5.07,1.07,1.11-4.32,2.34-8.7,2.74-9.73.5-1.27.5-3.66,0-7.51C1.85,36.65,88.31-16.64,153.44,26.86c67.68,45.2,44.99,151.23-35.36,165.24-10.87,1.9-10.31,2.09-18.71-6.25-8.16-8.1-8-8.17-4.75,2.22l1.64,5.26-4.77-.51c-7.24-.77-18.02-3.71-19.82-5.4-4.91-4.61-8.76-19.09-9.26-34.81-.18-5.6-.57-10.2-.87-10.23-3.89-.28-13.29,4.17-19.89,9.41-5.97,4.74-6.02,4.7-3.91-3.57,6.93-27.13,27.84-54.82,50.35-66.66l5.01-2.64-2.73-.51c-1.5-.28-5.67-.35-9.26-.14l-6.53.37,6.83-3.43c7.96-4,14.12-6.14,20.87-7.26,2.77-.46,4.82-1.16,4.82-1.64,0-3,19.12-26.83,21.52-26.83.35,0,.43,3.27.19,7.33-.55,9.09-.71,8.86,10.82,15.24,10.14,5.61,11.94,7.09,11.65,9.57-.35,3.02-2.34,2.57-4.44-1-2.17-3.7-4.25-4.99-9.58-5.96-4.45-.81-4.68-.56-3.68,3.9,1.24,5.55,4.55,7.99,13.36,9.85,10.08,2.14,26.65,9.16,26.14,11.07-.19.73.21,2.18.9,3.22,1.2,1.82,1.19,1.98-.18,3.83-4.8,6.48-11.34,7.82-18.59,3.82-15.61-8.62-25.89-12.19-36.77-12.79-10.3-.56-18.56,1.78-10.29,2.91,3.9.54,7.97,2.52,10.33,5.05l1.73,1.86h-3.52c-23.99.1-42.29,20.92-37.02,42.13,1.83,7.37,6.88,16.46,13.12,23.61,2.46,2.82,2.6,2.69,1.05-1.03-4.13-9.89-2.1-27.31,4-34.25,4.43-5.04,4.84-4.83,4.44,2.3-1.22,21.5,13.44,35.47,40.58,38.67,4.98.59,5.26.26,1.59-1.84-14.85-8.47-26.07-27.76-23.68-40.69,2.92-15.8,12.63-21.34,34.29-19.56l10.56.87,3.15-1.84c7.55-4.42,17.35-15.45,15.44-17.36-.58-.58-6.99-3.62-14.23-6.75l-13.18-5.69-.36-4.47c-.44-5.58-.41-5.53-6.84-9.87-4.85-3.27-5.31-3.79-5.31-6,0-6.16-3.48-18.85-4.93-17.96-.37.23-.9,3.95-1.18,8.27-.52,8.03-1.12,9.27-3.25,6.71-.68-.82-.96-3.68-.98-9.93-.03-9.38-.86-11.79-4.09-11.79-5.02,0-19.77,11.77-29.26,23.35-1.53,1.87-3.02,2.57-8.57,4.05-21.98,5.85-51.41,23.66-59.59,36.06-2.69,4.07-2.28,4.17,3.95.94,12.69-6.58,26.23-10.86,19.26-6.1-20.43,13.98-33.21,37.15-37.46,67.92-1.03,7.47,22.04,28.83,40.44,37.46,76.3,35.75,160.95-30.76,143.93-113.09C191.73,29.3,139.44-7.25,87.69,1.22"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
Powered by Waterwolf
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
0
mail/html/welcome.hbs
Normal file
0
mail/html/welcome.hbs
Normal file
0
mail/text/password-changed.txt
Normal file
0
mail/text/password-changed.txt
Normal file
12
mail/text/verify-email.txt
Normal file
12
mail/text/verify-email.txt
Normal file
|
@ -0,0 +1,12 @@
|
|||
Verify Your Email Address
|
||||
|
||||
Hi there,
|
||||
|
||||
Thanks for signing up! Please confirm your email address by clicking the link below.
|
||||
|
||||
Verify Email: {{verificationUrl}}
|
||||
|
||||
If you did not create an account, no further action is required.
|
||||
|
||||
Regards,
|
||||
WaterWolf
|
0
mail/text/welcome.txt
Normal file
0
mail/text/welcome.txt
Normal file
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
"class-validator": "^0.14.1",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"dotenv": "^16.4.5",
|
||||
"handlebars": "^4.7.8",
|
||||
"hbs": "^4.2.0",
|
||||
"ioredis": "^5.4.1",
|
||||
"keygrip": "^1.1.0",
|
||||
|
|
|
@ -59,6 +59,9 @@ importers:
|
|||
dotenv:
|
||||
specifier: ^16.4.5
|
||||
version: 16.4.5
|
||||
handlebars:
|
||||
specifier: ^4.7.8
|
||||
version: 4.7.8
|
||||
hbs:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
|
@ -2222,6 +2225,11 @@ packages:
|
|||
engines: {node: '>=0.4.7'}
|
||||
hasBin: true
|
||||
|
||||
handlebars@4.7.8:
|
||||
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
|
||||
engines: {node: '>=0.4.7'}
|
||||
hasBin: true
|
||||
|
||||
has-flag@3.0.0:
|
||||
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
||||
engines: {node: '>=4'}
|
||||
|
@ -6551,6 +6559,15 @@ snapshots:
|
|||
optionalDependencies:
|
||||
uglify-js: 3.18.0
|
||||
|
||||
handlebars@4.7.8:
|
||||
dependencies:
|
||||
minimist: 1.2.8
|
||||
neo-async: 2.6.2
|
||||
source-map: 0.6.1
|
||||
wordwrap: 1.0.0
|
||||
optionalDependencies:
|
||||
uglify-js: 3.18.0
|
||||
|
||||
has-flag@3.0.0: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Body, Controller, Get, Post, Render, Res } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Post, Render, Res, UseGuards } from '@nestjs/common';
|
||||
import { ApiExcludeEndpoint } from '@nestjs/swagger';
|
||||
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
@ -7,6 +7,7 @@ import { CreateUserDto } from '../dto/register.dto';
|
|||
import { LoginUserDto } from '../dto/loginUser.dto';
|
||||
import { Response } from 'express';
|
||||
import { User } from '../decorators/user.decorator';
|
||||
import { LoginGuard } from '../guard/login.guard';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
|
@ -42,6 +43,7 @@ export class AuthController {
|
|||
|
||||
// Render pages
|
||||
@Get('login')
|
||||
@UseGuards(LoginGuard)
|
||||
@Render('auth/login')
|
||||
@ApiExcludeEndpoint()
|
||||
public async getHello(): Promise<any> {
|
||||
|
@ -53,6 +55,7 @@ export class AuthController {
|
|||
}
|
||||
|
||||
@Get('register')
|
||||
@UseGuards(LoginGuard)
|
||||
@Render('auth/register')
|
||||
@ApiExcludeEndpoint()
|
||||
public async getRegister(): Promise<any> {
|
||||
|
@ -62,6 +65,7 @@ export class AuthController {
|
|||
}
|
||||
|
||||
@Get('forgot-password')
|
||||
@UseGuards(LoginGuard)
|
||||
@Render('auth/forgot-password')
|
||||
@ApiExcludeEndpoint()
|
||||
public async getForgotPassword(): Promise<any> {
|
||||
|
|
0
src/auth/decorators/requiresRole.decorator.ts
Normal file
0
src/auth/decorators/requiresRole.decorator.ts
Normal file
|
@ -10,7 +10,10 @@ export const User = createParamDecorator(() => {
|
|||
}
|
||||
|
||||
const user = cls.get('user');
|
||||
// remove the password from the user object
|
||||
delete user.password;
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return user;
|
||||
});
|
||||
|
|
0
src/auth/guard/auth.guard.ts
Normal file
0
src/auth/guard/auth.guard.ts
Normal file
19
src/auth/guard/login.guard.ts
Normal file
19
src/auth/guard/login.guard.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Response } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class LoginGuard implements CanActivate {
|
||||
constructor(private readonly clsService: ClsService) {}
|
||||
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
|
||||
const authType = this.clsService.get('authType');
|
||||
const response = context.switchToHttp().getResponse() as Response;
|
||||
|
||||
if (authType === 'session') {
|
||||
response.redirect('/auth/auth-test');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -75,7 +75,7 @@ export class AuthService {
|
|||
}
|
||||
|
||||
await this.userService.createUser(username, email, password);
|
||||
await this.mailService.sendWelcomeEmail(email);
|
||||
await this.mailService.sendVerificationEmail(email, '11111');
|
||||
return { error: false, message: 'User registered' };
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PostalClientService } from 'nestjs-postal-client';
|
||||
import Handlebars from 'handlebars';
|
||||
import * as path from 'path';
|
||||
import { readFile } from 'fs/promises';
|
||||
|
||||
@Injectable()
|
||||
export class MailService {
|
||||
|
@ -21,26 +24,74 @@ export class MailService {
|
|||
* Send a welcome email to the user
|
||||
* @param email The email address of the user
|
||||
*/
|
||||
public async sendWelcomeEmail(email: string): Promise<void> {
|
||||
this.logger.debug(`Sending welcome email to ${email}`);
|
||||
|
||||
this.postalService.sendMessage({
|
||||
to: [email],
|
||||
subject: 'Welcome to the app!',
|
||||
from: this.configService.getOrThrow('MAIL_FROM'),
|
||||
plain_body: 'Welcome to the app!',
|
||||
});
|
||||
}
|
||||
public async sendWelcomeEmail(email: string): Promise<void> {}
|
||||
|
||||
/**
|
||||
* Send a verification email to the user
|
||||
* @param email The email address of the user
|
||||
*/
|
||||
public async sendVerificationEmail(email: string): Promise<void> {}
|
||||
public async sendVerificationEmail(email: string, code: string): Promise<void> {
|
||||
this.logger.debug(`Sending welcome email to ${email}`);
|
||||
|
||||
const verificationUrl = `${this.configService.getOrThrow('BASE_URL')}/auth/verify?code=${code}`;
|
||||
|
||||
//handlebar render email template
|
||||
const { html: templateHtml, text: templateText } = await this.fetchTemplate('verify-email');
|
||||
|
||||
//replace the placeholders with the actual values
|
||||
const template = Handlebars.compile(templateHtml);
|
||||
const html = template({ verificationUrl });
|
||||
const text = Handlebars.compile(templateText)({ verificationUrl });
|
||||
|
||||
this.postalService.sendMessage({
|
||||
to: [email],
|
||||
subject: 'Verify your email address',
|
||||
from: this.configService.getOrThrow('MAIL_FROM'),
|
||||
plain_body: text,
|
||||
html_body: html,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a password changed email to the user
|
||||
* @param email The email address of the user
|
||||
*/
|
||||
public async sendPasswordChangedEmail(email: string): Promise<void> {}
|
||||
|
||||
/**
|
||||
* Private function. Reads the html email template from the file system and returns the content
|
||||
* @param templateName The name of the template
|
||||
* @returns The content of the template
|
||||
*/
|
||||
private async readHtmlTemplate(templateName: string): Promise<string> {
|
||||
return await readFile(
|
||||
path.join(__dirname, '../..', 'mail/html', `${templateName}.hbs`),
|
||||
).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Private function. Reads the text email template from the file system and returns the content
|
||||
* @param templateName The name of the template
|
||||
* @returns The content of the template
|
||||
*/
|
||||
private async readTextTemplate(templateName: string): Promise<string> {
|
||||
return await readFile(
|
||||
path.join(__dirname, '../..', 'mail/text', `${templateName}.txt`),
|
||||
).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Private template fetch function
|
||||
* @param templateName The name of the template
|
||||
* @returns The content of the template
|
||||
*/
|
||||
private async fetchTemplate(templateName: string): Promise<{ html: string; text: string }> {
|
||||
// await both promises
|
||||
const [html, text] = await Promise.all([
|
||||
this.readHtmlTemplate(templateName),
|
||||
this.readTextTemplate(templateName),
|
||||
]);
|
||||
|
||||
return { html, text };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { join } from 'path';
|
||||
|
@ -13,7 +14,13 @@ async function bootstrap() {
|
|||
app.use(cookieParser());
|
||||
|
||||
// Doing this to make sure it's always the first middleware. Since it does hold auth data.
|
||||
app.use(new ClsMiddleware().use);
|
||||
app.use(
|
||||
new ClsMiddleware({
|
||||
setup: (cls, _context) => {
|
||||
cls.set('authType', 'none');
|
||||
},
|
||||
}).use,
|
||||
);
|
||||
|
||||
// Rendering
|
||||
app.useStaticAssets(join(__dirname, '..', 'public'));
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
USER_NOT_FOUND_ERROR,
|
||||
userCacheKey,
|
||||
} from './user.constant';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
|
@ -19,6 +20,7 @@ export class UserService {
|
|||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
private readonly redisService: RedisService,
|
||||
private readonly clsService: ClsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
@ -30,6 +32,11 @@ export class UserService {
|
|||
*/
|
||||
@Span()
|
||||
async getUserById(id: string, relations: string[] = []): Promise<User> {
|
||||
if (this.clsService.get('authType') === 'session') {
|
||||
if (this.clsService.get('user').id === id) {
|
||||
return this.clsService.get('user');
|
||||
}
|
||||
}
|
||||
const cacheKey = userCacheKey + id;
|
||||
const cachedUser = await this.redisService.get<User>(cacheKey);
|
||||
|
||||
|
|
|
@ -23,4 +23,4 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
0
views/home/privacy-policy.hbs
Normal file
0
views/home/privacy-policy.hbs
Normal file
0
views/home/terms-and-conditions.hbs
Normal file
0
views/home/terms-and-conditions.hbs
Normal file
Loading…
Reference in a new issue