Skip to content

Commit 06d73a8

Browse files
committed
fix(emails): Fix user's locale in emails from being overwritten by another user
1 parent 807c636 commit 06d73a8

4 files changed

Lines changed: 125 additions & 17 deletions

File tree

libs/accounts/email-renderer/src/renderer/email-helpers.spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,5 +136,60 @@ describe('EmailHelpers', () => {
136136
expect(resultDefault).toEqual('01/15/2024');
137137
expect(resultEst).toEqual('01/14/2024');
138138
});
139+
140+
it('does not leak locale between concurrent calls', async () => {
141+
const date = new Date('2025-03-13T12:00:00Z');
142+
143+
const [enResult, gbResult] = await Promise.all([
144+
Promise.resolve(constructLocalDateString(undefined, 'en', date)),
145+
Promise.resolve(constructLocalDateString(undefined, 'en-GB', date)),
146+
]);
147+
148+
expect(enResult).toEqual('03/13/2025');
149+
expect(gbResult).toEqual('13/03/2025');
150+
});
151+
152+
it('does not leak locale into subsequent calls', () => {
153+
const date = new Date('2025-03-13T12:00:00Z');
154+
155+
constructLocalDateString(undefined, 'en-GB', date);
156+
const enResult = constructLocalDateString(undefined, 'en', date);
157+
158+
expect(enResult).toEqual('03/13/2025');
159+
});
160+
});
161+
162+
describe('constructLocalTimeAndDateStrings - locale isolation', () => {
163+
it('does not leak locale between concurrent calls', async () => {
164+
const [enResult, esResult] = await Promise.all([
165+
Promise.resolve(
166+
constructLocalTimeAndDateStrings('America/Los_Angeles', 'en')
167+
),
168+
Promise.resolve(
169+
constructLocalTimeAndDateStrings('America/Los_Angeles', 'es')
170+
),
171+
]);
172+
173+
const enDays = [
174+
'Monday',
175+
'Tuesday',
176+
'Wednesday',
177+
'Thursday',
178+
'Friday',
179+
'Saturday',
180+
'Sunday',
181+
];
182+
const esDays = [
183+
'lunes',
184+
'martes',
185+
'miércoles',
186+
'jueves',
187+
'viernes',
188+
'sábado',
189+
'domingo',
190+
];
191+
expect(enDays).toContain(enResult.date.split(',')[0]);
192+
expect(esDays).toContain(esResult.date.split(',')[0]);
193+
});
139194
});
140195
});

