use nodemailer/smtp instead of sendgrid

This commit is contained in:
Amruth Pillai
2022-08-22 19:26:13 +02:00
parent 02587255fe
commit 5b6f6b7621
11 changed files with 120 additions and 103 deletions

View File

@ -22,10 +22,12 @@ JWT_SECRET=
JWT_EXPIRY_TIME=604800 JWT_EXPIRY_TIME=604800
GOOGLE_CLIENT_SECRET= GOOGLE_CLIENT_SECRET=
GOOGLE_API_KEY= GOOGLE_API_KEY=
SENDGRID_API_KEY= MAIL_FROM_NAME=
SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID= MAIL_FROM_EMAIL=
SENDGRID_FROM_NAME= MAIL_HOST=
SENDGRID_FROM_EMAIL= MAIL_PORT=
MAIL_USERNAME=
MAIL_PASSWORD=
STORAGE_BUCKET= STORAGE_BUCKET=
STORAGE_REGION= STORAGE_REGION=
STORAGE_ENDPOINT= STORAGE_ENDPOINT=

View File

@ -6,10 +6,7 @@ services:
container_name: postgres container_name: postgres
ports: ports:
- 5432:5432 - 5432:5432
environment: env_file: .env.docker
- POSTGRES_DB=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
healthcheck: healthcheck:
@ -38,7 +35,7 @@ services:
context: . context: .
dockerfile: ./server/Dockerfile dockerfile: ./server/Dockerfile
container_name: server container_name: server
env_file: .env env_file: .env.docker
depends_on: depends_on:
- traefik - traefik
- postgres - postgres
@ -57,7 +54,7 @@ services:
context: . context: .
dockerfile: ./client/Dockerfile dockerfile: ./client/Dockerfile
container_name: client container_name: client
env_file: .env env_file: .env.docker
depends_on: depends_on:
- traefik - traefik
- server - server

View File

@ -136,30 +136,16 @@ You can get your own key here: https://developers.google.com/fonts/docs/develope
If you do not have a Google API Key, it was make use of the cached response JSON that's stored within the project source. Please note that this cache is not updated and may not have all the latest fonts that Google Fonts has to offer. If you do not have a Google API Key, it was make use of the cached response JSON that's stored within the project source. Please note that this cache is not updated and may not have all the latest fonts that Google Fonts has to offer.
## SendGrid ## Mail
The server makes use of SendGrid to send the password reset email to those who have forgotten their password. **This section is completely optional for those who do not require this functionality.** The server makes use of SMTP to send the password reset email to those who have forgotten their password. **This section is completely optional for those who do not require this functionality.**
### `SENDGRID_API_KEY` ### `MAIL_FROM_NAME`
**Required**: `no`
**Description:** SendGrid API Key
Does not require any payment or credit card information to obtain an API key.
You can get your own key here: https://docs.sendgrid.com/ui/account-and-settings/api-keys
### `SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID`
**Required**: `no`
**Description:** Dynamic Template ID for Forgot Password
### `SENDGRID_FROM_NAME`
**Required**: `no` **Required**: `no`
**Description:** Sender's Name **Description:** Sender's Name
### `SENDGRID_FROM_EMAIL` ### `MAIL_FROM_EMAIL`
**Required**: `no` **Required**: `no`
**Description:** Sender's Email Address **Description:** Sender's Email Address

55
pnpm-lock.yaml generated
View File

