From ff12defb8bfec75afdc0429f7be81176b6828512 Mon Sep 17 00:00:00 2001 From: Benjamin Petetot Date: Tue, 19 Nov 2024 15:54:12 +0100 Subject: [PATCH 1/6] feat(api): add a repo to call Privacy user API from IAM context --- .../privacy-users-api.repository.js | 7 +++++++ .../privacy-users-api.repository.test.js | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 api/src/identity-access-management/infrastructure/repositories/privacy-users-api.repository.js create mode 100644 api/tests/identity-access-management/unit/infrastructure/repositories/privacy-users-api.repository.test.js diff --git a/api/src/identity-access-management/infrastructure/repositories/privacy-users-api.repository.js b/api/src/identity-access-management/infrastructure/repositories/privacy-users-api.repository.js new file mode 100644 index 00000000000..7167b1e5a8e --- /dev/null +++ b/api/src/identity-access-management/infrastructure/repositories/privacy-users-api.repository.js @@ -0,0 +1,7 @@ +import * as privacyUsersApi from '../../../privacy/application/api/users-api.js'; + +const canSelfDeleteAccount = async ({ userId, dependencies = { privacyUsersApi } }) => { + return dependencies.privacyUsersApi.canSelfDeleteAccount({ userId }); +}; + +export { canSelfDeleteAccount }; diff --git a/api/tests/identity-access-management/unit/infrastructure/repositories/privacy-users-api.repository.test.js b/api/tests/identity-access-management/unit/infrastructure/repositories/privacy-users-api.repository.test.js new file mode 100644 index 00000000000..cdbae4096cb --- /dev/null +++ b/api/tests/identity-access-management/unit/infrastructure/repositories/privacy-users-api.repository.test.js @@ -0,0 +1,21 @@ +import { canSelfDeleteAccount } from '../../../../../src/identity-access-management/infrastructure/repositories/privacy-users-api.repository.js'; +import { expect } from '../../../../test-helper.js'; + +describe('Unit | Identity Access Management | Infrastructure | Repositories | privacy-users-api', function () { + describe('#canSelfDeleteAccount', function () { + it('indicates if user can self delete their account', async function () { + // given + const dependencies = { + privacyUsersApi: { + canSelfDeleteAccount: async () => true, + }, + }; + + // when + const result = await canSelfDeleteAccount({ userId: '123', dependencies }); + + // then + expect(result).to.be.true; + }); + }); +}); From 2ddb8bb89dc5bad0d750c931b61ea0f54a9b100a Mon Sep 17 00:00:00 2001 From: Benjamin Petetot Date: Tue, 19 Nov 2024 15:54:47 +0100 Subject: [PATCH 2/6] feat(api): add use-case to get User Account info --- .../domain/models/UserAccountInfo.js | 8 +++++++ .../usecases/get-user-account-info.usecase.js | 16 ++++++++++++++ .../domain/usecases/index.js | 2 ++ .../get-user-account-info.usecase.test.js | 21 +++++++++++++++++++ 4 files changed, 47 insertions(+) create mode 100644 api/src/identity-access-management/domain/models/UserAccountInfo.js create mode 100644 api/src/identity-access-management/domain/usecases/get-user-account-info.usecase.js create mode 100644 api/tests/identity-access-management/integration/domain/usecases/get-user-account-info.usecase.test.js diff --git a/api/src/identity-access-management/domain/models/UserAccountInfo.js b/api/src/identity-access-management/domain/models/UserAccountInfo.js new file mode 100644 index 00000000000..4a08866209a --- /dev/null +++ b/api/src/identity-access-management/domain/models/UserAccountInfo.js @@ -0,0 +1,8 @@ +export class UserAccountInfo { + constructor({ id, email, username, canSelfDeleteAccount } = {}) { + this.id = id; + this.email = email; + this.username = username; + this.canSelfDeleteAccount = canSelfDeleteAccount; + } +} diff --git a/api/src/identity-access-management/domain/usecases/get-user-account-info.usecase.js b/api/src/identity-access-management/domain/usecases/get-user-account-info.usecase.js new file mode 100644 index 00000000000..48777a05a47 --- /dev/null +++ b/api/src/identity-access-management/domain/usecases/get-user-account-info.usecase.js @@ -0,0 +1,16 @@ +import { UserAccountInfo } from '../models/UserAccountInfo.js'; + +const getUserAccountInfo = async ({ userId, userRepository, privacyUsersApiRepository }) => { + const user = await userRepository.get(userId); + + const canSelfDeleteAccount = await privacyUsersApiRepository.canSelfDeleteAccount({ userId }); + + return new UserAccountInfo({ + id: user.id, + email: user.email, + username: user.username, + canSelfDeleteAccount, + }); +}; + +export { getUserAccountInfo }; diff --git a/api/src/identity-access-management/domain/usecases/index.js b/api/src/identity-access-management/domain/usecases/index.js index f988ed9b16f..480e5bc9670 100644 --- a/api/src/identity-access-management/domain/usecases/index.js +++ b/api/src/identity-access-management/domain/usecases/index.js @@ -31,6 +31,7 @@ import { eventLoggingJobRepository } from '../../infrastructure/repositories/job import { garAnonymizedBatchEventsLoggingJobRepository } from '../../infrastructure/repositories/jobs/gar-anonymized-batch-events-logging-job-repository.js'; import { userAnonymizedEventLoggingJobRepository } from '../../infrastructure/repositories/jobs/user-anonymized-event-logging-job-repository.js'; import { oidcProviderRepository } from '../../infrastructure/repositories/oidc-provider-repository.js'; +import * as privacyUsersApiRepository from '../../infrastructure/repositories/privacy-users-api.repository.js'; import { refreshTokenRepository } from '../../infrastructure/repositories/refresh-token.repository.js'; import { resetPasswordDemandRepository } from '../../infrastructure/repositories/reset-password-demand.repository.js'; import * as userRepository from '../../infrastructure/repositories/user.repository.js'; @@ -58,6 +59,7 @@ const repositories = { membershipRepository, oidcProviderRepository, organizationLearnerRepository, + privacyUsersApiRepository, refreshTokenRepository, resetPasswordDemandRepository, userAnonymizedEventLoggingJobRepository, diff --git a/api/tests/identity-access-management/integration/domain/usecases/get-user-account-info.usecase.test.js b/api/tests/identity-access-management/integration/domain/usecases/get-user-account-info.usecase.test.js new file mode 100644 index 00000000000..cb28d7d1584 --- /dev/null +++ b/api/tests/identity-access-management/integration/domain/usecases/get-user-account-info.usecase.test.js @@ -0,0 +1,21 @@ +import { usecases } from '../../../../../src/identity-access-management/domain/usecases/index.js'; +import { databaseBuilder, expect } from '../../../../test-helper.js'; + +describe('Integration | Identity Access Management | Domain | Usecases | get-user-account-info', function () { + it('returns user account info', async function () { + // given + const user = databaseBuilder.factory.buildUser(); + await databaseBuilder.commit(); + + // when + const userAccountInfo = await usecases.getUserAccountInfo({ userId: user.id }); + + // then + expect(userAccountInfo).to.deep.equal({ + id: user.id, + email: user.email, + username: user.username, + canSelfDeleteAccount: false, + }); + }); +}); From a0a6f36bbaa2f414fd772944a041f7fbddb57da3 Mon Sep 17 00:00:00 2001 From: Benjamin Petetot Date: Tue, 19 Nov 2024 15:55:21 +0100 Subject: [PATCH 3/6] feat(api): add getCurrentUserAccountInfo and serializer --- .../application/user/user.controller.js | 18 +++++++++++ .../jsonapi/user-account-info.serializer.js | 12 ++++++++ .../user-account-info.serializer.test.js | 30 +++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 api/src/identity-access-management/infrastructure/serializers/jsonapi/user-account-info.serializer.js create mode 100644 api/tests/identity-access-management/unit/infrastructure/serializers/jsonapi/user-account-info.serializer.test.js diff --git a/api/src/identity-access-management/application/user/user.controller.js b/api/src/identity-access-management/application/user/user.controller.js index b6b30219362..cc8cf9d0573 100644 --- a/api/src/identity-access-management/application/user/user.controller.js +++ b/api/src/identity-access-management/application/user/user.controller.js @@ -5,6 +5,7 @@ import { usecases } from '../../domain/usecases/index.js'; import { authenticationMethodsSerializer } from '../../infrastructure/serializers/jsonapi/authentication-methods.serializer.js'; import { emailVerificationSerializer } from '../../infrastructure/serializers/jsonapi/email-verification.serializer.js'; import * as updateEmailSerializer from '../../infrastructure/serializers/jsonapi/update-email.serializer.js'; +import { userAccountInfoSerializer } from '../../infrastructure/serializers/jsonapi/user-account-info.serializer.js'; import { userWithActivitySerializer } from '../../infrastructure/serializers/jsonapi/user-with-activity.serializer.js'; const acceptPixCertifTermsOfService = async function (request, h) { @@ -88,6 +89,22 @@ const getCurrentUser = async function (request, h, dependencies = { userWithActi return dependencies.userWithActivitySerializer.serialize(result); }; +/** + * @param request + * @param h + * @param {{ + * userAccountInfoSerializer: UserAccountInfoSerializer + * }} dependencies + * @return {Promise<*>} + */ +const getCurrentUserAccountInfo = async function (request, h, dependencies = { userAccountInfoSerializer }) { + const authenticatedUserId = request.auth.credentials.userId; + + const userAccountInfo = await usecases.getUserAccountInfo({ userId: authenticatedUserId }); + + return dependencies.userAccountInfoSerializer.serialize(userAccountInfo); +}; + /** * @param request * @param h @@ -214,6 +231,7 @@ export const userController = { acceptPixOrgaTermsOfService, changeUserLanguage, getCurrentUser, + getCurrentUserAccountInfo, getUserAuthenticationMethods, rememberUserHasSeenLastDataProtectionPolicyInformation, createUser, diff --git a/api/src/identity-access-management/infrastructure/serializers/jsonapi/user-account-info.serializer.js b/api/src/identity-access-management/infrastructure/serializers/jsonapi/user-account-info.serializer.js new file mode 100644 index 00000000000..bd6cdc8cdc7 --- /dev/null +++ b/api/src/identity-access-management/infrastructure/serializers/jsonapi/user-account-info.serializer.js @@ -0,0 +1,12 @@ +import jsonapiSerializer from 'jsonapi-serializer'; + +const { Serializer } = jsonapiSerializer; + +const serialize = function (userAccountInfo, meta) { + return new Serializer('account-info', { + attributes: ['email', 'username', 'canSelfDeleteAccount'], + meta, + }).serialize(userAccountInfo); +}; + +export const userAccountInfoSerializer = { serialize }; diff --git a/api/tests/identity-access-management/unit/infrastructure/serializers/jsonapi/user-account-info.serializer.test.js b/api/tests/identity-access-management/unit/infrastructure/serializers/jsonapi/user-account-info.serializer.test.js new file mode 100644 index 00000000000..f15f05de415 --- /dev/null +++ b/api/tests/identity-access-management/unit/infrastructure/serializers/jsonapi/user-account-info.serializer.test.js @@ -0,0 +1,30 @@ +import { userAccountInfoSerializer } from '../../../../../../src/identity-access-management/infrastructure/serializers/jsonapi/user-account-info.serializer.js'; +import { expect } from '../../../../../test-helper.js'; + +describe('Unit | Identity Access Management | Infrastructure | Serializer | JSONAPI | user-account-info', function () { + describe('#serialize', function () { + it('serializes user account information', function () { + // given + const userAccountInfo = { + email: 'user@email.com', + username: 'my-username', + canSelfDeleteAccount: true, + }; + + // when + const json = userAccountInfoSerializer.serialize(userAccountInfo); + + // then + expect(json).to.be.deep.equal({ + data: { + type: 'account-infos', + attributes: { + email: 'user@email.com', + username: 'my-username', + 'can-self-delete-account': true, + }, + }, + }); + }); + }); +}); From 5780b54f243e5fed22c02bd063d242b5617830f2 Mon Sep 17 00:00:00 2001 From: Benjamin Petetot Date: Tue, 19 Nov 2024 15:56:07 +0100 Subject: [PATCH 4/6] feat(api): create GET /api/users/my-account endpoint --- .../application/user/user.route.js | 12 ++++++++ .../application/user/user.route.test.js | 29 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/api/src/identity-access-management/application/user/user.route.js b/api/src/identity-access-management/application/user/user.route.js index 6b475ad9d8b..48151b477e8 100644 --- a/api/src/identity-access-management/application/user/user.route.js +++ b/api/src/identity-access-management/application/user/user.route.js @@ -80,6 +80,18 @@ export const userRoutes = [ tags: ['identity-access-management', 'api', 'user'], }, }, + { + method: 'GET', + path: '/api/users/my-account', + config: { + handler: (request, h) => userController.getCurrentUserAccountInfo(request, h), + notes: [ + '- **Cette route est restreinte aux utilisateurs authentifiés**\n' + + '- Récupération des information de compte utilisateur\n', + ], + tags: ['identity-access-management', 'api', 'user', 'my-account'], + }, + }, { method: 'GET', path: '/api/users/{id}/authentication-methods', diff --git a/api/tests/identity-access-management/acceptance/application/user/user.route.test.js b/api/tests/identity-access-management/acceptance/application/user/user.route.test.js index 88c9942861d..87da799f397 100644 --- a/api/tests/identity-access-management/acceptance/application/user/user.route.test.js +++ b/api/tests/identity-access-management/acceptance/application/user/user.route.test.js @@ -260,6 +260,35 @@ describe('Acceptance | Identity Access Management | Application | Route | User', }); }); + describe('GET /api/users/my-account', function () { + it('returns 200 HTTP status code', async function () { + // given + const user = databaseBuilder.factory.buildUser(); + await databaseBuilder.commit(); + + // when + const response = await server.inject({ + method: 'GET', + url: '/api/users/my-account', + headers: { authorization: generateValidRequestAuthorizationHeader(user.id) }, + }); + + // then + expect(response.statusCode).to.equal(200); + expect(response.result).to.deep.equal({ + data: { + type: 'my-accounts', + id: user.id.toString(), + attributes: { + 'can-self-delete-account': false, + email: user.email, + username: user.username, + }, + }, + }); + }); + }); + describe('GET /api/users/{id}/authentication-methods', function () { it('returns 200 HTTP status code', async function () { // given From 9c10594cfaf753e63084af5faccd2e33f0fc6e6d Mon Sep 17 00:00:00 2001 From: Benjamin Petetot Date: Tue, 19 Nov 2024 16:57:45 +0100 Subject: [PATCH 5/6] feat(api): add relationship to my-account in GET users/me --- .../application/user/user.route.js | 2 +- .../serializers/jsonapi/user-with-activity.serializer.js | 7 +++++++ .../acceptance/application/user/user.route.test.js | 7 ++++++- .../jsonapi/user-with-activity.serializer.test.js | 5 +++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/api/src/identity-access-management/application/user/user.route.js b/api/src/identity-access-management/application/user/user.route.js index 48151b477e8..cfd021438fd 100644 --- a/api/src/identity-access-management/application/user/user.route.js +++ b/api/src/identity-access-management/application/user/user.route.js @@ -87,7 +87,7 @@ export const userRoutes = [ handler: (request, h) => userController.getCurrentUserAccountInfo(request, h), notes: [ '- **Cette route est restreinte aux utilisateurs authentifiés**\n' + - '- Récupération des information de compte utilisateur\n', + '- Récupération des informations du compte utilisateur authentifié\n', ], tags: ['identity-access-management', 'api', 'user', 'my-account'], }, diff --git a/api/src/identity-access-management/infrastructure/serializers/jsonapi/user-with-activity.serializer.js b/api/src/identity-access-management/infrastructure/serializers/jsonapi/user-with-activity.serializer.js index ebcb11e9746..e0bdbd1c397 100644 --- a/api/src/identity-access-management/infrastructure/serializers/jsonapi/user-with-activity.serializer.js +++ b/api/src/identity-access-management/infrastructure/serializers/jsonapi/user-with-activity.serializer.js @@ -18,6 +18,7 @@ const serialize = function (users, meta) { 'pixCertifTermsOfServiceAccepted', 'lang', 'isAnonymous', + 'accountInfo', 'profile', 'hasSeenAssessmentInstructions', 'isCertifiable', @@ -60,6 +61,12 @@ const serialize = function (users, meta) { }, }, }, + accountInfo: { + ref: 'id', + ignoreRelationshipData: true, + nullIfMissing: true, + relationshipLinks: { related: () => '/api/users/my-account' }, + }, meta, }).serialize(users); }; diff --git a/api/tests/identity-access-management/acceptance/application/user/user.route.test.js b/api/tests/identity-access-management/acceptance/application/user/user.route.test.js index 87da799f397..c189b80fdc3 100644 --- a/api/tests/identity-access-management/acceptance/application/user/user.route.test.js +++ b/api/tests/identity-access-management/acceptance/application/user/user.route.test.js @@ -232,6 +232,11 @@ describe('Acceptance | Identity Access Management | Application | Route | User', 'last-data-protection-policy-seen-at': null, }, relationships: { + 'account-info': { + links: { + related: '/api/users/my-account', + }, + }, profile: { links: { related: `/api/users/${user.id}/profile`, @@ -277,7 +282,7 @@ describe('Acceptance | Identity Access Management | Application | Route | User', expect(response.statusCode).to.equal(200); expect(response.result).to.deep.equal({ data: { - type: 'my-accounts', + type: 'account-infos', id: user.id.toString(), attributes: { 'can-self-delete-account': false, diff --git a/api/tests/identity-access-management/unit/infrastructure/serializers/jsonapi/user-with-activity.serializer.test.js b/api/tests/identity-access-management/unit/infrastructure/serializers/jsonapi/user-with-activity.serializer.test.js index 3484d496586..50a9beea57b 100644 --- a/api/tests/identity-access-management/unit/infrastructure/serializers/jsonapi/user-with-activity.serializer.test.js +++ b/api/tests/identity-access-management/unit/infrastructure/serializers/jsonapi/user-with-activity.serializer.test.js @@ -65,6 +65,11 @@ describe('Unit | Identity Access Management | Infrastructure | Serializer | JSON userModelObject.shouldSeeDataProtectionPolicyInformationBanner, }, relationships: { + 'account-info': { + links: { + related: '/api/users/my-account', + }, + }, profile: { links: { related: `/api/users/${userModelObject.id}/profile`, From 1fb4563d722fc96a227afa154d2335ee09a09fd1 Mon Sep 17 00:00:00 2001 From: Benjamin Petetot Date: Tue, 19 Nov 2024 16:58:48 +0100 Subject: [PATCH 6/6] feat(mon-pix): call /my-account endpoint on MyAccount page --- mon-pix/app/models/account-info.js | 7 +++++++ mon-pix/app/models/user.js | 1 + mon-pix/app/templates/authenticated/user-account.hbs | 5 +++++ 3 files changed, 13 insertions(+) create mode 100644 mon-pix/app/models/account-info.js diff --git a/mon-pix/app/models/account-info.js b/mon-pix/app/models/account-info.js new file mode 100644 index 00000000000..43fee59bedf --- /dev/null +++ b/mon-pix/app/models/account-info.js @@ -0,0 +1,7 @@ +import Model, { attr } from '@ember-data/model'; + +export default class AccountInfo extends Model { + @attr('string') email; + @attr('string') username; + @attr('boolean') canSelfDeleteAccount; +} diff --git a/mon-pix/app/models/user.js b/mon-pix/app/models/user.js index d7cd6b6cfd0..f6f917c4ffb 100644 --- a/mon-pix/app/models/user.js +++ b/mon-pix/app/models/user.js @@ -26,6 +26,7 @@ export default class User extends Model { // includes @belongsTo('is-certifiable', { async: true, inverse: null }) isCertifiable; @belongsTo('profile', { async: true, inverse: null }) profile; + @belongsTo('account-info', { async: true, inverse: null }) accountInfo; @hasMany('certification', { async: true, inverse: 'user' }) certifications; @hasMany('scorecard', { async: true, inverse: null }) scorecards; @hasMany('training', { async: true, inverse: null }) trainings; diff --git a/mon-pix/app/templates/authenticated/user-account.hbs b/mon-pix/app/templates/authenticated/user-account.hbs index 65ce44d7748..fe78beff1b8 100644 --- a/mon-pix/app/templates/authenticated/user-account.hbs +++ b/mon-pix/app/templates/authenticated/user-account.hbs @@ -32,6 +32,11 @@ {{/if}} + {{#if @model.accountInfo.canSelfDeleteAccount}} +
  • + {{! ADD DELETE ACCOUNT LINK HERE }} +
  • + {{/if}}