Skip to content

Commit

Permalink
fix: email authenication link messages (#54152)
Browse files Browse the repository at this point in the history
  • Loading branch information
ojeytonwilliams committed Apr 2, 2024
1 parent 38e4f80 commit 0f0a268
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 23 deletions.
48 changes: 47 additions & 1 deletion api/src/routes/settings.test.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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.'
);
});
});
49 changes: 27 additions & 22 deletions api/src/routes/settings.ts
Expand Up @@ -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<WaitMesssageArgs>) {
if (sentAt == null) return 0;
return 5 - differenceInMinutes(now, sentAt);
}

/**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 0f0a268

Please sign in to comment.