@ -221,13 +221,13 @@ importers:
'@nestjs/terminus': ^9.1.1 '@nestjs/terminus': ^9.1.1
'@nestjs/typeorm': ^9.0.1 '@nestjs/typeorm': ^9.0.1
'@reactive-resume/schema': workspace:* '@reactive-resume/schema': workspace:*
'@sendgrid/mail': ^7.7.0
'@types/bcryptjs': ^2.4.2 '@types/bcryptjs': ^2.4.2
'@types/cookie-parser': ^1.4.3 '@types/cookie-parser': ^1.4.3
'@types/express': ^4.17.13 '@types/express': ^4.17.13
'@types/lodash': ^4.14.184 '@types/lodash': ^4.14.184
'@types/multer': ^1.4.7 '@types/multer': ^1.4.7
'@types/node': ^18.7.9 '@types/node': ^18.7.9
'@types/nodemailer': ^6.4.5
'@types/passport': ^1.0.10 '@types/passport': ^1.0.10
'@types/passport-jwt': ^3.0.6 '@types/passport-jwt': ^3.0.6
'@types/passport-local': ^1.0.34 '@types/passport-local': ^1.0.34
@ -244,6 +244,7 @@ importers:
multer: ^1.4.4 multer: ^1.4.4
nanoid: ^3.3.4 nanoid: ^3.3.4
node-stream-zip: ^1.15.0 node-stream-zip: ^1.15.0
nodemailer: ^6.7.8
passport: ^0.6.0 passport: ^0.6.0
passport-jwt: ^4.0.0 passport-jwt: ^4.0.0
passport-local: ^1.0.0 passport-local: ^1.0.0
@ -276,7 +277,6 @@ importers:
'@nestjs/serve-static': 3.0.0_khr6mt6ojlxbw7bo55fknouh34 '@nestjs/serve-static': 3.0.0_khr6mt6ojlxbw7bo55fknouh34
'@nestjs/terminus': 9.1.1_mr622mjrz7hsw5sxbt7k6brday '@nestjs/terminus': 9.1.1_mr622mjrz7hsw5sxbt7k6brday
'@nestjs/typeorm': 9.0.1_sp4gtzkbiyh24r2ydcb6yqstha '@nestjs/typeorm': 9.0.1_sp4gtzkbiyh24r2ydcb6yqstha
'@sendgrid/mail': 7.7.0
'@types/passport': 1.0.10 '@types/passport': 1.0.10
bcryptjs: 2.4.3 bcryptjs: 2.4.3
cache-manager: 4.1.0 cache-manager: 4.1.0
@ -291,6 +291,7 @@ importers:
multer: 1.4.4 multer: 1.4.4
nanoid: 3.3.4 nanoid: 3.3.4
node-stream-zip: 1.15.0 node-stream-zip: 1.15.0
nodemailer: 6.7.8
passport: 0.6.0 passport: 0.6.0
passport-jwt: 4.0.0 passport-jwt: 4.0.0
passport-local: 1.0.0 passport-local: 1.0.0
@ -312,6 +313,7 @@ importers:
'@types/lodash': 4.14.184 '@types/lodash': 4.14.184
'@types/multer': 1.4.7 '@types/multer': 1.4.7
'@types/node': 18.7.9 '@types/node': 18.7.9
'@types/nodemailer': 6.4.5
'@types/passport-jwt': 3.0.6 '@types/passport-jwt': 3.0.6
'@types/passport-local': 1.0.34 '@types/passport-local': 1.0.34
prettier: 2.7.1 prettier: 2.7.1
@ -4696,33 +4698,6 @@ packages:
resolution: {integrity: sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==} resolution: {integrity: sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==}
dev: true dev: true
/@sendgrid/client/7.7.0:
resolution: {integrity: sha512-SxH+y8jeAQSnDavrTD0uGDXYIIkFylCo+eDofVmZLQ0f862nnqbC3Vd1ej6b7Le7lboyzQF6F7Fodv02rYspuA==}
engines: {node: 6.* || 8.* || >=10.*}
dependencies:
'@sendgrid/helpers': 7.7.0
axios: 0.26.1
transitivePeerDependencies:
- debug
dev: false
/@sendgrid/helpers/7.7.0:
resolution: {integrity: sha512-3AsAxfN3GDBcXoZ/y1mzAAbKzTtUZ5+ZrHOmWQ279AuaFXUNCh9bPnRpN504bgveTqoW+11IzPg3I0WVgDINpw==}
engines: {node: '>= 6.0.0'}
dependencies:
deepmerge: 4.2.2
dev: false
/@sendgrid/mail/7.7.0:
resolution: {integrity: sha512-5+nApPE9wINBvHSUxwOxkkQqM/IAAaBYoP9hw7WwgDNQPxraruVqHizeTitVtKGiqWCKm2mnjh4XGN3fvFLqaw==}
engines: {node: 6.* || 8.* || >=10.*}
dependencies:
'@sendgrid/client': 7.7.0
'@sendgrid/helpers': 7.7.0
transitivePeerDependencies:
- debug
dev: false
/@sideway/address/4.1.4: /@sideway/address/4.1.4:
resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==}
dependencies: dependencies:
@ -5101,10 +5076,17 @@ packages:
/@types/node/18.6.2: /@types/node/18.6.2:
resolution: {integrity: sha512-KcfkBq9H4PI6Vpu5B/KoPeuVDAbmi+2mDBqGPGUgoL7yXQtcWGu2vJWmmRkneWK3Rh0nIAX192Aa87AqKHYChQ==} resolution: {integrity: sha512-KcfkBq9H4PI6Vpu5B/KoPeuVDAbmi+2mDBqGPGUgoL7yXQtcWGu2vJWmmRkneWK3Rh0nIAX192Aa87AqKHYChQ==}
dev: false
/@types/node/18.7.9: /@types/node/18.7.9:
resolution: {integrity: sha512-0N5Y1XAdcl865nDdjbO0m3T6FdmQ4ijE89/urOHLREyTXbpMWbSafx9y7XIsgWGtwUP2iYTinLyyW3FatAxBLQ==} resolution: {integrity: sha512-0N5Y1XAdcl865nDdjbO0m3T6FdmQ4ijE89/urOHLREyTXbpMWbSafx9y7XIsgWGtwUP2iYTinLyyW3FatAxBLQ==}
/@types/nodemailer/6.4.5:
resolution: {integrity: sha512-zuP3nBRQHI6M2PkXnGGy1Ww4VB+MyYHGgnfV2T+JR9KLkeWqPJuyVUgLpKXuFnA/b7pZaIDFh2sV4759B7jK1g==}
dependencies:
'@types/node': 18.7.9
dev: true
/@types/normalize-package-data/2.4.1: /@types/normalize-package-data/2.4.1:
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
dev: true dev: true
@ -5961,14 +5943,6 @@ packages:
- debug - debug
dev: false dev: false
/axios/0.26.1:
resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==}
dependencies:
follow-redirects: 1.14.9
transitivePeerDependencies:
- debug
dev: false
/axios/0.27.2: /axios/0.27.2:
resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
dependencies: dependencies:
@ -9804,7 +9778,7 @@ packages:
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
engines: {node: '>= 10.13.0'} engines: {node: '>= 10.13.0'}
dependencies: dependencies:
'@types/node': 18.6.2 '@types/node': 18.7.9
merge-stream: 2.0.0 merge-stream: 2.0.0
supports-color: 8.1.1 supports-color: 8.1.1
@ -11069,6 +11043,11 @@ packages:
engines: {node: '>=0.12.0'} engines: {node: '>=0.12.0'}
dev: false dev: false
/nodemailer/6.7.8:
resolution: {integrity: sha512-2zaTFGqZixVmTxpJRCFC+Vk5eGRd/fYtvIR+dl5u9QXLTQWGIf48x/JXvo58g9sa0bU6To04XUv554Paykum3g==}
engines: {node: '>=6.0.0'}
dev: false
/normalize-package-data/2.5.0: /normalize-package-data/2.5.0:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
dependencies: dependencies:

