From e308e2c73676b8a0d9a1db663afeebf88915ed74 Mon Sep 17 00:00:00 2001 From: Niall Shaw <100220424+niall-shaw@users.noreply.github.com> Date: Thu, 18 Jan 2024 20:30:08 +0800 Subject: [PATCH] refactor: email service (#207) --- packages/backend/src/context.ts | 6 +- packages/backend/src/email/client-stub.ts | 16 +---- packages/backend/src/email/client.ts | 26 +------ packages/backend/src/email/helpers.ts | 4 ++ packages/backend/src/email/service.ts | 70 +++++++++++++++++++ packages/backend/src/server.test.ts | 4 +- packages/backend/src/submission/controller.ts | 64 +++++++---------- .../backend/src/submission/email-utils.ts | 0 8 files changed, 106 insertions(+), 84 deletions(-) create mode 100644 packages/backend/src/email/service.ts create mode 100644 packages/backend/src/submission/email-utils.ts diff --git a/packages/backend/src/context.ts b/packages/backend/src/context.ts index 32a37255..d5cb36ba 100644 --- a/packages/backend/src/context.ts +++ b/packages/backend/src/context.ts @@ -5,6 +5,7 @@ import { createInvitationRepository, createSubmissionRepository, } from './submission/mongoRepository' +import { createEmailService } from './email/service' import { createSubmissionService } from './submission/service' import { createSchemaRepository } from './schema/mongoRepository' import { createSchemaService } from './schema/service' @@ -22,6 +23,7 @@ interface IO { export async function createAppContext(io: IO) { const { database, didResolver, emailClient } = io + const emailService = await createEmailService(emailClient) const invitationRepository = await createInvitationRepository(database) const submissionRepository = await createSubmissionRepository(database) const schemaRepository = await createSchemaRepository(database) @@ -42,14 +44,14 @@ export async function createAppContext(io: IO) { ) const authController = await createAuthController() - const submissionController = await createSubmissionController( submissionService, validationService, - emailClient, + emailService, ) return { + emailService, invitationRepository, submissionRepository, submissionService, diff --git a/packages/backend/src/email/client-stub.ts b/packages/backend/src/email/client-stub.ts index 2beed8ff..3ba4920a 100644 --- a/packages/backend/src/email/client-stub.ts +++ b/packages/backend/src/email/client-stub.ts @@ -1,5 +1,4 @@ -import { compileEmailTemplate, getSubmitUrls } from './helpers' -import { Invitation } from '@ssi-trust-registry/common' +import { compileEmailTemplate } from './helpers' type SentMessage = { to: string @@ -14,7 +13,6 @@ export interface EmailClientStub { templatePath: string, templateParams: Record, ) => Promise - sendInvitationEmail: (invitation: Invitation) => Promise sentMessages: SentMessage[] } @@ -30,18 +28,6 @@ export function createEmailClientStub(): EmailClientStub { const html = await compileEmailTemplate(templatePath, templateParams) sentMessages.push({ to, subject, html }) }, - async sendInvitationEmail(invitation: Invitation) { - const { submitApiUrl, submitUiUrl } = getSubmitUrls(invitation) - await this.sendMailFromTemplate( - invitation.emailAddress, - 'Invitation', - './src/email/templates/invitation.html', - { - submitApiUrl, - submitUiUrl, - }, - ) - }, sentMessages, } } diff --git a/packages/backend/src/email/client.ts b/packages/backend/src/email/client.ts index 44765119..0759ec94 100644 --- a/packages/backend/src/email/client.ts +++ b/packages/backend/src/email/client.ts @@ -1,10 +1,6 @@ import partial from 'lodash.partial' import nodemailer, { SentMessageInfo, Transporter } from 'nodemailer' -import { compileEmailTemplate, getSubmitUrls } from './helpers' -import { Invitation } from '@ssi-trust-registry/common' -import { createLogger } from '../logger' - -const logger = createLogger(__filename) +import { compileEmailTemplate } from './helpers' export interface EmailClient { sendMailFromTemplate: ( @@ -13,7 +9,6 @@ export interface EmailClient { templatePath: string, templateParams: Record, ) => Promise - sendInvitationEmail: (invitation: Invitation) => Promise } interface SmtpConfig { @@ -29,7 +24,6 @@ export function createEmailClient(config: SmtpConfig): EmailClient { const transporter = nodemailer.createTransport(config) return { sendMailFromTemplate: partial(sendMailFromTemplate, transporter), - sendInvitationEmail: partial(sendInvitationEmail, transporter), } } @@ -47,21 +41,3 @@ async function sendMailFromTemplate( html, }) } - -async function sendInvitationEmail( - transporter: Transporter, - invitation: Invitation, -) { - const { submitApiUrl, submitUiUrl } = getSubmitUrls(invitation) - logger.info(`Sending invitation via email to: `, invitation.emailAddress) - await sendMailFromTemplate( - transporter, - invitation.emailAddress, - 'Invitation', - './src/email/templates/invitation.html', - { - submitApiUrl, - submitUiUrl, - }, - ) -} diff --git a/packages/backend/src/email/helpers.ts b/packages/backend/src/email/helpers.ts index 6e7d4ec6..04dcdc2d 100644 --- a/packages/backend/src/email/helpers.ts +++ b/packages/backend/src/email/helpers.ts @@ -20,3 +20,7 @@ export function getSubmitUrls(invitation: Invitation) { submitUiUrl, } } + +export function getEntityUrl(entityId: string) { + return `${config.server.frontendUrl}/entities/${entityId}` +} diff --git a/packages/backend/src/email/service.ts b/packages/backend/src/email/service.ts new file mode 100644 index 00000000..a1780e36 --- /dev/null +++ b/packages/backend/src/email/service.ts @@ -0,0 +1,70 @@ +import { Invitation } from '@ssi-trust-registry/common' +import partial from 'lodash.partial' +import { EmailClient } from './client' +import { getEntityUrl, getSubmitUrls } from './helpers' +import { createLogger } from '../logger' + +const logger = createLogger(__filename) + +export interface EmailService { + sendInvitationEmail: (invitation: Invitation) => Promise + sendApprovalEmail: (invitation: Invitation, entityId: string) => Promise + sendRejectionEmail: (invitation: Invitation) => Promise +} + +export async function createEmailService( + emailClient: EmailClient, +): Promise { + return { + sendInvitationEmail: partial(sendInvitationEmail, emailClient), + sendApprovalEmail: partial(sendApprovalEmail, emailClient), + sendRejectionEmail: partial(sendRejectionEmail, emailClient), + } +} + +async function sendInvitationEmail( + emailClient: EmailClient, + invitation: Invitation, +) { + const { submitApiUrl, submitUiUrl } = getSubmitUrls(invitation) + logger.info(`Sending submission approved email to: `, invitation.emailAddress) + await emailClient.sendMailFromTemplate( + invitation.emailAddress, + 'SSI Trust Registry - Invitation', + './src/email/templates/invitation.html', + { + submitApiUrl, + submitUiUrl, + }, + ) +} + +async function sendApprovalEmail( + emailClient: EmailClient, + invitation: Invitation, + entityId: string, +) { + const entityUrl = getEntityUrl(entityId) + logger.info(`Sending submission approved email to: `, invitation.emailAddress) + await emailClient.sendMailFromTemplate( + invitation.emailAddress, + 'SSI Trust Registry - Submission Approved', + './src/email/templates/approved.html', + { + entityUrl, + }, + ) +} + +async function sendRejectionEmail( + emailClient: EmailClient, + invitation: Invitation, +) { + logger.info(`Sending submission rejected email to: `, invitation.emailAddress) + await emailClient.sendMailFromTemplate( + invitation.emailAddress, + 'SSI Trust Registry - Submission Rejected', + './src/email/templates/rejected.html', + {}, + ) +} diff --git a/packages/backend/src/server.test.ts b/packages/backend/src/server.test.ts index f2bb1570..b825e951 100644 --- a/packages/backend/src/server.test.ts +++ b/packages/backend/src/server.test.ts @@ -309,7 +309,7 @@ describe('api', () => { expect(emailClient.sentMessages).toEqual( expect.arrayContaining([ expect.objectContaining({ - subject: 'Congratulations! Your submission has been approved!', + subject: 'SSI Trust Registry - Submission Approved', }), ]), ) @@ -332,7 +332,7 @@ describe('api', () => { expect(emailClient.sentMessages).toEqual( expect.arrayContaining([ expect.objectContaining({ - subject: 'Sorry. Your submission has been rejected.', + subject: 'SSI Trust Registry - Submission Rejected', }), ]), ) diff --git a/packages/backend/src/submission/controller.ts b/packages/backend/src/submission/controller.ts index 224be4b0..f772e037 100644 --- a/packages/backend/src/submission/controller.ts +++ b/packages/backend/src/submission/controller.ts @@ -9,9 +9,8 @@ import { RequestWithToken } from '../auth/middleware' import partial from 'lodash.partial' import { ValidationService } from '../entity/validationService' import { SubmissionService } from './service' -import { EmailClient } from '../email/client' -import { config } from '../config' import { getSubmitUrls } from '../email/helpers' +import { EmailService } from '../email/service' const logger = createLogger(__filename) @@ -33,10 +32,14 @@ export interface SubmissionController { export async function createSubmissionController( submissionService: SubmissionService, validationService: ValidationService, - emailClient: EmailClient, + emailService: EmailService, ): Promise { return { - createInvitation: partial(createInvitation, submissionService, emailClient), + createInvitation: partial( + createInvitation, + submissionService, + emailService, + ), getAllInvitations: partial(getAllInvitations, submissionService), getInvitationById: partial(getInvitationById, submissionService), createSubmission: partial( @@ -54,15 +57,19 @@ export async function createSubmissionController( updateSubmissionState, submissionService, validationService, - emailClient, + emailService, + ), + resendInvitation: partial( + resendInvitation, + submissionService, + emailService, ), - resendInvitation: partial(resendInvitation, submissionService, emailClient), } } async function createInvitation( service: SubmissionService, - emailClient: EmailClient, + emailService: EmailService, req: Request, res: Response, ) { @@ -70,19 +77,19 @@ async function createInvitation( logger.info(`Creating new invitation for: `, invitationDto.emailAddress) const invitation = await service.createInvitation(invitationDto) const { submitUiUrl } = getSubmitUrls(invitation) - await emailClient.sendInvitationEmail(invitation) + await emailService.sendInvitationEmail(invitation) res.status(201).json({ ...invitation, url: submitUiUrl }) } async function resendInvitation( service: SubmissionService, - emailClient: EmailClient, + emailService: EmailService, req: Request, res: Response, ) { const invitation = await service.getInvitationById(req.params.id) const { submitUiUrl } = getSubmitUrls(invitation) - await emailClient.sendInvitationEmail(invitation) + await emailService.sendInvitationEmail(invitation) res.status(200).json({ ...invitation, url: submitUiUrl }) } @@ -161,7 +168,7 @@ async function getSubmissionsByInvitationId( async function updateSubmissionState( submissionService: SubmissionService, validationService: ValidationService, - emailClient: EmailClient, + emailService: EmailService, req: RequestWithToken, res: Response, ) { @@ -179,36 +186,13 @@ async function updateSubmissionState( await validationService.validateDids(submission.dids) await validationService.validateSchemas(submission.credentials) + let result if (state === 'approved') { - const result = await submissionService.approveSubmission(submission) - const entityUrl = `${config.server.frontendUrl}/entities/${result.entity.id}` - logger.info( - `Sending submission approved email to: `, - invitation.emailAddress, - ) - await emailClient.sendMailFromTemplate( - invitation.emailAddress, - 'Congratulations! Your submission has been approved!', - './src/email/templates/approved.html', - { - entityUrl, - }, - ) - res.status(200).json(result) + result = await submissionService.approveSubmission(submission) + await emailService.sendApprovalEmail(invitation, result.entity.id) } else { - const result = await submissionService.rejectSubmission(submission) - logger.info( - `Sending submission rejected email to: `, - invitation.emailAddress, - ) - await emailClient.sendMailFromTemplate( - invitation.emailAddress, - 'Sorry. Your submission has been rejected.', - './src/email/templates/rejected.html', - { - invitationUrl: `${config.server.frontendUrl}/submit/${invitation.id}`, - }, - ) - res.status(200).json(result) + result = await submissionService.rejectSubmission(submission) + await emailService.sendRejectionEmail(invitation) } + res.status(200).json(result) } diff --git a/packages/backend/src/submission/email-utils.ts b/packages/backend/src/submission/email-utils.ts new file mode 100644 index 00000000..e69de29b