Skip to content

Commit 2711dce

Browse files
committed
fix(backend): fix history decision
1 parent 4ddefe5 commit 2711dce

File tree

9 files changed

+372
-69
lines changed

9 files changed

+372
-69
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
import { v4 as uuidv4 } from "uuid";
3+
import { appLogger } from "../util";
4+
import { UsagerTable } from "../database";
5+
6+
export class AddUuidToUsagerHistorique1756117243342
7+
implements MigrationInterface
8+
{
9+
name = "addUuidToUsagerHistorique1756117243342";
10+
11+
public async up(queryRunner: QueryRunner): Promise<void> {
12+
appLogger.warn(
13+
"Début de la migration: ajout UUID aux éléments d'historique"
14+
);
15+
16+
const countResult = await queryRunner.query(`
17+
SELECT COUNT(*) as total
18+
FROM usager u
19+
WHERE u.historique IS NOT NULL
20+
AND jsonb_array_length(u.historique) > 0
21+
AND EXISTS (
22+
SELECT 1
23+
FROM jsonb_array_elements(u.historique) AS hist_elem
24+
WHERE hist_elem->>'uuid' IS NULL
25+
OR hist_elem->'uuid' IS NULL
26+
OR NOT (hist_elem ? 'uuid')
27+
)
28+
`);
29+
30+
const totalUsagers = parseInt(countResult[0].total, 10);
31+
appLogger.warn(`Total d'usagers concernés: ${totalUsagers}`);
32+
33+
if (totalUsagers === 0) {
34+
appLogger.warn("Aucun usager concerné, fin de la migration");
35+
return;
36+
}
37+
38+
// Récupérer tous les usagers concernés
39+
const usagersResult = await queryRunner.query(`
40+
SELECT u.uuid, u.historique
41+
FROM usager u
42+
WHERE u.historique IS NOT NULL
43+
AND jsonb_array_length(u.historique) > 0
44+
AND EXISTS (
45+
SELECT 1
46+
FROM jsonb_array_elements(u.historique) AS hist_elem
47+
WHERE hist_elem->>'uuid' IS NULL
48+
OR hist_elem->'uuid' IS NULL
49+
OR NOT (hist_elem ? 'uuid')
50+
)
51+
ORDER BY u.uuid
52+
`);
53+
54+
const batchSize = 200;
55+
let processedCount = 0;
56+
57+
// Traitement par batch de 200
58+
for (let i = 0; i < usagersResult.length; i += batchSize) {
59+
const batch = usagersResult.slice(i, i + batchSize);
60+
61+
const manager = queryRunner.manager;
62+
await queryRunner.startTransaction();
63+
64+
try {
65+
appLogger.warn(
66+
`Traitement du batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(
67+
usagersResult.length / batchSize
68+
)} (${batch.length} usagers)`
69+
);
70+
71+
for (const usager of batch) {
72+
const historique = usager.historique;
73+
let modified = false;
74+
75+
for (const decision of historique) {
76+
if (
77+
!decision?.uuid ||
78+
decision?.uuid === null ||
79+
decision?.uuid === undefined
80+
) {
81+
decision.uuid = uuidv4();
82+
modified = true;
83+
}
84+
}
85+
if (modified) {
86+
await manager.update(
87+
UsagerTable,
88+
{ uuid: usager.uuid },
89+
{ historique }
90+
);
91+
processedCount++;
92+
}
93+
}
94+
95+
await queryRunner.commitTransaction();
96+
appLogger.warn(
97+
`Batch traité avec succès (${processedCount} usagers modifiés au total)`
98+
);
99+
} catch (error) {
100+
await queryRunner.rollbackTransaction();
101+
appLogger.error(
102+
`Erreur lors du traitement du batch ${
103+
Math.floor(i / batchSize) + 1
104+
}:`,
105+
error
106+
);
107+
throw error;
108+
}
109+
}
110+
111+
// Vérification finale
112+
const finalCountResult = await queryRunner.query(`
113+
SELECT COUNT(*) as remaining
114+
FROM usager u
115+
WHERE u.historique IS NOT NULL
116+
AND jsonb_array_length(u.historique) > 0
117+
AND EXISTS (
118+
SELECT 1
119+
FROM jsonb_array_elements(u.historique) AS hist_elem
120+
WHERE hist_elem->>'uuid' IS NULL
121+
OR hist_elem->'uuid' IS NULL
122+
OR NOT (hist_elem ? 'uuid')
123+
)
124+
`);
125+
126+
const remainingUsagers = parseInt(finalCountResult[0].remaining, 10);
127+
128+
if (remainingUsagers === 0) {
129+
appLogger.warn(
130+
`Migration réussie: ${processedCount} usagers modifiés, 0 usager restant sans UUID`
131+
);
132+
} else {
133+
appLogger.error(
134+
`Migration incomplète: ${remainingUsagers} usagers restent sans UUID`
135+
);
136+
throw new Error(
137+
`Migration incomplète: ${remainingUsagers} usagers restent sans UUID`
138+
);
139+
}
140+
}
141+
142+
public async down(): Promise<void> {
143+
appLogger.warn(
144+
"Rollback de la migration: suppression des UUID ajoutés dans l'historique"
145+
);
146+
147+
appLogger.warn(
148+
"Rollback ignoré pour éviter la suppression d'UUID légitimes"
149+
);
150+
}
151+
}

