diff --git a/back/src/adapters/primary/routers/admin/createAdminRouter.ts b/back/src/adapters/primary/routers/admin/createAdminRouter.ts index 996183a12f..bee8c62c62 100644 --- a/back/src/adapters/primary/routers/admin/createAdminRouter.ts +++ b/back/src/adapters/primary/routers/admin/createAdminRouter.ts @@ -82,6 +82,17 @@ export const createAdminRouter = (deps: AppDependencies): Router => { ), ); + sharedAdminRouter.addUserForAgency( + deps.inclusionConnectAuthMiddleware, + (req, res) => + sendHttpResponse(req, res.status(201), () => + deps.useCases.updateIcUserRoleForAgency.execute( + req.body, + req.payloads?.currentUser, + ), + ), + ); + sharedAdminRouter.rejectIcUserForAgency( deps.inclusionConnectAuthMiddleware, (req, res) => diff --git a/back/src/domains/inclusion-connected-users/use-cases/CreateUserForAgency.ts b/back/src/domains/inclusion-connected-users/use-cases/CreateUserForAgency.ts new file mode 100644 index 0000000000..2cdbe04487 --- /dev/null +++ b/back/src/domains/inclusion-connected-users/use-cases/CreateUserForAgency.ts @@ -0,0 +1,65 @@ +import { + AgencyRight, + InclusionConnectedUser, + UserCreateParamsForAgency, + errors, + userCreateParamsForAgencySchema, +} from "shared"; +import { createTransactionalUseCase } from "../../core/UseCase"; +import { TimeGateway } from "../../core/time-gateway/ports/TimeGateway"; +import { UuidGenerator } from "../../core/uuid-generator/ports/UuidGenerator"; +import { throwIfNotAdmin } from "../helpers/throwIfIcUserNotBackofficeAdmin"; + +export type CreateUserForAgency = ReturnType; + +export const makeCreateUserForAgency = createTransactionalUseCase< + UserCreateParamsForAgency, + void, + InclusionConnectedUser, + { timeGateway: TimeGateway; uuidGenerator: UuidGenerator } +>( + { + name: "CreateUserForAgency", + inputSchema: userCreateParamsForAgencySchema, + }, + async ({ + inputParams: { agencyId, email, isNotifiedByEmail, roles, userId }, + uow, + currentUser, + deps, + }) => { + throwIfNotAdmin(currentUser); + const agency = await uow.agencyRepository.getById(agencyId); + if (!agency) throw errors.agency.notFound({ agencyId }); + + if (agency.refersToAgencyId && roles.includes("validator")) + throw errors.agency.invalidRoleUpdateForAgencyWithRefersTo({ + agencyId: agency.id, + role: "validator", + }); + + const existingUser = await uow.userRepository.getById(userId); + + if (!existingUser) { + await uow.userRepository.save({ + createdAt: deps.timeGateway.now().toISOString(), + email, + externalId: null, + firstName: "", + id: userId, + lastName: "", + }); + } + + const existingUserAgencyRights = existingUser?.agencyRights ?? []; + const agencyRight: AgencyRight = { + roles, + isNotifiedByEmail: isNotifiedByEmail, + agency, + }; + await uow.userRepository.updateAgencyRights({ + userId: userId, + agencyRights: [...existingUserAgencyRights, agencyRight], + }); + }, +); diff --git a/back/src/domains/inclusion-connected-users/use-cases/CreateUserForAgency.unit.test.ts b/back/src/domains/inclusion-connected-users/use-cases/CreateUserForAgency.unit.test.ts new file mode 100644 index 0000000000..013188f4e5 --- /dev/null +++ b/back/src/domains/inclusion-connected-users/use-cases/CreateUserForAgency.unit.test.ts @@ -0,0 +1,234 @@ +import { + AgencyDtoBuilder, + InclusionConnectedUserBuilder, + errors, + expectPromiseToFailWithError, + expectToEqual, +} from "shared"; +import { InMemoryAgencyRepository } from "../../agency/adapters/InMemoryAgencyRepository"; +import { InMemoryUserRepository } from "../../core/authentication/inclusion-connect/adapters/InMemoryUserRepository"; +import { CustomTimeGateway } from "../../core/time-gateway/adapters/CustomTimeGateway"; +import { TimeGateway } from "../../core/time-gateway/ports/TimeGateway"; +import { InMemoryUowPerformer } from "../../core/unit-of-work/adapters/InMemoryUowPerformer"; +import { createInMemoryUow } from "../../core/unit-of-work/adapters/createInMemoryUow"; +import { TestUuidGenerator } from "../../core/uuid-generator/adapters/UuidGeneratorImplementations"; +import { UuidGenerator } from "../../core/uuid-generator/ports/UuidGenerator"; +import { + CreateUserForAgency, + makeCreateUserForAgency, +} from "./CreateUserForAgency"; + +const backofficeAdminUser = new InclusionConnectedUserBuilder() + .withId("backoffice-admin-id") + .withIsAdmin(true) + .build(); + +const notAdminUser = new InclusionConnectedUserBuilder() + .withId("not-admin-id") + .withIsAdmin(false) + .build(); + +const agency = new AgencyDtoBuilder() + .withCounsellorEmails(["fake-email@gmail.com"]) + .build(); + +describe("CreateUserForAgency", () => { + let createUserForAgency: CreateUserForAgency; + let uowPerformer: InMemoryUowPerformer; + let userRepository: InMemoryUserRepository; + let agencyRepository: InMemoryAgencyRepository; + let timeGateway: TimeGateway; + let uuidGenerator: UuidGenerator; + + beforeEach(() => { + const uow = createInMemoryUow(); + + userRepository = uow.userRepository; + agencyRepository = uow.agencyRepository; + uowPerformer = new InMemoryUowPerformer(uow); + userRepository.setInclusionConnectedUsers([ + backofficeAdminUser, + notAdminUser, + ]); + timeGateway = new CustomTimeGateway(); + uuidGenerator = new TestUuidGenerator(); + createUserForAgency = makeCreateUserForAgency({ + uowPerformer, + deps: { timeGateway, uuidGenerator }, + }); + agencyRepository.setAgencies([agency]); + }); + + it("throws Forbidden if token payload is not backoffice token", async () => { + await expectPromiseToFailWithError( + createUserForAgency.execute( + { + userId: uuidGenerator.new(), + roles: ["counsellor"], + agencyId: "agency-1", + isNotifiedByEmail: true, + email: "any@email.fr", + }, + notAdminUser, + ), + errors.user.forbidden({ userId: notAdminUser.id }), + ); + }); + + it("throws not found if agency does not exist", async () => { + userRepository.setInclusionConnectedUsers([ + backofficeAdminUser, + { + ...notAdminUser, + agencyRights: [], + dashboards: { + agencies: {}, + establishments: {}, + }, + }, + ]); + + const agencyId = "Fake-Agency-Id"; + + await expectPromiseToFailWithError( + createUserForAgency.execute( + { + userId: uuidGenerator.new(), + roles: ["counsellor"], + agencyId, + isNotifiedByEmail: true, + email: "notAdminUser@email.fr", + }, + backofficeAdminUser, + ), + errors.agency.notFound({ + agencyId, + }), + ); + }); + + describe("Agency with refers to agency", () => { + const agencyWithRefersTo = new AgencyDtoBuilder() + .withId("agency-with-refers-to") + .withValidatorEmails([]) + .withCounsellorEmails(["fake-counsellor-email@gmail.com"]) + .withRefersToAgencyId(agency.id) + .build(); + + it("Throw when user have role validator", async () => { + agencyRepository.insert(agencyWithRefersTo); + + expectPromiseToFailWithError( + createUserForAgency.execute( + { + userId: uuidGenerator.new(), + agencyId: agencyWithRefersTo.id, + roles: ["validator"], + isNotifiedByEmail: true, + email: "new-user@email.fr", + }, + backofficeAdminUser, + ), + errors.agency.invalidRoleUpdateForAgencyWithRefersTo({ + agencyId: agencyWithRefersTo.id, + role: "validator", + }), + ); + }); + }); + + it("create new user with its agency rights", async () => { + const newUserId = uuidGenerator.new(); + userRepository.users = []; + + await createUserForAgency.execute( + { + userId: newUserId, + agencyId: agency.id, + roles: ["counsellor"], + isNotifiedByEmail: false, + email: "new-user@email.fr", + }, + backofficeAdminUser, + ); + + expect(userRepository.users.length).toBe(1); + expectToEqual(await userRepository.getById(newUserId), { + createdAt: timeGateway.now().toISOString(), + externalId: null, + firstName: "", + id: newUserId, + email: "new-user@email.fr", + lastName: "", + agencyRights: [ + { agency, isNotifiedByEmail: false, roles: ["counsellor"] }, + ], + dashboards: { + agencies: {}, + establishments: {}, + }, + }); + }); + + it("add agency rights to an existing user", async () => { + const anotherAgency = new AgencyDtoBuilder() + .withId("another-agency-id") + .build(); + await agencyRepository.insert(anotherAgency); + const userId = uuidGenerator.new(); + await userRepository.save({ + id: userId, + email: "user@email.fr", + firstName: "John", + lastName: "Doe", + externalId: null, + createdAt: timeGateway.now().toISOString(), + }); + await userRepository.updateAgencyRights({ + userId, + agencyRights: [ + { + agency, + isNotifiedByEmail: true, + roles: ["validator"], + }, + ], + }); + + await createUserForAgency.execute( + { + userId, + agencyId: anotherAgency.id, + roles: ["counsellor"], + isNotifiedByEmail: false, + email: "user@email.fr", + }, + backofficeAdminUser, + ); + + expectToEqual(await userRepository.getById(userId), { + createdAt: timeGateway.now().toISOString(), + externalId: null, + firstName: "John", + id: userId, + email: "user@email.fr", + lastName: "Doe", + agencyRights: [ + { + agency, + isNotifiedByEmail: true, + roles: ["validator"], + }, + { + agency: anotherAgency, + isNotifiedByEmail: false, + roles: ["counsellor"], + }, + ], + dashboards: { + agencies: {}, + establishments: {}, + }, + }); + }); +}); diff --git a/shared/src/admin/admin.dto.ts b/shared/src/admin/admin.dto.ts index 8b5e1a457b..1388ac4314 100644 --- a/shared/src/admin/admin.dto.ts +++ b/shared/src/admin/admin.dto.ts @@ -20,6 +20,10 @@ export type UserUpdateParamsForAgency = { email: Email | null; }; +export type UserCreateParamsForAgency = UserUpdateParamsForAgency & { + email: Email; +}; + export type RejectIcUserRoleForAgencyParams = OmitFromExistingKeys< UserUpdateParamsForAgency, "roles" | "isNotifiedByEmail" | "email" diff --git a/shared/src/admin/admin.routes.ts b/shared/src/admin/admin.routes.ts index baecca5653..4cda193118 100644 --- a/shared/src/admin/admin.routes.ts +++ b/shared/src/admin/admin.routes.ts @@ -59,6 +59,20 @@ export const adminRoutes = defineRoutes({ 404: httpErrorSchema, }, }), + + addUserForAgency: defineRoute({ + method: "post", + url: "/admin/inclusion-connected/users", + requestBodySchema: userUpdateParamsForAgencySchema, + ...withAuthorizationHeaders, + responses: { + 201: expressEmptyResponseBody, + 400: httpErrorSchema, + 401: httpErrorSchema, + 404: httpErrorSchema, + }, + }), + rejectIcUserForAgency: defineRoute({ method: "delete", url: "/admin/inclusion-connected/users", diff --git a/shared/src/admin/admin.schema.ts b/shared/src/admin/admin.schema.ts index 4ee19ce128..19b5666923 100644 --- a/shared/src/admin/admin.schema.ts +++ b/shared/src/admin/admin.schema.ts @@ -12,6 +12,7 @@ import { ManageConventionAdminForm, ManageEstablishmentAdminForm, RejectIcUserRoleForAgencyParams, + UserCreateParamsForAgency, UserUpdateParamsForAgency, WithUserFilters, } from "./admin.dto"; @@ -25,6 +26,15 @@ export const userUpdateParamsForAgencySchema: z.Schema = + z.object({ + userId: userIdSchema, + agencyId: agencyIdSchema, + roles: z.array(agencyRoleSchema), + isNotifiedByEmail: z.boolean(), + email: emailSchema, + }); + export const rejectIcUserRoleForAgencyParamsSchema: z.Schema = z.object({ agencyId: agencyIdSchema, diff --git a/shared/src/errors/errors.ts b/shared/src/errors/errors.ts index 6ff97cac90..6a89f00921 100644 --- a/shared/src/errors/errors.ts +++ b/shared/src/errors/errors.ts @@ -288,6 +288,14 @@ export const errors = { new BadRequestError( `Le role "${role}" n'est pas autorisé pour l'agence "${agencyId}" car cette agence n'a qu'une seul étape de validation.`, ), + + invalidRoleUpdateForAgencyWithRefersTo: ({ + agencyId, + role, + }: { agencyId: AgencyId; role: AgencyRole }) => + new BadRequestError( + `Le role "${role}" n'est pas autorisé pour l'agence "${agencyId}" car cette agence est une structure d'accompagnement.`, + ), }, user: { unauthorized: () => new UnauthorizedError(),