From 0f0a2682f04cd7d1d8df08c952b4cabcda28fce2 Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Tue, 2 Apr 2024 09:23:50 +0200 Subject: [PATCH] fix: email authenication link messages (#54152) --- api/src/routes/settings.test.ts | 48 +++++++++++++++++++++++++++++++- api/src/routes/settings.ts | 49 ++++++++++++++++++--------------- 2 files changed, 74 insertions(+), 23 deletions(-) diff --git a/api/src/routes/settings.test.ts b/api/src/routes/settings.test.ts index 6a28800dbeae36..2b0e83ffd7f8c8 100644 --- a/api/src/routes/settings.test.ts +++ b/api/src/routes/settings.test.ts @@ -6,7 +6,7 @@ import { } from '../../jest.utils'; import { createUserInput } from '../utils/create-user'; -import { isPictureWithProtocol } from './settings'; +import { isPictureWithProtocol, getWaitMessage } from './settings'; const baseProfileUI = { isLocked: false, @@ -766,3 +766,49 @@ Please wait 5 minutes to resend an authentication link.` }); }); }); + +describe('getWaitMessage', () => { + const sec = 1000; + const min = 60 * 1000; + it.each([ + { + sentAt: new Date(0), + now: new Date(0), + expected: 'Please wait 5 minutes to resend an authentication link.' + }, + { + sentAt: new Date(0), + now: new Date(59 * sec), + expected: 'Please wait 5 minutes to resend an authentication link.' + }, + { + sentAt: new Date(0), + now: new Date(4 * min), + expected: 'Please wait 1 minute to resend an authentication link.' + }, + { + sentAt: new Date(0), + now: new Date(4 * min + 59 * sec), + expected: 'Please wait 1 minute to resend an authentication link.' + }, + { + sentAt: new Date(0), + now: new Date(5 * min), + expected: null + } + ])( + `returns "$expected" when sentAt is $sentAt and now is $now`, + ({ sentAt, now, expected }) => { + expect(getWaitMessage({ sentAt, now })).toEqual(expected); + } + ); + + it('returns null when sentAt is null', () => { + expect(getWaitMessage({ sentAt: null, now: new Date(0) })).toBeNull(); + }); + it('uses the current time when now is not provided', () => { + expect(getWaitMessage({ sentAt: new Date() })).toEqual( + 'Please wait 5 minutes to resend an authentication link.' + ); + }); +}); diff --git a/api/src/routes/settings.ts b/api/src/routes/settings.ts index 708594a4f461dc..2127e281b31d6f 100644 --- a/api/src/routes/settings.ts +++ b/api/src/routes/settings.ts @@ -13,37 +13,38 @@ import type { RouteGenericInterface } from 'fastify'; import { ResolveFastifyReplyType } from 'fastify/types/type-provider'; -import { getMinutes, isBefore, sub } from 'date-fns'; +import { differenceInMinutes } from 'date-fns'; import { isProfane } from 'no-profanity'; import { blocklistedUsernames } from '../../../shared/config/constants'; import { isValidUsername } from '../../../shared/utils/validate'; import { schemas } from '../schemas'; -// TODO: move getWaitMessage and getWaitPeriod to own module and add tests -function getWaitMessage(lastEmailSentAt: Date | null) { - const minutesLeft = getWaitPeriod(lastEmailSentAt); - if (minutesLeft <= 0) { - return null; - } +type WaitMesssageArgs = { + sentAt: Date | null; + now?: Date; +}; - const timeToWait = minutesLeft - ? `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` - : 'a few seconds'; +/** + * Get a message to display to the user about how long they need to wait before + * they can request an authentication link. + * + * @param param The parameters. + * @param param.sentAt The date the last email was sent at. + * @param param.now The current date. + * @returns The message to display to the user. + */ +export function getWaitMessage({ sentAt, now = new Date() }: WaitMesssageArgs) { + const minutesLeft = getWaitPeriod({ sentAt, now }); + if (minutesLeft <= 0) return null; + const timeToWait = `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}`; return `Please wait ${timeToWait} to resend an authentication link.`; } -function getWaitPeriod(lastEmailSentAt: Date | null) { - if (!lastEmailSentAt) return 0; - - const now = new Date(); - const fiveMinutesAgo = sub(now, { minutes: 5 }); - const isWaitPeriodOver = isBefore(lastEmailSentAt, fiveMinutesAgo); - - return isWaitPeriodOver - ? 0 - : 5 - (getMinutes(now) - getMinutes(lastEmailSentAt)); +function getWaitPeriod({ sentAt, now }: Required) { + if (sentAt == null) return 0; + return 5 - differenceInMinutes(now, sentAt); } /** @@ -191,7 +192,9 @@ You can update a new email address instead.` const isResendUpdateToSameEmail = newEmail === user.newEmail?.toLowerCase(); - const isLinkSentWithinLimitTTL = getWaitMessage(user.emailVerifyTTL); + const isLinkSentWithinLimitTTL = getWaitMessage({ + sentAt: user.emailVerifyTTL + }); if (isResendUpdateToSameEmail && isLinkSentWithinLimitTTL) { void reply.code(429); @@ -228,7 +231,9 @@ ${isLinkSentWithinLimitTTL}` // we need emailVeriftyTTL given that the main thing we want is to // restrict the rate of attempts and the emailAuthLinkTTL already does // that. - const tooManyRequestsMessage = getWaitMessage(user.emailAuthLinkTTL); + const tooManyRequestsMessage = getWaitMessage({ + sentAt: user.emailAuthLinkTTL + }); if (tooManyRequestsMessage) { void reply.code(429);