diff --git a/.env.sample b/.env.sample index 0c14410..8721626 100644 --- a/.env.sample +++ b/.env.sample @@ -114,9 +114,10 @@ JWT_SECRET="my-secret" LEGACY_BLOWFISH_KEY=!!!_REPLACE_WITH_BASE64_ENCODED_KEY_!!! ## SENDGRID -SENDGRID_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID="d-73c29be82bfa4d68beea2208b6a3c4b2" -SENDGRID_SELFSERVICE_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID="d-73c29be82bfa4d68beea2208b6a3c4b2" -SENDGRID_WELCOME_EMAIL_TEMPLATE_ID="d-26c8962fb48c42a3997053ebe5954516" +SENDGRID_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID="d-73c29be82bfa4d68beea2208b6a3c4b2" +SENDGRID_SELFSERVICE_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID="d-73c29be82bfa4d68beea2208b6a3c4b2" +SENDGRID_TEMPLATE_ID_OTP_CODE="d-2d0ab9f6c9cc4efba50080668a9c35c1" +SENDGRID_WELCOME_EMAIL_TEMPLATE_ID="d-26c8962fb48c42a3997053ebe5954516" SENDGRID_SELFSERVICE_WELCOME_EMAIL_TEMPLATE_ID="d-26c8962fb48c42a3997053ebe5954516" SSO_TOKEN_SALT=change-me diff --git a/README.md b/README.md index 0c810f4..703863d 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ The following table summarizes the environment variables used by the application | `SLACK_CHANNEL_ID` | Default Slack channel ID for sending notifications. | `C04ENKCU4TZ` (example) | | | **SendGrid Integration** | | | `SENDGRID_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID` | SendGrid template ID for resend activation email. | `d-73c29be82bfa4d68beea2208b6a3c4b2` (example) | +| `SENDGRID_TEMPLATE_ID_OTP_CODE` | SendGrid template ID shared with wallet one-time-password emails. | `d-2d0ab9f6c9cc4efba50080668a9c35c1` | | `SENDGRID_WELCOME_EMAIL_TEMPLATE_ID` | SendGrid template ID for welcome email. | `d-26c8962fb48c42a3997053ebe5954516` (example) | | `EMAIL_CHANGE_OTP_EXPIRY_SECONDS` | Lifetime of the code sent to the current primary email. | `600` | | `EMAIL_CHANGE_OTP_RESEND_SECONDS` | Minimum delay between current-email code requests. | `60` | diff --git a/src/api/user/email-change.service.spec.ts b/src/api/user/email-change.service.spec.ts index 1d97b1d..f778835 100644 --- a/src/api/user/email-change.service.spec.ts +++ b/src/api/user/email-change.service.spec.ts @@ -38,7 +38,9 @@ describe('EmailChangeService', () => { user: { findUnique: jest.fn().mockImplementation(({ select }) => Promise.resolve({ + first_name: 'Justin', handle: 'memberHandle', + last_name: 'Gasper', ...(select?.status && { status: MemberStatus.ACTIVE }), }), ), @@ -59,7 +61,6 @@ describe('EmailChangeService', () => { EMAIL_CHANGE_PROOF_EXPIRY_SECONDS: '600', EMAIL_CHANGE_VALIDATION_EXPIRY_SECONDS: '3600', JWT_SECRET: 'email-change-test-secret', - SENDGRID_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID: 'template-id', }; return values[key]; }), @@ -90,9 +91,21 @@ describe('EmailChangeService', () => { }); const otpEmailPayload = eventService.postDirectBusMessage.mock.calls[0][1]; - const otp = otpEmailPayload.data.code; + const otp = otpEmailPayload.data.otp; expect(otp).toMatch(/^\d{6}$/); - expect(otpEmailPayload.recipients).toEqual(['old@example.com']); + expect(otpEmailPayload).toEqual({ + data: { + name: 'Justin Gasper', + otp, + }, + from: { + email: 'noreply@topcoder-dev.com', + name: 'Topcoder', + }, + recipients: ['old@example.com'], + sendgrid_template_id: 'd-2d0ab9f6c9cc4efba50080668a9c35c1', + version: 'v3', + }); const proof = await service.verifyCurrentEmailOtp('2', otp); expect(proof.verificationToken).toEqual(expect.any(String)); @@ -146,7 +159,7 @@ describe('EmailChangeService', () => { it('rejects validation if the primary email changed while pending', async () => { await service.sendCurrentEmailOtp('2'); - const otp = eventService.postDirectBusMessage.mock.calls[0][1].data.code; + const otp = eventService.postDirectBusMessage.mock.calls[0][1].data.otp; const proof = await service.verifyCurrentEmailOtp('2', otp); await service.initiateEmailChange( '2', diff --git a/src/api/user/email-change.service.ts b/src/api/user/email-change.service.ts index fca86d2..755ebf9 100644 --- a/src/api/user/email-change.service.ts +++ b/src/api/user/email-change.service.ts @@ -37,6 +37,8 @@ const DEFAULT_OTP_EXPIRY_SECONDS = 10 * 60; const DEFAULT_OTP_RESEND_SECONDS = 60; const DEFAULT_PROOF_EXPIRY_SECONDS = 10 * 60; const DEFAULT_VALIDATION_EXPIRY_SECONDS = 60 * 60; +const DEFAULT_SENDGRID_OTP_TEMPLATE_ID = + 'd-2d0ab9f6c9cc4efba50080668a9c35c1'; const MAX_OTP_ATTEMPTS = 5; interface CachedOtp { @@ -146,7 +148,12 @@ export class EmailChangeService { const user = await this.prismaClient.user.findUnique({ where: { user_id: userId }, - select: { handle: true, status: true }, + select: { + first_name: true, + handle: true, + last_name: true, + status: true, + }, }); if (!user) { @@ -187,8 +194,12 @@ export class EmailChangeService { ); try { + const recipientName = [user.first_name, user.last_name] + .map((name) => name?.trim()) + .filter(Boolean) + .join(' ') || user.handle; await this.sendOtpEmail( - user.handle, + recipientName, currentPrimaryEmail.address, otp, ); @@ -544,37 +555,33 @@ export class EmailChangeService { /** * Publishes the current-email OTP through the configured SendGrid template. - * @param handle Topcoder handle used by the email template. + * @param recipientName member name used by the wallet OTP email template. * @param email current primary email recipient. * @param otp six-digit ownership code. * @returns a promise resolved after the event is published. - * @throws InternalServerErrorException when the template is not configured. */ private async sendOtpEmail( - handle: string, + recipientName: string, email: string, otp: string, ): Promise { - const sendgridTemplateId = this.configService.get( - 'SENDGRID_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID', - ); - if (!sendgridTemplateId) { - throw new InternalServerErrorException( - 'Email verification template is not configured.', - ); - } + const sendgridTemplateId = + this.configService.get('SENDGRID_TEMPLATE_ID_OTP_CODE') || + DEFAULT_SENDGRID_OTP_TEMPLATE_ID; const domain = CommonUtils.getAppDomain(this.configService); await this.eventService.postDirectBusMessage('external.action.email', { data: { - code: otp, - duration: Math.ceil(this.otpExpirySeconds / 60), - handle, + name: recipientName, + otp, + }, + from: { + email: `noreply@${domain}`, + name: 'Topcoder', }, - from: { email: `Topcoder ` }, recipients: [email], sendgrid_template_id: sendgridTemplateId, - version: 'v6', + version: 'v3', }); }