Skip to content

Commit cb4064c

Browse files
committed
fix(emails): Fix user's locale in emails from being overwritten by another user
1 parent 0cee199 commit cb4064c

3 files changed

Lines changed: 67 additions & 29 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: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,11 @@ 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);
62-
if (timeZone) {
63-
timeMoment = timeMoment.tz(timeZone);
64-
}
58+
const timeMoment = moment(date ? date : undefined)
59+
.locale(locale)
60+
.tz(timeZone || DEFAULT_TIMEZONE);
6561

6662
const formattedTime = timeMoment.format('LTS (z)');
6763
const formattedDate = timeMoment.format('dddd, ll');
@@ -88,15 +84,11 @@ export const constructLocalDateString = (
8884
date?: Date | number,
8985
formatString = 'L'
9086
): string => {
91-
moment.tz.setDefault(DEFAULT_TIMEZONE);
92-
9387
const locale = determineLocale(acceptLanguage) || DEFAULT_LOCALE;
94-
moment.locale(locale);
9588

96-
let time = moment(date);
97-
if (timeZone) {
98-
time = time.tz(timeZone);
99-
}
89+
const time = moment(date)
90+
.locale(locale)
91+
.tz(timeZone || DEFAULT_TIMEZONE);
10092

10193
return time.format(formatString);
10294
};

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

Lines changed: 6 additions & 15 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';
@@ -186,15 +185,11 @@ module.exports = function (log, config, bounces, statsd) {
186185
}
187186

188187
function constructLocalTimeString(timeZone, locale) {
189-
// if no timeZone is passed, use DEFAULT_TIMEZONE
190-
moment.tz.setDefault(DEFAULT_TIMEZONE);
191188
// if no locale is passed, use DEFAULT_LOCALE
192189
locale = locale || DEFAULT_LOCALE;
193-
moment.locale(locale);
194-
let timeMoment = moment();
195-
if (timeZone) {
196-
timeMoment = timeMoment.tz(timeZone);
197-
}
190+
const timeMoment = moment()
191+
.locale(locale)
192+
.tz(timeZone || 'Etc/UTC');
198193
// return a locale-specific time
199194
// if date or time is passed, return it as the current date or time
200195
const timeNow = timeMoment.format('LTS (z)');
@@ -208,15 +203,11 @@ module.exports = function (log, config, bounces, statsd) {
208203
date,
209204
formatString = 'L'
210205
) {
211-
// if no timeZone is passed, use DEFAULT_TIMEZONE
212-
moment.tz.setDefault(DEFAULT_TIMEZONE);
213206
// if no locale is passed, use DEFAULT_LOCALE
214207
locale = locale || DEFAULT_LOCALE;
215-
moment.locale(locale);
216-
let time = moment(date);
217-
if (timeZone) {
218-
time = time.tz(timeZone);
219-
}
208+
const time = moment(date)
209+
.locale(locale)
210+
.tz(timeZone || 'Etc/UTC');
220211
// return a locale-specific date
221212
return time.format(formatString);
222213
}

0 commit comments

Comments
 (0)