libs/accounts/email-renderer/src/renderer/email-helpers.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,9 @@ export const constructLocalTimeAndDateStrings = (
5353
time: string;
5454
timeZone: string;
5555
} => {
56-
moment.tz.setDefault(DEFAULT_TIMEZONE);
57-
5856
const locale = determineLocale(acceptLanguage) || DEFAULT_LOCALE;
59-
moment.locale(locale);
6057

61-
let timeMoment = moment(date ? date : undefined);
58+
let timeMoment = moment(date ? date : undefined).locale(locale);
6259
if (timeZone) {
6360
timeMoment = timeMoment.tz(timeZone);
6461
}
@@ -88,12 +85,9 @@ export const constructLocalDateString = (
8885
date?: Date | number,
8986
formatString = 'L'
9087
): string => {
91-
moment.tz.setDefault(DEFAULT_TIMEZONE);
92-
9388
const locale = determineLocale(acceptLanguage) || DEFAULT_LOCALE;
94-
moment.locale(locale);
9589

96-
let time = moment(date);
90+
let time = moment(date).locale(locale);
9791
if (timeZone) {
9892
time = time.tz(timeZone);
9993
}

packages/fxa-auth-server/lib/senders/email.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ const { ProductConfigurationManager } = require('@fxa/shared/cms');
2323
const { Container } = require('typedi');
2424

2525
const DEFAULT_LOCALE = 'en';
26-
const DEFAULT_TIMEZONE = 'Etc/UTC';
2726
const UTM_PREFIX = 'fx-';
2827

2928
const X_SES_CONFIGURATION_SET = 'X-SES-CONFIGURATION-SET';
@@ -184,12 +183,9 @@ module.exports = function (log, config, bounces, statsd) {
184183
}
185184

186185
function constructLocalTimeString(timeZone, locale) {
187-
// if no timeZone is passed, use DEFAULT_TIMEZONE
188-
moment.tz.setDefault(DEFAULT_TIMEZONE);
189186
// if no locale is passed, use DEFAULT_LOCALE
190187
locale = locale || DEFAULT_LOCALE;
191-
moment.locale(locale);
192-
let timeMoment = moment();
188+
let timeMoment = moment().locale(locale);
193189
if (timeZone) {
194190
timeMoment = timeMoment.tz(timeZone);
195191
}
@@ -206,12 +202,9 @@ module.exports = function (log, config, bounces, statsd) {
206202
date,
207203
formatString = 'L'
208204
) {
209-
// if no timeZone is passed, use DEFAULT_TIMEZONE
210-
moment.tz.setDefault(DEFAULT_TIMEZONE);
211205
// if no locale is passed, use DEFAULT_LOCALE
212206
locale = locale || DEFAULT_LOCALE;
213-
moment.locale(locale);
214-
let time = moment(date);
207+
let time = moment(date).locale(locale);
215208
if (timeZone) {
216209
time = time.tz(timeZone);
217210
}

packages/fxa-auth-server/test/local/senders/emails.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6449,6 +6449,72 @@ describe('lib/senders/emails:', () => {
64496449
});
64506450
});
64516451

6452+
describe('constructLocalDateString - locale isolation', () => {
6453+
it('does not leak locale between concurrent calls', async () => {
6454+
const date = new Date('2025-03-13T12:00:00Z');
6455+
6456+
// Simulate concurrent calls with different locales
6457+
const [enResult, gbResult] = await Promise.all([
6458+
Promise.resolve(
6459+
mailer._constructLocalDateString(undefined, 'en', date)
6460+
),
6461+
Promise.resolve(
6462+
mailer._constructLocalDateString(undefined, 'en-GB', date)
6463+
),
6464+
]);
6465+
6466+
// en formats as MM/DD/YYYY, en-GB formats as DD/MM/YYYY
6467+
assert.equal(enResult, '03/13/2025');
6468+
assert.equal(gbResult, '13/03/2025');
6469+
});
6470+
6471+
it('does not leak locale into subsequent calls', () => {
6472+
const date = new Date('2025-03-13T12:00:00Z');
6473+
6474+
// Call with en-GB first
6475+
mailer._constructLocalDateString(undefined, 'en-GB', date);
6476+
// Then call with en
6477+
const enResult = mailer._constructLocalDateString(undefined, 'en', date);
6478+
6479+
assert.equal(enResult, '03/13/2025');
6480+
});
6481+
});
6482+
6483+
describe('constructLocalTimeString - locale isolation', () => {
6484+
it('does not leak locale between concurrent calls', async () => {
6485+
const [enResult, esResult] = await Promise.all([
6486+
Promise.resolve(
6487+
mailer._constructLocalTimeString('America/Los_Angeles', 'en')
6488+
),
6489+
Promise.resolve(
6490+
mailer._constructLocalTimeString('America/Los_Angeles', 'es')
6491+
),
6492+
]);
6493+
6494+
// en day names vs es day names
6495+
const enDays = [
6496+
'Monday',
6497+
'Tuesday',
6498+
'Wednesday',
6499+
'Thursday',
6500+
'Friday',
6501+
'Saturday',
6502+
'Sunday',
6503+
];
6504+
const esDays = [
6505+
'lunes',
6506+
'martes',
6507+
'miércoles',
6508+
'jueves',
6509+
'viernes',
6510+
'sábado',
6511+
'domingo',
6512+
];
6513+
assert.include(enDays, enResult[1].split(',')[0]);
6514+
assert.include(esDays, esResult[1].split(',')[0]);
6515+
});
6516+
});
6517+
64526518
describe('constructLocalTimeString - returns date/time', () => {
64536519
// Moment expects a single locale identifier. This tests to ensure
64546520
// we account for this in _constructLocalTimeString

0 commit comments

Comments
 (0)