diff --git a/packages/backend/src/auth/guards/usager-note-access.guard.ts b/packages/backend/src/auth/guards/usager-note-access.guard.ts index 766a523ba7..c1e956f011 100644 --- a/packages/backend/src/auth/guards/usager-note-access.guard.ts +++ b/packages/backend/src/auth/guards/usager-note-access.guard.ts @@ -50,7 +50,8 @@ export class UsagerNoteAccessGuard implements CanActivate { r.usagerNote = usagerNote; return r; } catch (e) { - appLogger.error("[UsagerNoteAccessGuard] usager doc not found", { + appLogger.error(e); + appLogger.error("[UsagerNoteAccessGuard] usager note not found", { sentry: true, context: { usagerRef, diff --git a/packages/backend/src/modules/structures/controllers/structure-doc.controller.ts b/packages/backend/src/modules/structures/controllers/structure-doc.controller.ts index a8495a03b4..a68b31085f 100644 --- a/packages/backend/src/modules/structures/controllers/structure-doc.controller.ts +++ b/packages/backend/src/modules/structures/controllers/structure-doc.controller.ts @@ -81,6 +81,7 @@ export class StructureDocController { @ApiOperation({ summary: "Upload de documents personnalisables" }) @Post("") + @AllowUserStructureRoles("responsable", "admin") @UseInterceptors( FileInterceptor("file", { limits: FILES_SIZE_LIMIT, @@ -193,6 +194,7 @@ export class StructureDocController { } @Delete(":uuid") + @AllowUserStructureRoles("responsable", "admin") public async deleteDocument( @Param("uuid", new ParseUUIDPipe()) uuid: string, @CurrentUser() user: UserStructureAuthenticated, diff --git a/packages/backend/src/usagers/controllers/usagers-decision.controller.ts b/packages/backend/src/usagers/controllers/usagers-decision.controller.ts index 479b36ec9a..95424b787f 100644 --- a/packages/backend/src/usagers/controllers/usagers-decision.controller.ts +++ b/packages/backend/src/usagers/controllers/usagers-decision.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Delete, + ForbiddenException, Get, HttpStatus, Param, @@ -34,11 +35,10 @@ import { UsagerDecision, UsagerNote, Usager, - UsagerDecisionStatut, - UserStructureRole, } from "@domifa/common"; import { format } from "date-fns"; import { getLastInteractionOut } from "../../modules/interactions/services"; +import { canAddDecision } from "../guards"; @Controller("usagers-decision") @ApiTags("usagers-decision") @@ -61,18 +61,9 @@ export class UsagersDecisionController { // eslint-disable-next-line @typescript-eslint/no-unused-vars @Param("usagerRef", new ParseIntPipe()) _usagerRef: number ): Promise { - const decisionsToCheck: UsagerDecisionStatut[] = [ - "ATTENTE_DECISION", - "INSTRUCTION", - ]; - const rightsToCheck: UserStructureRole[] = ["responsable", "admin"]; - if ( - !decisionsToCheck.includes(decision.statut) && - !rightsToCheck.includes(user.role) - ) { - throw new Error("CANNOT_SET_DECISION"); + if (!canAddDecision(user.role, decision.statut)) { + throw new ForbiddenException("INSUFFICIENT_PERMISSIONS_FOR_DECISION"); } - decision.userName = `${user.prenom} ${user.nom}`; decision.userId = user.id; diff --git a/packages/backend/src/usagers/decorators/index.ts b/packages/backend/src/usagers/decorators/index.ts new file mode 100644 index 0000000000..3583c6b9df --- /dev/null +++ b/packages/backend/src/usagers/decorators/index.ts @@ -0,0 +1,2 @@ +// @index('./*decorator.ts', f => `export * from '${f.path}'`) +export * from "./validate-search-field.decorator"; diff --git a/packages/backend/src/usagers/utils/validate-search-field.decorator.ts b/packages/backend/src/usagers/decorators/validate-search-field.decorator.ts similarity index 93% rename from packages/backend/src/usagers/utils/validate-search-field.decorator.ts rename to packages/backend/src/usagers/decorators/validate-search-field.decorator.ts index 42809a8a77..f96474483c 100644 --- a/packages/backend/src/usagers/utils/validate-search-field.decorator.ts +++ b/packages/backend/src/usagers/decorators/validate-search-field.decorator.ts @@ -3,7 +3,7 @@ import { registerDecorator, ValidationArguments, } from "class-validator"; -import { validateSearchField } from "./validate-search-field"; +import { validateSearchField } from "../utils/validate-search-field"; export function ValidateSearchField(validationOptions?: ValidationOptions) { return function (object: object, propertyName: string) { diff --git a/packages/backend/src/usagers/dto/search-usager.dto.ts b/packages/backend/src/usagers/dto/search-usager.dto.ts index c9924280cb..4908e8fe71 100644 --- a/packages/backend/src/usagers/dto/search-usager.dto.ts +++ b/packages/backend/src/usagers/dto/search-usager.dto.ts @@ -9,7 +9,7 @@ import { UsagersFilterCriteriaEntretien, } from "@domifa/common"; import { Transform } from "class-transformer"; -import { ValidateSearchField } from "../utils"; +import { ValidateSearchField } from "../decorators"; export class SearchUsagerDto { @ApiProperty({ diff --git a/packages/backend/src/usagers/guards/can-add-decision.spec.ts b/packages/backend/src/usagers/guards/can-add-decision.spec.ts new file mode 100644 index 0000000000..0560b593e0 --- /dev/null +++ b/packages/backend/src/usagers/guards/can-add-decision.spec.ts @@ -0,0 +1,69 @@ +import { UserStructureRole, UsagerDecisionStatut } from "@domifa/common"; +import { canAddDecision } from "./can-add-decision"; + +interface TestCase { + role: UserStructureRole; + decisionStatut: UsagerDecisionStatut; + expectedAccess: boolean; +} + +describe("canAddDecision", () => { + const testCases: TestCase[] = [ + { role: "facteur", decisionStatut: "INSTRUCTION", expectedAccess: false }, + { + role: "facteur", + decisionStatut: "ATTENTE_DECISION", + expectedAccess: false, + }, + { role: "facteur", decisionStatut: "RADIE", expectedAccess: false }, + { role: "facteur", decisionStatut: "VALIDE", expectedAccess: false }, + { role: "facteur", decisionStatut: "REFUS", expectedAccess: false }, + + { role: "agent", decisionStatut: "INSTRUCTION", expectedAccess: false }, + { + role: "agent", + decisionStatut: "ATTENTE_DECISION", + expectedAccess: false, + }, + { role: "agent", decisionStatut: "RADIE", expectedAccess: false }, + { role: "agent", decisionStatut: "VALIDE", expectedAccess: false }, + { role: "agent", decisionStatut: "REFUS", expectedAccess: false }, + + { role: "simple", decisionStatut: "INSTRUCTION", expectedAccess: true }, + { + role: "simple", + decisionStatut: "ATTENTE_DECISION", + expectedAccess: true, + }, + { role: "simple", decisionStatut: "RADIE", expectedAccess: true }, + { role: "simple", decisionStatut: "VALIDE", expectedAccess: false }, + { role: "simple", decisionStatut: "REFUS", expectedAccess: false }, + + { + role: "responsable", + decisionStatut: "INSTRUCTION", + expectedAccess: true, + }, + { + role: "responsable", + decisionStatut: "ATTENTE_DECISION", + expectedAccess: true, + }, + { role: "responsable", decisionStatut: "RADIE", expectedAccess: true }, + { role: "responsable", decisionStatut: "VALIDE", expectedAccess: true }, + { role: "responsable", decisionStatut: "REFUS", expectedAccess: true }, + + { role: "admin", decisionStatut: "INSTRUCTION", expectedAccess: true }, + { role: "admin", decisionStatut: "ATTENTE_DECISION", expectedAccess: true }, + { role: "admin", decisionStatut: "RADIE", expectedAccess: true }, + { role: "admin", decisionStatut: "VALIDE", expectedAccess: true }, + { role: "admin", decisionStatut: "REFUS", expectedAccess: true }, + ]; + + test.each(testCases)( + 'Rôle "$role" avec statut "$decisionStatut" doit retourner $expectedAccess', + ({ role, decisionStatut, expectedAccess }) => { + expect(canAddDecision(role, decisionStatut)).toBe(expectedAccess); + } + ); +}); diff --git a/packages/backend/src/usagers/guards/can-add-decision.ts b/packages/backend/src/usagers/guards/can-add-decision.ts new file mode 100644 index 0000000000..03b53d7e41 --- /dev/null +++ b/packages/backend/src/usagers/guards/can-add-decision.ts @@ -0,0 +1,22 @@ +import { UserStructureRole, UsagerDecisionStatut } from "@domifa/common"; + +export const canAddDecision = ( + userRole: UserStructureRole, + decisionStatus: UsagerDecisionStatut +): boolean => { + const instructRoles: UserStructureRole[] = ["simple", "responsable", "admin"]; + const validateOrRefuseRoles: UserStructureRole[] = ["responsable", "admin"]; + + const canInstruct = instructRoles.includes(userRole); + const canValidateOrRefuse = validateOrRefuseRoles.includes(userRole); + + const permissions: { [key in UsagerDecisionStatut]: boolean } = { + INSTRUCTION: canInstruct, + ATTENTE_DECISION: canInstruct, + RADIE: canInstruct, + VALIDE: canValidateOrRefuse, + REFUS: canValidateOrRefuse, + }; + + return permissions[decisionStatus]; +}; diff --git a/packages/backend/src/usagers/guards/index.ts b/packages/backend/src/usagers/guards/index.ts new file mode 100644 index 0000000000..c7d96acbd6 --- /dev/null +++ b/packages/backend/src/usagers/guards/index.ts @@ -0,0 +1,2 @@ +// @index('./*guard.ts', f => `export * from '${f.path}'`) +export * from "./can-add-decision"; diff --git a/packages/backend/src/usagers/utils/index.ts b/packages/backend/src/usagers/utils/index.ts index e9c4b92045..83efa1b4b3 100644 --- a/packages/backend/src/usagers/utils/index.ts +++ b/packages/backend/src/usagers/utils/index.ts @@ -2,6 +2,5 @@ export * from "./cerfa"; export * from "./custom-docs"; export * from "./dataCleanerForStats.service"; -export * from "./validate-search-field.decorator"; export * from "./validate-search-field"; export * from "./xlsx-structure-usagers-renderer"; diff --git a/packages/backend/src/usagers/utils/validate-search-field.ts b/packages/backend/src/usagers/utils/validate-search-field.ts index c854d7ddea..53a575872f 100644 --- a/packages/backend/src/usagers/utils/validate-search-field.ts +++ b/packages/backend/src/usagers/utils/validate-search-field.ts @@ -11,25 +11,22 @@ export function validateSearchField( } try { - switch (searchField) { - case CriteriaSearchField.BIRTH_DATE: - const cleanDate = value.replace(/\D/g, ""); - if (cleanDate.length !== 8) { - throw new BadRequestException( - 'Format de date invalide. La date doit être au format "jj/mm/aaaa"' - ); - } + if (searchField === CriteriaSearchField.BIRTH_DATE) { + const cleanDate = value.replace(/\D/g, ""); + if (cleanDate.length !== 8) { + throw new BadRequestException( + 'Format de date invalide. La date doit être au format "jj/mm/aaaa"' + ); + } - const parsedDate = parse(cleanDate, "ddMMyyyy", new Date()); - if (!isValid(parsedDate)) { - throw new BadRequestException( - "Date invalide. Vérifiez que le jour et le mois sont corrects" - ); - } - return true; - default: - return true; + const parsedDate = parse(cleanDate, "ddMMyyyy", new Date()); + if (!isValid(parsedDate)) { + throw new BadRequestException( + "Date invalide. Vérifiez que le jour et le mois sont corrects" + ); + } } + return true; } catch (error) { if (error instanceof BadRequestException) { throw error;