View File

@ -20,7 +20,6 @@
"@nestjs/serve-static": "^3.0.0", "@nestjs/serve-static": "^3.0.0",
"@nestjs/terminus": "^9.1.1", "@nestjs/terminus": "^9.1.1",
"@nestjs/typeorm": "^9.0.1", "@nestjs/typeorm": "^9.0.1",
"@sendgrid/mail": "^7.7.0",
"@types/passport": "^1.0.10", "@types/passport": "^1.0.10",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cache-manager": "^4.1.0", "cache-manager": "^4.1.0",
@ -35,6 +34,7 @@
"multer": "^1.4.4", "multer": "^1.4.4",
"nanoid": "^3.3.4", "nanoid": "^3.3.4",
"node-stream-zip": "^1.15.0", "node-stream-zip": "^1.15.0",
"nodemailer": "^6.7.8",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-jwt": "^4.0.0", "passport-jwt": "^4.0.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
@ -57,6 +57,7 @@
"@types/lodash": "^4.14.184", "@types/lodash": "^4.14.184",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"@types/node": "^18.7.9", "@types/node": "^18.7.9",
"@types/nodemailer": "^6.4.5",
"@types/passport-jwt": "^3.0.6", "@types/passport-jwt": "^3.0.6",
"@types/passport-local": "^1.0.34", "@types/passport-local": "^1.0.34",
"prettier": "^2.7.1", "prettier": "^2.7.1",

