-
Notifications
You must be signed in to change notification settings - Fork 57
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[TECH] Ajouter une API de suppression de compte utilisateur (PIX-14910)
- Loading branch information
Showing
20 changed files
with
673 additions
and
363 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 9 additions & 77 deletions
86
api/src/identity-access-management/domain/usecases/anonymize-user.usecase.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,105 +1,37 @@ | ||
import { config } from '../../../shared/config.js'; | ||
import { withTransaction } from '../../../shared/domain/DomainTransaction.js'; | ||
import { UserNotFoundError } from '../../../shared/domain/errors.js'; | ||
import { UserAnonymizedEventLoggingJob } from '../models/UserAnonymizedEventLoggingJob.js'; | ||
|
||
/** | ||
* @param params | ||
* @param{string} params.userId | ||
* @param{string} params.updatedByUserId | ||
* @param{boolean} params.preventAuditLogging | ||
* @param{UserRepository} params.userRepository | ||
* @param{AuthenticationMethodRepository} params.authenticationMethodRepository | ||
* @param{MembershipRepository} params.membershipRepository | ||
* @param{CertificationCenterMembershipRepository} params.certificationCenterMembershipRepository | ||
* @param{OrganizationLearnerRepository} params.organizationLearnerRepository | ||
* @param{RefreshTokenRepository} params.refreshTokenRepository | ||
* @param{ResetPasswordDemandRepository} params.resetPasswordDemandRepository | ||
* @param{UserLoginRepository} params.userLoginRepository | ||
* @param{AdminMemberRepository} params.adminMemberRepository | ||
* @param{UserAnonymizedEventLoggingJobRepository} params.userAnonymizedEventLoggingJobRepository | ||
* @returns {Promise<null>} | ||
*/ | ||
const anonymizeUser = async function ({ | ||
const anonymizeUser = withTransaction(async function ({ | ||
userId, | ||
updatedByUserId, | ||
preventAuditLogging = false, | ||
userRepository, | ||
authenticationMethodRepository, | ||
membershipRepository, | ||
certificationCenterMembershipRepository, | ||
organizationLearnerRepository, | ||
refreshTokenRepository, | ||
resetPasswordDemandRepository, | ||
userLoginRepository, | ||
adminMemberRepository, | ||
userAnonymizedEventLoggingJobRepository, | ||
privacyUsersApiRepository, | ||
}) { | ||
const user = await userRepository.get(userId); | ||
|
||
const anonymizedBy = await _getAdminUser({ | ||
adminUserId: updatedByUserId || user.hasBeenAnonymisedBy, | ||
adminUserId: updatedByUserId, | ||
adminMemberRepository, | ||
}); | ||
|
||
await authenticationMethodRepository.removeAllAuthenticationMethodsByUserId({ userId }); | ||
|
||
await refreshTokenRepository.revokeAllByUserId({ userId }); | ||
|
||
if (user.email) { | ||
await resetPasswordDemandRepository.removeAllByEmail(user.email); | ||
} | ||
|
||
await membershipRepository.disableMembershipsByUserId({ userId, updatedByUserId: anonymizedBy?.userId }); | ||
|
||
await certificationCenterMembershipRepository.disableMembershipsByUserId({ | ||
updatedByUserId: anonymizedBy?.userId, | ||
userId, | ||
}); | ||
|
||
await organizationLearnerRepository.dissociateAllStudentsByUserId({ userId }); | ||
const anonymizedByUserId = updatedByUserId; | ||
const anonymizedByUserRole = anonymizedBy.role; | ||
const client = 'PIX_ADMIN'; | ||
|
||
await _anonymizeUserLogin({ userId, userLoginRepository }); | ||
|
||
await _anonymizeUser({ user, anonymizedByUserId: anonymizedBy?.userId, userRepository }); | ||
|
||
if (anonymizedBy && !preventAuditLogging && config.auditLogger.isEnabled) { | ||
await userAnonymizedEventLoggingJobRepository.performAsync( | ||
new UserAnonymizedEventLoggingJob({ | ||
userId, | ||
updatedByUserId: anonymizedBy.userId, | ||
role: anonymizedBy.role, | ||
}), | ||
); | ||
} | ||
return null; | ||
}; | ||
await privacyUsersApiRepository.anonymizeUser({ userId, anonymizedByUserId, anonymizedByUserRole, client }); | ||
}); | ||
|
||
async function _getAdminUser({ adminUserId, adminMemberRepository }) { | ||
if (!adminUserId) return undefined; | ||
|
||
const admin = await adminMemberRepository.get({ userId: adminUserId }); | ||
if (!admin) { | ||
throw new UserNotFoundError(`Admin not found for id: ${adminUserId}`); | ||
} | ||
return admin; | ||
} | ||
|
||
async function _anonymizeUserLogin({ userId, userLoginRepository }) { | ||
const userLogin = await userLoginRepository.findByUserId(userId); | ||
if (!userLogin) return; | ||
|
||
const anonymizedUserLogin = userLogin.anonymize(); | ||
|
||
await userLoginRepository.update(anonymizedUserLogin, { preventUpdatedAt: true }); | ||
} | ||
|
||
async function _anonymizeUser({ user, anonymizedByUserId, userRepository }) { | ||
const anonymizedUser = user.anonymize(anonymizedByUserId).mapToDatabaseDto(); | ||
|
||
await userRepository.updateUserDetailsForAdministration( | ||
{ id: user.id, userAttributes: anonymizedUser }, | ||
{ preventUpdatedAt: true }, | ||
); | ||
} | ||
|
||
export { anonymizeUser }; |
24 changes: 24 additions & 0 deletions
24
api/src/identity-access-management/domain/usecases/self-delete-user-account.usecase.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { ForbiddenAccess } from '../../../shared/domain/errors.js'; | ||
|
||
/** | ||
* @typedef {import('../../infrastructure/repositories/privacy-users-api.repository.js')} PrivacyUsersApiRepository | ||
*/ | ||
|
||
/** | ||
* @param{object} params | ||
* @param{number} params.userId | ||
* @param{PrivacyUsersApiRepository} privacyUsersApiRepository | ||
* @returns {Promise<boolean>} | ||
*/ | ||
export const selfDeleteUserAccount = async function ({ userId, privacyUsersApiRepository }) { | ||
const canSelfDeleteAccount = await privacyUsersApiRepository.canSelfDeleteAccount({ userId }); | ||
|
||
if (!canSelfDeleteAccount) { | ||
throw new ForbiddenAccess(); | ||
} | ||
|
||
const anonymizedByUserId = userId; | ||
const anonymizedByUserRole = 'USER'; | ||
const client = 'PIX_APP'; | ||
await privacyUsersApiRepository.anonymizeUser({ userId, anonymizedByUserId, anonymizedByUserRole, client }); | ||
}; |
12 changes: 11 additions & 1 deletion
12
...rc/identity-access-management/infrastructure/repositories/privacy-users-api.repository.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,17 @@ | ||
import * as privacyUsersApi from '../../../privacy/application/api/users-api.js'; | ||
|
||
const anonymizeUser = async ({ | ||
userId, | ||
anonymizedByUserId, | ||
anonymizedByUserRole, | ||
client, | ||
dependencies = { privacyUsersApi }, | ||
}) => { | ||
return dependencies.privacyUsersApi.anonymizeUser({ userId, anonymizedByUserId, anonymizedByUserRole, client }); | ||
}; | ||
|
||
const canSelfDeleteAccount = async ({ userId, dependencies = { privacyUsersApi } }) => { | ||
return dependencies.privacyUsersApi.canSelfDeleteAccount({ userId }); | ||
}; | ||
|
||
export { canSelfDeleteAccount }; | ||
export { anonymizeUser, canSelfDeleteAccount }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,27 @@ | ||
import { usecases } from '../../domain/usecases/index.js'; | ||
|
||
/** | ||
* Anonymizes everything related to the user. | ||
* | ||
* @param{string} params.userId | ||
* @param{string} params.anonymizedByUserId | ||
* @param{string} params.anonymizedByUserRole | ||
* @param{string} params.client | ||
* @returns {Promise<void>} | ||
*/ | ||
const anonymizeUser = async ({ userId, anonymizedByUserId, anonymizedByUserRole, client }) => { | ||
return usecases.anonymizeUser({ userId, anonymizedByUserId, anonymizedByUserRole, client }); | ||
}; | ||
|
||
/** | ||
* Determines if a user can self-delete their account. | ||
* | ||
* @param {Object} params - The parameters for the function. | ||
* @param {number} params.userId - The ID of the user. | ||
* @returns {Promise<boolean>} - A promise that resolves to a boolean indicating if the user can self-delete their account. | ||
*/ | ||
export const canSelfDeleteAccount = async ({ userId }) => { | ||
const canSelfDeleteAccount = async ({ userId }) => { | ||
return usecases.canSelfDeleteAccount({ userId }); | ||
}; | ||
|
||
export { anonymizeUser, canSelfDeleteAccount }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { UserAnonymizedEventLoggingJob } from '../../../identity-access-management/domain/models/UserAnonymizedEventLoggingJob.js'; | ||
import { config } from '../../../shared/config.js'; | ||
import { withTransaction } from '../../../shared/domain/DomainTransaction.js'; | ||
|
||
/** | ||
* @param params | ||
* @param{string} params.userId | ||
* @param{string} params.anonymizedByUserId | ||
* @param{string} params.anonymizedByUserRole | ||
* @param{string} params.client | ||
* @param{UserRepository} params.userRepository | ||
* @param{AuthenticationMethodRepository} params.authenticationMethodRepository | ||
* @param{MembershipRepository} params.membershipRepository | ||
* @param{CertificationCenterMembershipRepository} params.certificationCenterMembershipRepository | ||
* @param{OrganizationLearnerRepository} params.organizationLearnerRepository | ||
* @param{RefreshTokenRepository} params.refreshTokenRepository | ||
* @param{ResetPasswordDemandRepository} params.resetPasswordDemandRepository | ||
* @param{UserLoginRepository} params.userLoginRepository | ||
* @param{UserAnonymizedEventLoggingJobRepository} params.userAnonymizedEventLoggingJobRepository | ||
* @returns {Promise<void>} | ||
*/ | ||
const anonymizeUser = withTransaction(async function ({ | ||
userId, | ||
anonymizedByUserId, | ||
anonymizedByUserRole, | ||
client, | ||
userRepository, | ||
authenticationMethodRepository, | ||
membershipRepository, | ||
certificationCenterMembershipRepository, | ||
organizationLearnerRepository, | ||
refreshTokenRepository, | ||
resetPasswordDemandRepository, | ||
userLoginRepository, | ||
userAnonymizedEventLoggingJobRepository, | ||
}) { | ||
const user = await userRepository.get(userId); | ||
|
||
await userRepository.get(anonymizedByUserId); | ||
|
||
await authenticationMethodRepository.removeAllAuthenticationMethodsByUserId({ userId }); | ||
|
||
await refreshTokenRepository.revokeAllByUserId({ userId }); | ||
|
||
if (user.email) { | ||
await resetPasswordDemandRepository.removeAllByEmail(user.email); | ||
} | ||
|
||
await membershipRepository.disableMembershipsByUserId({ userId, updatedByUserId: anonymizedByUserId }); | ||
|
||
await certificationCenterMembershipRepository.disableMembershipsByUserId({ | ||
updatedByUserId: anonymizedByUserId, | ||
userId, | ||
}); | ||
|
||
await organizationLearnerRepository.dissociateAllStudentsByUserId({ userId }); | ||
|
||
await _anonymizeUserLogin({ userId, userLoginRepository }); | ||
|
||
await _anonymizeUser({ user, anonymizedByUserId, userRepository }); | ||
|
||
if (config.auditLogger.isEnabled) { | ||
await userAnonymizedEventLoggingJobRepository.performAsync( | ||
new UserAnonymizedEventLoggingJob({ | ||
userId, | ||
updatedByUserId: anonymizedByUserId, | ||
client, | ||
role: anonymizedByUserRole, | ||
}), | ||
); | ||
} | ||
}); | ||
|
||
async function _anonymizeUserLogin({ userId, userLoginRepository }) { | ||
const userLogin = await userLoginRepository.findByUserId(userId); | ||
if (!userLogin) return; | ||
|
||
const anonymizedUserLogin = userLogin.anonymize(); | ||
|
||
await userLoginRepository.update(anonymizedUserLogin, { preventUpdatedAt: true }); | ||
} | ||
|
||
async function _anonymizeUser({ user, anonymizedByUserId, userRepository }) { | ||
const anonymizedUser = user.anonymize(anonymizedByUserId).mapToDatabaseDto(); | ||
|
||
await userRepository.updateUserDetailsForAdministration( | ||
{ id: user.id, userAttributes: anonymizedUser }, | ||
{ preventUpdatedAt: true }, | ||
); | ||
} | ||
|
||
export { anonymizeUser }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,44 @@ | ||
import { dirname, join } from 'node:path'; | ||
import { fileURLToPath } from 'node:url'; | ||
|
||
import * as organizationLearnerRepository from '../../../../lib/infrastructure/repositories/organization-learner-repository.js'; | ||
import * as authenticationMethodRepository from '../../../identity-access-management/infrastructure/repositories/authentication-method.repository.js'; | ||
import { userAnonymizedEventLoggingJobRepository } from '../../../identity-access-management/infrastructure/repositories/jobs/user-anonymized-event-logging-job-repository.js'; | ||
import { refreshTokenRepository } from '../../../identity-access-management/infrastructure/repositories/refresh-token.repository.js'; | ||
import { resetPasswordDemandRepository } from '../../../identity-access-management/infrastructure/repositories/reset-password-demand.repository.js'; | ||
import * as userRepository from '../../../identity-access-management/infrastructure/repositories/user.repository.js'; | ||
import * as userLoginRepository from '../../../shared/infrastructure/repositories/user-login-repository.js'; | ||
import { injectDependencies } from '../../../shared/infrastructure/utils/dependency-injection.js'; | ||
import { importNamedExportsFromDirectory } from '../../../shared/infrastructure/utils/import-named-exports-from-directory.js'; | ||
import { certificationCenterMembershipRepository } from '../../../team/infrastructure/repositories/certification-center-membership.repository.js'; | ||
import * as membershipRepository from '../../../team/infrastructure/repositories/membership.repository.js'; | ||
import * as candidatesApiRepository from '../../infrastructure/repositories/candidates-api.repository.js'; | ||
import * as learnersApiRepository from '../../infrastructure/repositories/learners-api.repository.js'; | ||
import * as userTeamsApiRepository from '../../infrastructure/repositories/user-teams-api.repository.js'; | ||
|
||
const path = dirname(fileURLToPath(import.meta.url)); | ||
|
||
const usecasesWithoutInjectedDependencies = { | ||
...(await importNamedExportsFromDirectory({ path: join(path, './'), ignoredFileNames: ['index.js'] })), | ||
}; | ||
|
||
const dependencies = { | ||
const repositories = { | ||
authenticationMethodRepository, | ||
candidatesApiRepository, | ||
certificationCenterMembershipRepository, | ||
learnersApiRepository, | ||
membershipRepository, | ||
organizationLearnerRepository, | ||
refreshTokenRepository, | ||
resetPasswordDemandRepository, | ||
userAnonymizedEventLoggingJobRepository, | ||
userLoginRepository, | ||
userRepository, | ||
userTeamsApiRepository, | ||
}; | ||
|
||
const usecasesWithoutInjectedDependencies = { | ||
...(await importNamedExportsFromDirectory({ path: join(path, './'), ignoredFileNames: ['index.js'] })), | ||
}; | ||
|
||
const dependencies = Object.assign({}, repositories); | ||
|
||
const usecases = injectDependencies(usecasesWithoutInjectedDependencies, dependencies); | ||
|
||
export { usecases }; |
Oops, something went wrong.