packages/backend/src/usagers/controllers/security-tests/usager-docs.controller.security-tests.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,103 @@ const CONTROLLER = "UsagerDocsController";
1515

1616
export const UsagerDocsControllerSecurityTests: AppTestHttpClientSecurityTestDef[] =
1717
[
18+
{
19+
label: `${CONTROLLER}.getCerfa - without decisionUuid`,
20+
query: async (context: AppTestContext) => ({
21+
response: await AppTestHttpClient.get("/docs/cerfa/1/attestation", {
22+
context,
23+
}),
24+
expectedStatus: expectedResponseStatusBuilder.allowStructureOnly(
25+
context.user,
26+
{
27+
roles: ["simple", "responsable", "admin", "agent"],
28+
validExpectedResponseStatus: HttpStatus.OK,
29+
invalidStructureIdExpectedResponseStatus: HttpStatus.BAD_REQUEST,
30+
validStructureIds: [1],
31+
}
32+
),
33+
}),
34+
},
35+
{
36+
label: `${CONTROLLER}.getCerfa - with valid decisionUuid`,
37+
query: async (context: AppTestContext) => ({
38+
response: await AppTestHttpClient.get(
39+
"/docs/cerfa/1/attestation?decisionUuid=52ba789e-eb21-4d84-9176-abe1e0d3c778",
40+
{
41+
context,
42+
}
43+
),
44+
expectedStatus: expectedResponseStatusBuilder.allowStructureOnly(
45+
context.user,
46+
{
47+
roles: ["simple", "responsable", "admin", "agent"],
48+
validExpectedResponseStatus: HttpStatus.OK,
49+
invalidStructureIdExpectedResponseStatus: HttpStatus.BAD_REQUEST,
50+
validStructureIds: [1],
51+
}
52+
),
53+
}),
54+
},
55+
{
56+
label: `${CONTROLLER}.getCerfa - with invalid UUID format`,
57+
query: async (context: AppTestContext) => ({
58+
response: await AppTestHttpClient.get(
59+
"/docs/cerfa/1/attestation?decisionUuid=invalid-uuid",
60+
{
61+
context,
62+
}
63+
),
64+
expectedStatus: expectedResponseStatusBuilder.allowStructureOnly(
65+
context.user,
66+
{
67+
roles: ["simple", "responsable", "admin", "agent"],
68+
validExpectedResponseStatus: HttpStatus.BAD_REQUEST, // Invalid UUID format
69+
invalidStructureIdExpectedResponseStatus: HttpStatus.BAD_REQUEST,
70+
validStructureIds: [1],
71+
}
72+
),
73+
}),
74+
},
75+
{
76+
label: `${CONTROLLER}.getCerfa - with non-existing decisionUuid`,
77+
query: async (context: AppTestContext) => ({
78+
response: await AppTestHttpClient.get(
79+
"/docs/cerfa/1/attestation?decisionUuid=f47ac10b-58cc-4372-a567-0e02b2c3d479",
80+
{
81+
context,
82+
}
83+
),
84+
expectedStatus: expectedResponseStatusBuilder.allowStructureOnly(
85+
context.user,
86+
{
87+
roles: ["simple", "responsable", "admin", "agent"],
88+
validExpectedResponseStatus: HttpStatus.BAD_REQUEST, // Non-existing decision
89+
invalidStructureIdExpectedResponseStatus: HttpStatus.BAD_REQUEST,
90+
validStructureIds: [1],
91+
}
92+
),
93+
}),
94+
},
95+
{
96+
label: `${CONTROLLER}.getCerfa - legacy route support (if maintained)`,
97+
query: async (context: AppTestContext) => ({
98+
response: await AppTestHttpClient.get(
99+
"/docs/1/c669f08b-74a8-4ffc-8128-c9e29e2fd535",
100+
{
101+
context,
102+
}
103+
),
104+
expectedStatus: expectedResponseStatusBuilder.allowStructureOnly(
105+
context.user,
106+
{
107+
roles: ["simple", "responsable", "admin", "agent"],
108+
validExpectedResponseStatus: HttpStatus.OK,
109+
invalidStructureIdExpectedResponseStatus: HttpStatus.BAD_REQUEST,
110+
validStructureIds: [1],
111+
}
112+
),
113+
}),
114+
},
18115
{
19116
label: `${CONTROLLER}.getDocument`,
20117
query: async (context: AppTestContext) => ({

packages/backend/src/usagers/controllers/usager-docs.controller.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import {
2+
BadRequestException,
23
Body,
34
Controller,
45
Delete,
56
Get,
67
HttpStatus,
78
Param,
9+
ParseEnumPipe,
810
ParseIntPipe,
911
ParseUUIDPipe,
1012
Patch,
1113
Post,
14+
Query,
1215
Res,
1316
UploadedFile,
1417
UseGuards,
@@ -40,16 +43,20 @@ import { PatchUsagerDocDto, PostUsagerDocDto } from "../dto";
4043
import crypto from "node:crypto";
4144
import { ExpressRequest } from "../../util/express";
4245
import { FILES_SIZE_LIMIT } from "../../util/file-manager";
43-
import { join } from "node:path";
46+
import { join, resolve } from "node:path";
4447
import { FileManagerService } from "../../util/file-manager/file-manager.service";
45-
import { Usager, UsagerDoc } from "@domifa/common";
48+
import { CerfaDocType, Usager, UsagerDoc } from "@domifa/common";
4649
import {
4750
USAGER_DOCS_FIELDS_TO_SELECT,
4851
usagerDocsRepository,
4952
UsagerDocsTable,
5053
} from "../../database";
5154
import { Response } from "express";
5255
import { appLogger } from "../../util";
56+
import { input } from "node-pdftk";
57+
import { generateCerfaData } from "../utils";
58+
import { readFile } from "fs-extra";
59+
import { isUUID } from "class-validator";
5360

5461
@UseGuards(AuthGuard("jwt"), AppUserGuard)
5562
@ApiTags("docs")
@@ -223,6 +230,48 @@ export class UsagerDocsController {
223230
return res.status(HttpStatus.OK).json(docs);
224231
}
225232

233+
@UseGuards(UsagerAccessGuard)
234+
@AllowUserStructureRoles("admin", "agent", "responsable", "simple")
235+
@Get("cerfa/:usagerRef/:typeCerfa")
236+
public async getCerfa(
237+
@Res() res: Response,
238+
@Param("typeCerfa", new ParseEnumPipe(CerfaDocType))
239+
typeCerfa: CerfaDocType,
240+
@Param("usagerRef", new ParseIntPipe()) _usagerRef: number,
241+
@CurrentUser() user: UserStructureAuthenticated,
242+
@CurrentUsager() currentUsager: Usager,
243+
@Query("decisionUuid") decisionUuid?: string
244+
) {
245+
if (decisionUuid && !isUUID(decisionUuid)) {
246+
throw new BadRequestException("decisionUuid must be a valid UUID");
247+
}
248+
249+
const pdfForm =
250+
typeCerfa === CerfaDocType.attestation ||
251+
typeCerfa === CerfaDocType.attestation_future
252+
? "../../_static/static-docs/attestation.pdf"
253+
: "../../_static/static-docs/demande.pdf";
254+
255+
const pdfInfos = generateCerfaData(
256+
currentUsager,
257+
user,
258+
typeCerfa,
259+
decisionUuid
260+
);
261+
262+
const filePath = await readFile(resolve(__dirname, pdfForm));
263+
264+
try {
265+
const buffer = await input(filePath).fillForm(pdfInfos).output();
266+
return res.setHeader("content-type", "application/pdf").send(buffer);
267+
} catch (err) {
268+
appLogger.error(err);
269+
return res
270+
.status(HttpStatus.INTERNAL_SERVER_ERROR)
271+
.json({ message: "CERFA_ERROR" });
272+
}
273+
}
274+
226275
@Get(":usagerRef")
227276
@UseGuards(UsagerAccessGuard)
228277
public async getUsagerDocuments(

packages/backend/src/usagers/controllers/usagers-decision.controller.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import {
3434
UsagerDecision,
3535
UsagerNote,
3636
Usager,
37+
UsagerDecisionStatut,
38+
UserStructureRole,
3739
} from "@domifa/common";
3840
import { format } from "date-fns";
3941
import { getLastInteractionOut } from "../../modules/interactions/services";
@@ -59,9 +61,14 @@ export class UsagersDecisionController {
5961
// eslint-disable-next-line @typescript-eslint/no-unused-vars
6062
@Param("usagerRef", new ParseIntPipe()) _usagerRef: number
6163
): Promise<Usager> {
64+
const decisionsToCheck: UsagerDecisionStatut[] = [
65+
"ATTENTE_DECISION",
66+
"INSTRUCTION",
67+
];
68+
const rightsToCheck: UserStructureRole[] = ["responsable", "admin"];
6269
if (
63-
!["ATTENTE_DECISION", "INSTRUCTION"].includes(decision.statut) &&
64-
!["reposable", "admmin"].includes(user.role)
70+
!decisionsToCheck.includes(decision.statut) &&
71+
!rightsToCheck.includes(user.role)
6572
) {
6673
throw new Error("CANNOT_SET_DECISION");
6774
}

0 commit comments

Comments
 (0)