Skip to content

Commit bbee960

Browse files
[FEATURE] Affichage de la dernière connexion par app dans l'onglet "Méthodes de connexion" (PIX-16995)
#11774
2 parents c9fb9e3 + 308ff82 commit bbee960

File tree

15 files changed

+177
-46
lines changed

15 files changed

+177
-46
lines changed

admin/app/components/users/user-detail-personal-information/authentication-method.gjs

+13
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,19 @@ export default class AuthenticationMethod extends Component {
235235
{{#if this.shouldChangePassword}}{{t "common.words.yes"}}{{else}}{{t "common.words.no"}}{{/if}}
236236
</li>
237237
{{/if}}
238+
239+
{{#each @user.orderedLastApplicationConnections as |orderedLastApplicationConnection|}}
240+
<li>
241+
{{t
242+
"components.users.user-detail-personal-information.authentication-method.last-application-connection-date"
243+
}}
244+
{{orderedLastApplicationConnection.label}}
245+
:
246+
{{#if orderedLastApplicationConnection.lastLoggedAt}}
247+
{{dayjsFormat orderedLastApplicationConnection.lastLoggedAt "DD/MM/YYYY"}}
248+
{{/if}}
249+
</li>
250+
{{/each}}
238251
</ul>
239252

240253
<table class="authentication-method-table">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import Model, { attr } from '@ember-data/model';
2+
3+
export default class LastApplicationConnection extends Model {
4+
@attr() application;
5+
@attr() lastLoggedAt;
6+
}

admin/app/models/user.js

+21
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
import { computed } from '@ember/object';
33
import Model, { attr, belongsTo, hasMany } from '@ember-data/model';
44

5+
const orderedApplicationNames = ['app', 'orga', 'certif'];
6+
7+
const applicationLabels = {
8+
app: 'Pix App',
9+
orga: 'Pix Orga',
10+
certif: 'Pix Certif',
11+
};
12+
513
export default class User extends Model {
614
@attr() firstName;
715
@attr() lastName;
@@ -31,6 +39,7 @@ export default class User extends Model {
3139
@hasMany('certification-center-membership', { async: true, inverse: 'user' }) certificationCenterMemberships;
3240
@hasMany('organization-learner', { async: true, inverse: 'user' }) organizationLearners;
3341
@hasMany('authentication-method', { async: true, inverse: null }) authenticationMethods;
42+
@hasMany('last-application-connection', { async: false, inverse: null }) lastApplicationConnections;
3443
@hasMany('user-participation', { async: true, inverse: null }) participations;
3544

3645
@computed('firstName', 'lastName')
@@ -54,4 +63,16 @@ export default class User extends Model {
5463
get authenticationMethodCount() {
5564
return this.username && this.email ? this.authenticationMethods.length + 1 : this.authenticationMethods.length;
5665
}
66+
67+
get orderedLastApplicationConnections() {
68+
const connections = orderedApplicationNames.map((applicationName) => {
69+
const lastLoggedAt = this.lastApplicationConnections?.find((connection) => {
70+
return connection.application === applicationName;
71+
})?.lastLoggedAt;
72+
73+
return { lastLoggedAt, label: applicationLabels[applicationName] };
74+
});
75+
76+
return connections;
77+
}
5778
}

admin/tests/integration/components/users/user-detail-personal-information/authentication-method-test.gjs

+37-22
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,19 @@ import setupIntlRenderingTest from '../../../../helpers/setup-intl-rendering';
1010
module('Integration | Component | users | user-detail-personal-information | authentication-method', function (hooks) {
1111
setupIntlRenderingTest(hooks);
1212

13-
module('When the admin member has access to users actions scope', function () {
14-
class AccessControlStub extends Service {
13+
module('When the admin member has access to users actions scope', function (hooks) {
14+
const stub = class AccessControlStub extends Service {
1515
hasAccessToUsersActionsScope = true;
16-
}
16+
};
17+
hooks.beforeEach(function () {
18+
this.owner.register('service:access-control', stub);
19+
});
1720

1821
module('When user has authentication methods', function () {
1922
module('when user has confirmed his email address', function () {
2023
test('should display email confirmed date', async function (assert) {
2124
// given
2225
const user = { emailConfirmedAt: new Date('2020-10-30'), authenticationMethods: [] };
23-
this.owner.register('service:access-control', AccessControlStub);
2426

2527
// when
2628
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);
@@ -44,7 +46,6 @@ module('Integration | Component | users | user-detail-personal-information | aut
4446
test('it should display "Adresse e-mail non confirmée"', async function (assert) {
4547
// given
4648
const user = { emailConfirmedAt: null, authenticationMethods: [] };
47-
this.owner.register('service:access-control', AccessControlStub);
4849

4950
// when
5051
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);
@@ -71,7 +72,6 @@ module('Integration | Component | users | user-detail-personal-information | aut
7172
},
7273
],
7374
};
74-
this.owner.register('service:access-control', AccessControlStub);
7575

7676
// when
7777
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);
@@ -102,8 +102,6 @@ module('Integration | Component | users | user-detail-personal-information | aut
102102
],
103103
};
104104

105-
this.owner.register('service:access-control', AccessControlStub);
106-
107105
// when
108106
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);
109107

@@ -133,7 +131,6 @@ module('Integration | Component | users | user-detail-personal-information | aut
133131
},
134132
],
135133
};
136-
this.owner.register('service:access-control', AccessControlStub);
137134

138135
// when
139136
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);
@@ -159,7 +156,6 @@ module('Integration | Component | users | user-detail-personal-information | aut
159156
test('should display information', async function (assert) {
160157
// given
161158
const user = { email: 'pix.aile@example.net', authenticationMethods: [{ identityProvider: 'PIX' }] };
162-
this.owner.register('service:access-control', AccessControlStub);
163159

164160
// when
165161
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);
@@ -173,7 +169,6 @@ module('Integration | Component | users | user-detail-personal-information | aut
173169
test('should display information', async function (assert) {
174170
// given
175171
const user = { authenticationMethods: [] };
176-
this.owner.register('service:access-control', AccessControlStub);
177172

178173
// when
179174
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);
@@ -191,7 +186,6 @@ module('Integration | Component | users | user-detail-personal-information | aut
191186
test('should display information', async function (assert) {
192187
// given
193188
const user = { username: 'PixAile', authenticationMethods: [{ identityProvider: 'PIX' }] };
194-
this.owner.register('service:access-control', AccessControlStub);
195189

196190
// when
197191
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);
@@ -205,7 +199,6 @@ module('Integration | Component | users | user-detail-personal-information | aut
205199
test('should display information', async function (assert) {
206200
// given
207201
const user = { authenticationMethods: [] };
208-
this.owner.register('service:access-control', AccessControlStub);
209202

210203
// when
211204
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);
@@ -223,7 +216,6 @@ module('Integration | Component | users | user-detail-personal-information | aut
223216
test('should display information and reassign authentication method button', async function (assert) {
224217
// given
225218
const user = { authenticationMethods: [{ identityProvider: 'GAR' }] };
226-
this.owner.register('service:access-control', AccessControlStub);
227219

228220
// when
229221
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);
@@ -238,7 +230,6 @@ module('Integration | Component | users | user-detail-personal-information | aut
238230
test('should display information', async function (assert) {
239231
// given
240232
const user = { username: 'PixAile', authenticationMethods: [{ identityProvider: 'PIX' }] };
241-
this.owner.register('service:access-control', AccessControlStub);
242233

243234
// when
244235
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);
@@ -265,7 +256,6 @@ module('Integration | Component | users | user-detail-personal-information | aut
265256
test('should display information', async function (assert) {
266257
// given
267258
const user = { authenticationMethods: [] };
268-
this.owner.register('service:access-control', AccessControlStub);
269259
this.owner.register('service:oidc-identity-providers', OidcIdentityProvidersStub);
270260

271261
// when
@@ -287,7 +277,6 @@ module('Integration | Component | users | user-detail-personal-information | aut
287277
authenticationMethods: [{ identityProvider: 'PIX' }, { identityProvider: 'SUNLIGHT_NAVIGATIONS' }],
288278
};
289279

290-
this.owner.register('service:access-control', AccessControlStub);
291280
this.owner.register('service:oidc-identity-providers', OidcIdentityProvidersStub);
292281

293282
// when
@@ -312,7 +301,6 @@ module('Integration | Component | users | user-detail-personal-information | aut
312301
authenticationMethods: [{ identityProvider: 'PIX' }],
313302
};
314303

315-
this.owner.register('service:access-control', AccessControlStub);
316304
this.owner.register('service:oidc-identity-providers', OidcIdentityProvidersStub);
317305

318306
// when
@@ -340,7 +328,6 @@ module('Integration | Component | users | user-detail-personal-information | aut
340328
test('it should not display a remove authentication method link', async function (assert) {
341329
// given
342330
const user = { username: 'PixAile', authenticationMethods: [{ identityProvider: 'PIX' }] };
343-
this.owner.register('service:access-control', AccessControlStub);
344331

345332
// when
346333
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);
@@ -367,7 +354,6 @@ module('Integration | Component | users | user-detail-personal-information | aut
367354
username: 'PixAile',
368355
authenticationMethods: [{ identityProvider: 'SUNLIGHT_NAVIGATIONS' }],
369356
};
370-
this.owner.register('service:access-control', AccessControlStub);
371357

372358
// when
373359
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);
@@ -383,7 +369,6 @@ module('Integration | Component | users | user-detail-personal-information | aut
383369
const user = {
384370
authenticationMethods: [],
385371
};
386-
this.owner.register('service:access-control', AccessControlStub);
387372

388373
// when
389374
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);
@@ -397,7 +382,6 @@ module('Integration | Component | users | user-detail-personal-information | aut
397382
test('it should not display add authentication method button', async function (assert) {
398383
// given
399384
const user = { username: 'PixAile', authenticationMethods: [{ identityProvider: 'PIX' }] };
400-
this.owner.register('service:access-control', AccessControlStub);
401385

402386
// when
403387
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);
@@ -408,6 +392,37 @@ module('Integration | Component | users | user-detail-personal-information | aut
408392
});
409393
});
410394
});
395+
396+
test('it displays last application connections', async function (assert) {
397+
// given
398+
const store = this.owner.lookup('service:store');
399+
const user = store.createRecord('user', {
400+
username: 'PixAile',
401+
authenticationMethods: [store.createRecord('authentication-method', { identityProvider: 'PIX' })],
402+
lastApplicationConnections: [
403+
store.createRecord('last-application-connection', {
404+
application: 'app',
405+
lastLoggedAt: new Date('2022-05-01T00:00:00Z'),
406+
}),
407+
store.createRecord('last-application-connection', {
408+
application: 'orga',
409+
lastLoggedAt: new Date('2022-01-01T00:00:00Z'),
410+
}),
411+
store.createRecord('last-application-connection', {
412+
application: 'certif',
413+
lastLoggedAt: new Date('2022-02-01T00:00:00Z'),
414+
}),
415+
],
416+
});
417+
418+
// when
419+
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);
420+
421+
// then
422+
assert.dom(screen.getByText('Date de dernière connexion Pix App : 01/05/2022')).exists();
423+
assert.dom(screen.getByText('Date de dernière connexion Pix Orga : 01/01/2022')).exists();
424+
assert.dom(screen.getByText('Date de dernière connexion Pix Certif : 01/02/2022')).exists();
425+
});
411426
});
412427

413428
module('When the admin member does not have access to users actions scope', function () {

admin/translations/en.json

+1
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,7 @@
511511
"copy-username": "Copy user id"
512512
},
513513
"authentication-method": {
514+
"last-application-connection-date": "Last application connection date",
514515
"last-logged-at": "Last logged at {date}",
515516
"should-change-password-status": "Temporary password :"
516517
},

admin/translations/fr.json

+1
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@
521521
"copy-username": "Copier l’identifiant"
522522
},
523523
"authentication-method": {
524+
"last-application-connection-date": "Date de dernière connexion",
524525
"last-logged-at": "Dernière connexion le {date}",
525526
"should-change-password-status": "Mot de passe temporaire :"
526527
},

api/src/identity-access-management/domain/models/UserDetailsForAdmin.js

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class UserDetailsForAdmin {
2828
anonymisedByFirstName,
2929
anonymisedByLastName,
3030
isPixAgent,
31+
lastApplicationConnections,
3132
} = {},
3233
dependencies = { localeService },
3334
) {
@@ -60,6 +61,7 @@ class UserDetailsForAdmin {
6061
this.anonymisedByFirstName = anonymisedByFirstName;
6162
this.anonymisedByLastName = anonymisedByLastName;
6263
this.isPixAgent = isPixAgent;
64+
this.lastApplicationConnections = lastApplicationConnections;
6365
}
6466

6567
get anonymisedByFullName() {

api/src/identity-access-management/infrastructure/repositories/user.repository.js

+10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { Membership } from '../../../shared/domain/models/Membership.js';
1717
import { fetchPage, isUniqConstraintViolated } from '../../../shared/infrastructure/utils/knex-utils.js';
1818
import { NON_OIDC_IDENTITY_PROVIDERS } from '../../domain/constants/identity-providers.js';
1919
import { QUERY_TYPES } from '../../domain/constants/user-query.js';
20+
import { LastUserApplicationConnection } from '../../domain/models/LastUserApplicationConnection.js';
2021
import { User } from '../../domain/models/User.js';
2122
import { UserDetailsForAdmin } from '../../domain/models/UserDetailsForAdmin.js';
2223
import { UserLogin } from '../../domain/models/UserLogin.js';
@@ -129,6 +130,8 @@ const getUserDetailsForAdmin = async function (userId) {
129130
type: 'TOS',
130131
});
131132

133+
const lastUserApplicationConnectionsDTO = await knex('last-user-application-connections').where({ userId });
134+
132135
const authenticationMethodsDTO = await knex('authentication-methods')
133136
.select([
134137
'authentication-methods.id',
@@ -166,6 +169,7 @@ const getUserDetailsForAdmin = async function (userId) {
166169
organizationLearnersDTO,
167170
authenticationMethodsDTO,
168171
pixAdminRolesDTO,
172+
lastUserApplicationConnectionsDTO,
169173
});
170174
};
171175

@@ -489,6 +493,7 @@ function _fromKnexDTOToUserDetailsForAdmin({
489493
organizationLearnersDTO,
490494
authenticationMethodsDTO,
491495
pixAdminRolesDTO,
496+
lastUserApplicationConnectionsDTO,
492497
}) {
493498
const organizationLearners = organizationLearnersDTO.map(
494499
(organizationLearnerDTO) =>
@@ -518,6 +523,10 @@ function _fromKnexDTOToUserDetailsForAdmin({
518523
blockedAt: userDTO.blockedAt,
519524
});
520525

526+
const lastApplicationConnections = lastUserApplicationConnectionsDTO.map(
527+
(lastUserApplicationConnectionDTO) => new LastUserApplicationConnection(lastUserApplicationConnectionDTO),
528+
);
529+
521530
const authenticationMethods = authenticationMethodsDTO.map((authenticationMethod) => {
522531
const isPixAuthenticationMethodWithAuthenticationComplement =
523532
authenticationMethod.identityProvider === NON_OIDC_IDENTITY_PROVIDERS.PIX.code &&
@@ -560,6 +569,7 @@ function _fromKnexDTOToUserDetailsForAdmin({
560569
anonymisedByFirstName: userDTO.anonymisedByFirstName,
561570
anonymisedByLastName: userDTO.anonymisedByLastName,
562571
isPixAgent: pixAdminRolesDTO.length > 0,
572+
lastApplicationConnections,
563573
});
564574
}
565575

api/src/identity-access-management/infrastructure/serializers/jsonapi/user-details-for-admin.serializer.js

+6
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const serialize = function (usersDetailsForAdmin) {
3434
'anonymisedByFullName',
3535
'organizationLearners',
3636
'authenticationMethods',
37+
'lastApplicationConnections',
3738
'profile',
3839
'participations',
3940
'organizationMemberships',
@@ -63,6 +64,11 @@ const serialize = function (usersDetailsForAdmin) {
6364
includes: true,
6465
attributes: ['identityProvider', 'authenticationComplement', 'lastLoggedAt'],
6566
},
67+
lastApplicationConnections: {
68+
ref: 'id',
69+
includes: true,
70+
attributes: ['application', 'lastLoggedAt'],
71+
},
6672
userLogin: {
6773
ref: 'id',
6874
includes: true,

api/tests/identity-access-management/acceptance/application/user/user.admin.route.test.js

+3
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,9 @@ describe('Acceptance | Identity Access Management | Application | Route | Admin
322322
related: `/api/admin/users/${user.id}/participations`,
323323
},
324324
},
325+
'last-application-connections': {
326+
data: [],
327+
},
325328
});
326329
expect(response.result.included).to.deep.equal([
327330
{

0 commit comments

Comments
 (0)