Skip to content

Commit 9d542b1

Browse files
[FEATURE] Ajouter une validation de l'email dans les liens de l'email d'avertissement de connexion après un an d'inactivité (PIX-16127)
#11420
2 parents e916235 + dd9b1d7 commit 9d542b1

File tree

7 files changed

+102
-24
lines changed

7 files changed

+102
-24
lines changed

api/db/seeds/data/team-acces/build-users.js

+9
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ function _buildUsers(databaseBuilder) {
4848
email: 'chrono.post@example.net',
4949
createdAt: new Date('2000-12-31'),
5050
});
51+
52+
// user with an old last logged-at date (>1 year) and no email confirmation date
53+
const userWithOldLastLoggedAt = databaseBuilder.factory.buildUser.withRawPassword({
54+
firstName: 'Old',
55+
lastName: 'Connexion',
56+
email: 'old-connexion@example.net',
57+
emailConfirmedAt: null,
58+
});
59+
databaseBuilder.factory.buildUserLogin({ userId: userWithOldLastLoggedAt.id, lastLoggedAt: new Date('1970-01-01') });
5160
}
5261

5362
export function buildUsers(databaseBuilder) {

api/src/identity-access-management/domain/emails/create-warning-connection.email.js

+20-4
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@ import { mailer } from '../../../shared/mail/infrastructure/services/mailer.js';
1010
* @param {string} params.locale - The locale for the email.
1111
* @param {string} params.email - The recipient's email address.
1212
* @param {string} params.firstName - The recipient's first name.
13+
* @param {string} params.validationToken - The token for email validation.
1314
* @returns {Email} The email object.
1415
*/
15-
export function createWarningConnectionEmail({ locale, email, firstName }) {
16+
export function createWarningConnectionEmail({ locale, email, firstName, validationToken }) {
1617
locale = locale || LOCALE.FRENCH_FRANCE;
1718
const lang = new Intl.Locale(locale).language;
18-
const factory = new EmailFactory({ app: 'pix-app', locale });
19+
let localeSupport;
20+
if (locale.toLowerCase() === LOCALE.FRENCH_FRANCE) {
21+
localeSupport = LOCALE.FRENCH_FRANCE;
22+
} else {
23+
localeSupport = lang;
24+
}
25+
26+
const factory = new EmailFactory({ app: 'pix-app', locale: localeSupport });
1927

2028
const { i18n, defaultVariables } = factory;
2129
const pixAppUrl = urlBuilder.getPixAppBaseUrl(locale);
@@ -28,7 +36,11 @@ export function createWarningConnectionEmail({ locale, email, firstName }) {
2836
variables: {
2937
homeName: defaultVariables.homeName,
3038
homeUrl: defaultVariables.homeUrl,
31-
helpDeskUrl: defaultVariables.helpdeskUrl,
39+
helpDeskUrl: urlBuilder.getEmailValidationUrl({
40+
locale: localeSupport,
41+
redirectUrl: defaultVariables.helpdeskUrl,
42+
token: validationToken,
43+
}),
3244
displayNationalLogo: defaultVariables.displayNationalLogo,
3345
contactUs: i18n.__('common.email.contactUs'),
3446
doNotAnswer: i18n.__('common.email.doNotAnswer'),
@@ -39,7 +51,11 @@ export function createWarningConnectionEmail({ locale, email, firstName }) {
3951
disclaimer: i18n.__('warning-connection-email.params.disclaimer'),
4052
warningMessage: i18n.__('warning-connection-email.params.warningMessage'),
4153
resetMyPassword: i18n.__('warning-connection-email.params.resetMyPassword'),
42-
resetUrl,
54+
resetUrl: urlBuilder.getEmailValidationUrl({
55+
locale: localeSupport,
56+
redirectUrl: resetUrl,
57+
token: validationToken,
58+
}),
4359
supportContact: i18n.__('warning-connection-email.params.supportContact'),
4460
thanks: i18n.__('warning-connection-email.params.thanks'),
4561
signing: i18n.__('warning-connection-email.params.signing'),

api/src/identity-access-management/domain/usecases/authenticate-user.js

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const authenticateUser = async function ({
1717
userLoginRepository,
1818
adminMemberRepository,
1919
emailRepository,
20+
emailValidationDemandRepository,
2021
audience,
2122
}) {
2223
try {
@@ -49,11 +50,15 @@ const authenticateUser = async function ({
4950

5051
const userLogin = await userLoginRepository.findByUserId(foundUser.id);
5152
if (foundUser.email && userLogin?.shouldSendConnectionWarning()) {
53+
const validationToken = !foundUser.emailConfirmedAt
54+
? await emailValidationDemandRepository.save(foundUser.id)
55+
: null;
5256
await emailRepository.sendEmailAsync(
5357
createWarningConnectionEmail({
5458
locale: foundUser.locale,
5559
email: foundUser.email,
5660
firstName: foundUser.firstName,
61+
validationToken,
5762
}),
5863
);
5964
}

api/src/shared/infrastructure/utils/url-builder.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ function getEmailValidationUrl({ locale, redirectUrl, token } = {}) {
5050
const baseUrl = getPixAppBaseUrl(locale);
5151

5252
const params = new URLSearchParams();
53-
if (token) params.append('token', token);
53+
if (!token) return redirectUrl;
54+
params.append('token', token);
5455
if (redirectUrl) params.append('redirect_url', redirectUrl);
5556

5657
return `${baseUrl}/api/users/validate-email?${params.toString()}`;

api/tests/identity-access-management/unit/domain/emails/create-warning-connection.email.test.js

+54-9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe('Unit | Identity Access Management | Domain | Email | create-warning-co
99
email: 'test@example.com',
1010
locale: 'fr',
1111
firstName: 'John',
12+
validationToken: 'token',
1213
};
1314

1415
const email = createWarningConnectionEmail(emailParams);
@@ -40,56 +41,100 @@ describe('Unit | Identity Access Management | Domain | Email | create-warning-co
4041
});
4142

4243
describe('when the locale is en', function () {
43-
it('provides the correct reset password URL', function () {
44+
it('provides the correct urls', function () {
4445
// given
4546
const emailParams = {
4647
email: 'toto@example.net',
4748
locale: 'en',
4849
firstName: 'John',
50+
validationToken: 'token',
4951
};
5052

5153
// when
5254
const email = createWarningConnectionEmail(emailParams);
5355

5456
// then
55-
const resetUrl = email.variables.resetUrl;
56-
expect(resetUrl).to.equal('https://test.app.pix.org/mot-de-passe-oublie?lang=en');
57+
const { helpDeskUrl, resetUrl } = email.variables;
58+
const expectedSupportUrl =
59+
'https://test.app.pix.org/api/users/validate-email?token=token&redirect_url=https%3A%2F%2Fpix.org%2Fen%2Fsupport';
60+
61+
const expectedResetUrl =
62+
'https://test.app.pix.org/api/users/validate-email?token=token&redirect_url=https%3A%2F%2Ftest.app.pix.org%2Fmot-de-passe-oublie%3Flang%3Den';
63+
expect(resetUrl).to.equal(expectedResetUrl);
64+
expect(helpDeskUrl).to.equal(expectedSupportUrl);
5765
});
5866
});
5967

6068
describe('when the locale is fr-fr', function () {
61-
it('provides the correct reset password URL', function () {
69+
it('provides the correct urls', function () {
6270
// given
6371
const emailParams = {
6472
email: 'toto@example.net',
6573
locale: 'fr-fr',
6674
firstName: 'John',
75+
validationToken: 'token',
6776
};
6877

6978
// when
7079
const email = createWarningConnectionEmail(emailParams);
7180

7281
// then
73-
const resetUrl = email.variables.resetUrl;
74-
expect(resetUrl).to.equal('https://test.app.pix.fr/mot-de-passe-oublie?lang=fr');
82+
const { helpDeskUrl, resetUrl } = email.variables;
83+
const expectedSupportUrl =
84+
'https://test.app.pix.fr/api/users/validate-email?token=token&redirect_url=https%3A%2F%2Fpix.fr%2Fsupport';
85+
const expectedResetUrl =
86+
'https://test.app.pix.fr/api/users/validate-email?token=token&redirect_url=https%3A%2F%2Ftest.app.pix.fr%2Fmot-de-passe-oublie%3Flang%3Dfr';
87+
expect(resetUrl).to.equal(expectedResetUrl);
88+
expect(helpDeskUrl).to.equal(expectedSupportUrl);
89+
});
90+
});
91+
92+
describe('when the locale is fr', function () {
93+
it('provides the correct urls', function () {
94+
// given
95+
const emailParams = {
96+
email: 'toto@example.net',
97+
locale: 'fr',
98+
firstName: 'John',
99+
validationToken: 'token',
100+
};
101+
102+
// when
103+
const email = createWarningConnectionEmail(emailParams);
104+
105+
// then
106+
const { helpDeskUrl, resetUrl } = email.variables;
107+
const expectedSupportUrl =
108+
'https://test.app.pix.org/api/users/validate-email?token=token&redirect_url=https%3A%2F%2Fpix.org%2Ffr%2Fsupport';
109+
const expectedResetUrl =
110+
'https://test.app.pix.org/api/users/validate-email?token=token&redirect_url=https%3A%2F%2Ftest.app.pix.org%2Fmot-de-passe-oublie%3Flang%3Dfr';
111+
expect(resetUrl).to.equal(expectedResetUrl);
112+
expect(helpDeskUrl).to.equal(expectedSupportUrl);
75113
});
76114
});
77115

78116
describe('when the locale is nl-BE', function () {
79-
it('provides the correct reset password URL', function () {
117+
it('provides the correct urls', function () {
80118
// given
81119
const emailParams = {
82120
email: 'toto@example.net',
83121
locale: 'nl-BE',
84122
firstName: 'John',
123+
validationToken: 'token',
85124
};
86125

87126
// when
88127
const email = createWarningConnectionEmail(emailParams);
89128

90129
// then
91-
const resetUrl = email.variables.resetUrl;
92-
expect(resetUrl).to.equal('https://test.app.pix.org/mot-de-passe-oublie?lang=nl');
130+
const { resetUrl, helpDeskUrl } = email.variables;
131+
const expectedResetUrl =
132+
'https://test.app.pix.org/api/users/validate-email?token=token&redirect_url=https%3A%2F%2Ftest.app.pix.org%2Fmot-de-passe-oublie%3Flang%3Dnl';
133+
134+
const expectedSupportUrl =
135+
'https://test.app.pix.org/api/users/validate-email?token=token&redirect_url=https%3A%2F%2Fpix.org%2Fnl-be%2Fsupport';
136+
expect(resetUrl).to.equal(expectedResetUrl);
137+
expect(helpDeskUrl).to.equal(expectedSupportUrl);
93138
});
94139
});
95140
});

api/tests/identity-access-management/unit/domain/usecases/authenticate-user_test.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
2020
let adminMemberRepository;
2121
let pixAuthenticationService;
2222
let emailRepository;
23+
let emailValidationDemandRepository;
2324
let clock;
2425

2526
const userEmail = 'user@example.net';
@@ -51,7 +52,7 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
5152
pixAuthenticationService = {
5253
getUserByUsernameAndPassword: sinon.stub(),
5354
};
54-
55+
emailValidationDemandRepository = { save: sinon.stub() };
5556
emailRepository = { sendEmailAsync: sinon.stub() };
5657
});
5758

@@ -387,21 +388,21 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
387388
userId: user.id,
388389
lastLoggedAt: '2020-01-01',
389390
});
390-
391+
const validationToken = 'token';
391392
const expectedEmail = createWarningConnectionEmail({
392393
email: user.email,
393394
firstName: user.firstName,
394395
locale: user.locale,
396+
validationToken,
395397
});
396398

397399
pixAuthenticationService.getUserByUsernameAndPassword.resolves(user);
398400
tokenService.createAccessTokenFromUser
399401
.withArgs({ userId: user.id, source, audience })
400402
.resolves({ accessToken, expirationDelaySeconds });
401-
403+
emailValidationDemandRepository.save.withArgs(user.id).resolves(validationToken);
402404
userLoginRepository.findByUserId.resolves(userLogins);
403405

404-
// when
405406
await authenticateUser({
406407
username: userEmail,
407408
password,
@@ -413,6 +414,7 @@ describe('Unit | Identity Access Management | Domain | UseCases | authenticate-u
413414
userRepository,
414415
userLoginRepository,
415416
emailRepository,
417+
emailValidationDemandRepository,
416418
audience,
417419
});
418420

api/tests/shared/unit/infrastructure/utils/url-builder_test.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -119,25 +119,25 @@ describe('Unit | Shared | Infrastructure | Utils | url-builder', function () {
119119
it('returns email validation URL with domain .fr', function () {
120120
// given
121121
const redirectUrl = 'https://app.pix.fr/connexion';
122-
const expectedParams = new URLSearchParams({ redirect_url: redirectUrl });
123122

124123
// when
125124
const url = urlBuilder.getEmailValidationUrl({ redirectUrl });
126125

127126
// then
128-
expect(url).to.equal(
129-
`${config.domain.pixApp + config.domain.tldFr}/api/users/validate-email?${expectedParams.toString()}`,
130-
);
127+
expect(url).to.equal(redirectUrl);
131128
});
132129
});
133130

134131
context('when redirect_url is not given', function () {
135132
it('returns email validation URL with domain .fr', function () {
133+
// given
134+
const token = '00000000-0000-0000-0000-000000000000';
135+
136136
// when
137-
const url = urlBuilder.getEmailValidationUrl();
137+
const url = urlBuilder.getEmailValidationUrl({ token });
138138

139139
// then
140-
expect(url).to.equal(`${config.domain.pixApp + config.domain.tldFr}/api/users/validate-email?`);
140+
expect(url).to.equal(`${config.domain.pixApp + config.domain.tldFr}/api/users/validate-email?token=${token}`);
141141
});
142142
});
143143
});

0 commit comments

Comments
 (0)