Skip to content

Commit

Permalink
[FEATURE] Générer les identifiants en masse pour les élèves (PIX-12975)…
Browse files Browse the repository at this point in the history
… (#10096)

Co-authored-by: Marianne Bost <[email protected]>
  • Loading branch information
2 people authored and bpetetot committed Sep 25, 2024
1 parent f3803f2 commit de44845
Show file tree
Hide file tree
Showing 44 changed files with 1,585 additions and 407 deletions.
321 changes: 220 additions & 101 deletions api/db/seeds/data/team-acces/build-sco-organization-learners.js

Large diffs are not rendered by default.

30 changes: 29 additions & 1 deletion api/db/seeds/data/team-acces/build-sco-organizations.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ import { NON_OIDC_IDENTITY_PROVIDERS } from '../../../../src/identity-access-man
import { Membership } from '../../../../src/shared/domain/models/Membership.js';
import { PIX_PUBLIC_TARGET_PROFILE_ID, REAL_PIX_SUPER_ADMIN_ID } from '../common/constants.js';
import { PIX_ORGA_ADMIN_LEAVING_ID, PIX_ORGA_ALL_ORGA_ID } from './build-organization-users.js';
import { ACCESS_SCO_BAUDELAIRE_EXTERNAL_ID, SCO_ORGANIZATION_ID } from './constants.js';
import {
ACCESS_SCO_BAUDELAIRE_EXTERNAL_ID,
ACCESS_SCO_NO_GAR_EXTERNAL_ID,
SCO_NO_GAR_ORGANIZATION_ID,
SCO_ORGANIZATION_ID,
} from './constants.js';

export function buildScoOrganizations(databaseBuilder) {
_buildCollegeHouseOfTheDragonOrganization(databaseBuilder);
_buildJosephineBaker(databaseBuilder);
_buildScoManagingStudentsNoGarOrganization(databaseBuilder);
}

function _buildCollegeHouseOfTheDragonOrganization(databaseBuilder) {
Expand Down Expand Up @@ -86,6 +92,28 @@ function _buildJosephineBaker(databaseBuilder) {
].forEach(_buildAdminMembership(databaseBuilder));
}

function _buildScoManagingStudentsNoGarOrganization(databaseBuilder) {
const organization = databaseBuilder.factory.buildOrganization({
id: SCO_NO_GAR_ORGANIZATION_ID,
type: 'SCO',
name: 'Lycée pas-GAR',
isManagingStudents: true,
email: '[email protected]',
externalId: ACCESS_SCO_NO_GAR_EXTERNAL_ID,
documentationUrl: 'https://pix.fr/',
provinceCode: '13',
createdBy: REAL_PIX_SUPER_ADMIN_ID,
});

[
{
userId: PIX_ORGA_ALL_ORGA_ID,
organizationId: organization.id,
organizationRole: Membership.roles.ADMIN,
},
].forEach(_buildAdminMembership(databaseBuilder));
}

function _buildAdminMembership(databaseBuilder) {
return function ({ userId, organizationId, organizationRole }) {
databaseBuilder.factory.buildMembership({
Expand Down
9 changes: 8 additions & 1 deletion api/db/seeds/data/team-acces/constants.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
const ACCESS_SCO_BAUDELAIRE_EXTERNAL_ID = 'ACCESS_SCO_BAUDELAIRE';
const ACCESS_SCO_NO_GAR_EXTERNAL_ID = 'ACCESS_SCO_NO_GAR';
const SCO_ORGANIZATION_ID = 2023;
const SCO_NO_GAR_ORGANIZATION_ID = 2024;

export { ACCESS_SCO_BAUDELAIRE_EXTERNAL_ID, SCO_ORGANIZATION_ID };
export {
ACCESS_SCO_BAUDELAIRE_EXTERNAL_ID,
ACCESS_SCO_NO_GAR_EXTERNAL_ID,
SCO_NO_GAR_ORGANIZATION_ID,
SCO_ORGANIZATION_ID,
};
39 changes: 38 additions & 1 deletion api/lib/application/sco-organization-learners/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,43 @@ const register = async function (server) {
tags: ['api', 'sco-organization-learners'],
},
},
{
method: 'POST',
path: '/api/sco-organization-learners/batch-username-password-generate',
config: {
pre: [
{
method: securityPreHandlers.checkUserBelongsToScoOrganizationAndManagesStudents,
assign: 'belongsToScoOrganizationAndManageStudents',
},
],
handler: scoOrganizationLearnerController.batchGenerateOrganizationLearnersUsernameWithTemporaryPassword,
validate: {
options: {
allowUnknown: true,
},
payload: Joi.object({
data: {
attributes: {
'organization-id': identifiersType.campaignId,
'organization-learners-id': Joi.array().items(identifiersType.organizationLearnerId),
},
},
}),
failAction: (request, h) => {
return sendJsonApiError(
new BadRequestError('The server could not understand the request due to invalid syntax.'),
h,
);
},
},
notes: [
"- Réinitialise, avec un mot de passe à usage unique, les mots de passe des élèves dont les identifiants sont passés en paramètre et qui ont un identifiant comme méthode d'authentification\n" +
"- La demande de modification du mot de passe doit être effectuée par un membre de l'organisation à laquelle appartiennent les élèves.",
],
tags: ['api', 'sco-organization-learners'],
},
},
{
method: 'POST',
path: '/api/sco-organization-learners/password-reset',
Expand All @@ -191,7 +228,7 @@ const register = async function (server) {
assign: 'belongsToScoOrganizationAndManageStudents',
},
],
handler: scoOrganizationLearnerController.updateOrganizationLearnersPassword,
handler: scoOrganizationLearnerController.batchGenerateOrganizationLearnersUsernameWithTemporaryPassword,
validate: {
options: {
allowUnknown: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,14 @@ const checkScoAccountRecovery = async function (
);
};

const updateOrganizationLearnersPassword = async function (request, h) {
const batchGenerateOrganizationLearnersUsernameWithTemporaryPassword = async function (request, h) {
const payload = request.payload.data.attributes;
const userId = request.auth.credentials.userId;
const organizationId = payload['organization-id'];
const organizationLearnersId = payload['organization-learners-id'];

const generatedCsvContent = await DomainTransaction.execute(async () => {
const organizationLearnersPasswordResets = await usecases.resetOrganizationLearnersPassword({
const organizationLearnersPasswordResets = await usecases.generateOrganizationLearnersUsernameAndTemporaryPassword({
userId,
organizationId,
organizationLearnersId,
Expand All @@ -198,7 +198,7 @@ const scoOrganizationLearnerController = {
updatePassword,
generateUsernameWithTemporaryPassword,
checkScoAccountRecovery,
updateOrganizationLearnersPassword,
batchGenerateOrganizationLearnersUsernameWithTemporaryPassword,
};

export { scoOrganizationLearnerController };
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { OrganizationLearnerIdentities } from '../../../src/identity-access-management/domain/models/OrganizationLearnerIdentities.js';
import { UserNotAuthorizedToUpdatePasswordError } from '../../../src/shared/domain/errors.js';
import { OrganizationLearnerPasswordResetDTO } from '../../../src/shared/domain/models/OrganizationLearnerPasswordResetDTO.js';
import {
ORGANIZATION_LEARNER_DOES_NOT_BELONG_TO_ORGANIZATION_CODE,
ORGANIZATION_LEARNER_WITHOUT_USERNAME_CODE,
} from '../constants/generate-organization-learners-username-and-temporary-password-errors.js';

const generateOrganizationLearnersUsernameAndTemporaryPassword = async function ({
organizationId,
organizationLearnersId,
userId,
cryptoService,
passwordGenerator,
userReconciliationService,
authenticationMethodRepository,
organizationRepository,
organizationLearnerIdentityRepository,
userRepository,
}) {
const errorMessage = `User ${userId} cannot reset passwords of some students in organization ${organizationId}`;
const organization = await organizationRepository.get(organizationId);
const organizationLearnerIdentities = await _buildOrganizationLearnerIdentities({
errorMessage,
organization,
organizationLearnersId,
organizationLearnerIdentityRepository,
});
let organizationLearnerIdentitiesValues = organizationLearnerIdentities.values;

if (!organizationLearnerIdentities.hasScoGarIdentityProvider) {
organizationLearnerIdentitiesValues = await _generateAndUpdateUsernameForOrganizationLearnerIdentities({
organizationLearnerIdentities: organizationLearnerIdentitiesValues,
userReconciliationService,
userRepository,
});
}

const userIdWithPasswords = await _generateAndUpdateUsersWithTemporaryPassword({
errorMessage,
organizationLearnerIdentities: organizationLearnerIdentitiesValues,
authenticationMethodRepository,
cryptoService,
passwordGenerator,
});

return _buildOrganizationLearnerPasswordResetDTOs({
organizationLearnerIdentities: organizationLearnerIdentitiesValues,
userIdWithPasswords,
});
};

async function _buildOrganizationLearnerIdentities({
errorMessage,
organization,
organizationLearnersId,
organizationLearnerIdentityRepository,
}) {
try {
const organizationLearnerIdentities = await organizationLearnerIdentityRepository.getByIds(organizationLearnersId);

return new OrganizationLearnerIdentities({
id: organization.id,
hasScoGarIdentityProvider: organization.hasGarIdentityProvider,
values: organizationLearnerIdentities,
});
} catch (error) {
throw new UserNotAuthorizedToUpdatePasswordError(
errorMessage,
ORGANIZATION_LEARNER_DOES_NOT_BELONG_TO_ORGANIZATION_CODE,
);
}
}

async function _generateAndUpdateUsernameForOrganizationLearnerIdentities({
organizationLearnerIdentities,
userReconciliationService,
userRepository,
}) {
const result = [];
for (const organizationLearnerIdentity of organizationLearnerIdentities) {
const temporaryOrganizationLearnerIdentity = { ...organizationLearnerIdentity };
if (!organizationLearnerIdentity.username) {
const username = await userReconciliationService.createUsernameByUser({
user: organizationLearnerIdentity,
userRepository,
});
temporaryOrganizationLearnerIdentity.username = username;
await userRepository.updateUsername({ id: organizationLearnerIdentity.userId, username });
}

result.push(temporaryOrganizationLearnerIdentity);
}

return result;
}

async function _generateAndUpdateUsersWithTemporaryPassword({
errorMessage,
organizationLearnerIdentities,
authenticationMethodRepository,
cryptoService,
passwordGenerator,
}) {
_assertAllUsersHasAnUsername({ errorMessage, users: organizationLearnerIdentities });

const userIdWithPasswords = await _generateNewTemporaryPasswordForOrganizationLearnerIdentities({
organizationLearnerIdentities,
passwordGenerator,
cryptoService,
});
await authenticationMethodRepository.batchUpsertPasswordThatShouldBeChanged({
usersToUpdateWithNewPassword: userIdWithPasswords,
});

return userIdWithPasswords;
}

function _assertAllUsersHasAnUsername({ errorMessage, users }) {
const usersHaveAnUsername = users.every((student) => student.username);

if (!usersHaveAnUsername) {
throw new UserNotAuthorizedToUpdatePasswordError(errorMessage, ORGANIZATION_LEARNER_WITHOUT_USERNAME_CODE);
}
}

async function _generateNewTemporaryPasswordForOrganizationLearnerIdentities({
organizationLearnerIdentities,
passwordGenerator,
cryptoService,
}) {
return await Promise.all(
organizationLearnerIdentities.map(async ({ userId }) => {
const generatedPassword = passwordGenerator.generateSimplePassword();
const hashedPassword = await cryptoService.hashPassword(generatedPassword);

return { userId, hashedPassword, generatedPassword };
}),
);
}

function _buildOrganizationLearnerPasswordResetDTOs({ organizationLearnerIdentities, userIdWithPasswords }) {
const userIdWithPasswordsMap = new Map(
userIdWithPasswords.map(({ userId, generatedPassword }) => [userId, generatedPassword]),
);

return organizationLearnerIdentities.map(
({ userId, division, firstName, lastName, username }) =>
new OrganizationLearnerPasswordResetDTO({
division,
lastName,
firstName,
password: userIdWithPasswordsMap.get(userId),
username,
}),
);
}

export { generateOrganizationLearnersUsernameAndTemporaryPassword };
2 changes: 2 additions & 0 deletions api/lib/domain/usecases/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { accountRecoveryDemandRepository } from '../../../src/identity-access-ma
import * as authenticationMethodRepository from '../../../src/identity-access-management/infrastructure/repositories/authentication-method.repository.js';
import { emailValidationDemandRepository } from '../../../src/identity-access-management/infrastructure/repositories/email-validation-demand.repository.js';
import * as oidcProviderRepository from '../../../src/identity-access-management/infrastructure/repositories/oidc-provider-repository.js';
import { organizationLearnerIdentityRepository } from '../../../src/identity-access-management/infrastructure/repositories/organization-learner-identity.repository.js';
import { refreshTokenRepository } from '../../../src/identity-access-management/infrastructure/repositories/refresh-token.repository.js';
import { resetPasswordDemandRepository } from '../../../src/identity-access-management/infrastructure/repositories/reset-password-demand.repository.js';
import * as userRepository from '../../../src/identity-access-management/infrastructure/repositories/user.repository.js';
Expand Down Expand Up @@ -280,6 +281,7 @@ const dependencies = {
organizationInvitationRepository,
organizationInvitationService,
organizationLearnerActivityRepository,
organizationLearnerIdentityRepository,
organizationLearnerRepository,
organizationMemberIdentityRepository,
organizationRepository,
Expand Down
72 changes: 0 additions & 72 deletions api/lib/domain/usecases/reset-organization-learners-password.js

This file was deleted.

Loading

0 comments on commit de44845

Please sign in to comment.