Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Affichage de la dernière connexion par app dans l'onglet "Méthodes de connexion" (PIX-16995) #11774

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,19 @@ export default class AuthenticationMethod extends Component {
{{#if this.shouldChangePassword}}{{t "common.words.yes"}}{{else}}{{t "common.words.no"}}{{/if}}
</li>
{{/if}}

{{#each @user.orderedLastApplicationConnections as |orderedLastApplicationConnection|}}
<li>
{{t
"components.users.user-detail-personal-information.authentication-method.last-application-connection-date"
}}
{{orderedLastApplicationConnection.label}}
:
{{#if orderedLastApplicationConnection.lastLoggedAt}}
{{dayjsFormat orderedLastApplicationConnection.lastLoggedAt "DD/MM/YYYY"}}
{{/if}}
</li>
{{/each}}
</ul>

<table class="authentication-method-table">
Expand Down
6 changes: 6 additions & 0 deletions admin/app/models/last-application-connection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Model, { attr } from '@ember-data/model';

export default class LastApplicationConnection extends Model {
@attr() application;
@attr() lastLoggedAt;
}
21 changes: 21 additions & 0 deletions admin/app/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
import { computed } from '@ember/object';
import Model, { attr, belongsTo, hasMany } from '@ember-data/model';

const orderedApplicationNames = ['app', 'orga', 'certif'];

const applicationLabels = {
app: 'Pix App',
orga: 'Pix Orga',
certif: 'Pix Certif',
};

export default class User extends Model {
@attr() firstName;
@attr() lastName;
Expand Down Expand Up @@ -31,6 +39,7 @@ export default class User extends Model {
@hasMany('certification-center-membership', { async: true, inverse: 'user' }) certificationCenterMemberships;
@hasMany('organization-learner', { async: true, inverse: 'user' }) organizationLearners;
@hasMany('authentication-method', { async: true, inverse: null }) authenticationMethods;
@hasMany('last-application-connection', { async: false, inverse: null }) lastApplicationConnections;
@hasMany('user-participation', { async: true, inverse: null }) participations;

@computed('firstName', 'lastName')
Expand All @@ -54,4 +63,16 @@ export default class User extends Model {
get authenticationMethodCount() {
return this.username && this.email ? this.authenticationMethods.length + 1 : this.authenticationMethods.length;
}

get orderedLastApplicationConnections() {
const connections = orderedApplicationNames.map((applicationName) => {
const lastLoggedAt = this.lastApplicationConnections?.find((connection) => {
return connection.application === applicationName;
})?.lastLoggedAt;

return { lastLoggedAt, label: applicationLabels[applicationName] };
});

return connections;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,19 @@ import setupIntlRenderingTest from '../../../../helpers/setup-intl-rendering';
module('Integration | Component | users | user-detail-personal-information | authentication-method', function (hooks) {
setupIntlRenderingTest(hooks);

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

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

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

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

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

this.owner.register('service:access-control', AccessControlStub);

// when
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// when
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);
Expand All @@ -408,6 +392,37 @@ module('Integration | Component | users | user-detail-personal-information | aut
});
});
});

test('it displays last application connections', async function (assert) {
// given
const store = this.owner.lookup('service:store');
const user = store.createRecord('user', {
username: 'PixAile',
authenticationMethods: [store.createRecord('authentication-method', { identityProvider: 'PIX' })],
lastApplicationConnections: [
store.createRecord('last-application-connection', {
application: 'app',
lastLoggedAt: new Date('2022-05-01T00:00:00Z'),
}),
store.createRecord('last-application-connection', {
application: 'orga',
lastLoggedAt: new Date('2022-01-01T00:00:00Z'),
}),
store.createRecord('last-application-connection', {
application: 'certif',
lastLoggedAt: new Date('2022-02-01T00:00:00Z'),
}),
],
});

// when
const screen = await render(<template><AuthenticationMethod @user={{user}} /></template>);

// then
assert.dom(screen.getByText('Date de dernière connexion Pix App : 01/05/2022')).exists();
assert.dom(screen.getByText('Date de dernière connexion Pix Orga : 01/01/2022')).exists();
assert.dom(screen.getByText('Date de dernière connexion Pix Certif : 01/02/2022')).exists();
});
});

module('When the admin member does not have access to users actions scope', function () {
Expand Down
1 change: 1 addition & 0 deletions admin/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@
"copy-username": "Copy user id"
},
"authentication-method": {
"last-application-connection-date": "Last application connection date",
"last-logged-at": "Last logged at {date}",
"should-change-password-status": "Temporary password :"
},
Expand Down
1 change: 1 addition & 0 deletions admin/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@
"copy-username": "Copier l’identifiant"
},
"authentication-method": {
"last-application-connection-date": "Date de dernière connexion",
"last-logged-at": "Dernière connexion le {date}",
"should-change-password-status": "Mot de passe temporaire :"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class UserDetailsForAdmin {
anonymisedByFirstName,
anonymisedByLastName,
isPixAgent,
lastApplicationConnections,
} = {},
dependencies = { localeService },
) {
Expand Down Expand Up @@ -60,6 +61,7 @@ class UserDetailsForAdmin {
this.anonymisedByFirstName = anonymisedByFirstName;
this.anonymisedByLastName = anonymisedByLastName;
this.isPixAgent = isPixAgent;
this.lastApplicationConnections = lastApplicationConnections;
}

get anonymisedByFullName() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Membership } from '../../../shared/domain/models/Membership.js';
import { fetchPage, isUniqConstraintViolated } from '../../../shared/infrastructure/utils/knex-utils.js';
import { NON_OIDC_IDENTITY_PROVIDERS } from '../../domain/constants/identity-providers.js';
import { QUERY_TYPES } from '../../domain/constants/user-query.js';
import { LastUserApplicationConnection } from '../../domain/models/LastUserApplicationConnection.js';
import { User } from '../../domain/models/User.js';
import { UserDetailsForAdmin } from '../../domain/models/UserDetailsForAdmin.js';
import { UserLogin } from '../../domain/models/UserLogin.js';
Expand Down Expand Up @@ -129,6 +130,8 @@ const getUserDetailsForAdmin = async function (userId) {
type: 'TOS',
});

const lastUserApplicationConnectionsDTO = await knex('last-user-application-connections').where({ userId });

const authenticationMethodsDTO = await knex('authentication-methods')
.select([
'authentication-methods.id',
Expand Down Expand Up @@ -166,6 +169,7 @@ const getUserDetailsForAdmin = async function (userId) {
organizationLearnersDTO,
authenticationMethodsDTO,
pixAdminRolesDTO,
lastUserApplicationConnectionsDTO,
});
};

Expand Down Expand Up @@ -489,6 +493,7 @@ function _fromKnexDTOToUserDetailsForAdmin({
organizationLearnersDTO,
authenticationMethodsDTO,
pixAdminRolesDTO,
lastUserApplicationConnectionsDTO,
}) {
const organizationLearners = organizationLearnersDTO.map(
(organizationLearnerDTO) =>
Expand Down Expand Up @@ -518,6 +523,10 @@ function _fromKnexDTOToUserDetailsForAdmin({
blockedAt: userDTO.blockedAt,
});

const lastApplicationConnections = lastUserApplicationConnectionsDTO.map(
(lastUserApplicationConnectionDTO) => new LastUserApplicationConnection(lastUserApplicationConnectionDTO),
);

const authenticationMethods = authenticationMethodsDTO.map((authenticationMethod) => {
const isPixAuthenticationMethodWithAuthenticationComplement =
authenticationMethod.identityProvider === NON_OIDC_IDENTITY_PROVIDERS.PIX.code &&
Expand Down Expand Up @@ -560,6 +569,7 @@ function _fromKnexDTOToUserDetailsForAdmin({
anonymisedByFirstName: userDTO.anonymisedByFirstName,
anonymisedByLastName: userDTO.anonymisedByLastName,
isPixAgent: pixAdminRolesDTO.length > 0,
lastApplicationConnections,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const serialize = function (usersDetailsForAdmin) {
'anonymisedByFullName',
'organizationLearners',
'authenticationMethods',
'lastApplicationConnections',
'profile',
'participations',
'organizationMemberships',
Expand Down Expand Up @@ -63,6 +64,11 @@ const serialize = function (usersDetailsForAdmin) {
includes: true,
attributes: ['identityProvider', 'authenticationComplement', 'lastLoggedAt'],
},
lastApplicationConnections: {
ref: 'id',
includes: true,
attributes: ['application', 'lastLoggedAt'],
},
userLogin: {
ref: 'id',
includes: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,9 @@ describe('Acceptance | Identity Access Management | Application | Route | Admin
related: `/api/admin/users/${user.id}/participations`,
},
},
'last-application-connections': {
data: [],
},
});
expect(response.result.included).to.deep.equal([
{
Expand Down
Loading