feat: Implement draft version of reset password email (#949)
This commit is contained in:
@@ -45,6 +45,7 @@
|
|||||||
"graphql-shield": "^7.5.0",
|
"graphql-shield": "^7.5.0",
|
||||||
"graphql-tools": "^8.2.0",
|
"graphql-tools": "^8.2.0",
|
||||||
"graphql-type-json": "^0.3.2",
|
"graphql-type-json": "^0.3.2",
|
||||||
|
"handlebars": "^4.7.7",
|
||||||
"http-errors": "~1.6.3",
|
"http-errors": "~1.6.3",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"knex": "^2.4.0",
|
"knex": "^2.4.0",
|
||||||
|
@@ -32,6 +32,12 @@ type AppConfig = {
|
|||||||
bullMQDashboardPassword: string;
|
bullMQDashboardPassword: string;
|
||||||
telemetryEnabled: boolean;
|
telemetryEnabled: boolean;
|
||||||
requestBodySizeLimit: string;
|
requestBodySizeLimit: string;
|
||||||
|
smtpHost: string;
|
||||||
|
smtpPort: number;
|
||||||
|
smtpSecure: boolean;
|
||||||
|
smtpUser: string;
|
||||||
|
smtpPassword: string;
|
||||||
|
fromEmail: string;
|
||||||
licenseKey: string;
|
licenseKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,6 +98,12 @@ const appConfig: AppConfig = {
|
|||||||
webhookUrl,
|
webhookUrl,
|
||||||
telemetryEnabled: process.env.TELEMETRY_ENABLED === 'false' ? false : true,
|
telemetryEnabled: process.env.TELEMETRY_ENABLED === 'false' ? false : true,
|
||||||
requestBodySizeLimit: '1mb',
|
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,
|
licenseKey: process.env.LICENSE_KEY,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
import User from '../../models/user';
|
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 = {
|
type Params = {
|
||||||
input: {
|
input: {
|
||||||
@@ -16,7 +21,24 @@ const forgotPassword = async (_parent: unknown, params: Params) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await user.generateResetPasswordToken();
|
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;
|
return;
|
||||||
};
|
};
|
||||||
|
12
packages/backend/src/helpers/compile-email.ee.ts
Normal file
12
packages/backend/src/helpers/compile-email.ee.ts
Normal file
@@ -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;
|
14
packages/backend/src/helpers/mailer.ee.ts
Normal file
14
packages/backend/src/helpers/mailer.ee.ts
Normal file
@@ -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;
|
25
packages/backend/src/queues/email.ts
Normal file
25
packages/backend/src/queues/email.ts
Normal file
@@ -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;
|
@@ -0,0 +1,16 @@
|
|||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Title</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
Hello {{ email }}
|
||||||
|
|
||||||
|
Someone has requested a link to change your password, and you can do this through the link below.
|
||||||
|
|
||||||
|
<a href="/reset-password">Change my password</a>
|
||||||
|
|
||||||
|
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.
|
||||||
|
</body>
|
||||||
|
</html>
|
@@ -3,6 +3,7 @@ import './helpers/check-worker-readiness';
|
|||||||
import './workers/flow';
|
import './workers/flow';
|
||||||
import './workers/trigger';
|
import './workers/trigger';
|
||||||
import './workers/action';
|
import './workers/action';
|
||||||
|
import './workers/email';
|
||||||
import telemetry from './helpers/telemetry';
|
import telemetry from './helpers/telemetry';
|
||||||
|
|
||||||
telemetry.setServiceType('worker');
|
telemetry.setServiceType('worker');
|
||||||
|
37
packages/backend/src/workers/email.ts
Normal file
37
packages/backend/src/workers/email.ts
Normal file
@@ -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();
|
||||||
|
});
|
Reference in New Issue
Block a user