From 6d8e887817cdbbb64c8e6109eded7fdf10ef8881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20L=C3=B3pez?= Date: Mon, 16 Oct 2023 16:58:03 +0200 Subject: [PATCH] Add Sendgrid email templates --- .../server/src/common/constants/index.ts | 18 ++++- .../src/modules/auth/auth.service.spec.ts | 43 +++++++---- .../server/src/modules/auth/auth.service.ts | 71 ++++++++++------- .../src/modules/sendgrid/sendgrid.service.ts | 14 ++-- .../server/src/common/constants/index.ts | 7 ++ .../src/modules/auth/auth.service.spec.ts | 37 ++++++--- .../server/src/modules/auth/auth.service.ts | 77 +++++++++++++------ .../src/modules/sendgrid/sendgrid.service.ts | 6 +- 8 files changed, 188 insertions(+), 85 deletions(-) diff --git a/packages/apps/job-launcher/server/src/common/constants/index.ts b/packages/apps/job-launcher/server/src/common/constants/index.ts index 974868484d..e448d501fa 100644 --- a/packages/apps/job-launcher/server/src/common/constants/index.ts +++ b/packages/apps/job-launcher/server/src/common/constants/index.ts @@ -1,6 +1,7 @@ import { ChainId } from '@human-protocol/sdk'; import { JobRequestType, JobStatus } from '../enums/job'; +export const SERVICE_NAME = 'Job Launcher'; export const NS = 'hmt'; export const COINGECKO_API_URL = 'https://api.coingecko.com/api/v3/simple/price'; @@ -24,6 +25,19 @@ export const SENDGRID_API_KEY_REGEX = export const HEADER_SIGNATURE_KEY = 'human-signature'; -export const CVAT_JOB_TYPES = [JobRequestType.IMAGE_BOXES, JobRequestType.IMAGE_POINTS] +export const CVAT_JOB_TYPES = [ + JobRequestType.IMAGE_BOXES, + JobRequestType.IMAGE_POINTS, +]; + +export const CANCEL_JOB_STATUSES = [ + JobStatus.PENDING, + JobStatus.PAID, + JobStatus.LAUNCHED, +]; -export const CANCEL_JOB_STATUSES = [JobStatus.PENDING, JobStatus.PAID, JobStatus.LAUNCHED] \ No newline at end of file +export const SENDGRID_TEMPLATES = { + signup: 'd-ca99cc7410aa4e6dab3e6042d5ecb9a3', + resetPassword: 'd-3ac74546352a4e1abdd1689947632c22', + passwordChanged: 'd-ca0ac7e6fff845829cd0167af09f25cf', +}; diff --git a/packages/apps/job-launcher/server/src/modules/auth/auth.service.spec.ts b/packages/apps/job-launcher/server/src/modules/auth/auth.service.spec.ts index fd7688c9c2..bce81af8c5 100644 --- a/packages/apps/job-launcher/server/src/modules/auth/auth.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/auth/auth.service.spec.ts @@ -29,6 +29,7 @@ import { PaymentService } from '../payment/payment.service'; import { UserStatus } from '../../common/enums/user'; import { SendGridService } from '../sendgrid/sendgrid.service'; import { NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { SENDGRID_TEMPLATES, SERVICE_NAME } from '../../common/constants'; jest.mock('@human-protocol/sdk'); @@ -394,7 +395,7 @@ describe('AuthService', () => { } as UserEntity; userService.getByEmail = jest.fn().mockResolvedValue(userEntity); - + const existingToken = { id: 2, userId: userEntity.id, @@ -402,9 +403,9 @@ describe('AuthService', () => { remove: jest.fn(), }; tokenRepository.findOne = jest.fn().mockResolvedValue(existingToken); - + await authService.forgotPassword({ email: 'user@example.com' }); - + expect(existingToken.remove).toHaveBeenCalled(); }); @@ -412,13 +413,25 @@ describe('AuthService', () => { userService.getByEmail = jest.fn().mockResolvedValueOnce(userEntity); sendGridService.sendEmail = jest.fn(); + const email = 'user@example.com'; - await authService.forgotPassword({ email: 'user@example.com' }); + await authService.forgotPassword({ email }); expect(createTokenMock).toHaveBeenCalled(); expect(sendGridService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - text: expect.stringContaining(tokenEntity.uuid), + personalizations: [ + { + dynamicTemplateData: { + service_name: SERVICE_NAME, + url: expect.stringContaining( + 'undefined/reset-password?token=mocked-uuid', + ), + }, + to: email, + }, + ], + templateId: SENDGRID_TEMPLATES.resetPassword, }), ); }); @@ -530,12 +543,6 @@ describe('AuthService', () => { status: UserStatus.PENDING, }; - const tokenEntity = { - uuid: v4(), - tokenType: TokenType.EMAIL, - user: userEntity, - }; - let createTokenMock: any; beforeEach(() => { @@ -558,13 +565,23 @@ describe('AuthService', () => { userService.getByEmail = jest.fn().mockResolvedValueOnce(userEntity); sendGridService.sendEmail = jest.fn(); + const email = 'user@example.com'; - await authService.resendEmailVerification({ email: 'user@example.com' }); + await authService.resendEmailVerification({ email }); expect(createTokenMock).toHaveBeenCalled(); expect(sendGridService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - text: expect.stringContaining(tokenEntity.uuid), + personalizations: [ + { + dynamicTemplateData: { + service_name: SERVICE_NAME, + url: expect.stringContaining('/verify?token=mocked-uuid'), + }, + to: email, + }, + ], + templateId: SENDGRID_TEMPLATES.signup, }), ); }); diff --git a/packages/apps/job-launcher/server/src/modules/auth/auth.service.ts b/packages/apps/job-launcher/server/src/modules/auth/auth.service.ts index cef638f7bf..5f4971de21 100644 --- a/packages/apps/job-launcher/server/src/modules/auth/auth.service.ts +++ b/packages/apps/job-launcher/server/src/modules/auth/auth.service.ts @@ -1,5 +1,4 @@ import { - ConflictException, Injectable, Logger, NotFoundException, @@ -27,6 +26,7 @@ import { ConfigNames } from '../../common/config'; import { ConfigService } from '@nestjs/config'; import { createHash } from 'crypto'; import { SendGridService } from '../sendgrid/sendgrid.service'; +import { SENDGRID_TEMPLATES, SERVICE_NAME } from '../../common/constants'; @Injectable() export class AuthService { @@ -84,12 +84,16 @@ export class AuthService { }); await this.sendgridService.sendEmail({ - to: data.email, - subject: 'Verify your email', - html: `Welcome to the Job Launcher Service.
-Click here to complete sign up.`, - text: `Welcome to the Job Launcher Service. -Click ${this.feURL}/verify?token=${tokenEntity.uuid} to complete sign up.`, + personalizations: [ + { + to: data.email, + dynamicTemplateData: { + service_name: SERVICE_NAME, + url: `${this.feURL}/verify?token=${tokenEntity.uuid}`, + }, + }, + ], + templateId: SENDGRID_TEMPLATES.signup, }); return userEntity; @@ -147,21 +151,27 @@ Click ${this.feURL}/verify?token=${tokenEntity.uuid} to complete sign up.`, userId: userEntity.id, tokenType: TokenType.PASSWORD, }); - + if (existingToken) { await existingToken.remove(); } - + const newTokenEntity = await this.tokenRepository.create({ tokenType: TokenType.PASSWORD, user: userEntity, }); - this.sendgridService.sendEmail({ - to: data.email, - subject: 'Reset password', - html: `Click here to reset the password.`, - text: `Click ${this.feURL}/reset-password?token=${newTokenEntity.uuid} to reset the password.`, + await this.sendgridService.sendEmail({ + personalizations: [ + { + to: data.email, + dynamicTemplateData: { + service_name: SERVICE_NAME, + url: `${this.feURL}/reset-password?token=${newTokenEntity.uuid}`, + }, + }, + ], + templateId: SENDGRID_TEMPLATES.resetPassword, }); } @@ -176,11 +186,16 @@ Click ${this.feURL}/verify?token=${tokenEntity.uuid} to complete sign up.`, } await this.userService.updatePassword(tokenEntity.user, data); - - this.sendgridService.sendEmail({ - to: tokenEntity.user.email, - subject: 'Password changed', - text: 'Password has been changed successfully!', + await this.sendgridService.sendEmail({ + personalizations: [ + { + to: tokenEntity.user.email, + dynamicTemplateData: { + service_name: SERVICE_NAME, + }, + }, + ], + templateId: SENDGRID_TEMPLATES.passwordChanged, }); await tokenEntity.remove(); @@ -216,13 +231,17 @@ Click ${this.feURL}/verify?token=${tokenEntity.uuid} to complete sign up.`, user: userEntity, }); - this.sendgridService.sendEmail({ - to: data.email, - subject: 'Verify your email', - html: `Welcome to the Job Launcher Service.
-Click here to complete sign up.`, - text: `Welcome to the Job Launcher Service. -Click ${this.feURL}/verify?token=${tokenEntity.uuid} to complete sign up.`, + await this.sendgridService.sendEmail({ + personalizations: [ + { + to: data.email, + dynamicTemplateData: { + service_name: SERVICE_NAME, + url: `${this.feURL}/verify?token=${tokenEntity.uuid}`, + }, + }, + ], + templateId: SENDGRID_TEMPLATES.signup, }); } diff --git a/packages/apps/job-launcher/server/src/modules/sendgrid/sendgrid.service.ts b/packages/apps/job-launcher/server/src/modules/sendgrid/sendgrid.service.ts index 41c9cf0b81..c36afe91aa 100644 --- a/packages/apps/job-launcher/server/src/modules/sendgrid/sendgrid.service.ts +++ b/packages/apps/job-launcher/server/src/modules/sendgrid/sendgrid.service.ts @@ -1,9 +1,4 @@ -import { - BadRequestException, - Injectable, - Logger, - UnauthorizedException, -} from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { MailDataRequired, MailService } from '@sendgrid/mail'; import { ConfigNames } from '../../common/config'; @@ -40,19 +35,22 @@ export class SendGridService { } async sendEmail({ - text = '', from = { email: this.defaultFromEmail, name: this.defaultFromName, }, + templateId = '', + personalizations, ...emailData }: Partial): Promise { try { await this.mailService.send({ from, - text, + templateId, + personalizations, ...emailData, }); + this.logger.log('Email sent successfully'); return; } catch (error) { diff --git a/packages/apps/reputation-oracle/server/src/common/constants/index.ts b/packages/apps/reputation-oracle/server/src/common/constants/index.ts index dc14fc7c60..68dacdedce 100644 --- a/packages/apps/reputation-oracle/server/src/common/constants/index.ts +++ b/packages/apps/reputation-oracle/server/src/common/constants/index.ts @@ -1,5 +1,6 @@ import { JobRequestType } from '../enums'; +export const SERVICE_NAME = 'Reputation Oracle'; export const NS = 'hmt'; export const RETRIES_COUNT_THRESHOLD = 3; export const INITIAL_REPUTATION = 0; @@ -7,6 +8,12 @@ export const JWT_PREFIX = 'bearer '; export const SENDGRID_API_KEY_REGEX = /^SG\.[A-Za-z0-9-_]{22}\.[A-Za-z0-9-_]{43}$/; +export const SENDGRID_TEMPLATES = { + signup: 'd-ca99cc7410aa4e6dab3e6042d5ecb9a3', + resetPassword: 'd-3ac74546352a4e1abdd1689947632c22', + passwordChanged: 'd-ca0ac7e6fff845829cd0167af09f25cf', +}; + export const CVAT_RESULTS_ANNOTATIONS_FILENAME = 'resulting_annotations.zip'; export const CVAT_VALIDATION_META_FILENAME = 'validation_meta.json'; diff --git a/packages/apps/reputation-oracle/server/src/modules/auth/auth.service.spec.ts b/packages/apps/reputation-oracle/server/src/modules/auth/auth.service.spec.ts index 01111e26d3..9f3fe8a3e6 100644 --- a/packages/apps/reputation-oracle/server/src/modules/auth/auth.service.spec.ts +++ b/packages/apps/reputation-oracle/server/src/modules/auth/auth.service.spec.ts @@ -28,6 +28,7 @@ import { v4 } from 'uuid'; import { UserStatus, UserType } from '../../common/enums/user'; import { SendGridService } from '../sendgrid/sendgrid.service'; import { NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { SENDGRID_TEMPLATES, SERVICE_NAME } from '../../common/constants'; jest.mock('@human-protocol/sdk'); @@ -390,13 +391,25 @@ describe('AuthService', () => { userService.getByEmail = jest.fn().mockResolvedValueOnce(userEntity); sendGridService.sendEmail = jest.fn(); + const email = 'user@example.com'; - await authService.forgotPassword({ email: 'user@example.com' }); + await authService.forgotPassword({ email }); expect(createTokenMock).toHaveBeenCalled(); expect(sendGridService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - text: expect.stringContaining(tokenEntity.uuid), + personalizations: [ + { + dynamicTemplateData: { + service_name: SERVICE_NAME, + url: expect.stringContaining( + 'undefined/reset-password?token=mocked-uuid', + ), + }, + to: email, + }, + ], + templateId: SENDGRID_TEMPLATES.resetPassword, }), ); }); @@ -508,12 +521,6 @@ describe('AuthService', () => { status: UserStatus.PENDING, }; - const tokenEntity = { - uuid: v4(), - tokenType: TokenType.EMAIL, - user: userEntity, - }; - let createTokenMock: any; beforeEach(() => { @@ -536,13 +543,23 @@ describe('AuthService', () => { userService.getByEmail = jest.fn().mockResolvedValueOnce(userEntity); sendGridService.sendEmail = jest.fn(); + const email = 'user@example.com'; - await authService.resendEmailVerification({ email: 'user@example.com' }); + await authService.resendEmailVerification({ email }); expect(createTokenMock).toHaveBeenCalled(); expect(sendGridService.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ - text: expect.stringContaining(tokenEntity.uuid), + personalizations: [ + { + dynamicTemplateData: { + service_name: SERVICE_NAME, + url: expect.stringContaining('/verify?token=mocked-uuid'), + }, + to: email, + }, + ], + templateId: SENDGRID_TEMPLATES.signup, }), ); }); diff --git a/packages/apps/reputation-oracle/server/src/modules/auth/auth.service.ts b/packages/apps/reputation-oracle/server/src/modules/auth/auth.service.ts index 64446a4133..5f4971de21 100644 --- a/packages/apps/reputation-oracle/server/src/modules/auth/auth.service.ts +++ b/packages/apps/reputation-oracle/server/src/modules/auth/auth.service.ts @@ -26,6 +26,7 @@ import { ConfigNames } from '../../common/config'; import { ConfigService } from '@nestjs/config'; import { createHash } from 'crypto'; import { SendGridService } from '../sendgrid/sendgrid.service'; +import { SENDGRID_TEMPLATES, SERVICE_NAME } from '../../common/constants'; @Injectable() export class AuthService { @@ -83,12 +84,16 @@ export class AuthService { }); await this.sendgridService.sendEmail({ - to: data.email, - subject: 'Verify your email', - html: `Welcome to the Reputation Oracle Service.
-Click here to complete sign up.`, - text: `Welcome to the Reputation Oracle Service. -Click ${this.feURL}/verify?token=${tokenEntity.uuid} to complete sign up.`, + personalizations: [ + { + to: data.email, + dynamicTemplateData: { + service_name: SERVICE_NAME, + url: `${this.feURL}/verify?token=${tokenEntity.uuid}`, + }, + }, + ], + templateId: SENDGRID_TEMPLATES.signup, }); return userEntity; @@ -142,16 +147,31 @@ Click ${this.feURL}/verify?token=${tokenEntity.uuid} to complete sign up.`, if (userEntity.status !== UserStatus.ACTIVE) throw new UnauthorizedException(ErrorAuth.UserNotActive); - const tokenEntity = await this.tokenRepository.create({ + const existingToken = await this.tokenRepository.findOne({ + userId: userEntity.id, + tokenType: TokenType.PASSWORD, + }); + + if (existingToken) { + await existingToken.remove(); + } + + const newTokenEntity = await this.tokenRepository.create({ tokenType: TokenType.PASSWORD, user: userEntity, }); - this.sendgridService.sendEmail({ - to: data.email, - subject: 'Reset password', - html: `Click here to reset the password.`, - text: `Click ${this.feURL}/reset-password?token=${tokenEntity.uuid} to reset the password.`, + await this.sendgridService.sendEmail({ + personalizations: [ + { + to: data.email, + dynamicTemplateData: { + service_name: SERVICE_NAME, + url: `${this.feURL}/reset-password?token=${newTokenEntity.uuid}`, + }, + }, + ], + templateId: SENDGRID_TEMPLATES.resetPassword, }); } @@ -166,11 +186,16 @@ Click ${this.feURL}/verify?token=${tokenEntity.uuid} to complete sign up.`, } await this.userService.updatePassword(tokenEntity.user, data); - - this.sendgridService.sendEmail({ - to: tokenEntity.user.email, - subject: 'Password changed', - text: 'Password has been changed successfully!', + await this.sendgridService.sendEmail({ + personalizations: [ + { + to: tokenEntity.user.email, + dynamicTemplateData: { + service_name: SERVICE_NAME, + }, + }, + ], + templateId: SENDGRID_TEMPLATES.passwordChanged, }); await tokenEntity.remove(); @@ -206,13 +231,17 @@ Click ${this.feURL}/verify?token=${tokenEntity.uuid} to complete sign up.`, user: userEntity, }); - this.sendgridService.sendEmail({ - to: data.email, - subject: 'Verify your email', - html: `Welcome to the Reputation Oracle Service.
-Click here to complete sign up.`, - text: `Welcome to the Reputation Oracles Service. -Click ${this.feURL}/verify?token=${tokenEntity.uuid} to complete sign up.`, + await this.sendgridService.sendEmail({ + personalizations: [ + { + to: data.email, + dynamicTemplateData: { + service_name: SERVICE_NAME, + url: `${this.feURL}/verify?token=${tokenEntity.uuid}`, + }, + }, + ], + templateId: SENDGRID_TEMPLATES.signup, }); } diff --git a/packages/apps/reputation-oracle/server/src/modules/sendgrid/sendgrid.service.ts b/packages/apps/reputation-oracle/server/src/modules/sendgrid/sendgrid.service.ts index 959413338d..365b6ab412 100644 --- a/packages/apps/reputation-oracle/server/src/modules/sendgrid/sendgrid.service.ts +++ b/packages/apps/reputation-oracle/server/src/modules/sendgrid/sendgrid.service.ts @@ -35,17 +35,19 @@ export class SendGridService { } async sendEmail({ - text = '', from = { email: this.defaultFromEmail, name: this.defaultFromName, }, + templateId = '', + personalizations, ...emailData }: Partial): Promise { try { await this.mailService.send({ from, - text, + templateId, + personalizations, ...emailData, }); this.logger.log('Email sent successfully');