From a0815b06a68d89a485d0889c50edcc6d016efd8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Ayd=C4=B1n?= Date: Fri, 24 Feb 2023 00:25:10 +0100 Subject: [PATCH] feat: Implement draft version of reset password email (#949) --- packages/backend/package.json | 1 + packages/backend/src/config/app.ts | 12 ++++++ .../graphql/mutations/forgot-password.ee.ts | 24 +++++++++++- .../backend/src/helpers/compile-email.ee.ts | 12 ++++++ packages/backend/src/helpers/mailer.ee.ts | 14 +++++++ packages/backend/src/queues/email.ts | 25 +++++++++++++ .../emails/reset-password-instructions.ee.hbs | 16 ++++++++ packages/backend/src/worker.ts | 1 + packages/backend/src/workers/email.ts | 37 +++++++++++++++++++ 9 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/helpers/compile-email.ee.ts create mode 100644 packages/backend/src/helpers/mailer.ee.ts create mode 100644 packages/backend/src/queues/email.ts create mode 100644 packages/backend/src/views/emails/reset-password-instructions.ee.hbs create mode 100644 packages/backend/src/workers/email.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index b6cca056..889199e5 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -45,6 +45,7 @@ "graphql-shield": "^7.5.0", "graphql-tools": "^8.2.0", "graphql-type-json": "^0.3.2", + "handlebars": "^4.7.7", "http-errors": "~1.6.3", "jsonwebtoken": "^9.0.0", "knex": "^2.4.0", diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 29893e18..9571af84 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -32,6 +32,12 @@ type AppConfig = { bullMQDashboardPassword: string; telemetryEnabled: boolean; requestBodySizeLimit: string; + smtpHost: string; + smtpPort: number; + smtpSecure: boolean; + smtpUser: string; + smtpPassword: string; + fromEmail: string; licenseKey: string; }; @@ -92,6 +98,12 @@ const appConfig: AppConfig = { webhookUrl, telemetryEnabled: process.env.TELEMETRY_ENABLED === 'false' ? false : true, requestBodySizeLimit: '1mb', + smtpHost: process.env.SMTP_HOST, + smtpPort: parseInt(process.env.SMTP_PORT || '587'), + smtpSecure: process.env.SMTP_SECURE === 'true', + smtpUser: process.env.SMTP_USER, + smtpPassword: process.env.SMTP_PASSWORD, + fromEmail: process.env.FROM_EMAIL, licenseKey: process.env.LICENSE_KEY, }; diff --git a/packages/backend/src/graphql/mutations/forgot-password.ee.ts b/packages/backend/src/graphql/mutations/forgot-password.ee.ts index a2e2ff86..0d0ef97f 100644 --- a/packages/backend/src/graphql/mutations/forgot-password.ee.ts +++ b/packages/backend/src/graphql/mutations/forgot-password.ee.ts @@ -1,4 +1,9 @@ import User from '../../models/user'; +import emailQueue from '../../queues/email'; +import { + REMOVE_AFTER_30_DAYS_OR_150_JOBS, + REMOVE_AFTER_7_DAYS_OR_50_JOBS, +} from '../../helpers/remove-job-configuration'; type Params = { input: { @@ -16,7 +21,24 @@ const forgotPassword = async (_parent: unknown, params: Params) => { } await user.generateResetPasswordToken(); - // TODO: Send email with reset password link + + const jobName = `Reset Password Email - ${user.id}`; + + const jobPayload = { + email: user.email, + subject: 'Reset Password', + template: 'reset-password-instructions', + params: { + token: user.resetPasswordToken, + }, + }; + + const jobOptions = { + removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS, + removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS, + }; + + await emailQueue.add(jobName, jobPayload, jobOptions); return; }; diff --git a/packages/backend/src/helpers/compile-email.ee.ts b/packages/backend/src/helpers/compile-email.ee.ts new file mode 100644 index 00000000..297b48e8 --- /dev/null +++ b/packages/backend/src/helpers/compile-email.ee.ts @@ -0,0 +1,12 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import * as handlebars from 'handlebars'; + +const compileEmail = (emailPath: string, replacements: object = {}): string => { + const filePath = path.join(__dirname, `../views/emails/${emailPath}.ee.hbs`); + const source = fs.readFileSync(filePath, 'utf-8').toString(); + const template = handlebars.compile(source); + return template(replacements); +}; + +export default compileEmail; diff --git a/packages/backend/src/helpers/mailer.ee.ts b/packages/backend/src/helpers/mailer.ee.ts new file mode 100644 index 00000000..ff6fff51 --- /dev/null +++ b/packages/backend/src/helpers/mailer.ee.ts @@ -0,0 +1,14 @@ +import nodemailer from 'nodemailer'; +import appConfig from '../config/app'; + +const mailer = nodemailer.createTransport({ + host: appConfig.smtpHost, + port: appConfig.smtpPort, + secure: appConfig.smtpSecure, + auth: { + user: appConfig.smtpUser, + pass: appConfig.smtpPassword, + }, +}); + +export default mailer; diff --git a/packages/backend/src/queues/email.ts b/packages/backend/src/queues/email.ts new file mode 100644 index 00000000..e8ae8b45 --- /dev/null +++ b/packages/backend/src/queues/email.ts @@ -0,0 +1,25 @@ +import process from 'process'; +import { Queue } from 'bullmq'; +import redisConfig from '../config/redis'; +import logger from '../helpers/logger'; + +const CONNECTION_REFUSED = 'ECONNREFUSED'; + +const redisConnection = { + connection: redisConfig, +}; + +const emailQueue = new Queue('email', redisConnection); + +process.on('SIGTERM', async () => { + await emailQueue.close(); +}); + +emailQueue.on('error', (err) => { + if ((err as any).code === CONNECTION_REFUSED) { + logger.error('Make sure you have installed Redis and it is running.', err); + process.exit(); + } +}); + +export default emailQueue; diff --git a/packages/backend/src/views/emails/reset-password-instructions.ee.hbs b/packages/backend/src/views/emails/reset-password-instructions.ee.hbs new file mode 100644 index 00000000..1fb678f4 --- /dev/null +++ b/packages/backend/src/views/emails/reset-password-instructions.ee.hbs @@ -0,0 +1,16 @@ + + + + Title + + + Hello {{ email }} + + Someone has requested a link to change your password, and you can do this through the link below. + + Change my password + + If you didn't request this, please ignore this email. + Your password won't change until you access the link above and create a new one. + + diff --git a/packages/backend/src/worker.ts b/packages/backend/src/worker.ts index 6a6358ee..8a5cb447 100644 --- a/packages/backend/src/worker.ts +++ b/packages/backend/src/worker.ts @@ -3,6 +3,7 @@ import './helpers/check-worker-readiness'; import './workers/flow'; import './workers/trigger'; import './workers/action'; +import './workers/email'; import telemetry from './helpers/telemetry'; telemetry.setServiceType('worker'); diff --git a/packages/backend/src/workers/email.ts b/packages/backend/src/workers/email.ts new file mode 100644 index 00000000..d8ff037c --- /dev/null +++ b/packages/backend/src/workers/email.ts @@ -0,0 +1,37 @@ +import { Worker } from 'bullmq'; +import redisConfig from '../config/redis'; +import logger from '../helpers/logger'; +import mailer from '../helpers/mailer.ee'; +import compileEmail from '../helpers/compile-email.ee'; +import appConfig from '../config/app'; + +export const worker = new Worker( + 'email', + async (job) => { + const { email, subject, templateName, params } = job.data; + + await mailer.sendMail({ + to: email, + from: appConfig.fromEmail, + subject: subject, + html: compileEmail(templateName, params), + }); + }, + { connection: redisConfig } +); + +worker.on('completed', (job) => { + logger.info( + `JOB ID: ${job.id} - ${job.data.subject} email sent to ${job.data.email}!` + ); +}); + +worker.on('failed', (job, err) => { + logger.info( + `JOB ID: ${job.id} - ${job.data.subject} email to ${job.data.email} has failed to send with ${err.message}` + ); +}); + +process.on('SIGTERM', async () => { + await worker.close(); +});