View File

@ -6,7 +6,7 @@ import appConfig from './app.config';
import authConfig from './auth.config'; import authConfig from './auth.config';
import databaseConfig from './database.config'; import databaseConfig from './database.config';
import googleConfig from './google.config'; import googleConfig from './google.config';
import sendgridConfig from './sendgrid.config'; import mailConfig from './mail.config';
import storageConfig from './storage.config'; import storageConfig from './storage.config';
const validationSchema = Joi.object({ const validationSchema = Joi.object({
@ -36,11 +36,13 @@ const validationSchema = Joi.object({
GOOGLE_CLIENT_SECRET: Joi.string().allow(''), GOOGLE_CLIENT_SECRET: Joi.string().allow(''),
PUBLIC_GOOGLE_CLIENT_ID: Joi.string().allow(''), PUBLIC_GOOGLE_CLIENT_ID: Joi.string().allow(''),
// SendGrid // Mail
SENDGRID_API_KEY: Joi.string().allow(''), MAIL_FROM_NAME: Joi.string().allow(''),
SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID: Joi.string().allow(''), MAIL_FROM_EMAIL: Joi.string().allow(''),
SENDGRID_FROM_NAME: Joi.string().allow(''), MAIL_HOST: Joi.string().allow(''),
SENDGRID_FROM_EMAIL: Joi.string().allow(''), MAIL_PORT: Joi.string().allow(''),
MAIL_USERNAME: Joi.string().allow(''),
MAIL_PASSWORD: Joi.string().allow(''),
// Storage // Storage
STORAGE_BUCKET: Joi.string().allow(''), STORAGE_BUCKET: Joi.string().allow(''),
@ -54,7 +56,7 @@ const validationSchema = Joi.object({
@Module({ @Module({
imports: [ imports: [
NestConfigModule.forRoot({ NestConfigModule.forRoot({
load: [appConfig, authConfig, databaseConfig, googleConfig, sendgridConfig, storageConfig], load: [appConfig, authConfig, databaseConfig, googleConfig, mailConfig, storageConfig],
validationSchema: validationSchema, validationSchema: validationSchema,
}), }),
], ],

View File

@ -0,0 +1,12 @@
import { registerAs } from '@nestjs/config';
export default registerAs('mail', () => ({
from: {
name: process.env.MAIL_FROM_NAME,
email: process.env.MAIL_FROM_EMAIL,
},
host: process.env.MAIL_HOST,
port: process.env.MAIL_PORT,
username: process.env.MAIL_USERNAME,
password: process.env.MAIL_PASSWORD,
}));

View File

@ -1,8 +0,0 @@
import { registerAs } from '@nestjs/config';
export default registerAs('sendgrid', () => ({
apiKey: process.env.SENDGRID_API_KEY,
forgotPasswordTemplateId: process.env.SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID,
fromName: process.env.SENDGRID_FROM_NAME,
fromEmail: process.env.SENDGRID_FROM_EMAIL,
}));

View File

@ -0,0 +1,30 @@
import { Type } from 'class-transformer';
import { IsDefined, IsNotEmpty, IsString } from 'class-validator';
export class MailRecipient {
@IsNotEmpty()
@IsString()
name: string;
@IsNotEmpty()
@IsString()
email: string;
}
export class SendMailDto {
@IsDefined()
@Type(() => MailRecipient)
from: MailRecipient;
@IsDefined()
@Type(() => MailRecipient)
to: MailRecipient;
@IsString()
@IsNotEmpty()
subject: string;
@IsString()
@IsNotEmpty()
message: string;
}

View File

@ -1,40 +1,56 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import SendGrid from '@sendgrid/mail'; import { createTransport, Transporter } from 'nodemailer';
import { User } from '@/users/entities/user.entity'; import { User } from '@/users/entities/user.entity';
import { SendMailDto } from './dto/send-mail.dto';
@Injectable() @Injectable()
export class MailService { export class MailService {
constructor(private configService: ConfigService) { transporter: Transporter;
const sendGridApiKey = this.configService.get<string>('sendgrid.apiKey');
if (sendGridApiKey) { constructor(private configService: ConfigService) {
SendGrid.setApiKey(this.configService.get<string>('sendgrid.apiKey')); this.transporter = createTransport({
} host: this.configService.get<string>('mail.host'),
port: this.configService.get<number>('mail.port'),
pool: true,
secure: false,
tls: { ciphers: 'SSLv3' },
auth: {
user: this.configService.get<string>('mail.username'),
pass: this.configService.get<string>('mail.password'),
},
});
} }
async sendEmail(mail: SendGrid.MailDataRequired) { async sendEmail(sendMailDto: SendMailDto) {
return SendGrid.send(mail); this.transporter.sendMail({
from: `${sendMailDto.from.name} <${sendMailDto.from.email}>`,
to: `${sendMailDto.to.name} <${sendMailDto.to.email}>`,
subject: sendMailDto.subject,
text: sendMailDto.message,
html: sendMailDto.message,
});
} }
async sendForgotPasswordEmail(user: User, resetToken: string): Promise<void> { async sendForgotPasswordEmail(user: User, resetToken: string): Promise<void> {
const appUrl = this.configService.get<string>('app.url'); const appUrl = this.configService.get<string>('app.url');
const url = `${appUrl}?modal=auth.reset&resetToken=${resetToken}`; const url = `${appUrl}?modal=auth.reset&resetToken=${resetToken}`;
const mailData: SendGrid.MailDataRequired = { const sendMailDto: SendMailDto = {
from: { from: {
name: this.configService.get<string>('sendgrid.fromName'), name: this.configService.get<string>('mail.from.name'),
email: this.configService.get<string>('sendgrid.fromEmail'), email: this.configService.get<string>('mail.from.email'),
}, },
to: user.email, to: {
hideWarnings: true, name: user.name,
dynamicTemplateData: { url }, email: user.email,
templateId: this.configService.get<string>('sendgrid.forgotPasswordTemplateId'), },
subject: 'Reset your Reactive Resume password',
message: `<p>Hey ${user.name}!</p> <p>You can reset your password by visiting this link: <a href="${url}">${url}</a>.</p> <p>But hurry, because it will expire in 30 minutes.</p>`,
}; };
await SendGrid.send(mailData); await this.sendEmail(sendMailDto);
return;
} }
} }

View File

@ -2,7 +2,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule'; import { SchedulerRegistry } from '@nestjs/schedule';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import { Connection, Repository } from 'typeorm'; import { DataSource, Repository } from 'typeorm';
import { MailService } from '@/mail/mail.service'; import { MailService } from '@/mail/mail.service';
@ -19,7 +19,7 @@ export class UsersService {
@InjectRepository(User) private userRepository: Repository<User>, @InjectRepository(User) private userRepository: Repository<User>,
private schedulerRegistry: SchedulerRegistry, private schedulerRegistry: SchedulerRegistry,
private mailService: MailService, private mailService: MailService,
private connection: Connection private dataSource: DataSource
) {} ) {}
async findById(id: number): Promise<User> { async findById(id: number): Promise<User> {
@ -93,7 +93,7 @@ export class UsersService {
const user = await this.findByEmail(email); const user = await this.findByEmail(email);
const resetToken = randomBytes(32).toString('hex'); const resetToken = randomBytes(32).toString('hex');
const queryRunner = this.connection.createQueryRunner(); const queryRunner = this.dataSource.createQueryRunner();
const timeout = setTimeout(async () => { const timeout = setTimeout(async () => {
await this.userRepository.update(user.id, { resetToken: null }); await this.userRepository.update(user.id, { resetToken: null });