From 2dc23ab6eb284aff6e0a45b686f700e285c1c6b8 Mon Sep 17 00:00:00 2001 From: PHAN Gabriel Date: Wed, 13 May 2026 00:46:18 +0200 Subject: [PATCH 1/6] Changement de serveur BDD --- backend/package-lock.json | 1585 +++++++++++++++++++++++++++++--- backend/package.json | 1 + backend/src/app.js | 5 +- backend/src/config/database.js | 45 +- backend/src/config/index.js | 2 +- backend/src/server.js | 7 +- 6 files changed, 1481 insertions(+), 164 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 3bffcf1..48d600d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -8,6 +8,7 @@ "name": "projectlabumhma-backend", "version": "1.0.0", "dependencies": { + "@aws-sdk/rds-signer": "^3.1045.0", "@mistralai/mistralai": "^2.2.0", "bcryptjs": "^2.4.3", "compression": "^1.8.1", @@ -29,6 +30,743 @@ "supertest": "^6.3.3" } }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cognito-identity": { + "version": "3.1045.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.1045.0.tgz", + "integrity": "sha512-3OEn8zvtfJoN0jFfjVJ9jF2GVRDL3IjDfk6CAgVTAqjfCVjajiUD0iFAGQ4cOzdcv1LGsZ0b/snJDWalY3OePQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-node": "^3.972.39", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.24", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-utf8": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.3.1.tgz", + "integrity": "sha512-FtRrSnriXtOs4+J8/y9SbQ1xmN71hrOsN/YJr5PQQj5nR1l7YNkGS/TEk4gr0WN7gyrUqw8/RFaYVjI18732ZA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.974.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.8.tgz", + "integrity": "sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.22", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core/node_modules/@smithy/util-utf8": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.3.1.tgz", + "integrity": "sha512-FtRrSnriXtOs4+J8/y9SbQ1xmN71hrOsN/YJr5PQQj5nR1l7YNkGS/TEk4gr0WN7gyrUqw8/RFaYVjI18732ZA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity": { + "version": "3.972.31", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.31.tgz", + "integrity": "sha512-W5JtzDp3ejzhOOknXlnt+vJsNN2GZdAcBK+hR7HQ1DCacXqS0UpmnIyihIU7CK0IB+XYWeBaN3bBv4pXavp7Vg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.34.tgz", + "integrity": "sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.36.tgz", + "integrity": "sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.25", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.38.tgz", + "integrity": "sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-env": "^3.972.34", + "@aws-sdk/credential-provider-http": "^3.972.36", + "@aws-sdk/credential-provider-login": "^3.972.38", + "@aws-sdk/credential-provider-process": "^3.972.34", + "@aws-sdk/credential-provider-sso": "^3.972.38", + "@aws-sdk/credential-provider-web-identity": "^3.972.38", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.38.tgz", + "integrity": "sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.39.tgz", + "integrity": "sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.34", + "@aws-sdk/credential-provider-http": "^3.972.36", + "@aws-sdk/credential-provider-ini": "^3.972.38", + "@aws-sdk/credential-provider-process": "^3.972.34", + "@aws-sdk/credential-provider-sso": "^3.972.38", + "@aws-sdk/credential-provider-web-identity": "^3.972.38", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.34.tgz", + "integrity": "sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.38.tgz", + "integrity": "sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/token-providers": "3.1041.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.38.tgz", + "integrity": "sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers": { + "version": "3.1045.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.1045.0.tgz", + "integrity": "sha512-J+it58HUGyMIAquB6pWtvmO4m0E/gQ/Tz9Xcoogk3Rety13likU5U8HioeIgE+aN1DDOAB//MARoIdLZS1Mpfw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.1045.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-cognito-identity": "^3.972.31", + "@aws-sdk/credential-provider-env": "^3.972.34", + "@aws-sdk/credential-provider-http": "^3.972.36", + "@aws-sdk/credential-provider-ini": "^3.972.38", + "@aws-sdk/credential-provider-login": "^3.972.38", + "@aws-sdk/credential-provider-node": "^3.972.39", + "@aws-sdk/credential-provider-process": "^3.972.34", + "@aws-sdk/credential-provider-sso": "^3.972.38", + "@aws-sdk/credential-provider-web-identity": "^3.972.38", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.10.tgz", + "integrity": "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", + "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.11.tgz", + "integrity": "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.37.tgz", + "integrity": "sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/@smithy/util-utf8": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.3.1.tgz", + "integrity": "sha512-FtRrSnriXtOs4+J8/y9SbQ1xmN71hrOsN/YJr5PQQj5nR1l7YNkGS/TEk4gr0WN7gyrUqw8/RFaYVjI18732ZA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.38.tgz", + "integrity": "sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@smithy/core": "^3.23.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-retry": "^4.3.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.6.tgz", + "integrity": "sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.24", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-utf8": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.3.1.tgz", + "integrity": "sha512-FtRrSnriXtOs4+J8/y9SbQ1xmN71hrOsN/YJr5PQQj5nR1l7YNkGS/TEk4gr0WN7gyrUqw8/RFaYVjI18732ZA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/rds-signer": { + "version": "3.1045.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/rds-signer/-/rds-signer-3.1045.0.tgz", + "integrity": "sha512-O/wNR6sX1x4fxgPVd331o7wreETRz4H3yP+teFdyBWRGWedUYaia2C+GT4rzHn5m7l1jJ5gCws/jH6UfVNjeqg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/credential-providers": "3.1045.0", + "@aws-sdk/util-format-url": "^3.972.10", + "@smithy/config-resolver": "^4.4.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.13.tgz", + "integrity": "sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/config-resolver": "^4.4.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.25.tgz", + "integrity": "sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.37", + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1041.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz", + "integrity": "sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.8.tgz", + "integrity": "sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-endpoints": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.10.tgz", + "integrity": "sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.10.tgz", + "integrity": "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.24.tgz", + "integrity": "sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/types": "^3.973.8", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz", + "integrity": "sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==", + "license": "Apache-2.0", + "dependencies": { + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -796,213 +1534,671 @@ "dev": true, "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.27.8" + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mistralai/mistralai": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.0.tgz", + "integrity": "sha512-JQUGIXjFWnw/J9LpTSf/ZXwVW3Sh8FBAcfTo5QvAHqkl4CfSiIwnjRJhMoAFcP6ncCe84YPU1ncDGX+p3OXnfg==", + "license": "Apache-2.0", + "dependencies": { + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.5.1.tgz", + "integrity": "sha512-abXk3LhODsvRHsk0ZS9ztrg/fZatTa9Z/z4pgx65YSLR+rY6kvUG/1IgcDKEUciR8MfdnkT5oPeHJTy/HhzDIQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.1.tgz", + "integrity": "sha512-3mT7o4qQyUWttYnVK3A0Z/u3Xha3E81tXn32Tz6vjZiUXhBrkEivpw1hBYfh84iFF9CSzkBU9Y1DJ3Q6RQ231g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.1.tgz", + "integrity": "sha512-0S/acwHnqX4WrjXzhdiDRxsG2s9SC0cpPIK9nZ1R6UOHd+j7uL28+4bHu22urbLk2TVw3fkp6na/+fkUt/pLNQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.1.tgz", + "integrity": "sha512-r7bN6spQ+caZC8AnyvSxkRUb57zt2jhhRw3Z+2Ez8hjq6coIikDBFUUI/+CQ1xx9K6eX1Gx6wUKo4ylU66TIqw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.3.1.tgz", + "integrity": "sha512-u0/zo11mg7yNneoYgTkH4sXwSmcBpbl49o4UNCtQ7hYsXxynsN25KYHmXzqi7TPk5HQL5klGnpU5koOY0O+9hw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.3.1.tgz", + "integrity": "sha512-cLmwtDoulyZvRepAfyV+3rx5oMvuh51dbE+6En3vGC09j3uVSRt1U4oguNu32ub3soGX0oYtBs8E7S2Q4SxTqg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.3.1.tgz", + "integrity": "sha512-l4BUIP+wljW/Ar+0/QcGdmElI9lalrywfzNijXMBG34Z510FRzPyrDLx/blNTZOAm0C4Mvx5t/bf760CZo1ajg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.5.1.tgz", + "integrity": "sha512-qtqu5TS+8Y18ZDkJoiXN5AMW1G4JAg1+xytzpsUvIR5a4EUsgd5HQg12lekEHWpm2TDUmOgg+hBaHK7dvyWdkA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.6.1.tgz", + "integrity": "sha512-eTaQhxs0rfUuAkL2MSKrH8DTO7YCeAgrdN0B2/RAeuHmXQ+x52dk5qUBsi/jtcqe5LxItgq5AG5tI6Cp8c0sow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.3.1.tgz", + "integrity": "sha512-t7YtUe076zWVypVmy1rX91oKi2TFJCkpfFpfMhJFpEIRPP0iL9JxjeSyFQ+1bF45JUfDzOzslUJa150WcSrBug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.3.1.tgz", + "integrity": "sha512-1jKwiKZxCMQNqmp4uVPYA6r+MLGjEtH07gnOUdPgbnjuOIrl/0JY/ICdpQtFgeBsQ/Up01gnSv8GYEL0fb8yvg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.4.1.tgz", + "integrity": "sha512-q7tDJEJXcaSG/8TVpu2f2l9bzxTzDM9geWmltbzsY6Hfh3yiuXXTpLIO8+zwYASPPVFaTJpdKwjSSjdoDoccgw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.1.tgz", + "integrity": "sha512-BdEYko85f/ldp68uH8XEyIvo810xFk6eyPH81SRggTOApYHWA+Xu7B2EzLuHbe37WVLaUA7F1fWR3/zBeme2WA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.3.1.tgz", + "integrity": "sha512-3NHoqVBhzpY2b4YBx9AqyKC4C8nnEjl5FyKuxrCjvnjinG0ODj+yg1xX360nNahT6wghYjSw1SooCt3kIdnqIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.4.1.tgz", + "integrity": "sha512-8irPNCQgYxcSFp1aGcnDNFkTwSA+xPUaFq9V/v1+JXWu8sKr5b3cFmg2kBTkjkvypDmGeNffuNu0x5iqw1NoAw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.3.1.tgz", + "integrity": "sha512-toyi8sXPWDNoVH6yK7sXJ9dm5uxw2tWLCHzPy/t16Fvl62Es4vXQXzlilyNaw+DqFwxSlrFClh0rGLPUF2p9Lg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.5.1.tgz", + "integrity": "sha512-FKoKxVzdFPhyynFI+SPTWrgOP60fZ4l1UwukWYj4eyhpSmEI7MJ6p58hawIIt9bwp+aek9NEm8Zika7E+GEoeg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.1.tgz", + "integrity": "sha512-728lZZEWYWubBESrfntNslZQYDKRlJDY4dcDnYbL50+gu35pGPLblu4S0/RH/RDLF6me1M87ECHsHELGL7dA/Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.13.1.tgz", + "integrity": "sha512-IcznNM8Qd9u1X3oflp12tkzyOB4HbT+sfYWlWiyEysgNzSHoWcHUUsTT4y1jjDjtVuuVVQbYks+g1kVd7u1eGQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/types": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", + "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/url-parser": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.3.1.tgz", + "integrity": "sha512-tuelFlF2PZR/wogFC58NIrPOv+Zna4N1+3kA161/33D1Gbwvl6Nh4WsAsW05ZyPp0O6CMGsdbb0S2b/qVjRMCw==", + "license": "Apache-2.0", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-base64": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.4.1.tgz", + "integrity": "sha512-fTHiwW2xbiRiWzfSk4IGAr3gNZCH4fuRYqt8+IuarsP/YON35576iVdePraZ6yJlFxlCL0eMec3/F7xYqoKzlg==", + "license": "Apache-2.0", "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-body-length-browser": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.3.1.tgz", + "integrity": "sha512-1scg5t4nV3hV7CZs996/XHb80aDZ5YotH4NcvkW/w/rHj+cSz0aCIzwz8aUNKB4nCDPSHRCbrKoj+TvycYefmw==", + "license": "Apache-2.0", "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-body-length-node": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.3.1.tgz", + "integrity": "sha512-VRC8MKVPKrgUYThTA7ughcKMfjW6/X92H0wXGJoda0Apw4O5xbXL0GMLz40DTWlsb5hh2iItk6+XL72uJdxYcw==", + "license": "Apache-2.0", "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-config-provider": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.3.1.tgz", + "integrity": "sha512-lw6L5GF5+W19vO6o3fZwRT2cXEG+8b2LH0b9ppjDT6nIxjUgmljEQGninx5XorylwKZZ4XLVABeroJ8oaF9RmQ==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.4.1.tgz", + "integrity": "sha512-1rA7w+LjK1WJClsffC81Z/ZtjFt22QsKhBjUYEnZsGVS2nOTfOENKBzdg4SxhdwFvBCjcbpjscUfXOPwE3UHWQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.3.1.tgz", + "integrity": "sha512-1fk1wfQHBenQD5NitVKOFgW0wsISYAFPIXGyStJWAeCtMyRhgHYvtJxBk2rwGWA0L5QX6oM6yeHSLKPFMk59ww==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@mistralai/mistralai": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.0.tgz", - "integrity": "sha512-JQUGIXjFWnw/J9LpTSf/ZXwVW3Sh8FBAcfTo5QvAHqkl4CfSiIwnjRJhMoAFcP6ncCe84YPU1ncDGX+p3OXnfg==", + "node_modules/@smithy/util-endpoints": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.5.1.tgz", + "integrity": "sha512-yORYzJD5zoGbSDkAACr0dIjDiSEA3X8h8lggDENl1dkKpCG0TQIoItPBqtvuJHzFFjRXumcoH+/09xIuixGyCw==", "license": "Apache-2.0", "dependencies": { - "ws": "^8.18.0", - "zod": "^3.25.0 || ^4.0.0", - "zod-to-json-schema": "^3.25.0" + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" + "node_modules/@smithy/util-middleware": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.3.1.tgz", + "integrity": "sha512-SRRMDcIgVXVhVbxviBaSZbuWuVW3jD08wv4ESV0V2oiw0Mki8TPVQ5IxwD3MvSTPg52QYsRP+JoMw5WdUdeWAg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", - "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-retry": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.4.1.tgz", + "integrity": "sha512-qkgWgwn1xw0GoY9Ea/B6FrYSPfHA0zyOtJkokwxZuvucRf2+2lfTut6adi4e4Y7LEAaxsFG7r6i05mtDCxbHKA==", + "license": "Apache-2.0", "dependencies": { - "@noble/hashes": "^1.1.5" + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/@smithy/util-stream": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.6.1.tgz", + "integrity": "sha512-GjZfEft0M0V3n2YM/LGkr5LeLd8gxHUIzW0rUz6VtTtlAq245GxHlJghvoPEjJHKTj255iHFAiA4IsIdK40Ueg==", + "license": "Apache-2.0", "dependencies": { - "type-detect": "4.0.8" + "@smithy/core": "^3.24.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" } }, "node_modules/@swc/helpers": { @@ -1444,6 +2640,12 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2389,6 +3591,43 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -4538,6 +5777,21 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -5571,6 +6825,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/superagent": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", @@ -6000,6 +7266,21 @@ } } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index 69daf24..8b84102 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,6 +13,7 @@ "testEnvironment": "node" }, "dependencies": { + "@aws-sdk/rds-signer": "^3.1045.0", "@mistralai/mistralai": "^2.2.0", "bcryptjs": "^2.4.3", "compression": "^1.8.1", diff --git a/backend/src/app.js b/backend/src/app.js index d335f45..77d5fd7 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -37,7 +37,7 @@ try { * @param {Object} options – { jwtSecret } * @returns {Promise} */ -async function createApp(pool, options = {}) { +async function createApp(pool, poolAdmin, options = {}) { const app = express(); app.disable('x-powered-by'); const jwtSecret = options.jwtSecret || process.env.JWT_SECRET; @@ -96,7 +96,7 @@ async function createApp(pool, options = {}) { app.use(extractUser(jwtSecret)); /* ---------- Init DB ---------- */ - await initDatabase(pool); + await initDatabase(poolAdmin); /* ---------- Garde de maintenance ---------- */ // Doit être positionnée APRÈS extractUser (pour reconnaître les admins) @@ -114,6 +114,7 @@ async function createApp(pool, options = {}) { } }); app.get('/healthz', (_req, res) => { + console.log('healthz hit'); res.json({ status: 'ok' }); }); diff --git a/backend/src/config/database.js b/backend/src/config/database.js index 8608644..6980a77 100644 --- a/backend/src/config/database.js +++ b/backend/src/config/database.js @@ -1,21 +1,54 @@ const { Pool } = require('pg'); +const fs = require('fs'); +const path = require('path'); +const { Signer } = require("@aws-sdk/rds-signer"); require('dotenv').config(); /** - * Crée et configure le pool de connexions PostgreSQL. + * Crée et configure le pool PostgreSQL. * Accepte un pool externe (utile pour les tests avec pg-mem). */ -function createPool(externalPool) { - if (externalPool) return externalPool; +function createPool(hostname, username, database, region, port = 5432, password=null) { + const signer = new Signer({ region, hostname, port, username }); // Pour récupérer le token return new Pool({ - connectionString: process.env.DATABASE_URL, - ssl: process.env.PGSSLMODE === 'require' ? { rejectUnauthorized: false } : false, + host: hostname, + port, + database, + user: username, + password: password || (() => signer.getAuthToken()), + ssl: { + rejectUnauthorized: true, + ca: fs.readFileSync(path.join(__dirname, 'global-bundle.pem')).toString(), + }, max: 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000, }); } -module.exports = { createPool }; +function createPoolApp(externalPool) { + if (externalPool) return externalPool; + + return createPool( + process.env.DB_HOST, + process.env.DB_USER, + process.env.DB_NAME, + process.env.AWS_REGION, + process.env.DB_PORT + ); +} + +function createPoolAdmin(externalPool) { + if (externalPool) return externalPool; + + return createPool( + process.env.DB_HOST, + process.env.DB_USER_ADMIN, + process.env.DB_NAME, + process.env.AWS_REGION, + process.env.DB_PORT + ); +} +module.exports = { createPoolApp, createPoolAdmin }; \ No newline at end of file diff --git a/backend/src/config/index.js b/backend/src/config/index.js index b14b7ab..1762ab5 100644 --- a/backend/src/config/index.js +++ b/backend/src/config/index.js @@ -10,7 +10,7 @@ const config = { }; function assertRequiredConfig() { - const required = ['JWT_SECRET', 'DATABASE_URL']; + const required = ['JWT_SECRET', 'DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_USER_ADMIN', 'AWS_REGION']; const missing = required.filter((name) => !process.env[name]); if (missing.length) { diff --git a/backend/src/server.js b/backend/src/server.js index 4a1d9a2..393c7be 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -1,13 +1,14 @@ -const { createPool } = require('./config/database'); +const { createPoolApp, createPoolAdmin } = require('./config/database'); const { createApp } = require('./app'); const config = require('./config'); -const pool = createPool(); +const poolApp = createPoolApp(); +const poolAdmin = createPoolAdmin(); const start = async () => { try { config.assertRequiredConfig(); - const app = await createApp(pool); + const app = await createApp(poolApp, poolAdmin); app.listen(config.port, () => { console.log(`API ProjectLabUMHMA démarrée sur le port ${config.port}`); }); From b1099c0385d3f71b548716094620906ea58175f9 Mon Sep 17 00:00:00 2001 From: PHAN Gabriel Date: Wed, 13 May 2026 01:21:21 +0200 Subject: [PATCH 2/6] Suppresion du fichier dbInit --- backend/src/app.js | 6 - backend/src/controllers/authController.js | 4 +- backend/src/services/dbInit.js | 551 ---------------------- 3 files changed, 2 insertions(+), 559 deletions(-) delete mode 100644 backend/src/services/dbInit.js diff --git a/backend/src/app.js b/backend/src/app.js index 77d5fd7..0460575 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -5,7 +5,6 @@ const rateLimit = require('express-rate-limit'); const { extractUser } = require('./middleware/auth'); const errorHandler = require('./middleware/errorHandler'); -const { initDatabase } = require('./services/dbInit'); const { isPasswordValid, passwordRequirements } = require('./utils/password'); const { csrfProtection } = require('./middleware/csrfProtection'); const { originProtection } = require('./middleware/originProtection'); @@ -95,9 +94,6 @@ async function createApp(pool, poolAdmin, options = {}) { app.use(csrfProtection()); app.use(extractUser(jwtSecret)); - /* ---------- Init DB ---------- */ - await initDatabase(poolAdmin); - /* ---------- Garde de maintenance ---------- */ // Doit être positionnée APRÈS extractUser (pour reconnaître les admins) // mais AVANT toutes les routes métier. @@ -139,8 +135,6 @@ async function createApp(pool, poolAdmin, options = {}) { module.exports = { createApp, // Re-exports pour compatibilité avec les tests existants - ensureUsersTable: require('./services/dbInit').ensureUsersTable, - ensureDossierTables: require('./services/dbInit').ensureDossierTables, isPasswordValid, passwordRequirements, }; diff --git a/backend/src/controllers/authController.js b/backend/src/controllers/authController.js index 15b382a..2f02e52 100644 --- a/backend/src/controllers/authController.js +++ b/backend/src/controllers/authController.js @@ -79,8 +79,8 @@ async function signup(pool, jwtSecret) { const newUserId = userResult.rows[0].uuid_user; await pool.query( - `INSERT INTO user_security (uuid_user, mdp_hash, mdp_sel, lockout, date_creation) - VALUES ($1, $2, $3, $4, CURRENT_DATE)`, + `INSERT INTO user_security (uuid_user, mdp_hash, mdp_sel, lockout) + VALUES ($1, $2, $3, $4)`, [newUserId, hashedPassword, '', false], ); diff --git a/backend/src/services/dbInit.js b/backend/src/services/dbInit.js deleted file mode 100644 index 9ed5a65..0000000 --- a/backend/src/services/dbInit.js +++ /dev/null @@ -1,551 +0,0 @@ -/** - * Fonctions d'initialisation / migration légère des tables PostgreSQL. - * Chaque fonction vérifie l'existence des tables et ajoute les colonnes - * supplémentaires nécessaires à l'API sans modifier le schéma existant. - */ - -const isTestEnv = () => process.env.NODE_ENV === 'test'; - -/* ------------------------------------------------------------------ */ -/* USER_ACCOUNT & USER_SECURITY */ -/* ------------------------------------------------------------------ */ -async function ensureUsersTable(pool) { - if (isTestEnv()) return; - - const accountTableExists = await pool.query(` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_schema = 'public' AND table_name = 'user_account' - ); - `); - - if (!accountTableExists.rows[0].exists) { - console.warn("Table USER_ACCOUNT n'existe pas. Elle devrait être créée par le script SQL principal."); - return; - } - - // Vérifier/configurer la séquence uuid_user - try { - const columnDefault = await pool.query(` - SELECT column_default - FROM information_schema.columns - WHERE table_schema = 'public' - AND table_name = 'user_account' - AND column_name = 'uuid_user'; - `); - - if (!columnDefault.rows[0]?.column_default || !columnDefault.rows[0].column_default.includes('nextval')) { - const sequenceExists = await pool.query(` - SELECT EXISTS ( - SELECT FROM pg_sequences - WHERE schemaname = 'public' - AND sequencename = 'user_account_uuid_user_seq' - ); - `); - - if (sequenceExists.rows[0].exists) { - await pool.query(`ALTER TABLE user_account ALTER COLUMN uuid_user SET DEFAULT nextval('user_account_uuid_user_seq');`); - console.log('Séquence user_account_uuid_user_seq liée à la colonne uuid_user'); - } else { - await pool.query(` - CREATE SEQUENCE IF NOT EXISTS user_account_uuid_user_seq; - ALTER TABLE user_account ALTER COLUMN uuid_user SET DEFAULT nextval('user_account_uuid_user_seq'); - ALTER SEQUENCE user_account_uuid_user_seq OWNED BY user_account.uuid_user; - `); - console.log('Séquence user_account_uuid_user_seq créée et liée'); - } - - const maxId = await pool.query('SELECT COALESCE(MAX(uuid_user), 0) as max_id FROM user_account;'); - if (maxId.rows[0].max_id > 0) { - await pool.query(`SELECT setval('user_account_uuid_user_seq', ${maxId.rows[0].max_id}, true);`); - console.log(`Séquence synchronisée à ${maxId.rows[0].max_id}`); - } - } else { - const maxId = await pool.query('SELECT COALESCE(MAX(uuid_user), 0) as max_id FROM user_account;'); - if (maxId.rows[0].max_id > 0) { - const currentVal = await pool.query('SELECT last_value FROM user_account_uuid_user_seq;'); - if (maxId.rows[0].max_id > currentVal.rows[0].last_value) { - await pool.query(`SELECT setval('user_account_uuid_user_seq', ${maxId.rows[0].max_id}, true);`); - console.log(`Séquence synchronisée à ${maxId.rows[0].max_id}`); - } - } - } - } catch (error) { - console.log('Vérification séquence uuid_user:', error.message); - } - - // Colonnes supplémentaires - try { - await pool.query(`ALTER TABLE user_account ADD COLUMN IF NOT EXISTS email_normalized TEXT;`); - await pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS uq_user_account_email_norm ON user_account(email_normalized);`); - await pool.query(`ALTER TABLE user_account ADD COLUMN IF NOT EXISTS name TEXT;`); - await pool.query(`ALTER TABLE user_account ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW();`); - await pool.query('CREATE INDEX IF NOT EXISTS idx_user_account_email_norm ON user_account(email_normalized);'); - } catch (error) { - console.log('Colonnes supplémentaires déjà présentes dans user_account:', error.message); - } - - // USER_SECURITY - const securityTableExists = await pool.query(` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_schema = 'public' AND table_name = 'user_security' - ); - `); - - if (!securityTableExists.rows[0].exists) { - console.warn("Table USER_SECURITY n'existe pas. Elle devrait être créée par le script SQL principal."); - return; - } - - try { - const mdpHashCheck = await pool.query(` - SELECT character_maximum_length - FROM information_schema.columns - WHERE table_schema = 'public' - AND table_name = 'user_security' - AND column_name = 'mdp_hash'; - `); - - if ( - mdpHashCheck.rows.length > 0 && - mdpHashCheck.rows[0].character_maximum_length && - mdpHashCheck.rows[0].character_maximum_length < 60 - ) { - await pool.query(`ALTER TABLE user_security ALTER COLUMN mdp_hash TYPE TEXT;`); - } - } catch (error) { - console.log('Vérification mdp_hash:', error.message); - } -} - -/* ------------------------------------------------------------------ */ -/* MDPH_DOCUMENT */ -/* ------------------------------------------------------------------ */ -async function ensureDocumentTables(pool) { - if (isTestEnv()) return; - - try { - await pool.query(` - ALTER TABLE mdph_document - ADD COLUMN IF NOT EXISTS user_id VARCHAR(42), - ADD COLUMN IF NOT EXISTS titre TEXT, - ADD COLUMN IF NOT EXISTS type TEXT, - ADD COLUMN IF NOT EXISTS chemin_vers_document TEXT, - ADD COLUMN IF NOT EXISTS taille TEXT, - ADD COLUMN IF NOT EXISTS status TEXT DEFAULT 'pending', - ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); - `); - // Permettre les documents non rattachés à un dossier/section - await pool.query('ALTER TABLE mdph_document ALTER COLUMN uuid_section_mdph DROP NOT NULL;').catch(() => {}); - await pool.query('CREATE INDEX IF NOT EXISTS idx_mdph_document_user ON mdph_document(user_id);'); - } catch (error) { - console.log('Table mdph_document déjà configurée:', error.message); - } -} - -/* ------------------------------------------------------------------ */ -/* FORUM_THREAD & FORUM_POST */ -/* ------------------------------------------------------------------ */ -async function ensureForumTables(pool) { - if (isTestEnv()) return; - - try { - await pool.query(` - ALTER TABLE FORUM_THREAD - ADD COLUMN IF NOT EXISTS owner_id TEXT, - ADD COLUMN IF NOT EXISTS uuid_user VARCHAR(42), - ADD COLUMN IF NOT EXISTS category TEXT DEFAULT 'general', - ADD COLUMN IF NOT EXISTS excerpt TEXT, - ADD COLUMN IF NOT EXISTS is_pinned BOOLEAN DEFAULT FALSE, - ADD COLUMN IF NOT EXISTS views INTEGER DEFAULT 0, - ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); - `); - - await pool.query(` - ALTER TABLE FORUM_POST - ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); - `); - - await pool.query('CREATE INDEX IF NOT EXISTS idx_forum_thread_category ON FORUM_THREAD(category);'); - await pool.query('CREATE INDEX IF NOT EXISTS idx_forum_thread_owner ON FORUM_THREAD(owner_id);'); - await pool.query('CREATE INDEX IF NOT EXISTS idx_forum_thread_user ON FORUM_THREAD(uuid_user);'); - await pool.query('CREATE INDEX IF NOT EXISTS idx_forum_post_thread ON FORUM_POST(uuid_thread);'); - await pool.query('CREATE INDEX IF NOT EXISTS idx_forum_post_user ON FORUM_POST(uuid_user);'); - } catch (error) { - console.log('Tables forum déjà configurées:', error.message); - } -} - -/* ------------------------------------------------------------------ */ -/* MDPH_DOSSIER & MDPH_SECTION */ -/* ------------------------------------------------------------------ */ -async function ensureDossierTables(pool) { - if (isTestEnv()) return; - - const dossierTableExists = await pool.query(` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_schema = 'public' AND table_name = 'mdph_dossier' - ); - `); - - if (!dossierTableExists.rows[0].exists) { - console.warn("Table MDPH_DOSSIER n'existe pas. Elle devrait être créée par le script SQL principal."); - return; - } - - try { - await pool.query(` - ALTER TABLE mdph_dossier - ADD COLUMN IF NOT EXISTS numero_dossier TEXT, - ADD COLUMN IF NOT EXISTS titre TEXT, - ADD COLUMN IF NOT EXISTS description TEXT, - ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); - `); - } catch (error) { - console.log('Colonnes supplémentaires déjà présentes dans mdph_dossier:', error.message); - } - - const sectionTableExists = await pool.query(` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_schema = 'public' AND table_name = 'mdph_section' - ); - `); - - if (!sectionTableExists.rows[0].exists) { - console.warn("Table MDPH_SECTION n'existe pas. Elle devrait être créée par le script SQL principal."); - return; - } - - try { - await pool.query(` - ALTER TABLE mdph_section - ADD COLUMN IF NOT EXISTS titre TEXT, - ADD COLUMN IF NOT EXISTS contenu JSONB DEFAULT '{}'::jsonb, - ADD COLUMN IF NOT EXISTS uuid_ai_session TEXT, - ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); - `); - - const aiSessionIndexExists = await pool.query(` - SELECT EXISTS ( - SELECT FROM pg_indexes - WHERE schemaname = 'public' - AND tablename = 'mdph_section' - AND indexname = 'uq_mdph_section_ai_session' - ); - `); - - if (!aiSessionIndexExists.rows[0].exists) { - await pool.query(` - CREATE UNIQUE INDEX IF NOT EXISTS uq_mdph_section_ai_session ON mdph_section(uuid_ai_session) - WHERE uuid_ai_session IS NOT NULL; - `); - } - - await pool.query('CREATE INDEX IF NOT EXISTS idx_mdph_section_numero ON mdph_section(numero_section);'); - await pool.query('CREATE INDEX IF NOT EXISTS idx_mdph_section_dossier ON mdph_section(uuid_dossier_mdph);'); - } catch (error) { - console.log('Colonnes supplémentaires déjà présentes dans mdph_section:', error.message); - } -} - -/* ------------------------------------------------------------------ */ -/* RESOURCE (centre de ressources) */ -/* ------------------------------------------------------------------ */ -async function ensureResourceTable(pool) { - if (isTestEnv()) return; - - const tableExists = await pool.query(` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_schema = 'public' AND table_name = 'resource' - ); - `); - - if (!tableExists.rows[0].exists) { - try { - await pool.query(` - CREATE TABLE resource ( - uuid_resource SERIAL PRIMARY KEY, - type TEXT NOT NULL, - icon TEXT, - category TEXT, - title TEXT NOT NULL, - description TEXT, - read_time TEXT, - keywords JSONB DEFAULT '[]'::jsonb, - created_by VARCHAR(42), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - ); - `); - await pool.query('CREATE INDEX IF NOT EXISTS idx_resource_type ON resource(type);'); - console.log('Table resource créée.'); - } catch (error) { - console.log('Erreur création resource:', error.message); - } - } else { - try { - await pool.query(` - ALTER TABLE resource - ADD COLUMN IF NOT EXISTS type TEXT, - ADD COLUMN IF NOT EXISTS icon TEXT, - ADD COLUMN IF NOT EXISTS category TEXT, - ADD COLUMN IF NOT EXISTS title TEXT, - ADD COLUMN IF NOT EXISTS description TEXT, - ADD COLUMN IF NOT EXISTS read_time TEXT, - ADD COLUMN IF NOT EXISTS keywords JSONB DEFAULT '[]'::jsonb, - ADD COLUMN IF NOT EXISTS created_by VARCHAR(42), - ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); - `); - await pool.query('CREATE INDEX IF NOT EXISTS idx_resource_type ON resource(type);'); - } catch (error) { - console.log('Colonnes resource déjà présentes:', error.message); - } - } -} - -/* ------------------------------------------------------------------ */ -/* AI_SESSION & AI_MESSAGE (chatbot MDPH) */ -/* ------------------------------------------------------------------ */ -async function ensureChatbotTables(pool) { - if (isTestEnv()) return; - - const sessionExists = await pool.query(` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_schema = 'public' AND table_name = 'ai_session' - ); - `); - - if (!sessionExists.rows[0].exists) { - try { - await pool.query(` - CREATE TABLE ai_session ( - uuid_ai_session VARCHAR(36) PRIMARY KEY, - uuid_dossier_mdph VARCHAR(42) NOT NULL, - section TEXT NOT NULL, - sous_section TEXT NOT NULL, - nom_formulaire TEXT NOT NULL, - resultat_json JSONB DEFAULT '{}'::jsonb, - traites JSONB DEFAULT '[]'::jsonb, - est_termine BOOLEAN DEFAULT FALSE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - UNIQUE (uuid_dossier_mdph, section, sous_section, nom_formulaire) - ); - `); - console.log('Table ai_session créée.'); - } catch (error) { - console.log('Erreur création ai_session:', error.message); - } - } else { - try { - await pool.query(` - ALTER TABLE ai_session - ADD COLUMN IF NOT EXISTS uuid_dossier_mdph VARCHAR(42), - ADD COLUMN IF NOT EXISTS section TEXT, - ADD COLUMN IF NOT EXISTS sous_section TEXT, - ADD COLUMN IF NOT EXISTS nom_formulaire TEXT, - ADD COLUMN IF NOT EXISTS resultat_json JSONB DEFAULT '{}'::jsonb, - ADD COLUMN IF NOT EXISTS traites JSONB DEFAULT '[]'::jsonb, - ADD COLUMN IF NOT EXISTS est_termine BOOLEAN DEFAULT FALSE, - ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(), - ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(); - `); - } catch (error) { - console.log('Colonnes ai_session déjà présentes:', error.message); - } - } - - const messageExists = await pool.query(` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_schema = 'public' AND table_name = 'ai_message' - ); - `); - - if (!messageExists.rows[0].exists) { - try { - await pool.query(` - CREATE TABLE ai_message ( - uuid_ai_message VARCHAR(36) PRIMARY KEY, - uuid_ai_session VARCHAR(36) NOT NULL REFERENCES ai_session(uuid_ai_session) ON DELETE CASCADE, - message_role TEXT NOT NULL, - message_conv TEXT, - token_input INTEGER DEFAULT 0, - token_output INTEGER DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - ); - `); - console.log('Table ai_message créée.'); - } catch (error) { - console.log('Erreur création ai_message:', error.message); - } - } else { - try { - await pool.query(` - ALTER TABLE ai_message - ADD COLUMN IF NOT EXISTS uuid_ai_session VARCHAR(36), - ADD COLUMN IF NOT EXISTS message_role TEXT, - ADD COLUMN IF NOT EXISTS message_conv TEXT, - ADD COLUMN IF NOT EXISTS token_input INTEGER DEFAULT 0, - ADD COLUMN IF NOT EXISTS token_output INTEGER DEFAULT 0, - ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(); - `); - } catch (error) { - console.log('Colonnes ai_message déjà présentes:', error.message); - } - } - - try { - const dossierFkType = await pool.query(` - SELECT data_type - FROM information_schema.columns - WHERE table_schema = 'public' - AND table_name = 'ai_session' - AND column_name = 'uuid_dossier_mdph'; - `); - - if (dossierFkType.rows[0]?.data_type === 'character varying') { - const invalid = await pool.query(` - SELECT COUNT(*)::int AS bad - FROM ai_session - WHERE uuid_dossier_mdph IS NOT NULL - AND uuid_dossier_mdph !~ '^[0-9]+$'; - `); - if (invalid.rows[0].bad === 0) { - await pool.query(` - ALTER TABLE ai_session - ALTER COLUMN uuid_dossier_mdph TYPE INTEGER - USING NULLIF(uuid_dossier_mdph, '')::integer; - `); - console.log('ai_session.uuid_dossier_mdph converti en INTEGER pour aligner avec mdph_dossier.'); - } else { - console.warn(`ai_session.uuid_dossier_mdph contient ${invalid.rows[0].bad} valeurs non numériques: conversion ignorée.`); - } - } - } catch (error) { - console.log('Migration ai_session.uuid_dossier_mdph:', error.message); - } - - try { - await pool.query('CREATE INDEX IF NOT EXISTS idx_ai_session_dossier ON ai_session(uuid_dossier_mdph);'); - await pool.query('CREATE INDEX IF NOT EXISTS idx_ai_message_session ON ai_message(uuid_ai_session);'); - await pool.query('CREATE UNIQUE INDEX IF NOT EXISTS uq_ai_session_form ON ai_session(uuid_section_mdph, sous_section, nom_formulaire) WHERE sous_section IS NOT NULL;'); - } catch (error) { - console.log('Index chatbot AI déjà configurés:', error.message); - } -} - -/* ------------------------------------------------------------------ */ -/* APP_SETTING (paramètres globaux) */ -/* ------------------------------------------------------------------ */ -const DEFAULT_APP_SETTINGS = { - site_name: 'UMHMA', - contact_email: 'contact@umhma.fr', - contact_phone: '01 23 45 67 89', - contact_address: 'Paris, France', - contact_hours: 'Disponibles du lundi au vendredi, de 9h à 18h.', - timezone: 'Europe/Paris', - language: 'fr', - maintenance_mode: 'false', - maintenance_message: - 'La plateforme est temporairement indisponible pour maintenance. Nous revenons très vite.', -}; - -async function ensureSettingsTable(pool) { - if (isTestEnv()) return; - try { - await pool.query(` - CREATE TABLE IF NOT EXISTS app_setting ( - key TEXT PRIMARY KEY, - value TEXT, - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - ); - `); - - for (const [key, value] of Object.entries(DEFAULT_APP_SETTINGS)) { - await pool.query( - `INSERT INTO app_setting (key, value) - VALUES ($1, $2) - ON CONFLICT (key) DO NOTHING`, - [key, value], - ); - } - } catch (error) { - console.log('Table app_setting déjà configurée:', error.message); - } -} - -/* ------------------------------------------------------------------ */ -/* FORUM_REPORT (modération communautaire) */ -/* ------------------------------------------------------------------ */ -async function ensureForumReportTable(pool) { - if (isTestEnv()) return; - try { - await pool.query(` - CREATE TABLE IF NOT EXISTS forum_report ( - uuid_report SERIAL PRIMARY KEY, - uuid_post INTEGER, - uuid_thread INTEGER, - uuid_reporter INTEGER, - target_type TEXT NOT NULL DEFAULT 'post', - reason TEXT NOT NULL, - priority TEXT NOT NULL DEFAULT 'medium', - status TEXT NOT NULL DEFAULT 'pending', - resolution_note TEXT, - resolved_by INTEGER, - resolved_at TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - ); - `); - await pool.query(`CREATE INDEX IF NOT EXISTS idx_forum_report_status ON forum_report(status);`); - await pool.query(`CREATE INDEX IF NOT EXISTS idx_forum_report_post ON forum_report(uuid_post);`); - } catch (error) { - console.log('Table forum_report déjà configurée:', error.message); - } - - try { - await pool.query(`ALTER TABLE forum_post ADD COLUMN IF NOT EXISTS is_hidden BOOLEAN DEFAULT FALSE;`); - await pool.query(`ALTER TABLE forum_thread ADD COLUMN IF NOT EXISTS is_hidden BOOLEAN DEFAULT FALSE;`); - } catch (error) { - console.log('Colonnes is_hidden déjà présentes:', error.message); - } -} - -/* ------------------------------------------------------------------ */ -/* Point d'entrée unique */ -/* ------------------------------------------------------------------ */ -async function initDatabase(pool) { - await ensureUsersTable(pool); - await ensureDossierTables(pool); - await ensureDocumentTables(pool); - await ensureForumTables(pool); - await ensureResourceTable(pool); - await ensureChatbotTables(pool); - await ensureSettingsTable(pool); - await ensureForumReportTable(pool); -} - -module.exports = { - ensureUsersTable, - ensureDossierTables, - ensureDocumentTables, - ensureForumTables, - ensureResourceTable, - ensureChatbotTables, - ensureSettingsTable, - ensureForumReportTable, - initDatabase, - DEFAULT_APP_SETTINGS, -}; - From cc387a830276ebe2f000a33c564fdbcc212054ac Mon Sep 17 00:00:00 2001 From: PHAN Gabriel Date: Wed, 13 May 2026 01:40:34 +0200 Subject: [PATCH 3/6] =?UTF-8?q?renommage=20colonnes=20mdph=20pour=20adapte?= =?UTF-8?q?r=20=C3=A0=20la=20AWS=20et=20=C3=A9viter=20erreurs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/controllers/adminController.js | 24 +++++----- backend/src/controllers/chatbotController.js | 40 ++++++++--------- backend/src/controllers/documentController.js | 16 +++---- backend/src/controllers/dossierController.js | 44 +++++++++---------- backend/src/controllers/sectionController.js | 34 +++++++------- backend/src/routes/documents.js | 2 +- backend/tests/test-gerant-dashboard.js | 2 +- backend/tests/test-integration-rls.js | 8 ++-- 8 files changed, 85 insertions(+), 85 deletions(-) diff --git a/backend/src/controllers/adminController.js b/backend/src/controllers/adminController.js index b709e76..709fad4 100644 --- a/backend/src/controllers/adminController.js +++ b/backend/src/controllers/adminController.js @@ -301,8 +301,8 @@ function deleteUser(pool) { WHERE uuid_ai_session IN ( SELECT s.uuid_ai_session FROM ai_session s - LEFT JOIN mdph_section sec ON sec.uuid_section_mdph = s.uuid_section_mdph - LEFT JOIN mdph_dossier d ON d.uuid_dossier_mdph = sec.uuid_dossier_mdph + LEFT JOIN mdph_section sec ON sec.uuid_mdph_section = s.uuid_mdph_section + LEFT JOIN mdph_dossier d ON d.uuid_mdph_dossier = sec.uuid_mdph_dossier WHERE s.uuid_patient = $1 OR d.uuid_patient = $1 )`, @@ -314,10 +314,10 @@ function deleteUser(pool) { // 2b. Sessions IA liées à des sections appartenant au patient await client.query( `DELETE FROM ai_session - WHERE uuid_section_mdph IN ( - SELECT sec.uuid_section_mdph + WHERE uuid_mdph_section IN ( + SELECT sec.uuid_mdph_section FROM mdph_section sec - JOIN mdph_dossier d ON d.uuid_dossier_mdph = sec.uuid_dossier_mdph + JOIN mdph_dossier d ON d.uuid_mdph_dossier = sec.uuid_mdph_dossier WHERE d.uuid_patient = $1 )`, [userId], @@ -326,10 +326,10 @@ function deleteUser(pool) { // 3. Documents liés aux sections du patient await client.query( `DELETE FROM mdph_document - WHERE uuid_section_mdph IN ( - SELECT sec.uuid_section_mdph + WHERE uuid_mdph_section IN ( + SELECT sec.uuid_mdph_section FROM mdph_section sec - JOIN mdph_dossier d ON d.uuid_dossier_mdph = sec.uuid_dossier_mdph + JOIN mdph_dossier d ON d.uuid_mdph_dossier = sec.uuid_mdph_dossier WHERE d.uuid_patient = $1 )`, [userId], @@ -338,8 +338,8 @@ function deleteUser(pool) { // 4. Sections MDPH du patient await client.query( `DELETE FROM mdph_section - WHERE uuid_dossier_mdph IN ( - SELECT uuid_dossier_mdph FROM mdph_dossier WHERE uuid_patient = $1 + WHERE uuid_mdph_dossier IN ( + SELECT uuid_mdph_dossier FROM mdph_dossier WHERE uuid_patient = $1 )`, [userId], ); @@ -348,8 +348,8 @@ function deleteUser(pool) { await client.query( `DELETE FROM gerer WHERE uuid_aidant = $1 - OR uuid_dossier_mdph IN ( - SELECT uuid_dossier_mdph FROM mdph_dossier WHERE uuid_patient = $1 + OR uuid_mdph_dossier IN ( + SELECT uuid_mdph_dossier FROM mdph_dossier WHERE uuid_patient = $1 )`, [userId], ); diff --git a/backend/src/controllers/chatbotController.js b/backend/src/controllers/chatbotController.js index c2f3c87..d13e4ee 100644 --- a/backend/src/controllers/chatbotController.js +++ b/backend/src/controllers/chatbotController.js @@ -39,7 +39,7 @@ function makeChatbotController(pool) { const existing = await pool.query( `SELECT uuid_ai_session, resultat_json, traites, est_termine FROM ai_session - WHERE uuid_section_mdph = $1::int AND sous_section = $2 AND nom_formulaire = $3`, + WHERE uuid_mdph_section = $1::int AND sous_section = $2 AND nom_formulaire = $3`, [sectionMdphId, sousSection, formulaire], ); @@ -74,7 +74,7 @@ function makeChatbotController(pool) { const patientId = patientRow.rows.length > 0 ? patientRow.rows[0].uuid_patient : null; const insertRes = await pool.query( - `INSERT INTO ai_session (uuid_section_mdph, uuid_patient, uuid_dossier_mdph, section, sous_section, nom_formulaire, resultat_json, traites) + `INSERT INTO ai_session (uuid_mdph_section, uuid_patient, uuid_mdph_dossier, section, sous_section, nom_formulaire, resultat_json, traites) VALUES ($1::int, $2::int, $3::int, $4, $5, $6, $7::jsonb, $8::jsonb) RETURNING uuid_ai_session`, [sectionMdphId, patientId, dossierId, section, sousSection, formulaire, '{}', '[]'], @@ -129,7 +129,7 @@ function makeChatbotController(pool) { } const sessionRow = await pool.query( - `SELECT ais.uuid_ai_session, ais.uuid_dossier_mdph, ais.section, ais.sous_section, + `SELECT ais.uuid_ai_session, ais.uuid_mdph_dossier, ais.section, ais.sous_section, ais.nom_formulaire, ais.resultat_json, ais.traites, ais.est_termine FROM ai_session ais WHERE ais.uuid_ai_session = $1::int`, @@ -155,7 +155,7 @@ function makeChatbotController(pool) { const historique = await loadHistorique(pool, sessionId); - const note = await resolveNote(pool, session.uuid_dossier_mdph, session.section); + const note = await resolveNote(pool, session.uuid_mdph_dossier, session.section); const jsonCommence = session.resultat_json && Object.keys(session.resultat_json).length > 0 ? session.resultat_json : null; @@ -169,7 +169,7 @@ function makeChatbotController(pool) { await saveMessage(pool, sessionId, 'user', userMessage, 0, 0); await saveMessage(pool, sessionId, 'assistant', result.answer, result.input_tokens, result.output_tokens); await persistState(pool, sessionId, chatbot.dataFields, [...chatbot.traites], result.est_termine); - await syncChatbotToSection(pool, session.uuid_dossier_mdph, session.section, session.nom_formulaire, chatbot.dataFields); + await syncChatbotToSection(pool, session.uuid_mdph_dossier, session.section, session.nom_formulaire, chatbot.dataFields); const extra = (result.est_termine && session.section === 'start') ? computeRequiredSectionsFromBesoin((result.formData || {}).besoin) @@ -308,8 +308,8 @@ async function computeRequiredSections(pool, dossierId) { const row = await pool.query( `SELECT ais.resultat_json, ais.est_termine FROM ai_session ais - JOIN mdph_section ms ON ais.uuid_section_mdph = ms.uuid_section_mdph - WHERE ms.uuid_dossier_mdph = $1::int AND ais.section = 'start' + JOIN mdph_section ms ON ais.uuid_mdph_section = ms.uuid_mdph_section + WHERE ms.uuid_mdph_dossier = $1::int AND ais.section = 'start' AND ais.sous_section = 'debut' AND ais.nom_formulaire = 'debut'`, [dossierId], ); @@ -331,22 +331,22 @@ async function computeRequiredSections(pool, dossierId) { async function getOrCreateMdphSection(pool, dossierId, sectionName) { const existing = await pool.query( - `SELECT uuid_section_mdph FROM mdph_section - WHERE uuid_dossier_mdph = $1::int AND numero_section = $2`, + `SELECT uuid_mdph_section FROM mdph_section + WHERE uuid_mdph_dossier = $1::int AND numero_section = $2`, [dossierId, sectionName], ); if (existing.rows.length > 0) { - return existing.rows[0].uuid_section_mdph; + return existing.rows[0].uuid_mdph_section; } const result = await pool.query( - `INSERT INTO mdph_section (uuid_dossier_mdph, numero_section) + `INSERT INTO mdph_section (uuid_mdph_dossier, numero_section) VALUES ($1::int, $2) - RETURNING uuid_section_mdph`, + RETURNING uuid_mdph_section`, [dossierId, sectionName], ); - return result.rows[0].uuid_section_mdph; + return result.rows[0].uuid_mdph_section; } async function loadHistorique(pool, sessionId) { @@ -379,8 +379,8 @@ async function resolveNote(pool, dossierId, section) { const row = await pool.query( `SELECT ais.resultat_json, ais.est_termine FROM ai_session ais - JOIN mdph_section ms ON ais.uuid_section_mdph = ms.uuid_section_mdph - WHERE ms.uuid_dossier_mdph = $1::int AND ais.section = 'start' + JOIN mdph_section ms ON ais.uuid_mdph_section = ms.uuid_mdph_section + WHERE ms.uuid_mdph_dossier = $1::int AND ais.section = 'start' AND ais.sous_section = 'debut' AND ais.nom_formulaire = 'debut'`, [dossierId], ); @@ -421,19 +421,19 @@ async function syncChatbotToSection(pool, dossierId, section, formulaire, formDa try { const existing = await pool.query( - 'SELECT uuid_section_mdph, contenu FROM mdph_section WHERE uuid_dossier_mdph = $1::int AND numero_section = $2', + 'SELECT uuid_mdph_section, contenu FROM mdph_section WHERE uuid_mdph_dossier = $1::int AND numero_section = $2', [dossierId, section], ); if (existing.rows.length === 0) return; - const sectionId = existing.rows[0].uuid_section_mdph; + const sectionId = existing.rows[0].uuid_mdph_section; const contenu = existing.rows[0].contenu || {}; contenu[formulaire] = formData; await pool.query( - 'UPDATE mdph_section SET contenu = $1::jsonb, updated_at = NOW() WHERE uuid_section_mdph = $2::int', + 'UPDATE mdph_section SET contenu = $1::jsonb, updated_at = NOW() WHERE uuid_mdph_section = $2::int', [JSON.stringify(contenu), sectionId], ); } catch (err) { @@ -456,7 +456,7 @@ async function hasDossierAccess(pool, dossierId, userId) { const result = await pool.query( `SELECT 1 FROM mdph_dossier - WHERE uuid_dossier_mdph = $1::int AND uuid_patient = $2::int`, + WHERE uuid_mdph_dossier = $1::int AND uuid_patient = $2::int`, [dossierId, userId], ); return result.rows.length > 0; @@ -466,7 +466,7 @@ async function hasSessionAccess(pool, sessionId, userId) { const result = await pool.query( `SELECT 1 FROM ai_session ais - JOIN mdph_dossier d ON d.uuid_dossier_mdph = ais.uuid_dossier_mdph + JOIN mdph_dossier d ON d.uuid_mdph_dossier = ais.uuid_mdph_dossier WHERE ais.uuid_ai_session = $1::int AND d.uuid_patient = $2::int`, [sessionId, userId], ); diff --git a/backend/src/controllers/documentController.js b/backend/src/controllers/documentController.js index eed9177..517f372 100644 --- a/backend/src/controllers/documentController.js +++ b/backend/src/controllers/documentController.js @@ -2,17 +2,17 @@ function makeDocumentController(pool) { async function resolveSectionIdForDocument(userId, providedSectionId) { // Si une section est fournie, on la valide en s'assurant qu'elle appartient // bien à un dossier de l'utilisateur. Sinon, on retourne null : le document - // sera enregistré sans rattachement (uuid_section_mdph nullable). + // sera enregistré sans rattachement (uuid_mdph_section nullable). if (providedSectionId != null) { const sectionCheck = await pool.query( - `SELECT s.uuid_section_mdph + `SELECT s.uuid_mdph_section FROM mdph_section s - JOIN mdph_dossier d ON d.uuid_dossier_mdph = s.uuid_dossier_mdph - WHERE s.uuid_section_mdph = $1 AND d.uuid_patient = $2 + JOIN mdph_dossier d ON d.uuid_mdph_dossier = s.uuid_mdph_dossier + WHERE s.uuid_mdph_section = $1 AND d.uuid_patient = $2 LIMIT 1`, [providedSectionId, userId], ); - if (sectionCheck.rows.length > 0) return sectionCheck.rows[0].uuid_section_mdph; + if (sectionCheck.rows.length > 0) return sectionCheck.rows[0].uuid_mdph_section; } return null; } @@ -30,14 +30,14 @@ function makeDocumentController(pool) { return { async create(req, res, next) { try { - const { titre, type, chemin_vers_document, taille, status, uuid_section_mdph } = req.body || {}; + const { titre, type, chemin_vers_document, taille, status, uuid_mdph_section } = req.body || {}; if (!req.user || !req.user.id) { return res.status(401).json({ message: 'Authentification requise.' }); } - const sectionId = await resolveSectionIdForDocument(req.user.id, uuid_section_mdph); + const sectionId = await resolveSectionIdForDocument(req.user.id, uuid_mdph_section); const result = await pool.query( - `INSERT INTO mdph_document (uuid_section_mdph, user_id, titre_document, titre, type, chemin_vers_document, taille, status) + `INSERT INTO mdph_document (uuid_mdph_section, user_id, titre_document, titre, type, chemin_vers_document, taille, status) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING uuid_document_mdph as id, user_id, COALESCE(titre, titre_document) as titre, type, chemin_vers_document, taille, status, created_at, updated_at`, [ diff --git a/backend/src/controllers/dossierController.js b/backend/src/controllers/dossierController.js index 5882f7b..52312c8 100644 --- a/backend/src/controllers/dossierController.js +++ b/backend/src/controllers/dossierController.js @@ -3,7 +3,7 @@ const { generateCerfaPDF } = require('../services/pdfExport'); async function hasDossierAccess(pool, dossierId, userId) { const result = await pool.query( `SELECT 1 FROM mdph_dossier - WHERE uuid_dossier_mdph = $1 AND uuid_patient = $2`, + WHERE uuid_mdph_dossier = $1 AND uuid_patient = $2`, [dossierId, userId], ); return result.rows.length > 0; @@ -35,7 +35,7 @@ function makeDossierController(pool) { const result = await pool.query( `INSERT INTO mdph_dossier (uuid_patient, numero_dossier, titre, description) VALUES ($1, $2, $3, $4) - RETURNING uuid_dossier_mdph as id, uuid_patient, numero_dossier, titre, description, created_at, updated_at`, + RETURNING uuid_mdph_dossier as id, uuid_patient, numero_dossier, titre, description, created_at, updated_at`, [uuidPatient, numero_dossier || null, titre || null, description || null], ); return res.status(201).json({ dossier: result.rows[0] }); @@ -47,7 +47,7 @@ function makeDossierController(pool) { async list(req, res, next) { try { const result = await pool.query( - `SELECT uuid_dossier_mdph as id, numero_dossier, titre, description, created_at, updated_at + `SELECT uuid_mdph_dossier as id, numero_dossier, titre, description, created_at, updated_at FROM mdph_dossier WHERE uuid_patient = $1 ORDER BY created_at DESC`, @@ -67,18 +67,18 @@ function makeDossierController(pool) { return res.status(403).json({ message: 'Accès refusé.' }); } const dossierResult = await pool.query( - `SELECT uuid_dossier_mdph as id, numero_dossier, titre, description, created_at, updated_at + `SELECT uuid_mdph_dossier as id, numero_dossier, titre, description, created_at, updated_at FROM mdph_dossier - WHERE uuid_dossier_mdph = $1`, + WHERE uuid_mdph_dossier = $1`, [dossierId], ); if (dossierResult.rows.length === 0) { return res.status(404).json({ message: 'Dossier introuvable.' }); } const sectionsResult = await pool.query( - `SELECT uuid_section_mdph as id, uuid_dossier_mdph as dossier_id, numero_section, titre, contenu, uuid_ai_session, created_at, updated_at + `SELECT uuid_mdph_section as id, uuid_mdph_dossier as dossier_id, numero_section, titre, contenu, uuid_ai_session, created_at, updated_at FROM mdph_section - WHERE uuid_dossier_mdph = $1 + WHERE uuid_mdph_dossier = $1 ORDER BY created_at ASC`, [dossierId], ); @@ -101,8 +101,8 @@ function makeDossierController(pool) { titre = COALESCE($3, titre), description = COALESCE($4, description), updated_at = NOW() - WHERE uuid_dossier_mdph = $1 - RETURNING uuid_dossier_mdph as id, numero_dossier, titre, description, created_at, updated_at`, + WHERE uuid_mdph_dossier = $1 + RETURNING uuid_mdph_dossier as id, numero_dossier, titre, description, created_at, updated_at`, [dossierId, numero_dossier ?? null, titre ?? null, description ?? null], ); if (result.rows.length === 0) { @@ -127,35 +127,35 @@ function makeDossierController(pool) { // Aucune des FK liées au dossier n'a ON DELETE CASCADE : on purge // explicitement toutes les dépendances avant de supprimer le dossier. // Les documents auto-rattachés à une section du dossier sont supprimés - // en même temps (NOT NULL sur uuid_section_mdph). + // en même temps (NOT NULL sur uuid_mdph_section). await client.query( `DELETE FROM ai_message WHERE uuid_ai_session IN ( SELECT uuid_ai_session FROM ai_session - WHERE uuid_dossier_mdph::text = $1::text + WHERE uuid_mdph_dossier::text = $1::text )`, [dossierId], ); await client.query( - `DELETE FROM ai_session WHERE uuid_dossier_mdph::text = $1::text`, + `DELETE FROM ai_session WHERE uuid_mdph_dossier::text = $1::text`, [dossierId], ); await client.query( `DELETE FROM mdph_document - WHERE uuid_section_mdph IN ( - SELECT uuid_section_mdph FROM mdph_section - WHERE uuid_dossier_mdph = $1 + WHERE uuid_mdph_section IN ( + SELECT uuid_mdph_section FROM mdph_section + WHERE uuid_mdph_dossier = $1 )`, [dossierId], ); await client.query( - `DELETE FROM mdph_section WHERE uuid_dossier_mdph = $1`, + `DELETE FROM mdph_section WHERE uuid_mdph_dossier = $1`, [dossierId], ); const result = await client.query( `DELETE FROM mdph_dossier - WHERE uuid_dossier_mdph = $1 - RETURNING uuid_dossier_mdph as id`, + WHERE uuid_mdph_dossier = $1 + RETURNING uuid_mdph_dossier as id`, [dossierId], ); @@ -181,9 +181,9 @@ function makeDossierController(pool) { return res.status(403).json({ message: 'Accès refusé.' }); } const dossierResult = await pool.query( - `SELECT uuid_dossier_mdph as id, numero_dossier, titre, description, created_at, updated_at + `SELECT uuid_mdph_dossier as id, numero_dossier, titre, description, created_at, updated_at FROM mdph_dossier - WHERE uuid_dossier_mdph = $1`, + WHERE uuid_mdph_dossier = $1`, [dossierId], ); if (dossierResult.rows.length === 0) { @@ -191,9 +191,9 @@ function makeDossierController(pool) { } const sectionsResult = await pool.query( - `SELECT uuid_section_mdph as id, numero_section, titre, contenu + `SELECT uuid_mdph_section as id, numero_section, titre, contenu FROM mdph_section - WHERE uuid_dossier_mdph = $1 + WHERE uuid_mdph_dossier = $1 ORDER BY numero_section ASC`, [dossierId], ); diff --git a/backend/src/controllers/sectionController.js b/backend/src/controllers/sectionController.js index 1281f3b..4747246 100644 --- a/backend/src/controllers/sectionController.js +++ b/backend/src/controllers/sectionController.js @@ -1,7 +1,7 @@ function makeSectionController(pool) { async function canAccessDossier(dossierId, userId) { const result = await pool.query( - `SELECT 1 FROM mdph_dossier WHERE uuid_dossier_mdph = $1 AND uuid_patient = $2`, + `SELECT 1 FROM mdph_dossier WHERE uuid_mdph_dossier = $1 AND uuid_patient = $2`, [dossierId, userId], ); return result.rows.length > 0; @@ -9,10 +9,10 @@ function makeSectionController(pool) { async function getOwnedSection(sectionId, userId) { const result = await pool.query( - `SELECT s.uuid_section_mdph as id, s.uuid_dossier_mdph as dossier_id, s.numero_section, s.titre, s.contenu, s.uuid_ai_session, s.created_at, s.updated_at + `SELECT s.uuid_mdph_section as id, s.uuid_mdph_dossier as dossier_id, s.numero_section, s.titre, s.contenu, s.uuid_ai_session, s.created_at, s.updated_at FROM mdph_section s - JOIN mdph_dossier d ON d.uuid_dossier_mdph = s.uuid_dossier_mdph - WHERE s.uuid_section_mdph = $1 AND d.uuid_patient = $2`, + JOIN mdph_dossier d ON d.uuid_mdph_dossier = s.uuid_mdph_dossier + WHERE s.uuid_mdph_section = $1 AND d.uuid_patient = $2`, [sectionId, userId], ); return result.rows[0] || null; @@ -28,7 +28,7 @@ function makeSectionController(pool) { } const dossier = await pool.query( - `SELECT uuid_dossier_mdph FROM mdph_dossier WHERE uuid_dossier_mdph = $1`, + `SELECT uuid_mdph_dossier FROM mdph_dossier WHERE uuid_mdph_dossier = $1`, [dossierId], ); if (dossier.rows.length === 0) { @@ -36,9 +36,9 @@ function makeSectionController(pool) { } const result = await pool.query( - `INSERT INTO mdph_section (uuid_dossier_mdph, numero_section, titre, contenu, uuid_ai_session) + `INSERT INTO mdph_section (uuid_mdph_dossier, numero_section, titre, contenu, uuid_ai_session) VALUES ($1, $2, $3, $4, $5) - RETURNING uuid_section_mdph as id, uuid_dossier_mdph as dossier_id, numero_section, titre, contenu, uuid_ai_session, created_at, updated_at`, + RETURNING uuid_mdph_section as id, uuid_mdph_dossier as dossier_id, numero_section, titre, contenu, uuid_ai_session, created_at, updated_at`, [dossierId, numero_section || null, titre || null, contenu || {}, uuid_ai_session || null], ); return res.status(201).json({ section: result.rows[0] }); @@ -54,9 +54,9 @@ function makeSectionController(pool) { return res.status(403).json({ message: 'Accès refusé.' }); } const result = await pool.query( - `SELECT uuid_section_mdph as id, uuid_dossier_mdph as dossier_id, numero_section, titre, contenu, uuid_ai_session, created_at, updated_at + `SELECT uuid_mdph_section as id, uuid_mdph_dossier as dossier_id, numero_section, titre, contenu, uuid_ai_session, created_at, updated_at FROM mdph_section - WHERE uuid_dossier_mdph = $1 + WHERE uuid_mdph_dossier = $1 ORDER BY created_at ASC`, [dossierId], ); @@ -96,11 +96,11 @@ function makeSectionController(pool) { contenu = COALESCE($4, contenu), uuid_ai_session = COALESCE($5, uuid_ai_session), updated_at = NOW() - WHERE uuid_section_mdph = $1 - AND uuid_dossier_mdph IN ( - SELECT uuid_dossier_mdph FROM mdph_dossier WHERE uuid_patient = $6 + WHERE uuid_mdph_section = $1 + AND uuid_mdph_dossier IN ( + SELECT uuid_mdph_dossier FROM mdph_dossier WHERE uuid_patient = $6 ) - RETURNING uuid_section_mdph as id, uuid_dossier_mdph as dossier_id, numero_section, titre, contenu, uuid_ai_session, created_at, updated_at`, + RETURNING uuid_mdph_section as id, uuid_mdph_dossier as dossier_id, numero_section, titre, contenu, uuid_ai_session, created_at, updated_at`, [id, numero_section ?? null, titre ?? null, contenu ?? null, uuid_ai_session ?? null, req.user.id], ); @@ -122,11 +122,11 @@ function makeSectionController(pool) { } const result = await pool.query( `DELETE FROM mdph_section - WHERE uuid_section_mdph = $1 - AND uuid_dossier_mdph IN ( - SELECT uuid_dossier_mdph FROM mdph_dossier WHERE uuid_patient = $2 + WHERE uuid_mdph_section = $1 + AND uuid_mdph_dossier IN ( + SELECT uuid_mdph_dossier FROM mdph_dossier WHERE uuid_patient = $2 ) - RETURNING uuid_section_mdph as id`, + RETURNING uuid_mdph_section as id`, [id, req.user.id], ); if (result.rows.length === 0) { diff --git a/backend/src/routes/documents.js b/backend/src/routes/documents.js index 8e834fe..3b87e5b 100644 --- a/backend/src/routes/documents.js +++ b/backend/src/routes/documents.js @@ -10,7 +10,7 @@ function documentRoutes(pool) { router.post('/', validateBody({ titre: { type: 'string', maxLength: 200 }, - uuid_section_mdph: { type: 'string', maxLength: 80 }, + uuid_mdph_section: { type: 'string', maxLength: 80 }, type: { type: 'string', maxLength: 80 }, chemin_vers_document: { type: 'string', maxLength: 1000 }, taille: { type: 'string', maxLength: 80 }, diff --git a/backend/tests/test-gerant-dashboard.js b/backend/tests/test-gerant-dashboard.js index 8438331..0a2ad1a 100644 --- a/backend/tests/test-gerant-dashboard.js +++ b/backend/tests/test-gerant-dashboard.js @@ -94,7 +94,7 @@ async function testGerantDashboardAccess() { const sectionsResult = await queryWithUserRole(req, pool, async (client) => { return await client.query(` SELECT COUNT(*) as total_sections, - COUNT(DISTINCT uuid_dossier_mdph) as dossiers_avec_sections + COUNT(DISTINCT uuid_mdph_dossier) as dossiers_avec_sections FROM mdph_section `); }); diff --git a/backend/tests/test-integration-rls.js b/backend/tests/test-integration-rls.js index 3aa563f..2e01360 100644 --- a/backend/tests/test-integration-rls.js +++ b/backend/tests/test-integration-rls.js @@ -196,19 +196,19 @@ async function testMultipleUsersIsolation() { // Patient 1 await client1.query('SET ROLE patient'); await client1.query(`SET app.current_user_uuid = '${patient1.uuid_user}'`); - const dossiers1 = await client1.query('SELECT uuid_dossier_mdph FROM MDPH_DOSSIER'); + const dossiers1 = await client1.query('SELECT uuid_mdph_dossier FROM MDPH_DOSSIER'); // Patient 2 await client2.query('SET ROLE patient'); await client2.query(`SET app.current_user_uuid = '${patient2.uuid_user}'`); - const dossiers2 = await client2.query('SELECT uuid_dossier_mdph FROM MDPH_DOSSIER'); + const dossiers2 = await client2.query('SELECT uuid_mdph_dossier FROM MDPH_DOSSIER'); log.info(`Patient 1 voit ${dossiers1.rows.length} dossier(s)`); log.info(`Patient 2 voit ${dossiers2.rows.length} dossier(s)`); // Vérifier qu'il n'y a pas de chevauchement - const ids1 = dossiers1.rows.map(r => r.uuid_dossier_mdph); - const ids2 = dossiers2.rows.map(r => r.uuid_dossier_mdph); + const ids1 = dossiers1.rows.map(r => r.uuid_mdph_dossier); + const ids2 = dossiers2.rows.map(r => r.uuid_mdph_dossier); const overlap = ids1.filter(id => ids2.includes(id)); if (overlap.length === 0) { From d25ff8431aa4377803c00036f4882be81aea6d8f Mon Sep 17 00:00:00 2001 From: PHAN Gabriel Date: Mon, 18 May 2026 17:49:31 +0200 Subject: [PATCH 4/6] Ajout scripts SQL + ipynb d'explication pour connection --- backend/scripts_SQL/connecter_bdd_aws.ipynb | 143 ++++++ .../scripts_SQL/umhma_script_creation_bdd.sql | 409 ++++++++++++++++++ ..._script_creation_bdd_avant_corrections.sql | 308 +++++++++++++ .../umhma_script_remove_role_technique.sql | 17 + .../scripts_SQL/umhma_script_remove_roles.sql | 88 ++++ .../scripts_SQL/umhma_script_role_aidant.sql | 371 ++++++++++++++++ .../scripts_SQL/umhma_script_role_gerant.sql | 67 +++ .../scripts_SQL/umhma_script_role_patient.sql | 314 ++++++++++++++ .../umhma_script_role_technique.sql | 107 +++++ 9 files changed, 1824 insertions(+) create mode 100644 backend/scripts_SQL/connecter_bdd_aws.ipynb create mode 100644 backend/scripts_SQL/umhma_script_creation_bdd.sql create mode 100644 backend/scripts_SQL/umhma_script_creation_bdd_avant_corrections.sql create mode 100644 backend/scripts_SQL/umhma_script_remove_role_technique.sql create mode 100644 backend/scripts_SQL/umhma_script_remove_roles.sql create mode 100644 backend/scripts_SQL/umhma_script_role_aidant.sql create mode 100644 backend/scripts_SQL/umhma_script_role_gerant.sql create mode 100644 backend/scripts_SQL/umhma_script_role_patient.sql create mode 100644 backend/scripts_SQL/umhma_script_role_technique.sql diff --git a/backend/scripts_SQL/connecter_bdd_aws.ipynb b/backend/scripts_SQL/connecter_bdd_aws.ipynb new file mode 100644 index 0000000..ba10067 --- /dev/null +++ b/backend/scripts_SQL/connecter_bdd_aws.ipynb @@ -0,0 +1,143 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 18, + "id": "2caad7d0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1️⃣ Fichier certificat existe ?\n", + " True\n", + "\n", + "2️⃣ Token IAM généré ?\n", + " ✅ Token généré (356 caractères)\n", + "\n", + "3️⃣ Connexion RDS ?\n", + " ✅ Connecté !\n" + ] + } + ], + "source": [ + "import boto3\n", + "import psycopg2\n", + "import os\n", + "\n", + "db_host = \"umhma.ctk28i8kybio.eu-west-3.rds.amazonaws.com\"\n", + "db_port = 5432\n", + "db_name = \"UMHMA\"\n", + "db_user = \"umhma_admin\"\n", + "# db_user = \"app_umhma\"\n", + "region = \"eu-west-3\"\n", + "\n", + "# 1. Check certificate\n", + "print(\"1️⃣ Fichier certificat existe ?\")\n", + "print(f\" {os.path.exists('global-bundle.pem')}\")\n", + "\n", + "# 2. Generate IAM token\n", + "print(\"\\n2️⃣ Token IAM généré ?\")\n", + "try:\n", + " client = boto3.client(\"rds\", region_name=region)\n", + " token = client.generate_db_auth_token(\n", + " DBHostname=db_host,\n", + " Port=db_port,\n", + " DBUsername=db_user,\n", + " Region=region\n", + " )\n", + " print(f\" ✅ Token généré ({len(token)} caractères)\")\n", + "except Exception as e:\n", + " print(f\" ❌ Erreur token : {e}\")\n", + " exit()\n", + "\n", + "# 3. Connect to database\n", + "print(\"\\n3️⃣ Connexion RDS ?\")\n", + "try:\n", + " conn = psycopg2.connect(\n", + " host=db_host,\n", + " port=db_port,\n", + " database=db_name,\n", + " user=db_user,\n", + " # password=\"changer_mdp\",\n", + " password=token,\n", + " sslmode=\"verify-full\",\n", + " sslrootcert=\"global-bundle.pem\",\n", + " connect_timeout=10\n", + " )\n", + " print(\" ✅ Connecté !\")\n", + "except Exception as e:\n", + " print(f\" ❌ Erreur : {type(e).__name__}\")\n", + " print(f\" Message : {e}\") " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69bd5f0d", + "metadata": {}, + "outputs": [], + "source": [ + "print(token)\n", + "\n", + "# Avec le token généré, cela peut servir de MDP pour vous connecter directement dans le terminal de commande (username peut être \"app_umhma\" ou \"umhma_admin\" selon les droits que vous voulez tester) :\n", + "# autres roles mais non connectable avec ce token : \"aidant\", \"gerant\", \"patient\" (accessible via app_umhma avec SET ROLE) et \"postgres\" (voir avec commanditaire pour le mdp)\n", + "# psql --host=umhma.ctk28i8kybio.eu-west-3.rds.amazonaws.com --port=5432 --dbname=UMHMA --username=umhma_admin\n", + "\n", + "# NOTEZ pour que cela fonctionne, vous devez appliquer le \"aws configure\" afin de vous connecter sur le user IAM (voir commanditaire, sur le IAM de Gabriel_IT)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "94bd38f2", + "metadata": {}, + "outputs": [], + "source": [ + "cur = conn.cursor()\n", + "cur.execute(\"\"\"\n", + "SELECT * FROM USER_ACCOUNT;\n", + "\"\"\")\n", + "\n", + "# Récupérer toutes les lignes\n", + "rows = cur.fetchall()\n", + "\n", + "# Afficher chaque ligne\n", + "for row in rows:\n", + " print(row)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "5478a04a", + "metadata": {}, + "outputs": [], + "source": [ + "conn.close()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "env_umhma (3.12.6)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/backend/scripts_SQL/umhma_script_creation_bdd.sql b/backend/scripts_SQL/umhma_script_creation_bdd.sql new file mode 100644 index 0000000..1dc3ee6 --- /dev/null +++ b/backend/scripts_SQL/umhma_script_creation_bdd.sql @@ -0,0 +1,409 @@ +DROP TABLE IF EXISTS + CATEGORISER, + GERER, + PROPOSER_SUGGESTION, + EST_ELIGIBLE, + RENDEZ_VOUS, + AI_MESSAGE, + AI_SESSION, + AIDANT, + PATIENT, + GERANT, + MDPH_DOSSIER, + MDPH_SECTION, + MDPH_DOCUMENT, + FORUM_POST, + FORUM_REPORT, + FORUM_THREAD, + FORUM_TOPIC, + AUDIT_LOGS, + TYPE_ACTION, + ELIGIBILITE, + SUGGESTION, + USER_SECURITY, + USER_ACCOUNT, + RESOURCE, + APP_SETTING +CASCADE; + +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; +-- Pour avoir accès à la fonction gen_random_uuid() + +CREATE TABLE USER_ACCOUNT ( + uuid_user UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT, + email VARCHAR(511) UNIQUE NOT NULL, + email_normalized TEXT, + total_input_tokens_spent INT DEFAULT 0, + total_output_tokens_spent INT DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- INSERT INTO USER_ACCOUNT (email) VALUES (...); +-- Notes : uuid_user, total_input_tokens_spent et total_output_tokens_spent seront générées automatiquement + +CREATE TABLE USER_SECURITY( + uuid_user UUID PRIMARY KEY, + mdp_hash TEXT NOT NULL, + mdp_sel VARCHAR(511) NOT NULL, + lockout BOOLEAN NOT NULL DEFAULT FALSE, + date_derniere_connection TIMESTAMP, + FOREIGN KEY (uuid_user) REFERENCES USER_ACCOUNT(uuid_user) +); + +-- INSERT INTO USER_SECURITY (uuid_user, mdp_hash, mdp_sel) VALUES (...); +-- Notes : uuid_user doit exister dans USER_ACCOUNT +-- lockout est FALSE par défaut +-- date_creation est la date actuelle (+ heure) par défaut +-- date_derniere_connection est NULL par défaut +-- mdp_hash et mdp_sel doivent être générés par l'application + + +CREATE TABLE TYPE_ACTION ( + uuid_type_action UUID PRIMARY KEY DEFAULT gen_random_uuid(), + nom_action VARCHAR(255) UNIQUE NOT NULL +); + +-- INSERT INTO TYPE_ACTION (nom_action) VALUES (...); +-- Notes : uuid_type_action sera généré automatiquement +-- Uniquement depuis le rôle postgres + +CREATE TABLE AUDIT_LOGS ( + uuid_audit_log UUID PRIMARY KEY DEFAULT gen_random_uuid(), + commentaire TEXT NOT NULL, + uuid_user UUID NOT NULL, + uuid_type_action UUID, + FOREIGN KEY (uuid_user) REFERENCES USER_ACCOUNT(uuid_user), + FOREIGN KEY (uuid_type_action) REFERENCES TYPE_ACTION(uuid_type_action) +); + +-- INSERT INTO AUDIT_LOGS (commentaire, uuid_user, uuid_type_action) VALUES (...); +-- Notes : uuid_audit_log sera généré automatiquement +-- uuid_user doit exister dans USER_ACCOUNT +-- uuid_type_action doit exister dans TYPE_ACTION + +CREATE TABLE FORUM_TOPIC ( + uuid_topic UUID PRIMARY KEY DEFAULT gen_random_uuid(), + topic VARCHAR(255) UNIQUE NOT NULL +); + +-- INSERT INTO FORUM_TOPIC (topic) VALUES (...); +-- Notes : uuid_topic sera généré automatiquement + +CREATE TABLE FORUM_THREAD ( + uuid_thread UUID PRIMARY KEY DEFAULT gen_random_uuid(), + titre VARCHAR(1023) NOT NULL, + owner_id TEXT, -- Doublon de uuid_user... + uuid_user VARCHAR(42), + category TEXT DEFAULT 'general', + excerpt TEXT, + is_pinned BOOLEAN DEFAULT FALSE, + views INTEGER DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + is_hidden BOOLEAN DEFAULT FALSE +); + +-- INSERT INTO FORUM_THREAD (titre) VALUES (...); +-- Notes : uuid_thread sera généré automatiquement + +CREATE TABLE FORUM_POST ( + uuid_post UUID PRIMARY KEY DEFAULT gen_random_uuid(), + message_forum TEXT NOT NULL, + date_publication TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + uuid_thread UUID NOT NULL, + uuid_user UUID NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + is_hidden BOOLEAN DEFAULT FALSE, + FOREIGN KEY (uuid_thread) REFERENCES FORUM_THREAD(uuid_thread), + FOREIGN KEY (uuid_user) REFERENCES USER_ACCOUNT(uuid_user) +); + +-- INSERT INTO FORUM_POST (message_forum, uuid_thread, uuid_user) VALUES (...); +-- Notes : uuid_post sera généré automatiquement +-- date_publication est la date actuelle (+ heure) par défaut +-- uuid_thread doit exister dans FORUM_THREAD +-- uuid_user doit exister dans USER_ACCOUNT + +CREATE TABLE IF NOT EXISTS forum_report ( + uuid_report SERIAL PRIMARY KEY, + uuid_post INTEGER, + uuid_thread INTEGER, + uuid_reporter INTEGER, + target_type TEXT NOT NULL DEFAULT 'post', + reason TEXT NOT NULL, + priority TEXT NOT NULL DEFAULT 'medium', + status TEXT NOT NULL DEFAULT 'pending', + resolution_note TEXT, + resolved_by INTEGER, + resolved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE GERANT ( + uuid_gerant UUID PRIMARY KEY, + FOREIGN KEY (uuid_gerant) REFERENCES USER_ACCOUNT(uuid_user) +); + +--- INSERT INTO GERANT (uuid_gerant) VALUES (...); +-- Notes : uuid_gerant doit exister dans USER_ACCOUNT (uuid_user) + +CREATE TABLE PATIENT ( + uuid_patient UUID PRIMARY KEY, + date_derniere_modification_ai_session TIMESTAMP, + FOREIGN KEY (uuid_patient) REFERENCES USER_ACCOUNT(uuid_user) +); + +--- INSERT INTO PATIENT (uuid_patient) VALUES (...); +-- Notes : uuid_patient doit exister dans USER_ACCOUNT (uuid_user) + +CREATE TABLE AIDANT ( + uuid_aidant UUID PRIMARY KEY, + FOREIGN KEY (uuid_aidant) REFERENCES USER_ACCOUNT(uuid_user) +); + +-- INSERT INTO AIDANT (uuid_aidant) VALUES (...); +-- Notes : uuid_aidant doit exister dans USER_ACCOUNT (uuid_user) + +CREATE TABLE MDPH_DOSSIER ( + uuid_mdph_dossier UUID PRIMARY KEY DEFAULT gen_random_uuid(), + numero_dossier TEXT, -- Doublon de uuid_mdph_dossier... + titre TEXT, + description TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + uuid_patient UUID, + FOREIGN KEY (uuid_patient) REFERENCES PATIENT(uuid_patient) +); + +-- INSERT INTO MDPH_DOSSIER (uuid_patient) VALUES (...); +-- Notes : uuid_mdph_dossier sera généré automatiquement +-- uuid_patient doit exister dans PATIENT + +CREATE TABLE MDPH_SECTION ( + uuid_mdph_section UUID PRIMARY KEY DEFAULT gen_random_uuid(), + numero_section VARCHAR(50) NOT NULL, + titre TEXT, + contenu JSONB DEFAULT '{}'::jsonb, + uuid_ai_session TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + uuid_mdph_dossier UUID NOT NULL, + CONSTRAINT mdph_section_unique UNIQUE (numero_section, uuid_mdph_dossier), + FOREIGN KEY (uuid_mdph_dossier) REFERENCES MDPH_DOSSIER(uuid_mdph_dossier) +); + +-- INSERT INTO MDPH_SECTION (numero_section, uuid_mdph_dossier) VALUES (...); +-- Notes : uuid_mdph_section sera généré automatiquement +-- uuid_mdph_dossier doit exister dans MDPH_DOSSIER + +CREATE TABLE MDPH_DOCUMENT ( + uuid_document_mdph UUID PRIMARY KEY DEFAULT gen_random_uuid(), + chemin_vers_document TEXT NOT NULL, + type TEXT, + titre_document TEXT NOT NULL, + titre TEXT, -- Doublon de titre_document... + taille TEXT, + contenu_ocr_json JSON NOT NULL, + status TEXT DEFAULT 'pending', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- INSERT INTO MDPH_DOCUMENT (chemin_vers_document, titre_document, uuid_mdph_section) VALUES (...); +-- Notes : uuid_document_mdph sera généré automatiquement +-- uuid_mdph_section doit exister dans MDPH_SECTION + +CREATE TABLE AI_SESSION ( + uuid_ai_session UUID PRIMARY KEY DEFAULT gen_random_uuid(), + uuid_mdph_dossier INTEGER, + section TEXT, + sous_section TEXT, + nom_formulaire TEXT, + date_creation_session TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + resultat_json JSON NOT NULL, + traites JSONB DEFAULT '[]'::jsonb, + est_termine BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + uuid_mdph_section UUID, + uuid_patient UUID, + CONSTRAINT ai_session_unique UNIQUE (uuid_mdph_section, uuid_patient, sous_section, nom_formulaire), + FOREIGN KEY (uuid_mdph_section) REFERENCES MDPH_SECTION(uuid_mdph_section), + FOREIGN KEY (uuid_patient) REFERENCES PATIENT(uuid_patient) +); + +-- INSERT INTO AI_SESSION (uuid_mdph_section, uuid_patient, nom_fichier, resultat_json, est_termine) VALUES (...); +-- Notes : uuid_ai_session sera généré automatiquement +-- uuid_mdph_section doit exister dans MDPH_SECTION + +CREATE TABLE AI_MESSAGE ( + uuid_ai_message UUID PRIMARY KEY DEFAULT gen_random_uuid(), + uuid_ai_session UUID NOT NULL, + message_role TEXT, + message_conv TEXT, + token_input INT DEFAULT 0, + token_output INT DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW(), + FOREIGN KEY (uuid_ai_session) REFERENCES AI_SESSION(uuid_ai_session) ON DELETE CASCADE +); + +-- INSERT INTO AI_MESSAGE (message_role, message_conv, uuid_ai_session) VALUES (...); +-- Notes : uuid_ai_message sera généré automatiquement +-- uuid_ai_session doit exister dans AI_SESSION + +CREATE TABLE RENDEZ_VOUS ( + uuid_rdv UUID PRIMARY KEY DEFAULT gen_random_uuid(), + uuid_patient UUID NOT NULL, + uuid_aidant UUID NOT NULL, + date_rdv TIMESTAMP NOT NULL, + FOREIGN KEY (uuid_patient) REFERENCES PATIENT(uuid_patient), + FOREIGN KEY (uuid_aidant) REFERENCES AIDANT(uuid_aidant) +); + +-- INSERT INTO RENDEZ_VOUS (uuid_patient, uuid_aidant, date_rdv) VALUES (...); +-- Notes : uuid_rdv sera généré automatiquement +-- uuid_patient doit exister dans PATIENT +-- uuid_aidant doit exister dans AIDANT + +CREATE TABLE ELIGIBILITE ( + uuid_eligibilite UUID PRIMARY KEY DEFAULT gen_random_uuid(), + lien_vers_aide TEXT +); + +-- INSERT INTO ELIGIBILITE (lien_vers_aide) VALUES (...); +-- Notes : uuid_eligibilite sera généré automatiquement + +CREATE TABLE EST_ELIGIBLE ( + uuid_patient UUID NOT NULL, + uuid_eligibilite UUID NOT NULL, + PRIMARY KEY (uuid_patient, uuid_eligibilite), + FOREIGN KEY (uuid_patient) REFERENCES PATIENT(uuid_patient), + FOREIGN KEY (uuid_eligibilite) REFERENCES ELIGIBILITE(uuid_eligibilite) +); + +-- INSERT INTO EST_ELIGIBLE (uuid_patient, uuid_eligibilite) VALUES (...); +-- Notes : uuid_patient doit exister dans PATIENT +-- uuid_eligibilite doit exister dans ELIGIBILITE + +CREATE TABLE SUGGESTION ( + uuid_suggestion UUID PRIMARY KEY DEFAULT gen_random_uuid(), + suggestion_text TEXT +); + +-- INSERT INTO SUGGESTION (suggestion_text) VALUES (...); +-- Notes : uuid_suggestion sera généré automatiquement + +CREATE TABLE PROPOSER_SUGGESTION ( + uuid_patient UUID NOT NULL, + uuid_suggestion UUID NOT NULL, + PRIMARY KEY (uuid_patient, uuid_suggestion), + FOREIGN KEY (uuid_patient) REFERENCES PATIENT(uuid_patient), + FOREIGN KEY (uuid_suggestion) REFERENCES SUGGESTION(uuid_suggestion) +); + +-- INSERT INTO PROPOSER_SUGGESTION (uuid_patient, uuid_suggestion) VALUES (...); +-- Notes : uuid_patient doit exister dans PATIENT +-- uuid_suggestion doit exister dans SUGGESTION + +CREATE TABLE GERER ( + uuid_aidant UUID NOT NULL, + uuid_mdph_dossier UUID NOT NULL, + PRIMARY KEY (uuid_aidant, uuid_mdph_dossier), + FOREIGN KEY (uuid_aidant) REFERENCES AIDANT(uuid_aidant), + FOREIGN KEY (uuid_mdph_dossier) REFERENCES MDPH_DOSSIER(uuid_mdph_dossier) +); + +-- INSERT INTO GERER (uuid_aidant, uuid_mdph_dossier) VALUES (...); +-- Notes : uuid_aidant doit exister dans AIDANT +-- uuid_mdph_dossier doit exister dans MDPH_DOSSIER + +CREATE TABLE CATEGORISER ( + uuid_topic UUID NOT NULL, + uuid_thread UUID NOT NULL, + PRIMARY KEY (uuid_topic, uuid_thread), + FOREIGN KEY (uuid_topic) REFERENCES FORUM_TOPIC(uuid_topic), + FOREIGN KEY (uuid_thread) REFERENCES FORUM_THREAD(uuid_thread) +); + +-- INSERT INTO CATEGORISER (uuid_topic, uuid_thread) VALUES (...); +-- Notes : uuid_topic doit exister dans FORUM_TOPIC +-- uuid_thread doit exister dans FORUM_THREAD + +CREATE TABLE RESOURCE ( + uuid_resource UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type TEXT NOT NULL, + icon TEXT, + category TEXT, + title TEXT NOT NULL, + description TEXT, + read_time TEXT, + keywords JSONB DEFAULT '[]'::jsonb, + created_by VARCHAR(42), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE app_setting ( + key TEXT PRIMARY KEY, + value TEXT, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +INSERT INTO app_setting (key, value) VALUES +('site_name', 'UMHMA'), +('contact_email', 'contact@umhma.fr'), +('contact_phone', '01 23 45 67 89'), +('contact_address', 'Paris, France'), +('contact_hours', 'Disponibles du lundi au vendredi, de 9h à 18h.'), +('timezone', 'Europe/Paris'), +('language', 'fr'), +('maintenance_mode', 'false'), +('maintenance_message', 'La plateforme est temporairement indisponible pour maintenance. Nous revenons très vite.'); + +ALTER TABLE public.GERANT ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.TYPE_ACTION ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.AUDIT_LOGS ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.USER_SECURITY ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.USER_ACCOUNT ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.PATIENT ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.AIDANT ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.GERER ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.RENDEZ_VOUS ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.MDPH_DOSSIER ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.MDPH_SECTION ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.MDPH_DOCUMENT ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.EST_ELIGIBLE ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.PROPOSER_SUGGESTION ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.AI_SESSION ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.AI_MESSAGE ENABLE ROW LEVEL SECURITY; + +-- INDEX +CREATE UNIQUE INDEX IF NOT EXISTS uq_user_account_email_norm ON user_account(email_normalized); +CREATE INDEX IF NOT EXISTS idx_user_account_email_norm ON user_account(email_normalized); +CREATE INDEX IF NOT EXISTS idx_mdph_document_user ON mdph_document(user_id); +CREATE INDEX IF NOT EXISTS idx_forum_thread_category ON FORUM_THREAD(category); +CREATE INDEX IF NOT EXISTS idx_forum_thread_owner ON FORUM_THREAD(owner_id); +CREATE INDEX IF NOT EXISTS idx_forum_thread_user ON FORUM_THREAD(uuid_user); +CREATE INDEX IF NOT EXISTS idx_forum_post_thread ON FORUM_POST(uuid_thread); +CREATE INDEX IF NOT EXISTS idx_forum_post_user ON FORUM_POST(uuid_user); +CREATE UNIQUE INDEX IF NOT EXISTS uq_mdph_section_ai_session ON mdph_section(uuid_ai_session) + WHERE uuid_ai_session IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_mdph_section_numero ON mdph_section(numero_section); +CREATE INDEX IF NOT EXISTS idx_mdph_section_dossier ON mdph_section(uuid_mdph_dossier); +CREATE INDEX IF NOT EXISTS idx_resource_type ON resource(type); +CREATE INDEX IF NOT EXISTS idx_ai_session_dossier ON ai_session(uuid_mdph_dossier); +CREATE INDEX IF NOT EXISTS idx_ai_message_session ON ai_message(uuid_ai_session); +CREATE UNIQUE INDEX IF NOT EXISTS uq_ai_session_form ON ai_session(uuid_mdph_section, sous_section, nom_formulaire) WHERE sous_section IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_forum_report_status ON forum_report(status); +CREATE INDEX IF NOT EXISTS idx_forum_report_post ON forum_report(uuid_post); + +-- Remarque : pas de RLS sur les tables suivantes : +-- ELIGIBILITE, SUGGESTION, FORUM_TOPIC, FORUM_THREAD, FORUM_POST, CATEGORISER, RESOURCE + +-- Pour faire un update sur la table XXX : +-- UPDATE XXX SET col = valeur WHERE condition; + +-- Pour faire un delete sur la table XXX : +-- DELETE FROM XXX WHERE condition; \ No newline at end of file diff --git a/backend/scripts_SQL/umhma_script_creation_bdd_avant_corrections.sql b/backend/scripts_SQL/umhma_script_creation_bdd_avant_corrections.sql new file mode 100644 index 0000000..2badac1 --- /dev/null +++ b/backend/scripts_SQL/umhma_script_creation_bdd_avant_corrections.sql @@ -0,0 +1,308 @@ +DROP TABLE IF EXISTS + CATEGORISER, + GERER, + PROPOSER_SUGGESTION, + EST_ELIGIBLE, + RENDEZ_VOUS, + AI_MESSAGE, + AI_SESSION, + AIDANT, + PATIENT, + GERANT, + MDPH_DOSSIER, + MDPH_SECTION, + MDPH_DOCUMENT, + FORUM_POST, + FORUM_THREAD, + FORUM_TOPIC, + AUDIT_LOGS, + TYPE_ACTION, + ELIGIBILITE, + SUGGESTION, + USER_SECURITY, + USER_ACCOUNT +CASCADE; + +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; +-- Pour avoir accès à la fonction gen_random_uuid() + +CREATE TABLE USER_ACCOUNT ( + uuid_user UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(511) UNIQUE NOT NULL, + total_input_tokens_spent INT DEFAULT 0, + total_output_tokens_spent INT DEFAULT 0 +); + +-- INSERT INTO USER_ACCOUNT (email) VALUES (...); +-- Notes : uuid_user, total_input_tokens_spent et total_output_tokens_spent seront générées automatiquement + +CREATE TABLE USER_SECURITY( + uuid_user UUID PRIMARY KEY, + mdp_hash VARCHAR(511) NOT NULL, + mdp_sel VARCHAR(511) NOT NULL, + lockout BOOLEAN NOT NULL DEFAULT FALSE, + date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_derniere_connection TIMESTAMP, + FOREIGN KEY (uuid_user) REFERENCES USER_ACCOUNT(uuid_user) +); + +-- INSERT INTO USER_SECURITY (uuid_user, mdp_hash, mdp_sel) VALUES (...); +-- Notes : uuid_user doit exister dans USER_ACCOUNT +-- lockout est FALSE par défaut +-- date_creation est la date actuelle (+ heure) par défaut +-- date_derniere_connection est NULL par défaut +-- mdp_hash et mdp_sel doivent être générés par l'application + + +CREATE TABLE TYPE_ACTION ( + uuid_type_action UUID PRIMARY KEY DEFAULT gen_random_uuid(), + nom_action VARCHAR(255) UNIQUE NOT NULL +); + +-- INSERT INTO TYPE_ACTION (nom_action) VALUES (...); +-- Notes : uuid_type_action sera généré automatiquement +-- Uniquement depuis le rôle postgres + +CREATE TABLE AUDIT_LOGS ( + uuid_audit_log UUID PRIMARY KEY DEFAULT gen_random_uuid(), + commentaire TEXT NOT NULL, + uuid_user UUID NOT NULL, + uuid_type_action UUID, + FOREIGN KEY (uuid_user) REFERENCES USER_ACCOUNT(uuid_user), + FOREIGN KEY (uuid_type_action) REFERENCES TYPE_ACTION(uuid_type_action) +); + +-- INSERT INTO AUDIT_LOGS (commentaire, uuid_user, uuid_type_action) VALUES (...); +-- Notes : uuid_audit_log sera généré automatiquement +-- uuid_user doit exister dans USER_ACCOUNT +-- uuid_type_action doit exister dans TYPE_ACTION + +CREATE TABLE FORUM_TOPIC ( + uuid_topic UUID PRIMARY KEY DEFAULT gen_random_uuid(), + topic VARCHAR(255) UNIQUE NOT NULL +); + +-- INSERT INTO FORUM_TOPIC (topic) VALUES (...); +-- Notes : uuid_topic sera généré automatiquement + +CREATE TABLE FORUM_THREAD ( + uuid_thread UUID PRIMARY KEY DEFAULT gen_random_uuid(), + titre VARCHAR(1023) NOT NULL +); + +-- INSERT INTO FORUM_THREAD (titre) VALUES (...); +-- Notes : uuid_thread sera généré automatiquement + +CREATE TABLE FORUM_POST ( + uuid_post UUID PRIMARY KEY DEFAULT gen_random_uuid(), + message_forum TEXT NOT NULL, + date_publication TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + uuid_thread UUID NOT NULL, + uuid_user UUID NOT NULL, + FOREIGN KEY (uuid_thread) REFERENCES FORUM_THREAD(uuid_thread), + FOREIGN KEY (uuid_user) REFERENCES USER_ACCOUNT(uuid_user) +); + +-- INSERT INTO FORUM_POST (message_forum, uuid_thread, uuid_user) VALUES (...); +-- Notes : uuid_post sera généré automatiquement +-- date_publication est la date actuelle (+ heure) par défaut +-- uuid_thread doit exister dans FORUM_THREAD +-- uuid_user doit exister dans USER_ACCOUNT + +CREATE TABLE GERANT ( + uuid_gerant UUID PRIMARY KEY, + FOREIGN KEY (uuid_gerant) REFERENCES USER_ACCOUNT(uuid_user) +); + +--- INSERT INTO GERANT (uuid_gerant) VALUES (...); +-- Notes : uuid_gerant doit exister dans USER_ACCOUNT (uuid_user) + +CREATE TABLE PATIENT ( + uuid_patient UUID PRIMARY KEY, + date_derniere_modification_ai_session TIMESTAMP, + FOREIGN KEY (uuid_patient) REFERENCES USER_ACCOUNT(uuid_user) +); + +--- INSERT INTO PATIENT (uuid_patient) VALUES (...); +-- Notes : uuid_patient doit exister dans USER_ACCOUNT (uuid_user) + +CREATE TABLE AIDANT ( + uuid_aidant UUID PRIMARY KEY, + FOREIGN KEY (uuid_aidant) REFERENCES USER_ACCOUNT(uuid_user) +); + +-- INSERT INTO AIDANT (uuid_aidant) VALUES (...); +-- Notes : uuid_aidant doit exister dans USER_ACCOUNT (uuid_user) + +CREATE TABLE MDPH_DOSSIER ( + uuid_mdph_dossier UUID PRIMARY KEY DEFAULT gen_random_uuid(), + uuid_patient UUID, + FOREIGN KEY (uuid_patient) REFERENCES PATIENT(uuid_patient) +); + +-- INSERT INTO MDPH_DOSSIER (uuid_patient) VALUES (...); +-- Notes : uuid_mdph_dossier sera généré automatiquement +-- uuid_patient doit exister dans PATIENT + +CREATE TABLE MDPH_SECTION ( + uuid_mdph_section UUID PRIMARY KEY DEFAULT gen_random_uuid(), + numero_section VARCHAR(50) NOT NULL, + uuid_mdph_dossier UUID NOT NULL, + CONSTRAINT mdph_section_unique UNIQUE (numero_section, uuid_mdph_dossier), + FOREIGN KEY (uuid_mdph_dossier) REFERENCES MDPH_DOSSIER(uuid_mdph_dossier) +); + +-- INSERT INTO MDPH_SECTION (numero_section, uuid_mdph_dossier) VALUES (...); +-- Notes : uuid_mdph_section sera généré automatiquement +-- uuid_mdph_dossier doit exister dans MDPH_DOSSIER + +CREATE TABLE MDPH_DOCUMENT ( + uuid_document_mdph UUID PRIMARY KEY DEFAULT gen_random_uuid(), + chemin_vers_document TEXT NOT NULL, + titre_document TEXT NOT NULL, + contenu_ocr_json JSON NOT NULL, + uuid_mdph_section UUID NOT NULL, + FOREIGN KEY (uuid_mdph_section) REFERENCES MDPH_SECTION(uuid_mdph_section) +); + +-- INSERT INTO MDPH_DOCUMENT (chemin_vers_document, titre_document, uuid_mdph_section) VALUES (...); +-- Notes : uuid_document_mdph sera généré automatiquement +-- uuid_mdph_section doit exister dans MDPH_SECTION + +CREATE TABLE AI_SESSION ( + uuid_ai_session UUID PRIMARY KEY DEFAULT gen_random_uuid(), + nom_sous_section VARCHAR(10) NOT NULL, + nom_fichier TEXT NOT NULL, + date_creation_session TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + resultat_json JSON NOT NULL, + est_termine BOOLEAN NOT NULL DEFAULT FALSE, + uuid_mdph_section UUID NOT NULL, + uuid_patient UUID, + CONSTRAINT ai_session_unique UNIQUE (uuid_mdph_section, uuid_patient, nom_sous_section, nom_fichier), + FOREIGN KEY (uuid_mdph_section) REFERENCES MDPH_SECTION(uuid_mdph_section), + FOREIGN KEY (uuid_patient) REFERENCES PATIENT(uuid_patient) +); + +-- INSERT INTO AI_SESSION (uuid_mdph_section, uuid_patient, nom_fichier, resultat_json, est_termine) VALUES (...); +-- Notes : uuid_ai_session sera généré automatiquement +-- uuid_mdph_section doit exister dans MDPH_SECTION + +CREATE TABLE AI_MESSAGE ( + uuid_ai_message UUID PRIMARY KEY DEFAULT gen_random_uuid(), + message_role VARCHAR(50) NOT NULL, + message_conv TEXT NOT NULL, + token_input INT, + token_output INT, + message_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + uuid_ai_session UUID NOT NULL, + FOREIGN KEY (uuid_ai_session) REFERENCES AI_SESSION(uuid_ai_session) + ON DELETE CASCADE +); + +-- INSERT INTO AI_MESSAGE (message_role, message_conv, uuid_ai_session) VALUES (...); +-- Notes : uuid_ai_message sera généré automatiquement +-- uuid_ai_session doit exister dans AI_SESSION + +CREATE TABLE RENDEZ_VOUS ( + uuid_rdv UUID PRIMARY KEY DEFAULT gen_random_uuid(), + uuid_patient UUID NOT NULL, + uuid_aidant UUID NOT NULL, + date_rdv TIMESTAMP NOT NULL, + FOREIGN KEY (uuid_patient) REFERENCES PATIENT(uuid_patient), + FOREIGN KEY (uuid_aidant) REFERENCES AIDANT(uuid_aidant) +); + +-- INSERT INTO RENDEZ_VOUS (uuid_patient, uuid_aidant, date_rdv) VALUES (...); +-- Notes : uuid_rdv sera généré automatiquement +-- uuid_patient doit exister dans PATIENT +-- uuid_aidant doit exister dans AIDANT + +CREATE TABLE ELIGIBILITE ( + uuid_eligibilite UUID PRIMARY KEY DEFAULT gen_random_uuid(), + lien_vers_aide TEXT +); + +-- INSERT INTO ELIGIBILITE (lien_vers_aide) VALUES (...); +-- Notes : uuid_eligibilite sera généré automatiquement + +CREATE TABLE EST_ELIGIBLE ( + uuid_patient UUID NOT NULL, + uuid_eligibilite UUID NOT NULL, + PRIMARY KEY (uuid_patient, uuid_eligibilite), + FOREIGN KEY (uuid_patient) REFERENCES PATIENT(uuid_patient), + FOREIGN KEY (uuid_eligibilite) REFERENCES ELIGIBILITE(uuid_eligibilite) +); + +-- INSERT INTO EST_ELIGIBLE (uuid_patient, uuid_eligibilite) VALUES (...); +-- Notes : uuid_patient doit exister dans PATIENT +-- uuid_eligibilite doit exister dans ELIGIBILITE + +CREATE TABLE SUGGESTION ( + uuid_suggestion UUID PRIMARY KEY DEFAULT gen_random_uuid(), + suggestion_text TEXT +); + +-- INSERT INTO SUGGESTION (suggestion_text) VALUES (...); +-- Notes : uuid_suggestion sera généré automatiquement + +CREATE TABLE PROPOSER_SUGGESTION ( + uuid_patient UUID NOT NULL, + uuid_suggestion UUID NOT NULL, + PRIMARY KEY (uuid_patient, uuid_suggestion), + FOREIGN KEY (uuid_patient) REFERENCES PATIENT(uuid_patient), + FOREIGN KEY (uuid_suggestion) REFERENCES SUGGESTION(uuid_suggestion) +); + +-- INSERT INTO PROPOSER_SUGGESTION (uuid_patient, uuid_suggestion) VALUES (...); +-- Notes : uuid_patient doit exister dans PATIENT +-- uuid_suggestion doit exister dans SUGGESTION + +CREATE TABLE GERER ( + uuid_aidant UUID NOT NULL, + uuid_mdph_dossier UUID NOT NULL, + PRIMARY KEY (uuid_aidant, uuid_mdph_dossier), + FOREIGN KEY (uuid_aidant) REFERENCES AIDANT(uuid_aidant), + FOREIGN KEY (uuid_mdph_dossier) REFERENCES MDPH_DOSSIER(uuid_mdph_dossier) +); + +-- INSERT INTO GERER (uuid_aidant, uuid_mdph_dossier) VALUES (...); +-- Notes : uuid_aidant doit exister dans AIDANT +-- uuid_mdph_dossier doit exister dans MDPH_DOSSIER + +CREATE TABLE CATEGORISER ( + uuid_topic UUID NOT NULL, + uuid_thread UUID NOT NULL, + PRIMARY KEY (uuid_topic, uuid_thread), + FOREIGN KEY (uuid_topic) REFERENCES FORUM_TOPIC(uuid_topic), + FOREIGN KEY (uuid_thread) REFERENCES FORUM_THREAD(uuid_thread) +); + +-- INSERT INTO CATEGORISER (uuid_topic, uuid_thread) VALUES (...); +-- Notes : uuid_topic doit exister dans FORUM_TOPIC +-- uuid_thread doit exister dans FORUM_THREAD + +ALTER TABLE public.GERANT ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.TYPE_ACTION ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.AUDIT_LOGS ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.USER_SECURITY ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.USER_ACCOUNT ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.PATIENT ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.AIDANT ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.GERER ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.RENDEZ_VOUS ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.MDPH_DOSSIER ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.MDPH_SECTION ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.MDPH_DOCUMENT ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.EST_ELIGIBLE ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.PROPOSER_SUGGESTION ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.AI_SESSION ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.AI_MESSAGE ENABLE ROW LEVEL SECURITY; + +-- Remarque : pas de RLS sur les tables suivantes : +-- ELIGIBILITE, SUGGESTION, FORUM_TOPIC, FORUM_THREAD, FORUM_POST, CATEGORISER + +-- Pour faire un update sur la table XXX : +-- UPDATE XXX SET col = valeur WHERE condition; + +-- Pour faire un delete sur la table XXX : +-- DELETE FROM XXX WHERE condition; \ No newline at end of file diff --git a/backend/scripts_SQL/umhma_script_remove_role_technique.sql b/backend/scripts_SQL/umhma_script_remove_role_technique.sql new file mode 100644 index 0000000..4e777a5 --- /dev/null +++ b/backend/scripts_SQL/umhma_script_remove_role_technique.sql @@ -0,0 +1,17 @@ +-- app_umhma +DROP POLICY IF EXISTS app_umhma_user_account_select ON public.user_account; +DROP POLICY IF EXISTS app_umhma_user_account_insert ON public.user_account; +DROP POLICY IF EXISTS app_umhma_user_security_select ON public.user_security; +DROP POLICY IF EXISTS app_umhma_user_security_insert ON public.user_security; +DROP POLICY IF EXISTS app_umhma_patient_insert ON public.patient; +DROP POLICY IF EXISTS app_umhma_aidant_insert ON public.aidant; + +REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM app_umhma; +REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM app_umhma; +REVOKE ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public FROM app_umhma; +REVOKE ALL PRIVILEGES ON SCHEMA public FROM app_umhma; +REVOKE ALL PRIVILEGES ON SCHEMA pg_catalog FROM app_umhma; +REVOKE ALL PRIVILEGES ON SCHEMA information_schema FROM app_umhma; +REVOKE ALL ON PARAMETER app.current_user_uuid FROM app_umhma; + +DROP ROLE app_umhma; diff --git a/backend/scripts_SQL/umhma_script_remove_roles.sql b/backend/scripts_SQL/umhma_script_remove_roles.sql new file mode 100644 index 0000000..0d94cdd --- /dev/null +++ b/backend/scripts_SQL/umhma_script_remove_roles.sql @@ -0,0 +1,88 @@ +-- gerant +REVOKE ALL PRIVILEGES +ON ALL TABLES IN SCHEMA public +FROM gerant; + +REVOKE ALL PRIVILEGES +ON ALL SEQUENCES IN SCHEMA public +FROM gerant; + +REVOKE ALL PRIVILEGES +ON ALL FUNCTIONS IN SCHEMA public +FROM gerant; + +DROP POLICY IF EXISTS gerant_user_security_select ON public.user_security; +DROP POLICY IF EXISTS gerant_user_security_update ON public.user_security; +DROP ROLE gerant; + +-- aidant +DROP POLICY IF EXISTS aidant_user_account_all ON public.user_account; +DROP POLICY IF EXISTS aidant_user_security_select ON public.user_security; +DROP POLICY IF EXISTS aidant_user_security_update ON public.user_security; +DROP POLICY IF EXISTS aidant_aidant_all ON public.aidant; +DROP POLICY IF EXISTS aidant_gerer_all ON public.gerer; +DROP POLICY IF EXISTS aidant_rendez_vous_all ON public.rendez_vous; +DROP POLICY IF EXISTS aidant_mdph_dossier_all ON public.mdph_dossier; +DROP POLICY IF EXISTS aidant_mdph_section_all ON public.mdph_section; +DROP POLICY IF EXISTS aidant_mdph_document_all ON public.mdph_document; +DROP POLICY IF EXISTS aidant_est_eligible_all ON public.est_eligible; +DROP POLICY IF EXISTS aidant_proposer_suggestion_all ON public.proposer_suggestion; +DROP POLICY IF EXISTS aidant_patient_select ON public.patient; +DROP POLICY IF EXISTS aidant_ai_session_select ON public.ai_session; +DROP POLICY IF EXISTS aidant_ai_message_select ON public.ai_message; +DROP POLICY IF EXISTS aidant_forum_thread_select ON public.forum_thread; +DROP POLICY IF EXISTS aidant_forum_thread_all ON public.forum_thread; +DROP POLICY IF EXISTS aidant_forum_post_select ON public.forum_post; +DROP POLICY IF EXISTS aidant_forum_post_all ON public.forum_post; +DROP POLICY IF EXISTS aidant_categoriser_select ON public.categoriser; +DROP POLICY IF EXISTS aidant_categoriser_all ON public.categoriser; + +REVOKE ALL PRIVILEGES +ON ALL TABLES IN SCHEMA public +FROM aidant; + +REVOKE ALL PRIVILEGES +ON ALL SEQUENCES IN SCHEMA public +FROM aidant; + +REVOKE ALL PRIVILEGES +ON ALL FUNCTIONS IN SCHEMA public +FROM aidant; + +DROP ROLE aidant; + +-- patient +DROP POLICY IF EXISTS patient_user_account_all ON public.user_account; +DROP POLICY IF EXISTS patient_user_security_select ON public.user_security; +DROP POLICY IF EXISTS patient_user_security_update ON public.user_security; +DROP POLICY IF EXISTS patient_patient_all ON public.patient; +DROP POLICY IF EXISTS patient_gerer_select ON public.gerer; +DROP POLICY IF EXISTS patient_rendez_vous_all ON public.rendez_vous; +DROP POLICY IF EXISTS patient_mdph_dossier_all ON public.mdph_dossier; +DROP POLICY IF EXISTS patient_mdph_section_all ON public.mdph_section; +DROP POLICY IF EXISTS patient_mdph_document_all ON public.mdph_document; +DROP POLICY IF EXISTS patient_est_eligible_select ON public.est_eligible; +DROP POLICY IF EXISTS patient_proposer_suggestion_select ON public.proposer_suggestion; +DROP POLICY IF EXISTS patient_aidant_select ON public.aidant; +DROP POLICY IF EXISTS patient_ai_session_all ON public.ai_session; +DROP POLICY IF EXISTS patient_ai_message_all ON public.ai_message; +DROP POLICY IF EXISTS patient_forum_thread_select ON public.forum_thread; +DROP POLICY IF EXISTS patient_forum_thread_all ON public.forum_thread; +DROP POLICY IF EXISTS patient_forum_post_select ON public.forum_post; +DROP POLICY IF EXISTS patient_forum_post_all ON public.forum_post; +DROP POLICY IF EXISTS patient_categoriser_select ON public.categoriser; +DROP POLICY IF EXISTS patient_categoriser_all ON public.categoriser; + +REVOKE ALL PRIVILEGES +ON ALL TABLES IN SCHEMA public +FROM patient; + +REVOKE ALL PRIVILEGES +ON ALL SEQUENCES IN SCHEMA public +FROM patient; + +REVOKE ALL PRIVILEGES +ON ALL FUNCTIONS IN SCHEMA public +FROM patient; + +DROP ROLE patient; \ No newline at end of file diff --git a/backend/scripts_SQL/umhma_script_role_aidant.sql b/backend/scripts_SQL/umhma_script_role_aidant.sql new file mode 100644 index 0000000..cb8fa77 --- /dev/null +++ b/backend/scripts_SQL/umhma_script_role_aidant.sql @@ -0,0 +1,371 @@ +CREATE ROLE aidant NOLOGIN; + +-- eligibilite, suggestion, forum_topic, app_setting, resource +-- Tout / Aucun +GRANT SELECT ON + public.eligibilite, + public.suggestion, + public.forum_topic, + public.app_setting, + public.resource +TO aidant; + +REVOKE INSERT, UPDATE, DELETE ON + public.eligibilite, + public.suggestion, + public.forum_topic, + public.app_setting, + public.resource +FROM aidant; + +-- user_account, aidant, gerer, rendez_vous, mdph_dossier, mdph_section, mdph_document, est_eligible, proposer_suggestion +-- Lignes concernés / Lignes concernés +GRANT SELECT, INSERT, UPDATE, DELETE ON + public.user_account, + public.aidant, + public.gerer, + public.rendez_vous, + public.mdph_dossier, + public.mdph_section, + public.mdph_document, + public.est_eligible, + public.proposer_suggestion +TO aidant; + +-- patient, ai_session, ai_message +-- Lignes concernés / Aucun +GRANT SELECT ON + public.patient, + public.ai_session, + public.ai_message, +TO aidant; + +REVOKE INSERT, UPDATE, DELETE ON + public.patient, + public.ai_session, + public.ai_message, +FROM aidant; + +-- gerant, type_action +-- Aucun / Aucun +REVOKE ALL ON + public.gerant, + public.type_action +FROM aidant; + +-- user_security +REVOKE INSERT, DELETE ON + public.user_security +FROM aidant; + +GRANT SELECT (uuid_user, date_derniere_connection) ON + public.user_security +TO aidant; + +GRANT UPDATE (uuid_user, date_derniere_connection) ON + public.user_security +TO aidant; + +-- audits_logs +-- Autoriser les inserts (et rien d'autre) sur la table AUDIT_LOGS pour journaliser les actions +GRANT INSERT ON + public.audit_logs +TO aidant; + +-- forum_thread, forum_post, categoriser, forum_report +-- Tout / Lignes concernés +GRANT SELECT, INSERT, UPDATE, DELETE ON + public.forum_thread, + public.forum_post, + public.categoriser, + public.forum_report +TO aidant; + +-- POLICY +-- user_account +CREATE POLICY aidant_user_account_all +ON public.user_account +FOR ALL +TO aidant +USING (uuid_user = current_setting('app.current_user_uuid')::uuid) +WITH CHECK (uuid_user = current_setting('app.current_user_uuid')::uuid); + +-- user_security +CREATE POLICY aidant_user_security_select +ON public.user_security +FOR SELECT +TO aidant +USING (uuid_user = current_setting('app.current_user_uuid')::uuid); + +CREATE POLICY aidant_user_security_update +ON public.user_security +FOR UPDATE +TO aidant +USING (uuid_user = current_setting('app.current_user_uuid')::uuid) +WITH CHECK (uuid_user = current_setting('app.current_user_uuid')::uuid); + +-- aidant +CREATE POLICY aidant_aidant_all +ON public.aidant +FOR ALL +TO aidant +USING (uuid_aidant = current_setting('app.current_user_uuid')::uuid) +WITH CHECK (uuid_aidant = current_setting('app.current_user_uuid')::uuid); + +-- gerer +CREATE POLICY aidant_gerer_all +ON public.gerer +FOR ALL +TO aidant +USING (uuid_aidant = current_setting('app.current_user_uuid')::uuid) +WITH CHECK (uuid_aidant = current_setting('app.current_user_uuid')::uuid); + +-- rendez_vous +CREATE POLICY aidant_rendez_vous_all +ON public.rendez_vous +FOR ALL +TO aidant +USING (uuid_aidant = current_setting('app.current_user_uuid')::uuid) +WITH CHECK (uuid_aidant = current_setting('app.current_user_uuid')::uuid); + +-- mdph_dossier +CREATE POLICY aidant_mdph_dossier_all +ON public.mdph_dossier +FOR ALL +TO aidant +USING ( + EXISTS ( + SELECT 1 + FROM public.gerer g + WHERE g.uuid_mdph_dossier = public.mdph_dossier.uuid_mdph_dossier + AND g.uuid_aidant = current_setting('app.current_user_uuid')::uuid + ) +) +WITH CHECK ( + EXISTS ( + SELECT 1 + FROM public.gerer g + WHERE g.uuid_mdph_dossier = public.mdph_dossier.uuid_mdph_dossier + AND g.uuid_aidant = current_setting('app.current_user_uuid')::uuid + ) +); + +-- mdph_section +CREATE POLICY aidant_mdph_section_all +ON public.mdph_section +FOR ALL +TO aidant +USING ( + EXISTS ( + SELECT 1 + FROM public.mdph_dossier md + JOIN public.gerer g ON g.uuid_mdph_dossier = md.uuid_mdph_dossier + WHERE public.mdph_section.uuid_mdph_dossier = md.uuid_mdph_dossier + AND g.uuid_aidant = current_setting('app.current_user_uuid')::uuid + ) +) +WITH CHECK ( + EXISTS ( + SELECT 1 + FROM public.mdph_dossier md + JOIN public.gerer g ON g.uuid_mdph_dossier = md.uuid_mdph_dossier + WHERE public.mdph_section.uuid_mdph_dossier = md.uuid_mdph_dossier + AND g.uuid_aidant = current_setting('app.current_user_uuid')::uuid + ) +); + +-- mdph_document +CREATE POLICY aidant_mdph_document_all +ON public.mdph_document +FOR ALL +TO aidant +USING ( + EXISTS ( + SELECT 1 + FROM public.mdph_section ms + JOIN public.mdph_dossier md ON md.uuid_mdph_dossier = ms.uuid_mdph_dossier + JOIN public.gerer g ON g.uuid_mdph_dossier = md.uuid_mdph_dossier + WHERE public.mdph_document.uuid_mdph_section = ms.uuid_mdph_section + AND g.uuid_aidant = current_setting('app.current_user_uuid')::uuid + ) +) +WITH CHECK ( + EXISTS ( + SELECT 1 + FROM public.mdph_section ms + JOIN public.mdph_dossier md ON md.uuid_mdph_dossier = ms.uuid_mdph_dossier + JOIN public.gerer g ON g.uuid_mdph_dossier = md.uuid_mdph_dossier + WHERE public.mdph_document.uuid_mdph_section = ms.uuid_mdph_section + AND g.uuid_aidant = current_setting('app.current_user_uuid')::uuid + ) +); + +-- est_eligible +CREATE POLICY aidant_est_eligible_all +ON public.est_eligible +FOR ALL +TO aidant +USING ( + EXISTS ( + SELECT 1 + FROM public.gerer g + JOIN public.mdph_dossier md ON g.uuid_mdph_dossier = md.uuid_mdph_dossier + JOIN public.patient p ON md.uuid_patient = p.uuid_patient + WHERE public.est_eligible.uuid_patient = p.uuid_patient + AND g.uuid_aidant = current_setting('app.current_user_uuid')::uuid + ) +) +WITH CHECK ( + EXISTS ( + SELECT 1 + FROM public.gerer g + JOIN public.mdph_dossier md ON g.uuid_mdph_dossier = md.uuid_mdph_dossier + WHERE public.est_eligible.uuid_patient = md.uuid_patient + AND g.uuid_aidant = current_setting('app.current_user_uuid')::uuid + ) +); + +-- proposer_suggestion +CREATE POLICY aidant_proposer_suggestion_all +ON public.proposer_suggestion +FOR ALL +TO aidant +USING ( + EXISTS ( + SELECT 1 + FROM public.gerer g + JOIN public.mdph_dossier md ON g.uuid_mdph_dossier = md.uuid_mdph_dossier + JOIN public.patient p ON md.uuid_patient = p.uuid_patient + WHERE public.proposer_suggestion.uuid_patient = p.uuid_patient + AND g.uuid_aidant = current_setting('app.current_user_uuid')::uuid + ) +) +WITH CHECK ( + EXISTS ( + SELECT 1 + FROM public.gerer g + JOIN public.mdph_dossier md ON g.uuid_mdph_dossier = md.uuid_mdph_dossier + WHERE public.proposer_suggestion.uuid_patient = md.uuid_patient + AND g.uuid_aidant = current_setting('app.current_user_uuid')::uuid + ) +); + +-- patient (pour lecture seule) +CREATE POLICY aidant_patient_select +ON public.patient +FOR SELECT +TO aidant +USING ( + EXISTS ( + SELECT 1 + FROM public.gerer g + JOIN public.mdph_dossier md ON g.uuid_mdph_dossier = md.uuid_mdph_dossier + WHERE public.patient.uuid_patient = md.uuid_patient + AND g.uuid_aidant = current_setting('app.current_user_uuid')::uuid + ) +); + +-- ai_session (pour lecture seule) +CREATE POLICY aidant_ai_session_select +ON public.ai_session +FOR SELECT +TO aidant +USING ( + EXISTS ( + SELECT 1 + FROM public.gerer g + JOIN public.mdph_dossier md ON g.uuid_mdph_dossier = md.uuid_mdph_dossier + JOIN public.patient p ON md.uuid_patient = p.uuid_patient + WHERE public.ai_session.uuid_patient = p.uuid_patient + AND g.uuid_aidant = current_setting('app.current_user_uuid')::uuid + ) +); + +-- ai_message (pour lecture seule) +CREATE POLICY aidant_ai_message_select +ON public.ai_message +FOR SELECT +TO aidant +USING ( + EXISTS ( + SELECT 1 + FROM public.gerer g + JOIN public.mdph_dossier md ON g.uuid_mdph_dossier = md.uuid_mdph_dossier + JOIN public.patient p ON md.uuid_patient = p.uuid_patient + JOIN public.ai_session ais ON ais.uuid_patient = p.uuid_patient + WHERE public.ai_message.uuid_ai_session = ais.uuid_ai_session + AND g.uuid_aidant = current_setting('app.current_user_uuid')::uuid + ) +); + +-- forum_thread (lecture sur tout et écriture au concerné) +CREATE POLICY aidant_forum_thread_select +ON public.forum_thread +FOR SELECT +TO aidant +USING (true); -- aucune restriction + +CREATE POLICY aidant_forum_thread_all +ON public.forum_thread +FOR ALL -- INSERT, UPDATE, DELETE +TO aidant +USING ( + EXISTS ( + SELECT 1 + FROM public.forum_post fp + WHERE fp.uuid_thread = public.forum_thread.uuid_thread + AND fp.uuid_user = current_setting('app.current_user_uuid')::uuid + ) +) +WITH CHECK ( + EXISTS ( + SELECT 1 + FROM public.forum_post fp + WHERE fp.uuid_thread = public.forum_thread.uuid_thread + AND fp.uuid_user = current_setting('app.current_user_uuid')::uuid + ) +); + +-- forum_post (lecture sur tout et écriture au concerné) +CREATE POLICY aidant_forum_post_select +ON public.forum_post +FOR SELECT +TO aidant +USING (true); -- aucune restriction + +CREATE POLICY aidant_forum_post_all +ON public.forum_post +FOR ALL -- INSERT, UPDATE, DELETE +TO aidant +USING (uuid_user = current_setting('app.current_user_uuid')::uuid) +WITH CHECK (uuid_user = current_setting('app.current_user_uuid')::uuid); + +-- categoriser (lecture sur tout et écriture au concerné) +CREATE POLICY aidant_categoriser_select +ON public.categoriser +FOR SELECT +TO aidant +USING (true); -- aucune restriction + +CREATE POLICY aidant_categoriser_all +ON public.categoriser +FOR ALL -- INSERT, UPDATE, DELETE +TO aidant +USING ( + EXISTS ( + SELECT 1 + FROM public.forum_post fp + JOIN public.forum_thread ft ON fp.uuid_thread = ft.uuid_thread + WHERE public.categoriser.uuid_thread = ft.uuid_thread + AND fp.uuid_user = current_setting('app.current_user_uuid')::uuid + ) +) +WITH CHECK ( + EXISTS ( + SELECT 1 + FROM public.forum_post fp + JOIN public.forum_thread ft ON fp.uuid_thread = ft.uuid_thread + WHERE public.categoriser.uuid_thread = ft.uuid_thread + AND fp.uuid_user = current_setting('app.current_user_uuid')::uuid + ) +); diff --git a/backend/scripts_SQL/umhma_script_role_gerant.sql b/backend/scripts_SQL/umhma_script_role_gerant.sql new file mode 100644 index 0000000..860b37c --- /dev/null +++ b/backend/scripts_SQL/umhma_script_role_gerant.sql @@ -0,0 +1,67 @@ +CREATE ROLE gerant BYPASSRLS NOLOGIN; + +-- user_account, gerant, patient, aidant, gerer, rendez_vous, mdph_dossier, mdph_section, mdph_document, eligibilite, est_eligible, suggestion, proposer_suggestion, ai_session, ai_message, forum_topic, forum_thread, forum_post, forum_report, categoriser, app_setting, resource +-- Tout / Tout +GRANT SELECT, INSERT, UPDATE, DELETE ON + public.user_account, + public.gerant, + public.patient, + public.aidant, + public.gerer, + public.rendez_vous, + public.mdph_dossier, + public.mdph_section, + public.mdph_document, + public.eligibilite, + public.est_eligible, + public.suggestion, + public.proposer_suggestion, + public.ai_session, + public.ai_message, + public.forum_topic, + public.forum_thread, + public.forum_post, + public.forum_report, + public.categoriser, + public.app_setting, + public.resource +TO gerant; + +-- user_security +REVOKE INSERT, DELETE ON + public.user_security +FROM gerant; + +GRANT SELECT (uuid_user, date_derniere_connection) ON + public.user_security +TO gerant; + +GRANT UPDATE (uuid_user, date_derniere_connection) ON + public.user_security +TO gerant; + +-- audit_logs, type_action +-- Tout / Aucun +GRANT SELECT ON + public.audit_logs, + public.type_action +TO gerant; + +-- Autoriser les inserts (et rien d'autre) sur la table AUDIT_LOGS pour journaliser les actions +GRANT INSERT ON + public.audit_logs +TO gerant; + +-- user_security +CREATE POLICY gerant_user_security_select +ON public.user_security +FOR SELECT +TO gerant +USING (uuid_user = current_setting('app.current_user_uuid')::uuid); + +CREATE POLICY gerant_user_security_update +ON public.user_security +FOR UPDATE +TO gerant +USING (uuid_user = current_setting('app.current_user_uuid')::uuid) +WITH CHECK (uuid_user = current_setting('app.current_user_uuid')::uuid); \ No newline at end of file diff --git a/backend/scripts_SQL/umhma_script_role_patient.sql b/backend/scripts_SQL/umhma_script_role_patient.sql new file mode 100644 index 0000000..4d7b4e6 --- /dev/null +++ b/backend/scripts_SQL/umhma_script_role_patient.sql @@ -0,0 +1,314 @@ +CREATE ROLE patient NOLOGIN; + +-- eligibilite, suggestion, forum_topic, app_setting, resource +-- Tout / Aucun +GRANT SELECT ON + public.eligibilite, + public.suggestion, + public.forum_topic, + public.app_setting, + public.resource +TO patient; + +REVOKE INSERT, UPDATE, DELETE ON + public.eligibilite, + public.suggestion, + public.forum_topic, + public.app_setting, + public.resource +FROM patient; + +-- user_account, patient, rendez_vous, mdph_dossier, mdph_section, mdph_document, ai_session, ai_message +-- Lignes concernés / Lignes concernés +GRANT SELECT, INSERT, UPDATE, DELETE ON + public.user_account, + public.patient, + public.rendez_vous, + public.mdph_dossier, + public.mdph_section, + public.mdph_document, + public.ai_session, + public.ai_message +TO patient; + +-- gerant, type_action +-- Aucun / Aucun +REVOKE ALL ON + public.gerant, + public.type_action +FROM patient; + +-- user_security +REVOKE INSERT, DELETE ON + public.user_security +FROM patient; + +GRANT SELECT (uuid_user, date_derniere_connection) ON + public.user_security +TO patient; + +GRANT UPDATE (uuid_user, date_derniere_connection) ON + public.user_security +TO patient; + +-- aidant, gerer, est_eligible, proposer_suggestion +-- Lignes concernés / Aucun +GRANT SELECT ON + public.aidant, + public.gerer, + public.est_eligible, + public.proposer_suggestion, +TO patient; + +-- forum_thread, forum_post, categoriser, forum_report +-- Tout / Lignes concernés +GRANT SELECT, INSERT, UPDATE, DELETE ON + public.forum_thread, + public.forum_post, + public.categoriser, + public.forum_report +TO patient; + +-- audits_logs +-- Autoriser les inserts (et rien d'autre) sur la table AUDIT_LOGS pour journaliser les actions +GRANT INSERT ON + public.audit_logs +TO patient; + +-- POLICY +-- user_account +CREATE POLICY patient_user_account_all +ON public.user_account +FOR ALL +TO patient +USING (uuid_user = current_setting('app.current_user_uuid')::uuid) +WITH CHECK (uuid_user = current_setting('app.current_user_uuid')::uuid); + +-- user_security +CREATE POLICY patient_user_security_select +ON public.user_security +FOR SELECT +TO patient +USING (uuid_user = current_setting('app.current_user_uuid')::uuid); + +CREATE POLICY patient_user_security_update +ON public.user_security +FOR UPDATE +TO patient +USING (uuid_user = current_setting('app.current_user_uuid')::uuid) +WITH CHECK (uuid_user = current_setting('app.current_user_uuid')::uuid); + +-- patient +CREATE POLICY patient_patient_all +ON public.patient +FOR ALL +TO patient +USING (uuid_patient = current_setting('app.current_user_uuid')::uuid) +WITH CHECK (uuid_patient = current_setting('app.current_user_uuid')::uuid); + +-- gerer +CREATE POLICY patient_gerer_select +ON public.gerer +FOR SELECT +TO patient +USING ( + EXISTS ( + SELECT 1 + FROM public.mdph_dossier md + WHERE public.gerer.uuid_mdph_dossier = md.uuid_mdph_dossier + AND md.uuid_patient = current_setting('app.current_user_uuid')::uuid + ) +); + +-- rendez_vous +CREATE POLICY patient_rendez_vous_all +ON public.rendez_vous +FOR ALL +TO patient +USING (uuid_patient = current_setting('app.current_user_uuid')::uuid) +WITH CHECK (uuid_patient = current_setting('app.current_user_uuid')::uuid); + +-- mdph_dossier +CREATE POLICY patient_mdph_dossier_all +ON public.mdph_dossier +FOR ALL +TO patient +USING (uuid_patient = current_setting('app.current_user_uuid')::uuid) +WITH CHECK (uuid_patient = current_setting('app.current_user_uuid')::uuid); + +-- mdph_section +CREATE POLICY patient_mdph_section_all +ON public.mdph_section +FOR ALL +TO patient +USING ( + EXISTS ( + SELECT 1 + FROM public.mdph_dossier md + WHERE public.mdph_section.uuid_mdph_dossier = md.uuid_mdph_dossier + AND md.uuid_patient = current_setting('app.current_user_uuid')::uuid + ) +) +WITH CHECK ( + EXISTS ( + SELECT 1 + FROM public.mdph_dossier md + WHERE public.mdph_section.uuid_mdph_dossier = md.uuid_mdph_dossier + AND md.uuid_patient = current_setting('app.current_user_uuid')::uuid + ) +); + +-- mdph_document +CREATE POLICY patient_mdph_document_all +ON public.mdph_document +FOR ALL +TO patient +USING ( + EXISTS ( + SELECT 1 + FROM public.mdph_section ms + JOIN public.mdph_dossier md ON md.uuid_mdph_dossier = ms.uuid_mdph_dossier + WHERE public.mdph_document.uuid_mdph_section = ms.uuid_mdph_section + AND md.uuid_patient = current_setting('app.current_user_uuid')::uuid + ) +) +WITH CHECK ( + EXISTS ( + SELECT 1 + FROM public.mdph_section ms + JOIN public.mdph_dossier md ON md.uuid_mdph_dossier = ms.uuid_mdph_dossier + WHERE public.mdph_document.uuid_mdph_section = ms.uuid_mdph_section + AND md.uuid_patient = current_setting('app.current_user_uuid')::uuid + ) +); + +-- est_eligible (pour lecture seule) +CREATE POLICY patient_est_eligible_select +ON public.est_eligible +FOR SELECT +TO patient +USING (uuid_patient = current_setting('app.current_user_uuid')::uuid); + +-- proposer_suggestion (pour lecture seule) +CREATE POLICY patient_proposer_suggestion_select +ON public.proposer_suggestion +FOR SELECT +TO patient +USING (uuid_patient = current_setting('app.current_user_uuid')::uuid); + +-- aidant (pour lecture seule) +CREATE POLICY patient_aidant_select +ON public.aidant +FOR SELECT +TO patient +USING ( + EXISTS ( + SELECT 1 + FROM public.gerer g + JOIN public.mdph_dossier md ON g.uuid_mdph_dossier = md.uuid_mdph_dossier + WHERE public.aidant.uuid_aidant = g.uuid_aidant + AND md.uuid_patient = current_setting('app.current_user_uuid')::uuid + ) +); + +-- ai_session +CREATE POLICY patient_ai_session_all +ON public.ai_session +FOR ALL +TO patient +USING (uuid_patient = current_setting('app.current_user_uuid')::uuid) +WITH CHECK (uuid_patient = current_setting('app.current_user_uuid')::uuid); + +-- ai_message +CREATE POLICY patient_ai_message_all +ON public.ai_message +FOR ALL +TO patient +USING ( + EXISTS ( + SELECT 1 + FROM public.ai_session ais + WHERE public.ai_message.uuid_ai_session = ais.uuid_ai_session + AND ais.uuid_patient = current_setting('app.current_user_uuid')::uuid + ) +) +WITH CHECK ( + EXISTS ( + SELECT 1 + FROM public.ai_session ais + WHERE public.ai_message.uuid_ai_session = ais.uuid_ai_session + AND ais.uuid_patient = current_setting('app.current_user_uuid')::uuid + ) +); + +-- forum_thread (lecture sur tout et écriture au concerné) +CREATE POLICY patient_forum_thread_select +ON public.forum_thread +FOR SELECT +TO patient +USING (true); -- aucune restriction + +CREATE POLICY patient_forum_thread_all +ON public.forum_thread +FOR ALL -- INSERT, UPDATE, DELETE +TO patient +USING ( + EXISTS ( + SELECT 1 + FROM public.forum_post fp + WHERE fp.uuid_thread = public.forum_thread.uuid_thread + AND fp.uuid_user = current_setting('app.current_user_uuid')::uuid + ) +) +WITH CHECK ( + EXISTS ( + SELECT 1 + FROM public.forum_post fp + WHERE fp.uuid_thread = public.forum_thread.uuid_thread + AND fp.uuid_user = current_setting('app.current_user_uuid')::uuid + ) +); + +-- forum_post (lecture sur tout et écriture au concerné) +CREATE POLICY patient_forum_post_select +ON public.forum_post +FOR SELECT +TO patient +USING (true); -- aucune restriction + +CREATE POLICY patient_forum_post_all +ON public.forum_post +FOR ALL -- INSERT, UPDATE, DELETE +TO patient +USING (uuid_user = current_setting('app.current_user_uuid')::uuid) +WITH CHECK (uuid_user = current_setting('app.current_user_uuid')::uuid); + +-- categoriser (lecture sur tout et écriture au concerné) +CREATE POLICY patient_categoriser_select +ON public.categoriser +FOR SELECT +TO patient +USING (true); -- aucune restriction + +CREATE POLICY patient_categoriser_all +ON public.categoriser +FOR ALL -- INSERT, UPDATE, DELETE +TO patient +USING ( + EXISTS ( + SELECT 1 + FROM public.forum_post fp + JOIN public.forum_thread ft ON fp.uuid_thread = ft.uuid_thread + WHERE public.categoriser.uuid_thread = ft.uuid_thread + AND fp.uuid_user = current_setting('app.current_user_uuid')::uuid + ) +) +WITH CHECK ( + EXISTS ( + SELECT 1 + FROM public.forum_post fp + JOIN public.forum_thread ft ON fp.uuid_thread = ft.uuid_thread + WHERE public.categoriser.uuid_thread = ft.uuid_thread + AND fp.uuid_user = current_setting('app.current_user_uuid')::uuid + ) +); \ No newline at end of file diff --git a/backend/scripts_SQL/umhma_script_role_technique.sql b/backend/scripts_SQL/umhma_script_role_technique.sql new file mode 100644 index 0000000..5f9db65 --- /dev/null +++ b/backend/scripts_SQL/umhma_script_role_technique.sql @@ -0,0 +1,107 @@ +-- inscription : +-- INSERT dans user_account + user_security + patient/aidant selon le cas +-- COMMIT; +-- SET ROLE xxx; +-- SET app.current_user_uuid = '...'; + +-- à faire une fois connecté : +-- SET ROLE xxx; +-- SET app.current_user_uuid = '...'; +-- MAJ la date_derniere_connection = CURRENT_TIMESTAMP dans user_security pour l'utilisateur connecté (bien utiliser app.current_user_uuid) +-- Requête MAJ : update user_security set date_derniere_connection = CURRENT_TIMESTAMP where uuid_user = current_setting('app.current_user_uuid')::UUID; +-- COMMIT; + +CREATE ROLE app_umhma LOGIN BYPASSRLS NOCREATEDB NOCREATEROLE NOINHERIT; + +GRANT rds_iam, aidant, gerant, patient TO app_umhma; +-- Note : pour l'instant, app_umhma n’a AUCUN droit tant que SET ROLE n’est pas fait + +REVOKE ALL ON ALL TABLES IN SCHEMA public FROM app_umhma; +REVOKE ALL ON ALL SEQUENCES IN SCHEMA public FROM app_umhma; +REVOKE ALL ON ALL FUNCTIONS IN SCHEMA public FROM app_umhma; + +-- user_account +-- Lecture minimale pour pouvoir se login +GRANT SELECT (uuid_user, email) ON + public.user_account +TO app_umhma; + +CREATE POLICY app_umhma_user_account_select +ON public.user_account +FOR SELECT +TO app_umhma +USING (true); -- aucune restriction + +GRANT INSERT ON + public.user_account +TO app_umhma; + +CREATE POLICY app_umhma_user_account_insert +ON public.user_account +FOR INSERT +TO app_umhma +WITH CHECK (true); + +-- user_security +GRANT SELECT (uuid_user, mdp_hash, mdp_sel, lockout) ON + public.user_security +TO app_umhma; + +CREATE POLICY app_umhma_user_security_select +ON public.user_security +FOR SELECT +TO app_umhma +USING (true); -- aucune restriction + +GRANT INSERT ON + public.user_security +TO app_umhma; + +CREATE POLICY app_umhma_user_security_insert +ON public.user_security +FOR INSERT +TO app_umhma +WITH CHECK (true); + +GRANT USAGE ON SCHEMA public TO app_umhma; + +-- patient, aidant +GRANT INSERT ON + public.patient, + public.aidant +TO app_umhma; + +CREATE POLICY app_umhma_patient_insert +ON public.patient +FOR INSERT +TO app_umhma +WITH CHECK (true); + +CREATE POLICY app_umhma_aidant_insert +ON public.aidant +FOR INSERT +TO app_umhma +WITH CHECK (true); + +-- forum_topic, categoriser, forum_thread, forum_post, eligibilite, suggestion, app_setting, resource +-- Et lecture sur les données "publiques" +GRANT SELECT ON + public.forum_topic, + public.categoriser, + public.forum_thread, + public.forum_post, + public.eligibilite, + public.suggestion, + public.app_setting, + public.resource +TO app_umhma; + +-- audits_logs +-- Autoriser les inserts (et rien d'autre) sur la table AUDIT_LOGS pour journaliser les actions +GRANT INSERT ON + public.audit_logs +TO app_umhma; + +SET app.current_user_uuid = ''; +REVOKE ALL ON PARAMETER app.current_user_uuid FROM PUBLIC; +GRANT SET ON PARAMETER app.current_user_uuid TO app_umhma; \ No newline at end of file From c0fd0ef1ef235782de01031aa9efbb398d6cbcd2 Mon Sep 17 00:00:00 2001 From: Yanis MERABET <78480186+Yayabtw@users.noreply.github.com> Date: Tue, 26 May 2026 07:16:04 +0200 Subject: [PATCH 5/6] feat: integrate Mistral OCR and enhance document handling - Added Mistral OCR functionality to process documents and extract text, storing results in the database. - Updated document creation and update endpoints to handle base64 file uploads for OCR processing. - Introduced new scripts for AWS RDS setup, including SQL grants and schema adjustments. - Enhanced error handling and logging for OCR operations. - Updated environment configuration to support new database settings and API keys. --- .github/workflows/deploy.yml | 3 + backend/env.example | 11 +- backend/mistral_ocr.py | 241 +++ backend/scripts/apply-aws-bootstrap.js | 57 + backend/scripts/inspect-aws-rds-readonly.js | 106 + backend/scripts/test-api-alwaysdata.js | 110 + backend/scripts/test-compat-bdd.js | 133 ++ .../umhma_script_fixes_post_creation.sql | 13 + ...mhma_script_grants_app_umhma_bootstrap.sql | 47 + .../scripts_SQL/umhma_script_seed_test.sql | 59 + backend/src/app.js | 2 +- backend/src/constants/appSettings.js | 15 + backend/src/controllers/chatbotController.js | 139 +- backend/src/controllers/documentController.js | 141 +- backend/src/controllers/settingsController.js | 2 +- backend/src/routes/documents.js | 4 + backend/src/services/mistralOcrService.js | 154 ++ ..._avant_test_nouvelle_bdd_20260526_0623.sql | 1773 +++++++++++++++++ docs/rapport-compatibilite-bdd.md | 123 ++ e2e/.gitignore | 5 + e2e/README.md | 55 + e2e/fixtures/tiny.png | Bin 0 -> 70 bytes e2e/helpers/api-client.ts | 42 + e2e/package-lock.json | 90 + e2e/package.json | 14 + e2e/playwright.config.ts | 50 + e2e/tests/api-auth-documents.spec.ts | 91 + e2e/tests/ui-auth-documents.spec.ts | 48 + frontend/src/pages/AddDocument.js | 11 + 29 files changed, 3443 insertions(+), 96 deletions(-) create mode 100644 backend/mistral_ocr.py create mode 100644 backend/scripts/apply-aws-bootstrap.js create mode 100644 backend/scripts/inspect-aws-rds-readonly.js create mode 100644 backend/scripts/test-api-alwaysdata.js create mode 100644 backend/scripts/test-compat-bdd.js create mode 100644 backend/scripts_SQL/umhma_script_fixes_post_creation.sql create mode 100644 backend/scripts_SQL/umhma_script_grants_app_umhma_bootstrap.sql create mode 100644 backend/scripts_SQL/umhma_script_seed_test.sql create mode 100644 backend/src/constants/appSettings.js create mode 100644 backend/src/services/mistralOcrService.js create mode 100644 backups/alwaysdata_avant_test_nouvelle_bdd_20260526_0623.sql create mode 100644 docs/rapport-compatibilite-bdd.md create mode 100644 e2e/.gitignore create mode 100644 e2e/README.md create mode 100644 e2e/fixtures/tiny.png create mode 100644 e2e/helpers/api-client.ts create mode 100644 e2e/package-lock.json create mode 100644 e2e/package.json create mode 100644 e2e/playwright.config.ts create mode 100644 e2e/tests/api-auth-documents.spec.ts create mode 100644 e2e/tests/ui-auth-documents.spec.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2f31418..c53845a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -104,6 +104,9 @@ jobs: cd /var/www/umhma git fetch origin main git reset --hard origin/main + mkdir -p backend/src/config + curl -fsSL https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem \ + -o backend/src/config/global-bundle.pem cd backend npm ci --omit=dev pm2 restart umhma-api --update-env diff --git a/backend/env.example b/backend/env.example index 5e9d331..be5a93e 100644 --- a/backend/env.example +++ b/backend/env.example @@ -1,7 +1,14 @@ PORT=4000 JWT_SECRET=CHANGE_ME_OR_BOOT_FAILS_USE_AT_LEAST_32_CHARS -DATABASE_URL=postgres://db_user:db_password@db_host:5432/db_name -CORS_ALLOWED_ORIGINS=http://localhost:3000 +DB_HOST=your-rds-host.eu-west-3.rds.amazonaws.com +DB_PORT=5432 +DB_NAME=UMHMA +DB_USER=app_umhma +DB_USER_ADMIN=umhma_admin +AWS_REGION=eu-west-3 +CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 +# Optionnel : tests locaux avec URL Postgres classique +# DATABASE_URL=postgres://db_user:db_password@db_host:5432/db_name # Si vous utilisez SSL (hébergement managé), décommentez la ligne ci-dessous # PGSSLMODE=require diff --git a/backend/mistral_ocr.py b/backend/mistral_ocr.py new file mode 100644 index 0000000..6a0432d --- /dev/null +++ b/backend/mistral_ocr.py @@ -0,0 +1,241 @@ +# pip install mistralai python-dotenv + + + +from pathlib import Path +from datetime import datetime +from dotenv import load_dotenv +import os +import json +import base64 +import mimetypes +import re +import html + + +try: + from mistralai import Mistral +except ImportError: + from mistralai.client import Mistral + + +# ============================================================ +# CONFIG +# ============================================================ + +load_dotenv() + +BASE_DIR = Path("data/ocr") +OUTPUT_DIR = BASE_DIR / "resultats_json" + +OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + +SUPPORTED_EXTENSIONS = { + ".pdf", + ".png", + ".jpg", + ".jpeg" +} + + +# ============================================================ +# UTILS +# ============================================================ + +def clean_text(markdown: str) -> str: + if not markdown: + return "" + + text = html.unescape(markdown) + + text = re.sub(r"!\[[^\]]*\]\([^)]+\)", "", text) + text = re.sub(r"\n]+>", "", text) + text = re.sub(r"^#{1,6}\s*", "", text, flags=re.MULTILINE) + text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text) + + text = text.replace("**", "") + text = text.replace("__", "") + text = text.replace("*", "") + + lines = [] + + for line in text.splitlines(): + line = line.strip() + line = re.sub(r"\s+", " ", line) + + if not line: + continue + + lines.append(line) + + return "\n".join(lines) + + +def file_to_data_url(file_path: Path) -> str: + mime_type, _ = mimetypes.guess_type(file_path) + + if mime_type is None: + if file_path.suffix.lower() == ".pdf": + mime_type = "application/pdf" + else: + mime_type = "application/octet-stream" + + file_bytes = file_path.read_bytes() + encoded = base64.b64encode(file_bytes).decode("utf-8") + + return f"data:{mime_type};base64,{encoded}" + + +def response_to_dict(response): + if hasattr(response, "model_dump"): + return response.model_dump() + + if hasattr(response, "dict"): + return response.dict() + + if isinstance(response, dict): + return response + + return json.loads(response.json()) + + +def save_json(result: dict) -> Path: + file_stem = Path(result["fichier"]).stem + output_path = OUTPUT_DIR / f"mistral_ocr_{file_stem}.json" + + with output_path.open("w", encoding="utf-8") as f: + json.dump(result, f, ensure_ascii=False, indent=2) + + return output_path + + +# ============================================================ +# MISTRAL OCR +# ============================================================ + +def mistral_ocr(file_path: str) -> dict: + path = Path(file_path) + + if not path.exists(): + raise FileNotFoundError(f"Fichier introuvable : {path}") + + extension = path.suffix.lower() + + if extension not in SUPPORTED_EXTENSIONS: + raise ValueError( + f"Extension non supportée : {extension}. " + f"Extensions acceptées : {sorted(SUPPORTED_EXTENSIONS)}" + ) + + api_key = os.getenv("MISTRAL_API_KEY") + + if not api_key: + raise RuntimeError( + "Clé API Mistral introuvable. " + "Ajoute MISTRAL_API_KEY=ta_cle dans le fichier .env" + ) + + client = Mistral(api_key=api_key) + + data_url = file_to_data_url(path) + + if extension == ".pdf": + document = { + "type": "document_url", + "document_url": data_url + } + else: + document = { + "type": "image_url", + "image_url": data_url + } + + ocr_response = client.ocr.process( + model="mistral-ocr-latest", + document=document, + include_image_base64=False + ) + + raw = response_to_dict(ocr_response) + + pages = [] + + for i, page in enumerate(raw.get("pages", []), start=1): + markdown = page.get("markdown", "") or "" + texte = clean_text(markdown) + + pages.append({ + "page": i, + "texte": texte, + "texte_lignes": texte.splitlines(), + "markdown": markdown, + "images": page.get("images", []), + "tables": page.get("tables", []), + "dimensions": page.get("dimensions", {}), + "confidence_scores": page.get("confidence_scores") + }) + + return { + "fichier": path.name, + "chemin_source": str(path), + "extension": extension, + "type_extraction": "mistral_ocr", + "date_extraction": datetime.now().isoformat(timespec="seconds"), + "mode": "general", + "nb_pages": len(pages), + "pages": pages, + "model": raw.get("model", "mistral-ocr-latest"), + "usage_info": raw.get("usage_info", {}) + } + + +def process_folder(folder_path: str) -> list[dict]: + folder = Path(folder_path) + + if not folder.exists(): + raise FileNotFoundError(f"Dossier introuvable : {folder}") + + results = [] + + for file_path in folder.iterdir(): + if not file_path.is_file(): + continue + + if file_path.suffix.lower() not in SUPPORTED_EXTENSIONS: + continue + + try: + print(f"Traitement : {file_path.name}") + + result = mistral_ocr(str(file_path)) + output = save_json(result) + + print(f"OK : {output}") + + results.append(result) + + except Exception as e: + print(f"ERREUR {file_path.name} : {e}") + + return results + + +# ============================================================ +# TEST +# ============================================================ + +if __name__ == "__main__": + + # # Modifier ici le chemin du PDF ou de l'image à tester + # fichier_test = "data/ocr/image_test/CarteID.pdf" + + # result = mistral_ocr(fichier_test) + + # output = save_json(result) + + # print(json.dumps(result, ensure_ascii=False, indent=2)) + + # print(f"\nJSON sauvegardé : {output}") + + # Pour faire l'OCR de tous les fichiers d'un dossier : + process_folder("data/ocr/image_test") \ No newline at end of file diff --git a/backend/scripts/apply-aws-bootstrap.js b/backend/scripts/apply-aws-bootstrap.js new file mode 100644 index 0000000..a48f8c5 --- /dev/null +++ b/backend/scripts/apply-aws-bootstrap.js @@ -0,0 +1,57 @@ +/** + * Applique les correctifs SQL et grants app_umhma sur RDS (connexion umhma_admin). + * Usage unique : node scripts/apply-aws-bootstrap.js + */ +require('dotenv').config(); +const fs = require('fs'); +const path = require('path'); +const { createPoolAdmin } = require('../src/config/database'); + +const STATEMENTS = [ + `CREATE TABLE IF NOT EXISTS admin ( + uuid_admin UUID PRIMARY KEY REFERENCES user_account(uuid_user) + )`, + `ALTER TABLE mdph_document ADD COLUMN IF NOT EXISTS user_id VARCHAR(42)`, + `CREATE INDEX IF NOT EXISTS idx_mdph_document_user ON mdph_document(user_id)`, + `ALTER TABLE mdph_document ADD COLUMN IF NOT EXISTS uuid_mdph_section UUID REFERENCES mdph_section(uuid_mdph_section)`, + `GRANT USAGE ON SCHEMA public TO app_umhma`, + `GRANT SELECT (uuid_user, email, email_normalized, name) ON public.user_account TO app_umhma`, + `GRANT INSERT ON public.user_account TO app_umhma`, + `GRANT SELECT (uuid_user, mdp_hash, mdp_sel, lockout) ON public.user_security TO app_umhma`, + `GRANT INSERT ON public.user_security TO app_umhma`, + `GRANT INSERT ON public.patient, public.aidant TO app_umhma`, + `GRANT SELECT, INSERT, UPDATE, DELETE ON public.mdph_dossier TO app_umhma`, + `GRANT SELECT, INSERT, UPDATE, DELETE ON public.mdph_section TO app_umhma`, + `GRANT SELECT, INSERT, UPDATE, DELETE ON public.mdph_document TO app_umhma`, + `GRANT SELECT, INSERT, UPDATE, DELETE ON public.ai_session TO app_umhma`, + `GRANT SELECT, INSERT, UPDATE, DELETE ON public.ai_message TO app_umhma`, + `GRANT SELECT ON public.forum_topic, public.categoriser, public.forum_thread, + public.forum_post, public.eligibilite, public.suggestion, public.app_setting, + public.resource TO app_umhma`, + `GRANT INSERT ON public.audit_logs TO app_umhma`, + `GRANT SELECT, INSERT, UPDATE, DELETE ON public.admin TO app_umhma`, + `GRANT patient, aidant, gerant TO app_umhma`, +]; + +async function run() { + const pool = createPoolAdmin(); + console.log('Bootstrap AWS (umhma_admin)...'); + for (const sql of STATEMENTS) { + try { + await pool.query(sql); + console.log('OK:', sql.slice(0, 70).replace(/\s+/g, ' ')); + } catch (err) { + console.warn('SKIP:', err.message, '|', sql.slice(0, 50)); + } + } + const check = await pool.query( + `SELECT has_table_privilege('app_umhma', 'user_account', 'SELECT') AS can_select_users`, + ); + console.log('app_umhma can SELECT user_account:', check.rows[0].can_select_users); + await pool.end(); +} + +run().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/backend/scripts/inspect-aws-rds-readonly.js b/backend/scripts/inspect-aws-rds-readonly.js new file mode 100644 index 0000000..fef82ed --- /dev/null +++ b/backend/scripts/inspect-aws-rds-readonly.js @@ -0,0 +1,106 @@ +/** + * Inspection READ-ONLY du schéma RDS AWS (aucune écriture). + */ +const fs = require('fs'); +const { Pool } = require('pg'); +const { Signer } = require('@aws-sdk/rds-signer'); +require('dotenv').config({ path: require('path').join(__dirname, '../.env') }); + +const PEM = process.env.RDS_CA_BUNDLE || '/tmp/global-bundle.pem'; + +async function getPool(user) { + const host = process.env.DB_HOST; + const port = Number(process.env.DB_PORT || 5432); + const database = process.env.DB_NAME; + const region = process.env.AWS_REGION; + const signer = new Signer({ region, hostname: host, port, username: user }); + const password = await signer.getAuthToken(); + return new Pool({ + host, + port, + database, + user, + password, + ssl: { rejectUnauthorized: true, ca: fs.readFileSync(PEM, 'utf8') }, + connectionTimeoutMillis: 15000, + }); +} + +async function main() { + const user = process.env.DB_USER_ADMIN || 'umhma_admin'; + const pool = await getPool(user); + const q = (sql, params) => pool.query(sql, params).then((r) => r.rows); + + console.log('=== Connexion RDS (lecture seule) ==='); + console.log('user:', user, 'db:', process.env.DB_NAME, 'host:', process.env.DB_HOST); + + const ver = await q('SELECT version()'); + console.log('version:', ver[0].version.split(',')[0]); + + const tables = await q(` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' AND table_type = 'BASE TABLE' + ORDER BY table_name + `); + console.log('\n=== Tables (' + tables.length + ') ==='); + console.log(tables.map((t) => t.table_name).join(', ')); + + const roles = await q(` + SELECT rolname FROM pg_roles + WHERE rolname IN ('app_umhma','patient','aidant','gerant','umhma_admin','postgres') + ORDER BY rolname + `); + console.log('\n=== Rôles PostgreSQL ==='); + console.log(roles.map((r) => r.rolname).join(', ') || '(aucun des rôles attendus)'); + + const rls = await q(` + SELECT c.relname AS table_name, c.relrowsecurity AS rls_on, + (SELECT COUNT(*) FROM pg_policies p WHERE p.tablename = c.relname) AS policies + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE n.nspname = 'public' AND c.relkind = 'r' + AND c.relname IN ('user_account','mdph_dossier','patient','admin','gerant') + ORDER BY c.relname + `); + console.log('\n=== RLS (échantillon) ==='); + for (const r of rls) console.log(` ${r.table_name}: rls=${r.rls_on} policies=${r.policies}`); + + const keyTables = ['user_account', 'user_security', 'admin', 'gerant', 'patient', 'aidant', + 'mdph_dossier', 'mdph_section', 'mdph_document', 'ai_session', 'ai_message', 'forum_thread', 'app_setting']; + console.log('\n=== Colonnes clés ==='); + for (const t of keyTables) { + const cols = await q(` + SELECT column_name, data_type, udt_name + FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = $1 + ORDER BY ordinal_position + `, [t]); + if (cols.length === 0) { + console.log(`\n[${t}] ABSENT`); + continue; + } + console.log(`\n[${t}]`); + console.log(cols.map((c) => ` ${c.column_name}: ${c.data_type}${c.udt_name === 'uuid' ? ' (uuid)' : ''}`).join('\n')); + } + + const counts = await q(` + SELECT 'user_account' AS t, COUNT(*)::int AS n FROM user_account + UNION ALL SELECT 'admin', COUNT(*)::int FROM admin + UNION ALL SELECT 'gerant', COUNT(*)::int FROM gerant + UNION ALL SELECT 'patient', COUNT(*)::int FROM patient + UNION ALL SELECT 'mdph_dossier', COUNT(*)::int FROM mdph_dossier + `).catch(async () => { + // admin/gerant may not exist + return q(`SELECT 'user_account' AS t, COUNT(*)::int AS n FROM user_account`); + }); + console.log('\n=== Volumétrie ==='); + for (const r of counts) console.log(` ${r.t}: ${r.n}`); + + await pool.end(); +} + +main().catch((e) => { + console.error('ERREUR:', e.message); + process.exit(1); +}); diff --git a/backend/scripts/test-api-alwaysdata.js b/backend/scripts/test-api-alwaysdata.js new file mode 100644 index 0000000..c1da081 --- /dev/null +++ b/backend/scripts/test-api-alwaysdata.js @@ -0,0 +1,110 @@ +/** + * Smoke test API contre DATABASE_URL (AlwaysData). + * Usage: node scripts/test-api-alwaysdata.js + */ +const { Pool } = require('pg'); +const request = require('supertest'); +const { createApp } = require('../src/app'); +require('dotenv').config(); + +const PASSWORD = 'TestPass@1234'; + +async function main() { + const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: process.env.PGSSLMODE === 'require' ? { rejectUnauthorized: false } : false, + }); + + const results = []; + const log = (name, ok, detail = '') => { + results.push({ name, ok, detail }); + console.log(`${ok ? '✓' : '✗'} ${name}${detail ? ` — ${detail}` : ''}`); + }; + + try { + await pool.query('SELECT 1'); + log('Connexion PostgreSQL', true); + + const app = await createApp(pool, pool, { jwtSecret: process.env.JWT_SECRET }); + + // Signup nouveau compte + const signupEmail = `smoke.${Date.now()}@umhma.fr`; + const signupRes = await request(app) + .post('/signup') + .send({ name: 'Smoke Test', email: signupEmail, password: PASSWORD }); + log('POST /signup', signupRes.status === 201, `status=${signupRes.status}`); + + // Login patient seed + const loginPatient = await request(app) + .post('/login') + .send({ email: 'patient.test@umhma.fr', password: PASSWORD }); + log('POST /login (patient)', loginPatient.status === 200, `status=${loginPatient.status}`); + const patientCookie = loginPatient.headers['set-cookie']; + + if (patientCookie) { + const dossiers = await request(app) + .get('/dossiers') + .set('Cookie', patientCookie); + log('GET /dossiers (patient)', dossiers.status === 200, `status=${dossiers.status} count=${dossiers.body?.length ?? '?'}`); + + const me = await request(app) + .get('/me') + .set('Cookie', patientCookie); + log('GET /me (patient)', me.status === 200, `status=${me.status}`); + } else { + log('GET /dossiers (patient)', false, 'pas de cookie'); + log('GET /me (patient)', false, 'pas de cookie'); + } + + // Login gérant / admin + const loginGerant = await request(app) + .post('/login') + .send({ email: 'gerant.test@umhma.fr', password: PASSWORD }); + log('POST /login (gerant)', loginGerant.status === 200, `status=${loginGerant.status} is_admin=${loginGerant.body?.user?.is_admin}`); + const gerantCookie = loginGerant.headers['set-cookie']; + + if (gerantCookie) { + const adminUsers = await request(app) + .get('/admin/users') + .set('Cookie', gerantCookie); + log('GET /admin/users', adminUsers.status === 200, `status=${adminUsers.status}`); + } else { + log('GET /admin/users', false, 'pas de cookie'); + } + + // Test chatbot (souvent sensible aux types UUID vs int) + if (patientCookie) { + const patientId = loginPatient.body?.user?.id; + const dossierRow = await pool.query( + `SELECT uuid_mdph_dossier FROM mdph_dossier WHERE uuid_patient = $1 LIMIT 1`, + [patientId], + ); + const dossierId = dossierRow.rows[0]?.uuid_mdph_dossier; + if (dossierId) { + const chatbot = await request(app) + .post('/chatbot/session') + .set('Cookie', patientCookie) + .send({ + dossierId, + section: 'A1', + sousSection: '1', + nomFormulaire: 'test-form', + }); + log('POST /chatbot/session', chatbot.status < 500, `status=${chatbot.status} body=${JSON.stringify(chatbot.body)?.slice(0, 120)}`); + } else { + log('POST /chatbot/session', false, 'aucun dossier'); + } + } + + const failed = results.filter((r) => !r.ok); + console.log(`\n${results.length - failed.length}/${results.length} tests OK`); + process.exit(failed.length > 0 ? 1 : 0); + } catch (err) { + console.error('Erreur fatale:', err.message); + process.exit(1); + } finally { + await pool.end(); + } +} + +main(); diff --git a/backend/scripts/test-compat-bdd.js b/backend/scripts/test-compat-bdd.js new file mode 100644 index 0000000..cb40356 --- /dev/null +++ b/backend/scripts/test-compat-bdd.js @@ -0,0 +1,133 @@ +/** + * Tests de compatibilité code ↔ nouvelle BDD (sans démarrer Express). + */ +const { Pool } = require('pg'); +const bcrypt = require('bcryptjs'); +require('dotenv').config(); + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: process.env.PGSSLMODE === 'require' ? { rejectUnauthorized: false } : false, +}); + +const results = []; +const log = (name, ok, detail = '') => { + results.push({ name, ok, detail }); + console.log(`${ok ? '✓' : '✗'} ${name}${detail ? ` — ${detail}` : ''}`); +}; + +async function main() { + try { + // Auth login query (authController) + const login = await pool.query( + `SELECT ua.uuid_user AS id, ua.email, us.mdp_hash, + (a.uuid_admin IS NOT NULL) AS in_admin_table + FROM user_account ua + JOIN user_security us ON us.uuid_user = ua.uuid_user + LEFT JOIN admin a ON a.uuid_admin = ua.uuid_user + WHERE ua.email_normalized = $1 LIMIT 1`, + ['gerant.test@umhma.fr'], + ); + const user = login.rows[0]; + const pwdOk = user && (await bcrypt.compare('TestPass@1234', user.mdp_hash)); + log('Requête login (admin join)', pwdOk, `is_admin=${user?.in_admin_table}`); + + // fetchUserRoles (roles.js) + const roles = await pool.query( + `SELECT + EXISTS (SELECT 1 FROM gerant WHERE uuid_gerant = $1) AS is_gerant, + EXISTS (SELECT 1 FROM patient WHERE uuid_patient = $1) AS is_patient, + EXISTS (SELECT 1 FROM aidant WHERE uuid_aidant = $1) AS is_aidant`, + [user.id], + ); + log('fetchUserRoles', roles.rows[0]?.is_patient === true, JSON.stringify(roles.rows[0])); + + // Dossiers (dossierController) + const dossiers = await pool.query( + `SELECT uuid_mdph_dossier as id, numero_dossier, titre + FROM mdph_dossier WHERE uuid_patient = $1`, + [user.id], + ); + log('SELECT mdph_dossier', dossiers.rows.length >= 0, `count=${dossiers.rows.length}`); + + const patientRow = await pool.query( + `SELECT uuid_user FROM user_account WHERE email_normalized = 'patient.test@umhma.fr'`, + ); + const patientId = patientRow.rows[0]?.uuid_user; + + const patientDossiers = await pool.query( + `SELECT uuid_mdph_dossier as id FROM mdph_dossier WHERE uuid_patient = $1`, + [patientId], + ); + log('Dossiers patient seed', patientDossiers.rows.length > 0, `count=${patientDossiers.rows.length}`); + + // Sections + if (patientDossiers.rows[0]) { + const sections = await pool.query( + `SELECT uuid_mdph_section as id, uuid_mdph_dossier as dossier_id, numero_section, titre, contenu + FROM mdph_section WHERE uuid_mdph_dossier = $1`, + [patientDossiers.rows[0].id], + ); + log('SELECT mdph_section', sections.rows.length > 0, `count=${sections.rows.length}`); + } + + // Documents (user_id column) + try { + await pool.query( + `SELECT uuid_document_mdph as id, user_id, titre FROM mdph_document LIMIT 1`, + ); + log('SELECT mdph_document (user_id)', true); + } catch (e) { + log('SELECT mdph_document (user_id)', false, e.message); + } + + // Chatbot ::int casts (expected to fail with UUID) + const section = await pool.query( + `SELECT uuid_mdph_section FROM mdph_section LIMIT 1`, + ); + const sid = section.rows[0]?.uuid_mdph_section; + if (sid) { + try { + await pool.query( + `SELECT uuid_ai_session FROM ai_session + WHERE uuid_mdph_section = $1::int AND sous_section = $2 AND nom_formulaire = $3`, + [sid, '1', 'test'], + ); + log('chatbot query ::int sur UUID', true); + } catch (e) { + log('chatbot query ::int sur UUID', false, e.message); + } + } + + // SET ROLE (nécessite rôles PG) + const client = await pool.connect(); + try { + await client.query('SET ROLE patient'); + log('SET ROLE patient', false, 'ne devrait pas arriver si rôle absent'); + } catch (e) { + log('SET ROLE patient', false, e.message); + } finally { + try { await client.query('RESET ROLE'); } catch (_) {} + client.release(); + } + + // Rôles PG existants + const pgRoles = await pool.query( + `SELECT rolname FROM pg_roles WHERE rolname IN ('patient','aidant','gerant')`, + ); + log('Rôles PostgreSQL déployés', pgRoles.rows.length === 3, `trouvés=${pgRoles.rows.map((r) => r.rolname).join(',') || 'aucun'}`); + + const failed = results.filter((r) => !r.ok); + const critical = failed.filter((f) => !f.name.includes('SET ROLE') && !f.name.includes('::int')); + console.log(`\n${results.length - failed.length}/${results.length} OK`); + console.log(`Blocages critiques (hors RLS/chatbot int): ${critical.length}`); + process.exit(critical.length > 0 ? 1 : 0); + } finally { + await pool.end(); + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/backend/scripts_SQL/umhma_script_fixes_post_creation.sql b/backend/scripts_SQL/umhma_script_fixes_post_creation.sql new file mode 100644 index 0000000..02d44ac --- /dev/null +++ b/backend/scripts_SQL/umhma_script_fixes_post_creation.sql @@ -0,0 +1,13 @@ +-- Correctifs post umhma_script_creation_bdd.sql pour compatibilité backend + +-- Table admin (utilisée par authController, adminOnly, maintenanceMode) +CREATE TABLE IF NOT EXISTS admin ( + uuid_admin UUID PRIMARY KEY REFERENCES user_account(uuid_user) +); + +-- Colonne manquante pour documentController + index idx_mdph_document_user +ALTER TABLE mdph_document ADD COLUMN IF NOT EXISTS user_id VARCHAR(42); +CREATE INDEX IF NOT EXISTS idx_mdph_document_user ON mdph_document(user_id); + +-- Lien section sur documents (attendu par les policies RLS aidant/patient) +ALTER TABLE mdph_document ADD COLUMN IF NOT EXISTS uuid_mdph_section UUID REFERENCES mdph_section(uuid_mdph_section); diff --git a/backend/scripts_SQL/umhma_script_grants_app_umhma_bootstrap.sql b/backend/scripts_SQL/umhma_script_grants_app_umhma_bootstrap.sql new file mode 100644 index 0000000..19c089d --- /dev/null +++ b/backend/scripts_SQL/umhma_script_grants_app_umhma_bootstrap.sql @@ -0,0 +1,47 @@ +-- Grants minimaux pour que l'application (app_umhma) fonctionne sans RLS policies complètes. +-- À exécuter en tant que umhma_admin sur RDS. + +CREATE TABLE IF NOT EXISTS admin ( + uuid_admin UUID PRIMARY KEY REFERENCES user_account(uuid_user) +); + +ALTER TABLE mdph_document ADD COLUMN IF NOT EXISTS user_id VARCHAR(42); +CREATE INDEX IF NOT EXISTS idx_mdph_document_user ON mdph_document(user_id); +ALTER TABLE mdph_document ADD COLUMN IF NOT EXISTS uuid_mdph_section UUID REFERENCES mdph_section(uuid_mdph_section); + +GRANT USAGE ON SCHEMA public TO app_umhma; + +GRANT SELECT (uuid_user, email, email_normalized, name) ON public.user_account TO app_umhma; +GRANT INSERT ON public.user_account TO app_umhma; + +GRANT SELECT (uuid_user, mdp_hash, mdp_sel, lockout) ON public.user_security TO app_umhma; +GRANT INSERT ON public.user_security TO app_umhma; + +GRANT INSERT ON public.patient, public.aidant TO app_umhma; + +GRANT SELECT, INSERT, UPDATE, DELETE ON + public.mdph_dossier, + public.mdph_section, + public.mdph_document, + public.ai_session, + public.ai_message, + public.admin +TO app_umhma; + +GRANT SELECT ON + public.forum_topic, + public.categoriser, + public.forum_thread, + public.forum_post, + public.eligibilite, + public.suggestion, + public.app_setting, + public.resource +TO app_umhma; + +GRANT INSERT ON public.audit_logs TO app_umhma; + +GRANT patient, aidant, gerant TO app_umhma; + +REVOKE ALL ON PARAMETER app.current_user_uuid FROM PUBLIC; +GRANT SET ON PARAMETER app.current_user_uuid TO app_umhma; diff --git a/backend/scripts_SQL/umhma_script_seed_test.sql b/backend/scripts_SQL/umhma_script_seed_test.sql new file mode 100644 index 0000000..ee5bf29 --- /dev/null +++ b/backend/scripts_SQL/umhma_script_seed_test.sql @@ -0,0 +1,59 @@ +-- Données de test minimales pour validation compatibilité backend / nouvelle BDD +-- Mot de passe commun : TestPass@1234 + +INSERT INTO user_account (email, email_normalized, name) +VALUES + ('patient.test@umhma.fr', 'patient.test@umhma.fr', 'Patient Test'), + ('aidant.test@umhma.fr', 'aidant.test@umhma.fr', 'Aidant Test'), + ('gerant.test@umhma.fr', 'gerant.test@umhma.fr', 'Gérant Test') +ON CONFLICT DO NOTHING; + +-- user_security (bcrypt TestPass@1234) +INSERT INTO user_security (uuid_user, mdp_hash, mdp_sel, lockout) +SELECT uuid_user, '$2a$12$YyiLje3vdsaMhXs2RrMVP.xEoWBKGQUbDl4do5DtZ/3M2JjgC98xC', '', false +FROM user_account +WHERE email_normalized IN ('patient.test@umhma.fr', 'aidant.test@umhma.fr', 'gerant.test@umhma.fr') +ON CONFLICT (uuid_user) DO NOTHING; + +INSERT INTO patient (uuid_patient) +SELECT uuid_user FROM user_account WHERE email_normalized = 'patient.test@umhma.fr' +ON CONFLICT DO NOTHING; + +INSERT INTO aidant (uuid_aidant) +SELECT uuid_user FROM user_account WHERE email_normalized = 'aidant.test@umhma.fr' +ON CONFLICT DO NOTHING; + +INSERT INTO gerant (uuid_gerant) +SELECT uuid_user FROM user_account WHERE email_normalized = 'gerant.test@umhma.fr' +ON CONFLICT DO NOTHING; + +INSERT INTO admin (uuid_admin) +SELECT uuid_user FROM user_account WHERE email_normalized = 'gerant.test@umhma.fr' +ON CONFLICT DO NOTHING; + +-- Dossier MDPH pour le patient test +INSERT INTO mdph_dossier (uuid_patient, numero_dossier, titre, description) +SELECT uuid_user, 'DOS-TEST-001', 'Dossier test migration', 'Dossier créé par seed de test' +FROM user_account WHERE email_normalized = 'patient.test@umhma.fr'; + +INSERT INTO mdph_section (numero_section, uuid_mdph_dossier, titre, contenu) +SELECT 'A1', d.uuid_mdph_dossier, 'Section test', '{"nom":"Test"}'::jsonb +FROM mdph_dossier d +JOIN user_account ua ON ua.uuid_user = d.uuid_patient +WHERE ua.email_normalized = 'patient.test@umhma.fr' +LIMIT 1; + +-- Lien aidant -> dossier +INSERT INTO gerer (uuid_aidant, uuid_mdph_dossier) +SELECT a.uuid_aidant, d.uuid_mdph_dossier +FROM aidant a +CROSS JOIN mdph_dossier d +JOIN user_account ua ON ua.uuid_user = d.uuid_patient +WHERE ua.email_normalized = 'patient.test@umhma.fr' +LIMIT 1 +ON CONFLICT DO NOTHING; + +-- Forum thread de test +INSERT INTO forum_thread (titre, uuid_user, category, excerpt) +SELECT 'Discussion test migration', ua.uuid_user::text, 'general', 'Thread seed' +FROM user_account ua WHERE email_normalized = 'patient.test@umhma.fr'; diff --git a/backend/src/app.js b/backend/src/app.js index 0460575..36c2f4b 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -89,7 +89,7 @@ async function createApp(pool, poolAdmin, options = {}) { req.path.startsWith('/admin') || req.path === '/settings/public', })); - app.use(express.json()); + app.use(express.json({ limit: '20mb' })); app.use(originProtection(allowedOrigins)); app.use(csrfProtection()); app.use(extractUser(jwtSecret)); diff --git a/backend/src/constants/appSettings.js b/backend/src/constants/appSettings.js new file mode 100644 index 0000000..0baa7ad --- /dev/null +++ b/backend/src/constants/appSettings.js @@ -0,0 +1,15 @@ +/** Valeurs par défaut alignées sur INSERT dans umhma_script_creation_bdd.sql */ +const DEFAULT_APP_SETTINGS = { + site_name: 'UMHMA', + contact_email: 'contact@umhma.fr', + contact_phone: '01 23 45 67 89', + contact_address: 'Paris, France', + contact_hours: 'Disponibles du lundi au vendredi, de 9h à 18h.', + timezone: 'Europe/Paris', + language: 'fr', + maintenance_mode: 'false', + maintenance_message: + 'La plateforme est temporairement indisponible pour maintenance. Nous revenons très vite.', +}; + +module.exports = { DEFAULT_APP_SETTINGS }; diff --git a/backend/src/controllers/chatbotController.js b/backend/src/controllers/chatbotController.js index d13e4ee..cc589df 100644 --- a/backend/src/controllers/chatbotController.js +++ b/backend/src/controllers/chatbotController.js @@ -3,19 +3,14 @@ const { loadRegroupementSections, } = require('../services/chatbotService'); -function toInt(value) { +function normalizeId(value) { if (value == null || value === '') return null; - const n = typeof value === 'number' ? value : parseInt(value, 10); - return Number.isFinite(n) ? n : null; + return String(value).trim(); } function makeChatbotController(pool) { const regroupement = loadRegroupementSections(); - /* ---------------------------------------------------------------- */ - /* POST /chatbot/start */ - /* Démarre (ou reprend) une session chatbot pour un sous-formulaire */ - /* ---------------------------------------------------------------- */ async function start(req, res, next) { try { if (!req.user || !req.user.id) { @@ -23,7 +18,7 @@ function makeChatbotController(pool) { } const { dossierId: rawDossierId, section, sousSection, formulaire } = req.body || {}; - const dossierId = toInt(rawDossierId); + const dossierId = normalizeId(rawDossierId); if (!dossierId || !section || !sousSection || !formulaire) { return res.status(400).json({ message: 'dossierId, section, sousSection et formulaire sont requis.', @@ -39,7 +34,7 @@ function makeChatbotController(pool) { const existing = await pool.query( `SELECT uuid_ai_session, resultat_json, traites, est_termine FROM ai_session - WHERE uuid_mdph_section = $1::int AND sous_section = $2 AND nom_formulaire = $3`, + WHERE uuid_mdph_section = $1 AND sous_section = $2 AND nom_formulaire = $3`, [sectionMdphId, sousSection, formulaire], ); @@ -74,10 +69,21 @@ function makeChatbotController(pool) { const patientId = patientRow.rows.length > 0 ? patientRow.rows[0].uuid_patient : null; const insertRes = await pool.query( - `INSERT INTO ai_session (uuid_mdph_section, uuid_patient, uuid_mdph_dossier, section, sous_section, nom_formulaire, resultat_json, traites) - VALUES ($1::int, $2::int, $3::int, $4, $5, $6, $7::jsonb, $8::jsonb) + `INSERT INTO ai_session ( + uuid_mdph_section, uuid_patient, section, sous_section, + nom_formulaire, resultat_json, traites + ) + VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::jsonb) RETURNING uuid_ai_session`, - [sectionMdphId, patientId, dossierId, section, sousSection, formulaire, '{}', '[]'], + [ + sectionMdphId, + patientId, + section, + sousSection, + formulaire, + '{}', + '[]', + ], ); sessionId = insertRes.rows[0].uuid_ai_session; } @@ -112,10 +118,6 @@ function makeChatbotController(pool) { } } - /* ---------------------------------------------------------------- */ - /* POST /chatbot/message */ - /* Envoie un message utilisateur au chatbot */ - /* ---------------------------------------------------------------- */ async function message(req, res, next) { try { if (!req.user || !req.user.id) { @@ -123,16 +125,18 @@ function makeChatbotController(pool) { } const { sessionId: rawSessionId, message: userMessage } = req.body || {}; - const sessionId = toInt(rawSessionId); + const sessionId = normalizeId(rawSessionId); if (!sessionId || userMessage == null) { return res.status(400).json({ message: 'sessionId et message sont requis.' }); } const sessionRow = await pool.query( - `SELECT ais.uuid_ai_session, ais.uuid_mdph_dossier, ais.section, ais.sous_section, - ais.nom_formulaire, ais.resultat_json, ais.traites, ais.est_termine + `SELECT ais.uuid_ai_session, ais.section, ais.sous_section, + ais.nom_formulaire, ais.resultat_json, ais.traites, ais.est_termine, + ms.uuid_mdph_dossier FROM ai_session ais - WHERE ais.uuid_ai_session = $1::int`, + JOIN mdph_section ms ON ms.uuid_mdph_section = ais.uuid_mdph_section + WHERE ais.uuid_ai_session = $1`, [sessionId], ); @@ -154,7 +158,6 @@ function makeChatbotController(pool) { } const historique = await loadHistorique(pool, sessionId); - const note = await resolveNote(pool, session.uuid_mdph_dossier, session.section); const jsonCommence = session.resultat_json && Object.keys(session.resultat_json).length > 0 ? session.resultat_json @@ -169,7 +172,13 @@ function makeChatbotController(pool) { await saveMessage(pool, sessionId, 'user', userMessage, 0, 0); await saveMessage(pool, sessionId, 'assistant', result.answer, result.input_tokens, result.output_tokens); await persistState(pool, sessionId, chatbot.dataFields, [...chatbot.traites], result.est_termine); - await syncChatbotToSection(pool, session.uuid_mdph_dossier, session.section, session.nom_formulaire, chatbot.dataFields); + await syncChatbotToSection( + pool, + session.uuid_mdph_dossier, + session.section, + session.nom_formulaire, + chatbot.dataFields, + ); const extra = (result.est_termine && session.section === 'start') ? computeRequiredSectionsFromBesoin((result.formData || {}).besoin) @@ -188,19 +197,13 @@ function makeChatbotController(pool) { } } - /* ---------------------------------------------------------------- */ - /* GET /chatbot/sections */ - /* ---------------------------------------------------------------- */ function getSections(_req, res) { return res.json({ sections: regroupement }); } - /* ---------------------------------------------------------------- */ - /* GET /chatbot/session/:sessionId */ - /* ---------------------------------------------------------------- */ async function getSession(req, res, next) { try { - const sessionId = toInt(req.params.sessionId); + const sessionId = normalizeId(req.params.sessionId); if (!sessionId) { return res.status(400).json({ message: 'sessionId invalide.' }); } @@ -209,8 +212,9 @@ function makeChatbotController(pool) { return res.status(403).json({ message: 'Accès refusé à cette session.' }); } const sessionRow = await pool.query( - `SELECT uuid_ai_session, section, sous_section, nom_formulaire, resultat_json, traites, est_termine - FROM ai_session WHERE uuid_ai_session = $1::int`, + `SELECT uuid_ai_session, section, sous_section, nom_formulaire, + resultat_json, traites, est_termine + FROM ai_session WHERE uuid_ai_session = $1`, [sessionId], ); if (sessionRow.rows.length === 0) { @@ -219,7 +223,7 @@ function makeChatbotController(pool) { const histRows = await pool.query( `SELECT message_role, message_conv, created_at - FROM ai_message WHERE uuid_ai_session = $1::int + FROM ai_message WHERE uuid_ai_session = $1 ORDER BY created_at ASC`, [sessionId], ); @@ -233,12 +237,9 @@ function makeChatbotController(pool) { } } - /* ---------------------------------------------------------------- */ - /* DELETE /chatbot/session/:sessionId */ - /* ---------------------------------------------------------------- */ async function resetSession(req, res, next) { try { - const sessionId = toInt(req.params.sessionId); + const sessionId = normalizeId(req.params.sessionId); if (!sessionId) { return res.status(400).json({ message: 'sessionId invalide.' }); } @@ -246,21 +247,17 @@ function makeChatbotController(pool) { if (!ownsSession) { return res.status(403).json({ message: 'Accès refusé à cette session.' }); } - await pool.query('DELETE FROM ai_message WHERE uuid_ai_session = $1::int', [sessionId]); - await pool.query('DELETE FROM ai_session WHERE uuid_ai_session = $1::int', [sessionId]); + await pool.query('DELETE FROM ai_message WHERE uuid_ai_session = $1', [sessionId]); + await pool.query('DELETE FROM ai_session WHERE uuid_ai_session = $1', [sessionId]); return res.json({ message: 'Session réinitialisée.' }); } catch (err) { return next(err); } } - /* ---------------------------------------------------------------- */ - /* GET /chatbot/required-sections/:dossierId */ - /* Renvoie les sections obligatoires selon le résultat du "debut" */ - /* ---------------------------------------------------------------- */ async function getRequiredSections(req, res, next) { try { - const dossierId = toInt(req.params.dossierId); + const dossierId = normalizeId(req.params.dossierId); if (!dossierId) { return res.status(400).json({ message: 'dossierId invalide.' }); } @@ -278,10 +275,6 @@ function makeChatbotController(pool) { return { start, message, getSections, getSession, resetSession, getRequiredSections }; } -/* ------------------------------------------------------------------ */ -/* Logique sections obligatoires selon le besoin */ -/* ------------------------------------------------------------------ */ - const ALL_SECTIONS = ['A', 'B', 'C', 'D', 'E', 'F']; function computeRequiredSectionsFromBesoin(besoins) { @@ -309,7 +302,7 @@ async function computeRequiredSections(pool, dossierId) { `SELECT ais.resultat_json, ais.est_termine FROM ai_session ais JOIN mdph_section ms ON ais.uuid_mdph_section = ms.uuid_mdph_section - WHERE ms.uuid_mdph_dossier = $1::int AND ais.section = 'start' + WHERE ms.uuid_mdph_dossier = $1 AND ais.section = 'start' AND ais.sous_section = 'debut' AND ais.nom_formulaire = 'debut'`, [dossierId], ); @@ -325,14 +318,10 @@ async function computeRequiredSections(pool, dossierId) { } } -/* ------------------------------------------------------------------ */ -/* Utilitaires privés */ -/* ------------------------------------------------------------------ */ - async function getOrCreateMdphSection(pool, dossierId, sectionName) { const existing = await pool.query( `SELECT uuid_mdph_section FROM mdph_section - WHERE uuid_mdph_dossier = $1::int AND numero_section = $2`, + WHERE uuid_mdph_dossier = $1 AND numero_section = $2`, [dossierId, sectionName], ); @@ -342,7 +331,7 @@ async function getOrCreateMdphSection(pool, dossierId, sectionName) { const result = await pool.query( `INSERT INTO mdph_section (uuid_mdph_dossier, numero_section) - VALUES ($1::int, $2) + VALUES ($1, $2) RETURNING uuid_mdph_section`, [dossierId, sectionName], ); @@ -353,7 +342,7 @@ async function loadHistorique(pool, sessionId) { const histRows = await pool.query( `SELECT uuid_ai_message, message_role, message_conv, token_input, token_output FROM ai_message - WHERE uuid_ai_session = $1::int + WHERE uuid_ai_session = $1 ORDER BY created_at ASC`, [sessionId], ); @@ -362,7 +351,7 @@ async function loadHistorique(pool, sessionId) { if (rows.length > 0 && rows[rows.length - 1].message_role === 'assistant') { const lastId = rows[rows.length - 1].uuid_ai_message; rows.pop(); - await pool.query('DELETE FROM ai_message WHERE uuid_ai_message = $1::int', [lastId]); + await pool.query('DELETE FROM ai_message WHERE uuid_ai_message = $1', [lastId]); } return rows.map((r) => ({ @@ -380,16 +369,16 @@ async function resolveNote(pool, dossierId, section) { `SELECT ais.resultat_json, ais.est_termine FROM ai_session ais JOIN mdph_section ms ON ais.uuid_mdph_section = ms.uuid_mdph_section - WHERE ms.uuid_mdph_dossier = $1::int AND ais.section = 'start' + WHERE ms.uuid_mdph_dossier = $1 AND ais.section = 'start' AND ais.sous_section = 'debut' AND ais.nom_formulaire = 'debut'`, [dossierId], ); if ( - row.rows.length === 0 || - !row.rows[0].est_termine || - !row.rows[0].resultat_json || - row.rows[0].resultat_json.pour_qui == null + row.rows.length === 0 + || !row.rows[0].est_termine + || !row.rows[0].resultat_json + || row.rows[0].resultat_json.pour_qui == null ) { return ''; } @@ -398,7 +387,14 @@ async function resolveNote(pool, dossierId, section) { if (pourQui === 'Mon enfant' || pourQui === 'Votre enfant' || pourQui === 2) { return '- Le formulaire est destiné pour un enfant, une personne de moins de 18 ans'; } - if (pourQui === 'Moi-même' || pourQui === 'Autre personne (+ de 18 ans)' || pourQui === 'Vous' || pourQui === 'Autre (+ de 18 ans)' || pourQui === 1 || pourQui === 3) { + if ( + pourQui === 'Moi-même' + || pourQui === 'Autre personne (+ de 18 ans)' + || pourQui === 'Vous' + || pourQui === 'Autre (+ de 18 ans)' + || pourQui === 1 + || pourQui === 3 + ) { return '- Le formulaire est destiné pour un adulte, une personne de plus de 18 ans. Vous devez vérifier si certains champs du formulaire doivent être ignorés'; } return ''; @@ -411,7 +407,7 @@ async function persistState(pool, sessionId, formData, traites, estTermine) { await pool.query( `UPDATE ai_session SET resultat_json = $2::jsonb, traites = $3::jsonb, est_termine = $4, updated_at = NOW() - WHERE uuid_ai_session = $1::int`, + WHERE uuid_ai_session = $1`, [sessionId, JSON.stringify(formData), JSON.stringify(traites), estTermine], ); } @@ -421,7 +417,7 @@ async function syncChatbotToSection(pool, dossierId, section, formulaire, formDa try { const existing = await pool.query( - 'SELECT uuid_mdph_section, contenu FROM mdph_section WHERE uuid_mdph_dossier = $1::int AND numero_section = $2', + 'SELECT uuid_mdph_section, contenu FROM mdph_section WHERE uuid_mdph_dossier = $1 AND numero_section = $2', [dossierId, section], ); @@ -429,11 +425,10 @@ async function syncChatbotToSection(pool, dossierId, section, formulaire, formDa const sectionId = existing.rows[0].uuid_mdph_section; const contenu = existing.rows[0].contenu || {}; - contenu[formulaire] = formData; await pool.query( - 'UPDATE mdph_section SET contenu = $1::jsonb, updated_at = NOW() WHERE uuid_mdph_section = $2::int', + 'UPDATE mdph_section SET contenu = $1::jsonb, updated_at = NOW() WHERE uuid_mdph_section = $2', [JSON.stringify(contenu), sectionId], ); } catch (err) { @@ -445,7 +440,7 @@ async function saveMessage(pool, sessionId, role, content, tokensIn, tokensOut) if (!content || !content.trim()) return; await pool.query( `INSERT INTO ai_message (uuid_ai_session, message_role, message_conv, token_input, token_output) - VALUES ($1::int, $2, $3, $4, $5)`, + VALUES ($1, $2, $3, $4, $5)`, [sessionId, role, content, tokensIn || 0, tokensOut || 0], ); } @@ -454,9 +449,8 @@ module.exports = { makeChatbotController }; async function hasDossierAccess(pool, dossierId, userId) { const result = await pool.query( - `SELECT 1 - FROM mdph_dossier - WHERE uuid_mdph_dossier = $1::int AND uuid_patient = $2::int`, + `SELECT 1 FROM mdph_dossier + WHERE uuid_mdph_dossier = $1 AND uuid_patient = $2`, [dossierId, userId], ); return result.rows.length > 0; @@ -466,8 +460,9 @@ async function hasSessionAccess(pool, sessionId, userId) { const result = await pool.query( `SELECT 1 FROM ai_session ais - JOIN mdph_dossier d ON d.uuid_mdph_dossier = ais.uuid_mdph_dossier - WHERE ais.uuid_ai_session = $1::int AND d.uuid_patient = $2::int`, + JOIN mdph_section ms ON ms.uuid_mdph_section = ais.uuid_mdph_section + JOIN mdph_dossier d ON d.uuid_mdph_dossier = ms.uuid_mdph_dossier + WHERE ais.uuid_ai_session = $1 AND d.uuid_patient = $2`, [sessionId, userId], ); return result.rows.length > 0; diff --git a/backend/src/controllers/documentController.js b/backend/src/controllers/documentController.js index 517f372..d989447 100644 --- a/backend/src/controllers/documentController.js +++ b/backend/src/controllers/documentController.js @@ -1,8 +1,7 @@ +const { runMistralOcr, decodeBase64Payload } = require('../services/mistralOcrService'); + function makeDocumentController(pool) { async function resolveSectionIdForDocument(userId, providedSectionId) { - // Si une section est fournie, on la valide en s'assurant qu'elle appartient - // bien à un dossier de l'utilisateur. Sinon, on retourne null : le document - // sera enregistré sans rattachement (uuid_mdph_section nullable). if (providedSectionId != null) { const sectionCheck = await pool.query( `SELECT s.uuid_mdph_section @@ -19,39 +18,101 @@ function makeDocumentController(pool) { async function getOwnedDocument(id, userId) { const result = await pool.query( - `SELECT uuid_document_mdph as id, user_id, COALESCE(titre, titre_document) as titre, type, chemin_vers_document, taille, status, created_at, updated_at + `SELECT uuid_document_mdph as id, user_id, + COALESCE(titre, titre_document) as titre, type, + chemin_vers_document, taille, status, + contenu_ocr_json, created_at, updated_at FROM mdph_document WHERE uuid_document_mdph = $1 AND user_id = $2`, - [id, userId], + [id, String(userId)], ); return result.rows[0] || null; } + async function runOcrIfProvided({ file_base64, file_mime_type, chemin_vers_document }) { + if (!file_base64) { + return { contenu_ocr_json: {}, ocr_status: 'skipped' }; + } + + const { buffer, mimeType, fileName } = decodeBase64Payload( + file_base64, + file_mime_type, + chemin_vers_document, + ); + const ocrResult = await runMistralOcr(buffer, fileName, mimeType); + return { contenu_ocr_json: ocrResult, ocr_status: 'completed' }; + } + return { async create(req, res, next) { try { - const { titre, type, chemin_vers_document, taille, status, uuid_mdph_section } = req.body || {}; + const { + titre, + type, + chemin_vers_document, + taille, + status, + uuid_mdph_section, + file_base64, + file_mime_type, + } = req.body || {}; + if (!req.user || !req.user.id) { return res.status(401).json({ message: 'Authentification requise.' }); } + const sectionId = await resolveSectionIdForDocument(req.user.id, uuid_mdph_section); + let contenuOcrJson = {}; + let ocrStatus = 'skipped'; + + if (file_base64) { + try { + const ocr = await runOcrIfProvided({ + file_base64, + file_mime_type, + chemin_vers_document, + }); + contenuOcrJson = ocr.contenu_ocr_json; + ocrStatus = ocr.ocr_status; + } catch (ocrErr) { + console.error('OCR error:', ocrErr.message); + contenuOcrJson = { + erreur: ocrErr.message, + type_extraction: 'mistral_ocr', + date_extraction: new Date().toISOString(), + }; + ocrStatus = 'failed'; + } + } + const result = await pool.query( - `INSERT INTO mdph_document (uuid_mdph_section, user_id, titre_document, titre, type, chemin_vers_document, taille, status) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - RETURNING uuid_document_mdph as id, user_id, COALESCE(titre, titre_document) as titre, type, chemin_vers_document, taille, status, created_at, updated_at`, + `INSERT INTO mdph_document ( + uuid_mdph_section, user_id, titre_document, titre, type, + chemin_vers_document, taille, status, contenu_ocr_json + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::json) + RETURNING uuid_document_mdph as id, user_id, + COALESCE(titre, titre_document) as titre, type, + chemin_vers_document, taille, status, + contenu_ocr_json, created_at, updated_at`, [ sectionId, - req.user.id, + String(req.user.id), titre ?? chemin_vers_document ?? 'Document', titre ?? null, type ?? null, chemin_vers_document ?? null, taille ?? null, - status ?? null, + status ?? 'pending', + JSON.stringify(contenuOcrJson), ], ); - return res.status(201).json({ document: result.rows[0] }); + + return res.status(201).json({ + document: result.rows[0], + ocr_status: ocrStatus, + }); } catch (err) { return next(err); } @@ -60,11 +121,14 @@ function makeDocumentController(pool) { async list(req, res, next) { try { const result = await pool.query( - `SELECT uuid_document_mdph as id, user_id, COALESCE(titre, titre_document) as titre, type, chemin_vers_document, taille, status, created_at, updated_at + `SELECT uuid_document_mdph as id, user_id, + COALESCE(titre, titre_document) as titre, type, + chemin_vers_document, taille, status, + contenu_ocr_json, created_at, updated_at FROM mdph_document WHERE user_id = $1 ORDER BY created_at DESC`, - [req.user.id], + [String(req.user.id)], ); res.set('Cache-Control', 'private, max-age=10'); return res.json({ documents: result.rows }); @@ -89,12 +153,34 @@ function makeDocumentController(pool) { async update(req, res, next) { try { const { id } = req.params; - const { titre, type, chemin_vers_document, taille, status } = req.body || {}; + const { + titre, + type, + chemin_vers_document, + taille, + status, + file_base64, + file_mime_type, + } = req.body || {}; const existing = await getOwnedDocument(id, req.user.id); if (!existing) { return res.status(404).json({ message: 'Document introuvable.' }); } + let contenuOcrJson = existing.contenu_ocr_json; + if (file_base64) { + try { + const ocr = await runOcrIfProvided({ + file_base64, + file_mime_type, + chemin_vers_document: chemin_vers_document || existing.chemin_vers_document, + }); + contenuOcrJson = ocr.contenu_ocr_json; + } catch (ocrErr) { + console.error('OCR error on update:', ocrErr.message); + } + } + const result = await pool.query( `UPDATE mdph_document SET titre_document = COALESCE($2, titre_document), @@ -103,10 +189,23 @@ function makeDocumentController(pool) { chemin_vers_document = COALESCE($4, chemin_vers_document), taille = COALESCE($5, taille), status = COALESCE($6, status), + contenu_ocr_json = COALESCE($7::json, contenu_ocr_json), updated_at = NOW() - WHERE uuid_document_mdph = $1 AND user_id = $7 - RETURNING uuid_document_mdph as id, user_id, COALESCE(titre, titre_document) as titre, type, chemin_vers_document, taille, status, created_at, updated_at`, - [id, titre ?? null, type ?? null, chemin_vers_document ?? null, taille ?? null, status ?? null, req.user.id], + WHERE uuid_document_mdph = $1 AND user_id = $8 + RETURNING uuid_document_mdph as id, user_id, + COALESCE(titre, titre_document) as titre, type, + chemin_vers_document, taille, status, + contenu_ocr_json, created_at, updated_at`, + [ + id, + titre ?? null, + type ?? null, + chemin_vers_document ?? null, + taille ?? null, + status ?? null, + file_base64 ? JSON.stringify(contenuOcrJson) : null, + String(req.user.id), + ], ); if (result.rows.length === 0) { @@ -126,8 +225,10 @@ function makeDocumentController(pool) { return res.status(404).json({ message: 'Document introuvable.' }); } const result = await pool.query( - `DELETE FROM mdph_document WHERE uuid_document_mdph = $1 AND user_id = $2 RETURNING uuid_document_mdph as id`, - [id, req.user.id], + `DELETE FROM mdph_document + WHERE uuid_document_mdph = $1 AND user_id = $2 + RETURNING uuid_document_mdph as id`, + [id, String(req.user.id)], ); if (result.rows.length === 0) { return res.status(404).json({ message: 'Document introuvable.' }); diff --git a/backend/src/controllers/settingsController.js b/backend/src/controllers/settingsController.js index 798eb31..ff901dd 100644 --- a/backend/src/controllers/settingsController.js +++ b/backend/src/controllers/settingsController.js @@ -8,7 +8,7 @@ * sont protégées par `requireAuth + adminOnly`. */ -const { DEFAULT_APP_SETTINGS } = require('../services/dbInit'); +const { DEFAULT_APP_SETTINGS } = require('../constants/appSettings'); const ALLOWED_KEYS = Object.keys(DEFAULT_APP_SETTINGS); const PUBLIC_KEYS = [ diff --git a/backend/src/routes/documents.js b/backend/src/routes/documents.js index 3b87e5b..7fd79c7 100644 --- a/backend/src/routes/documents.js +++ b/backend/src/routes/documents.js @@ -15,6 +15,8 @@ function documentRoutes(pool) { chemin_vers_document: { type: 'string', maxLength: 1000 }, taille: { type: 'string', maxLength: 80 }, status: { type: 'string', maxLength: 50 }, + file_mime_type: { type: 'string', maxLength: 120 }, + file_base64: { type: 'string' }, }), ctrl.create); router.get('/', ctrl.list); router.get('/:id', validateFlexibleIdParam('id'), ctrl.getById); @@ -24,6 +26,8 @@ function documentRoutes(pool) { chemin_vers_document: { type: 'string', maxLength: 1000 }, taille: { type: 'string', maxLength: 80 }, status: { type: 'string', maxLength: 50 }, + file_mime_type: { type: 'string', maxLength: 120 }, + file_base64: { type: 'string' }, }), ctrl.update); router.delete('/:id', validateFlexibleIdParam('id'), ctrl.remove); diff --git a/backend/src/services/mistralOcrService.js b/backend/src/services/mistralOcrService.js new file mode 100644 index 0000000..0338982 --- /dev/null +++ b/backend/src/services/mistralOcrService.js @@ -0,0 +1,154 @@ +/** + * OCR Mistral (portage de backend/mistral_ocr.py). + * Appelle l'API REST Mistral OCR et renvoie un JSON stockable dans contenu_ocr_json. + */ + +const SUPPORTED_EXTENSIONS = new Set(['.pdf', '.png', '.jpg', '.jpeg']); + +function cleanText(markdown) { + if (!markdown) return ''; + + let text = markdown + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'"); + + text = text.replace(/!\[[^\]]*\]\([^)]+\)/g, ''); + text = text.replace(/<\/?[^>\n]+>/g, ''); + text = text.replace(/^#{1,6}\s*/gm, ''); + text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1'); + text = text.replace(/\*\*/g, '').replace(/__/g, '').replace(/\*/g, ''); + + const lines = []; + for (const raw of text.split('\n')) { + const line = raw.trim().replace(/\s+/g, ' '); + if (line) lines.push(line); + } + return lines.join('\n'); +} + +function extensionFromMime(mimeType, fileName) { + const lower = (fileName || '').toLowerCase(); + if (lower.endsWith('.pdf')) return '.pdf'; + if (lower.endsWith('.png')) return '.png'; + if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return '.jpg'; + + if (mimeType === 'application/pdf') return '.pdf'; + if (mimeType === 'image/png') return '.png'; + if (mimeType === 'image/jpeg') return '.jpg'; + return ''; +} + +function bufferToDataUrl(buffer, mimeType) { + const base64 = buffer.toString('base64'); + return `data:${mimeType};base64,${base64}`; +} + +function buildDocumentPayload(extension, dataUrl) { + if (extension === '.pdf') { + return { type: 'document_url', document_url: dataUrl }; + } + return { type: 'image_url', image_url: dataUrl }; +} + +/** + * @param {Buffer} fileBuffer + * @param {string} fileName + * @param {string} [mimeType] + */ +async function runMistralOcr(fileBuffer, fileName, mimeType) { + const apiKey = process.env.MISTRAL_API_KEY; + if (!apiKey) { + throw new Error('MISTRAL_API_KEY manquante dans les variables d\'environnement.'); + } + + const extension = extensionFromMime(mimeType, fileName); + if (!SUPPORTED_EXTENSIONS.has(extension)) { + throw new Error( + `Extension non supportée (${extension || 'inconnue'}). Formats acceptés : PDF, PNG, JPG.`, + ); + } + + const resolvedMime = mimeType + || (extension === '.pdf' ? 'application/pdf' : 'image/jpeg'); + const dataUrl = bufferToDataUrl(fileBuffer, resolvedMime); + const document = buildDocumentPayload(extension, dataUrl); + + const response = await fetch('https://api.mistral.ai/v1/ocr', { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: 'mistral-ocr-latest', + document, + include_image_base64: false, + }), + }); + + if (!response.ok) { + const errText = await response.text(); + throw new Error(`Mistral OCR HTTP ${response.status}: ${errText.slice(0, 300)}`); + } + + const raw = await response.json(); + const pages = []; + + for (let i = 0; i < (raw.pages || []).length; i += 1) { + const page = raw.pages[i] || {}; + const markdown = page.markdown || ''; + const texte = cleanText(markdown); + pages.push({ + page: i + 1, + texte, + texte_lignes: texte.split('\n'), + markdown, + images: page.images || [], + tables: page.tables || [], + dimensions: page.dimensions || {}, + confidence_scores: page.confidence_scores ?? null, + }); + } + + return { + fichier: fileName || 'document', + extension, + type_extraction: 'mistral_ocr', + date_extraction: new Date().toISOString(), + mode: 'general', + nb_pages: pages.length, + pages, + model: raw.model || 'mistral-ocr-latest', + usage_info: raw.usage_info || {}, + }; +} + +/** + * Décode un data URL ou base64 brut envoyé par le frontend. + */ +function decodeBase64Payload(fileBase64, mimeType, fileName) { + let base64 = String(fileBase64 || '').trim(); + let resolvedMime = mimeType || ''; + + const dataUrlMatch = /^data:([^;]+);base64,(.+)$/i.exec(base64); + if (dataUrlMatch) { + resolvedMime = dataUrlMatch[1]; + base64 = dataUrlMatch[2]; + } + + const buffer = Buffer.from(base64, 'base64'); + if (!buffer.length) { + throw new Error('Fichier vide ou encodage base64 invalide.'); + } + + return { buffer, mimeType: resolvedMime, fileName: fileName || 'document' }; +} + +module.exports = { + runMistralOcr, + decodeBase64Payload, + SUPPORTED_EXTENSIONS, +}; diff --git a/backups/alwaysdata_avant_test_nouvelle_bdd_20260526_0623.sql b/backups/alwaysdata_avant_test_nouvelle_bdd_20260526_0623.sql new file mode 100644 index 0000000..cdd2856 --- /dev/null +++ b/backups/alwaysdata_avant_test_nouvelle_bdd_20260526_0623.sql @@ -0,0 +1,1773 @@ +-- +-- PostgreSQL database dump +-- + +\restrict HJcEFrNAxy2cnQz42G9JestDL3b0jOZZdXe63smWe1OPYKyuM2SxTsBdbVkkS5b + +-- Dumped from database version 16.14 +-- Dumped by pg_dump version 16.11 (Homebrew) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: public; Type: SCHEMA; Schema: -; Owner: - +-- + +-- *not* creating schema, since initdb creates it + + +-- +-- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public; + + +-- +-- Name: EXTENSION pgcrypto; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION pgcrypto IS 'cryptographic functions'; + + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: admin; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.admin ( + uuid_admin integer NOT NULL +); + + +-- +-- Name: ai_message; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.ai_message ( + uuid_ai_message integer NOT NULL, + message_role character varying(50) NOT NULL, + message_conv text NOT NULL, + uuid_ai_session integer NOT NULL, + token_input integer DEFAULT 0, + token_output integer DEFAULT 0, + created_at timestamp with time zone DEFAULT now() +); + + +-- +-- Name: ai_message_uuid_ai_message_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.ai_message_uuid_ai_message_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: ai_message_uuid_ai_message_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.ai_message_uuid_ai_message_seq OWNED BY public.ai_message.uuid_ai_message; + + +-- +-- Name: ai_session; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.ai_session ( + uuid_ai_session integer NOT NULL, + uuid_section_mdph integer NOT NULL, + uuid_patient integer, + uuid_dossier_mdph integer, + section text, + sous_section text, + nom_formulaire text, + resultat_json jsonb DEFAULT '{}'::jsonb, + traites jsonb DEFAULT '[]'::jsonb, + est_termine boolean DEFAULT false, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now() +); + + +-- +-- Name: ai_session_uuid_ai_session_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.ai_session_uuid_ai_session_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: ai_session_uuid_ai_session_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.ai_session_uuid_ai_session_seq OWNED BY public.ai_session.uuid_ai_session; + + +-- +-- Name: aidant; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.aidant ( + uuid_aidant integer NOT NULL +); + + +-- +-- Name: app_setting; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.app_setting ( + key text NOT NULL, + value text, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: audit_logs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.audit_logs ( + uuid_audit_log integer NOT NULL, + commentaire text, + uuid_user integer NOT NULL, + uuid_type_action integer +); + + +-- +-- Name: audit_logs_uuid_audit_log_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.audit_logs_uuid_audit_log_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: audit_logs_uuid_audit_log_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.audit_logs_uuid_audit_log_seq OWNED BY public.audit_logs.uuid_audit_log; + + +-- +-- Name: categoriser; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.categoriser ( + uuid_topic integer NOT NULL, + uuid_thread integer NOT NULL +); + + +-- +-- Name: eligibilite; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.eligibilite ( + uuid_eligibilite integer NOT NULL, + lien_vers_aide text +); + + +-- +-- Name: eligibilite_uuid_eligibilite_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.eligibilite_uuid_eligibilite_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: eligibilite_uuid_eligibilite_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.eligibilite_uuid_eligibilite_seq OWNED BY public.eligibilite.uuid_eligibilite; + + +-- +-- Name: est_eligible; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.est_eligible ( + uuid_patient integer NOT NULL, + uuid_eligibilite integer NOT NULL +); + + +-- +-- Name: forum_post; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.forum_post ( + uuid_post integer NOT NULL, + message_forum text NOT NULL, + date_publication date NOT NULL, + uuid_thread integer NOT NULL, + uuid_user integer NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + is_hidden boolean DEFAULT false +); + + +-- +-- Name: forum_post_uuid_post_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.forum_post_uuid_post_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: forum_post_uuid_post_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.forum_post_uuid_post_seq OWNED BY public.forum_post.uuid_post; + + +-- +-- Name: forum_report; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.forum_report ( + uuid_report integer NOT NULL, + uuid_post integer, + uuid_thread integer, + uuid_reporter integer, + target_type text DEFAULT 'post'::text NOT NULL, + reason text NOT NULL, + priority text DEFAULT 'medium'::text NOT NULL, + status text DEFAULT 'pending'::text NOT NULL, + resolution_note text, + resolved_by integer, + resolved_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: forum_report_uuid_report_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.forum_report_uuid_report_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: forum_report_uuid_report_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.forum_report_uuid_report_seq OWNED BY public.forum_report.uuid_report; + + +-- +-- Name: forum_thread; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.forum_thread ( + uuid_thread integer NOT NULL, + titre character varying(1023) NOT NULL, + category text DEFAULT 'general'::text, + excerpt text, + is_pinned boolean DEFAULT false, + views integer DEFAULT 0, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + uuid_user character varying(42), + owner_id text, + is_hidden boolean DEFAULT false +); + + +-- +-- Name: forum_thread_uuid_thread_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.forum_thread_uuid_thread_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: forum_thread_uuid_thread_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.forum_thread_uuid_thread_seq OWNED BY public.forum_thread.uuid_thread; + + +-- +-- Name: forum_topic; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.forum_topic ( + uuid_topic integer NOT NULL, + topic character varying(255) NOT NULL +); + + +-- +-- Name: forum_topic_uuid_topic_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.forum_topic_uuid_topic_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: forum_topic_uuid_topic_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.forum_topic_uuid_topic_seq OWNED BY public.forum_topic.uuid_topic; + + +-- +-- Name: gerer; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.gerer ( + uuid_aidant integer NOT NULL, + uuid_dossier_mdph integer NOT NULL +); + + +-- +-- Name: mdph_document; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.mdph_document ( + uuid_document_mdph integer NOT NULL, + chemin_vers_document text NOT NULL, + titre_document text NOT NULL, + uuid_section_mdph integer, + user_id character varying(42), + titre text, + type text, + taille text, + status text DEFAULT 'pending'::text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: mdph_document_uuid_document_mdph_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.mdph_document_uuid_document_mdph_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: mdph_document_uuid_document_mdph_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.mdph_document_uuid_document_mdph_seq OWNED BY public.mdph_document.uuid_document_mdph; + + +-- +-- Name: mdph_dossier; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.mdph_dossier ( + uuid_dossier_mdph integer NOT NULL, + uuid_patient integer, + numero_dossier text, + titre text, + description text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: mdph_dossier_uuid_dossier_mdph_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.mdph_dossier_uuid_dossier_mdph_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: mdph_dossier_uuid_dossier_mdph_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.mdph_dossier_uuid_dossier_mdph_seq OWNED BY public.mdph_dossier.uuid_dossier_mdph; + + +-- +-- Name: mdph_section; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.mdph_section ( + uuid_section_mdph integer NOT NULL, + numero_section character varying(50) NOT NULL, + uuid_dossier_mdph integer NOT NULL, + titre text, + contenu jsonb DEFAULT '{}'::jsonb, + uuid_ai_session text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: mdph_section_uuid_section_mdph_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.mdph_section_uuid_section_mdph_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: mdph_section_uuid_section_mdph_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.mdph_section_uuid_section_mdph_seq OWNED BY public.mdph_section.uuid_section_mdph; + + +-- +-- Name: patient; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.patient ( + uuid_patient integer NOT NULL +); + + +-- +-- Name: proposer_suggestion; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.proposer_suggestion ( + uuid_patient integer NOT NULL, + uuid_suggestion integer NOT NULL +); + + +-- +-- Name: rendez_vous; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.rendez_vous ( + uuid_rdv integer NOT NULL, + uuid_patient integer NOT NULL, + uuid_aidant integer NOT NULL, + date_rdv date NOT NULL +); + + +-- +-- Name: rendez_vous_uuid_rdv_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.rendez_vous_uuid_rdv_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: rendez_vous_uuid_rdv_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.rendez_vous_uuid_rdv_seq OWNED BY public.rendez_vous.uuid_rdv; + + +-- +-- Name: resource; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.resource ( + uuid_resource integer NOT NULL, + type text NOT NULL, + icon text, + category text, + title text NOT NULL, + description text, + read_time text, + keywords jsonb DEFAULT '[]'::jsonb, + created_by character varying(42), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: resource_uuid_resource_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.resource_uuid_resource_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: resource_uuid_resource_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.resource_uuid_resource_seq OWNED BY public.resource.uuid_resource; + + +-- +-- Name: suggestion; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.suggestion ( + uuid_suggestion integer NOT NULL, + suggestion_text text +); + + +-- +-- Name: suggestion_uuid_suggestion_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.suggestion_uuid_suggestion_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: suggestion_uuid_suggestion_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.suggestion_uuid_suggestion_seq OWNED BY public.suggestion.uuid_suggestion; + + +-- +-- Name: type_action; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.type_action ( + uuid_type_action integer NOT NULL, + nom_action character varying(255) NOT NULL +); + + +-- +-- Name: type_action_uuid_type_action_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.type_action_uuid_type_action_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: type_action_uuid_type_action_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.type_action_uuid_type_action_seq OWNED BY public.type_action.uuid_type_action; + + +-- +-- Name: user_account; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_account ( + uuid_user integer NOT NULL, + email character varying(511) NOT NULL, + email_normalized text, + name text, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: user_account_uuid_user_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.user_account_uuid_user_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: user_account_uuid_user_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.user_account_uuid_user_seq OWNED BY public.user_account.uuid_user; + + +-- +-- Name: user_security; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_security ( + uuid_user integer NOT NULL, + mdp_hash character varying(511) NOT NULL, + mdp_sel character varying(511) NOT NULL, + lockout boolean DEFAULT false NOT NULL, + mfa text, + date_creation date NOT NULL, + date_derniere_connection date +); + + +-- +-- Name: ai_message uuid_ai_message; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.ai_message ALTER COLUMN uuid_ai_message SET DEFAULT nextval('public.ai_message_uuid_ai_message_seq'::regclass); + + +-- +-- Name: ai_session uuid_ai_session; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.ai_session ALTER COLUMN uuid_ai_session SET DEFAULT nextval('public.ai_session_uuid_ai_session_seq'::regclass); + + +-- +-- Name: audit_logs uuid_audit_log; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.audit_logs ALTER COLUMN uuid_audit_log SET DEFAULT nextval('public.audit_logs_uuid_audit_log_seq'::regclass); + + +-- +-- Name: eligibilite uuid_eligibilite; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.eligibilite ALTER COLUMN uuid_eligibilite SET DEFAULT nextval('public.eligibilite_uuid_eligibilite_seq'::regclass); + + +-- +-- Name: forum_post uuid_post; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.forum_post ALTER COLUMN uuid_post SET DEFAULT nextval('public.forum_post_uuid_post_seq'::regclass); + + +-- +-- Name: forum_report uuid_report; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.forum_report ALTER COLUMN uuid_report SET DEFAULT nextval('public.forum_report_uuid_report_seq'::regclass); + + +-- +-- Name: forum_thread uuid_thread; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.forum_thread ALTER COLUMN uuid_thread SET DEFAULT nextval('public.forum_thread_uuid_thread_seq'::regclass); + + +-- +-- Name: forum_topic uuid_topic; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.forum_topic ALTER COLUMN uuid_topic SET DEFAULT nextval('public.forum_topic_uuid_topic_seq'::regclass); + + +-- +-- Name: mdph_document uuid_document_mdph; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mdph_document ALTER COLUMN uuid_document_mdph SET DEFAULT nextval('public.mdph_document_uuid_document_mdph_seq'::regclass); + + +-- +-- Name: mdph_dossier uuid_dossier_mdph; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mdph_dossier ALTER COLUMN uuid_dossier_mdph SET DEFAULT nextval('public.mdph_dossier_uuid_dossier_mdph_seq'::regclass); + + +-- +-- Name: mdph_section uuid_section_mdph; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mdph_section ALTER COLUMN uuid_section_mdph SET DEFAULT nextval('public.mdph_section_uuid_section_mdph_seq'::regclass); + + +-- +-- Name: rendez_vous uuid_rdv; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.rendez_vous ALTER COLUMN uuid_rdv SET DEFAULT nextval('public.rendez_vous_uuid_rdv_seq'::regclass); + + +-- +-- Name: resource uuid_resource; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.resource ALTER COLUMN uuid_resource SET DEFAULT nextval('public.resource_uuid_resource_seq'::regclass); + + +-- +-- Name: suggestion uuid_suggestion; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.suggestion ALTER COLUMN uuid_suggestion SET DEFAULT nextval('public.suggestion_uuid_suggestion_seq'::regclass); + + +-- +-- Name: type_action uuid_type_action; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.type_action ALTER COLUMN uuid_type_action SET DEFAULT nextval('public.type_action_uuid_type_action_seq'::regclass); + + +-- +-- Name: user_account uuid_user; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_account ALTER COLUMN uuid_user SET DEFAULT nextval('public.user_account_uuid_user_seq'::regclass); + + +-- +-- Data for Name: admin; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.admin (uuid_admin) FROM stdin; +1 +\. + + +-- +-- Data for Name: ai_message; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.ai_message (uuid_ai_message, message_role, message_conv, uuid_ai_session, token_input, token_output, created_at) FROM stdin; +1 user Je veux renouveler ma RQTH et expliquer mes aménagements au travail. 1 22 0 2026-05-03 08:36:51.126+00 +2 assistant Indiquez votre poste, les tâches difficiles et les adaptations déjà mises en place. 1 0 31 2026-05-03 08:36:51.126+00 +3 user Je travaille 24h par semaine avec télétravail deux jours. 1 18 0 2026-05-03 08:36:51.126+00 +4 user J’ai besoin d’aide pour décrire les gestes du quotidien. 2 17 0 2026-05-05 08:36:51.166+00 +5 assistant Nous allons détailler les actes essentiels, leur fréquence et les aides déjà présentes. 2 0 34 2026-05-05 08:36:51.166+00 +6 user Je prépare le dossier scolaire de ma fille Emma. 3 14 0 2026-05-11 08:36:51.204+00 +7 assistant Ajoutons les difficultés observées en classe et les aménagements recommandés. 3 0 25 2026-05-11 08:36:51.204+00 +9 user C'est votre première demande à la MDPH 4 0 0 2026-05-12 08:59:39.741087+00 +11 user Moi-même 4 0 0 2026-05-12 08:59:44.868105+00 +13 user Homme 5 0 0 2026-05-12 08:59:52.918145+00 +15 user Yanis 5 0 0 2026-05-12 09:00:02.390143+00 +17 user Martin 5 0 0 2026-05-12 09:00:19.070974+00 +18 assistant Quelle est votre date de naissance ? 5 2004 281 2026-05-12 09:00:19.103687+00 +\. + + +-- +-- Data for Name: ai_session; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.ai_session (uuid_ai_session, uuid_section_mdph, uuid_patient, uuid_dossier_mdph, section, sous_section, nom_formulaire, resultat_json, traites, est_termine, created_at, updated_at) FROM stdin; +1 2 2 1 B B2 Vie professionnelle {"synthese": "Renouvellement RQTH cohérent avec limitations fonctionnelles documentées.", "champsCompletes": 8} ["poste_actuel", "limitations", "amenagements"] t 2026-05-03 08:36:51.126+00 2026-05-03 08:36:51.126+00 +2 3 3 2 D D1 Vie quotidienne {"besoins": ["aide humaine", "adaptation logement"], "brouillon": true} ["transferts", "repas"] f 2026-05-05 08:36:51.166+00 2026-05-05 08:36:51.166+00 +3 6 5 4 E E1-enfant Scolarisation enfant {"demande": "AESH mutualisée", "adaptations": ["consignes fractionnées", "temps supplémentaire"]} ["classe", "besoins_scolaires", "suivi"] t 2026-05-11 08:36:51.204+00 2026-05-11 08:36:51.204+00 +4 7 2 1 start debut debut {"besoin": [1], "pour_qui": "Moi-même", "dossier_mdph": {"departement": null, "numero_dossier": null}} ["besoin", "dossier_mdph.departement", "dossier_mdph.numero_dossier", "pour_qui"] t 2026-05-12 08:59:08.806395+00 2026-05-12 08:59:44.900479+00 +5 8 2 1 A A1 section_A/section_a1 {"sexe": 1, "prenoms": ["Yanis"], "nationalite": null, "secu_sociale": null, "nom_naissance": "Martin", "date_naissance": null, "pays_naissance": null, "commune_naissance": null, "complement_adresse": {"pays": null, "email": null, "adresse": null, "commune": null, "telephone": null, "code_postal": null, "moyen_contact": null, "nom_personne_heberge": null}, "date_arrive_france": null, "domicile_organisme": {"nom_organisme": null, "organisme_payeur": null, "numero_allocataire": null}, "nom_epoux_ou_usage": null, "enfant_secu_sociale": null, "departement_naissance": null} ["sexe", "prenoms", "nom_naissance"] f 2026-05-12 08:59:46.060748+00 2026-05-12 09:00:19.136801+00 +\. + + +-- +-- Data for Name: aidant; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.aidant (uuid_aidant) FROM stdin; +6 +7 +\. + + +-- +-- Data for Name: app_setting; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.app_setting (key, value, updated_at) FROM stdin; +site_name UMHMA 2026-05-12 09:14:53.213742+00 +contact_email contact@umhma.fr 2026-05-12 09:14:53.213742+00 +contact_phone 01 23 45 67 89 2026-05-12 09:14:53.213742+00 +contact_address Paris, France 2026-05-12 09:14:53.213742+00 +contact_hours Disponibles du lundi au vendredi, de 9h à 18h. 2026-05-12 09:14:53.213742+00 +timezone Europe/Paris 2026-05-12 09:14:53.213742+00 +language fr 2026-05-12 09:14:53.213742+00 +maintenance_mode false 2026-05-12 09:14:53.213742+00 +maintenance_message La plateforme est temporairement indisponible pour maintenance. Nous revenons très vite. 2026-05-12 09:14:53.213742+00 +\. + + +-- +-- Data for Name: audit_logs; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.audit_logs (uuid_audit_log, commentaire, uuid_user, uuid_type_action) FROM stdin; +1 Jeu de données de démonstration recréé pour la présentation finale. 1 1 +\. + + +-- +-- Data for Name: categoriser; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.categoriser (uuid_topic, uuid_thread) FROM stdin; +4 1 +2 2 +3 3 +\. + + +-- +-- Data for Name: eligibilite; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.eligibilite (uuid_eligibilite, lien_vers_aide) FROM stdin; +1 https://www.service-public.fr/particuliers/vosdroits/F12242 +2 https://www.monparcourshandicap.gouv.fr/aides/la-prestation-de-compensation-du-handicap-pch +\. + + +-- +-- Data for Name: est_eligible; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.est_eligible (uuid_patient, uuid_eligibilite) FROM stdin; +2 1 +3 2 +\. + + +-- +-- Data for Name: forum_post; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.forum_post (uuid_post, message_forum, date_publication, uuid_thread, uuid_user, created_at, updated_at, is_hidden) FROM stdin; +1 Je finalise mon renouvellement RQTH. Faut-il joindre les anciens courriers CDAPH ? 2026-05-04 1 2 2026-05-04 08:36:51.328+00 2026-05-04 08:36:51.328+00 f +2 Oui, c’est utile. Ajoutez aussi le certificat médical récent et les aménagements de poste. 2026-05-05 1 6 2026-05-05 08:36:51.328+00 2026-05-05 08:36:51.328+00 f +3 Pensez à vérifier que chaque document est lisible avant l’envoi. 2026-05-05 1 1 2026-05-05 08:36:51.328+00 2026-05-05 08:36:51.328+00 f +4 Je ne sais pas comment quantifier les aides pour les transferts et les repas. 2026-05-06 2 3 2026-05-06 08:36:51.364+00 2026-05-06 08:36:51.364+00 f +5 Découpez par moments de la journée et notez la durée approximative de chaque aide. 2026-05-07 2 7 2026-05-07 08:36:51.364+00 2026-05-07 08:36:51.364+00 f +6 Nous avons l’équipe éducative vendredi. Quels éléments mettre en avant ? 2026-05-10 3 5 2026-05-10 08:36:51.388+00 2026-05-10 08:36:51.388+00 f +7 Apportez les bilans, les adaptations déjà testées et les observations de l’enseignant. 2026-05-11 3 1 2026-05-11 08:36:51.388+00 2026-05-11 08:36:51.388+00 f +\. + + +-- +-- Data for Name: forum_report; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.forum_report (uuid_report, uuid_post, uuid_thread, uuid_reporter, target_type, reason, priority, status, resolution_note, resolved_by, resolved_at, created_at) FROM stdin; +\. + + +-- +-- Data for Name: forum_thread; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.forum_thread (uuid_thread, titre, category, excerpt, is_pinned, views, created_at, updated_at, uuid_user, owner_id, is_hidden) FROM stdin; +2 PCH aide humaine : comment décrire une journée type ? pch Méthode pour détailler les besoins du matin au soir. f 92 2026-05-06 08:36:51.364+00 2026-05-06 08:36:51.364+00 3 3 f +3 Préparer une demande AESH pour la rentrée scolarisation Retours d’expérience sur le PPS, les bilans et l’équipe éducative. f 75 2026-05-10 08:36:51.388+00 2026-05-10 08:36:51.388+00 5 5 f +1 Quels justificatifs joindre pour un renouvellement RQTH ? documents Liste des pièces utiles avant dépôt du dossier en ligne. t 130 2026-05-04 08:36:51.328+00 2026-05-04 08:36:51.328+00 2 2 f +\. + + +-- +-- Data for Name: forum_topic; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.forum_topic (uuid_topic, topic) FROM stdin; +1 Dossier MDPH +2 PCH +3 Scolarisation +4 Documents +5 Retour commission +\. + + +-- +-- Data for Name: gerer; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.gerer (uuid_aidant, uuid_dossier_mdph) FROM stdin; +6 2 +7 4 +\. + + +-- +-- Data for Name: mdph_document; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.mdph_document (uuid_document_mdph, chemin_vers_document, titre_document, uuid_section_mdph, user_id, titre, type, taille, status, created_at, updated_at) FROM stdin; +1 /demo/documents/claire-piece-identite.pdf Pièce identité Claire Martin 1 2 Pièce identité Claire Martin identite 420 Ko validated 2026-05-01 08:36:51.236+00 2026-05-01 08:36:51.236+00 +2 /demo/documents/claire-certificat-medical.pdf Certificat médical RQTH 2026 2 2 Certificat médical RQTH 2026 medical 1.2 Mo validated 2026-05-02 08:36:51.236+00 2026-05-02 08:36:51.236+00 +3 /demo/documents/nabil-devis-salle-bain.pdf Devis adaptation salle de bain 3 3 Devis adaptation salle de bain devis 860 Ko pending 2026-05-05 08:36:51.236+00 2026-05-05 08:36:51.236+00 +4 /demo/documents/nabil-projet-vie.pdf Projet de vie Nabil Benali 4 3 Projet de vie Nabil Benali projet_de_vie 310 Ko validated 2026-05-06 08:36:51.236+00 2026-05-06 08:36:51.236+00 +5 /demo/documents/sophie-compte-rendu.pdf Compte rendu mobilité 5 4 Compte rendu mobilité medical 740 Ko pending 2026-05-08 08:36:51.236+00 2026-05-08 08:36:51.236+00 +6 /demo/documents/emma-bilan-orthophonie.pdf Bilans orthophonie Emma 6 5 Bilans orthophonie Emma scolarite 1.6 Mo validated 2026-05-10 08:36:51.236+00 2026-05-10 08:36:51.236+00 +7 Liste-code-naf-ape.pdf Certificat médical \N 1 Certificat médical Certificat médical 0.07 MB pending 2026-05-19 07:31:01.766629+00 2026-05-19 07:31:01.766629+00 +\. + + +-- +-- Data for Name: mdph_dossier; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.mdph_dossier (uuid_dossier_mdph, uuid_patient, numero_dossier, titre, description, created_at, updated_at) FROM stdin; +1 2 MDPH-2026-000142 Renouvellement RQTH et AAH Dossier de renouvellement avec justificatifs médicaux et projet professionnel. 2026-04-30 08:36:51.026+00 2026-04-30 08:36:51.026+00 +2 3 MDPH-2026-000187 Première demande PCH domicile Evaluation du besoin d’aide humaine et adaptation du logement. 2026-05-02 08:36:51.034+00 2026-05-02 08:36:51.034+00 +3 4 MDPH-2026-000221 Carte mobilité inclusion Demande de CMI priorité et stationnement avec certificats récents. 2026-05-07 08:36:51.042+00 2026-05-07 08:36:51.042+00 +4 5 MDPH-2026-000248 Dossier enfant - PPS et AESH Préparation d’un projet personnalisé de scolarisation pour Emma. 2026-05-09 08:36:51.05+00 2026-05-09 08:36:51.05+00 +\. + + +-- +-- Data for Name: mdph_section; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.mdph_section (uuid_section_mdph, numero_section, uuid_dossier_mdph, titre, contenu, uuid_ai_session, created_at, updated_at) FROM stdin; +1 A1 1 Identité et situation administrative {"nom": "Martin", "prenom": "Claire", "commune": "Lyon", "situation": "Salariée à temps partiel thérapeutique"} \N 2026-05-01 08:36:51.074+00 2026-05-01 08:36:51.074+00 +4 F2 2 Projet de vie {"objectif": "Rester à domicile avec interventions planifiées", "priorite": "sécuriser les déplacements intérieurs"} \N 2026-05-04 08:36:51.102+00 2026-05-04 08:36:51.102+00 +5 E3 3 Mobilité et déplacements {"demande": "CMI priorité et stationnement", "limitation": "stations debout prolongées très difficiles"} \N 2026-05-08 08:36:51.11+00 2026-05-08 08:36:51.11+00 +2 B2 1 Vie professionnelle et RQTH {"poste": "Assistante administrative", "demande": "Renouvellement RQTH", "amenagements": ["horaires adaptés", "télétravail partiel"]} 1 2026-05-02 08:36:51.084+00 2026-05-02 08:36:51.084+00 +3 D1 2 Vie quotidienne et aide humaine {"besoin": "Aide aux transferts et préparation des repas", "logement": "Appartement avec salle de bain à adapter", "frequence": "quotidienne"} 2 2026-05-03 08:36:51.094+00 2026-05-03 08:36:51.094+00 +6 E1-enfant 4 Scolarisation et accompagnement {"besoin": "AESH mutualisée et adaptations pédagogiques", "classe": "CM1", "enfant": "Emma Rousseau"} 3 2026-05-10 08:36:51.118+00 2026-05-10 08:36:51.118+00 +7 start 1 \N {} \N 2026-05-12 08:59:08.707576+00 2026-05-12 08:59:08.707576+00 +8 A 1 \N {"section_A/section_a1": {"sexe": 1, "prenoms": ["Yanis"], "nationalite": null, "secu_sociale": null, "nom_naissance": "Martin", "date_naissance": null, "pays_naissance": null, "commune_naissance": null, "complement_adresse": {"pays": null, "email": null, "adresse": null, "commune": null, "telephone": null, "code_postal": null, "moyen_contact": null, "nom_personne_heberge": null}, "date_arrive_france": null, "domicile_organisme": {"nom_organisme": null, "organisme_payeur": null, "numero_allocataire": null}, "nom_epoux_ou_usage": null, "enfant_secu_sociale": null, "departement_naissance": null}} \N 2026-05-12 08:59:45.963882+00 2026-05-12 09:00:19.201898+00 +\. + + +-- +-- Data for Name: patient; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.patient (uuid_patient) FROM stdin; +2 +3 +4 +5 +1 +\. + + +-- +-- Data for Name: proposer_suggestion; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.proposer_suggestion (uuid_patient, uuid_suggestion) FROM stdin; +2 1 +5 2 +\. + + +-- +-- Data for Name: rendez_vous; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.rendez_vous (uuid_rdv, uuid_patient, uuid_aidant, date_rdv) FROM stdin; +1 3 6 2026-05-18 +2 5 7 2026-05-22 +\. + + +-- +-- Data for Name: resource; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.resource (uuid_resource, type, icon, category, title, description, read_time, keywords, created_by, created_at, updated_at) FROM stdin; +1 guide file-text Dossier MDPH Checklist avant dépôt du dossier Les points à vérifier avant de transmettre un dossier à la MDPH. 6 min ["mdph", "checklist", "documents"] 1 2026-05-12 08:36:50.181152+00 2026-05-12 08:36:50.181152+00 +2 guide clipboard PCH Décrire les besoins d’aide humaine Un guide pratique pour formuler une journée type et les gestes concernés. 8 min ["pch", "aide humaine"] 1 2026-05-12 08:36:50.181152+00 2026-05-12 08:36:50.181152+00 +3 article briefcase Emploi RQTH et aménagement de poste Comprendre les informations utiles pour une demande ou un renouvellement RQTH. 5 min ["rqth", "emploi"] 1 2026-05-12 08:36:50.181152+00 2026-05-12 08:36:50.181152+00 +4 article school Scolarisation Préparer un PPS pour un enfant Documents et acteurs à mobiliser avant l’équipe éducative. 7 min ["pps", "aesh", "enfant"] 1 2026-05-12 08:36:50.181152+00 2026-05-12 08:36:50.181152+00 +5 video play-circle Documents Scanner correctement ses justificatifs Bonnes pratiques pour éviter les refus liés à des fichiers illisibles. 4 min ["scan", "pdf"] 1 2026-05-12 08:36:50.181152+00 2026-05-12 08:36:50.181152+00 +6 video message-circle IA UMHMA Utiliser l’assistant formulaire Démonstration du remplissage guidé des sections du dossier. 3 min ["assistant", "formulaire"] 1 2026-05-12 08:36:50.181152+00 2026-05-12 08:36:50.181152+00 +\. + + +-- +-- Data for Name: suggestion; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.suggestion (uuid_suggestion, suggestion_text) FROM stdin; +1 Ajouter un indicateur de complétude par section du dossier. +2 Prévoir un rappel automatique avant rendez-vous aidant. +\. + + +-- +-- Data for Name: type_action; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.type_action (uuid_type_action, nom_action) FROM stdin; +1 seed_demo_data +2 resource_created +\. + + +-- +-- Data for Name: user_account; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.user_account (uuid_user, email, email_normalized, name, created_at) FROM stdin; +1 admin@umhma.fr admin@umhma.fr Admin UMHMA 2026-04-22 08:36:50.388+00 +2 claire.martin.demo@umhma.fr claire.martin.demo@umhma.fr Claire Martin 2026-04-24 08:36:50.88+00 +3 nabil.benali.demo@umhma.fr nabil.benali.demo@umhma.fr Nabil Benali 2026-04-27 08:36:50.88+00 +4 sophie.leroy.demo@umhma.fr sophie.leroy.demo@umhma.fr Sophie Leroy 2026-05-01 08:36:50.88+00 +5 parent.emma.rousseau.demo@umhma.fr parent.emma.rousseau.demo@umhma.fr Julien Rousseau 2026-05-05 08:36:50.88+00 +6 marie.dubois.aidante.demo@umhma.fr marie.dubois.aidante.demo@umhma.fr Marie Dubois 2026-04-29 08:36:50.88+00 +7 karim.haddad.aidant.demo@umhma.fr karim.haddad.aidant.demo@umhma.fr Karim Haddad 2026-05-03 08:36:50.88+00 +\. + + +-- +-- Data for Name: user_security; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.user_security (uuid_user, mdp_hash, mdp_sel, lockout, mfa, date_creation, date_derniere_connection) FROM stdin; +1 $2a$10$nL1ZJnyCnRq.ybzcbM/W0.5psStzxRBA4ecG08EZ6VYAliiQH2aSO f \N 2026-04-22 2026-05-11 +2 $2a$12$CAV7/nwsBPC8g1N.MVtFdeMZLvTx78PQhQhcuS7On12Le7TwldzEy f \N 2026-04-24 2026-05-11 +3 $2a$12$CAV7/nwsBPC8g1N.MVtFdeMZLvTx78PQhQhcuS7On12Le7TwldzEy f \N 2026-04-27 2026-05-11 +4 $2a$12$CAV7/nwsBPC8g1N.MVtFdeMZLvTx78PQhQhcuS7On12Le7TwldzEy f \N 2026-05-01 2026-05-11 +5 $2a$12$CAV7/nwsBPC8g1N.MVtFdeMZLvTx78PQhQhcuS7On12Le7TwldzEy f \N 2026-05-05 2026-05-11 +6 $2a$12$CAV7/nwsBPC8g1N.MVtFdeMZLvTx78PQhQhcuS7On12Le7TwldzEy f \N 2026-04-29 2026-05-11 +7 $2a$12$CAV7/nwsBPC8g1N.MVtFdeMZLvTx78PQhQhcuS7On12Le7TwldzEy f \N 2026-05-03 2026-05-11 +\. + + +-- +-- Name: ai_message_uuid_ai_message_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.ai_message_uuid_ai_message_seq', 18, true); + + +-- +-- Name: ai_session_uuid_ai_session_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.ai_session_uuid_ai_session_seq', 5, true); + + +-- +-- Name: audit_logs_uuid_audit_log_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.audit_logs_uuid_audit_log_seq', 1, true); + + +-- +-- Name: eligibilite_uuid_eligibilite_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.eligibilite_uuid_eligibilite_seq', 2, true); + + +-- +-- Name: forum_post_uuid_post_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.forum_post_uuid_post_seq', 7, true); + + +-- +-- Name: forum_report_uuid_report_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.forum_report_uuid_report_seq', 1, false); + + +-- +-- Name: forum_thread_uuid_thread_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.forum_thread_uuid_thread_seq', 3, true); + + +-- +-- Name: forum_topic_uuid_topic_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.forum_topic_uuid_topic_seq', 5, true); + + +-- +-- Name: mdph_document_uuid_document_mdph_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.mdph_document_uuid_document_mdph_seq', 7, true); + + +-- +-- Name: mdph_dossier_uuid_dossier_mdph_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.mdph_dossier_uuid_dossier_mdph_seq', 4, true); + + +-- +-- Name: mdph_section_uuid_section_mdph_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.mdph_section_uuid_section_mdph_seq', 8, true); + + +-- +-- Name: rendez_vous_uuid_rdv_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.rendez_vous_uuid_rdv_seq', 2, true); + + +-- +-- Name: resource_uuid_resource_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.resource_uuid_resource_seq', 6, true); + + +-- +-- Name: suggestion_uuid_suggestion_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.suggestion_uuid_suggestion_seq', 2, true); + + +-- +-- Name: type_action_uuid_type_action_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.type_action_uuid_type_action_seq', 2, true); + + +-- +-- Name: user_account_uuid_user_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.user_account_uuid_user_seq', 7, true); + + +-- +-- Name: admin admin_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.admin + ADD CONSTRAINT admin_pkey PRIMARY KEY (uuid_admin); + + +-- +-- Name: ai_message ai_message_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.ai_message + ADD CONSTRAINT ai_message_pkey PRIMARY KEY (uuid_ai_message); + + +-- +-- Name: ai_session ai_session_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.ai_session + ADD CONSTRAINT ai_session_pkey PRIMARY KEY (uuid_ai_session); + + +-- +-- Name: aidant aidant_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aidant + ADD CONSTRAINT aidant_pkey PRIMARY KEY (uuid_aidant); + + +-- +-- Name: app_setting app_setting_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.app_setting + ADD CONSTRAINT app_setting_pkey PRIMARY KEY (key); + + +-- +-- Name: audit_logs audit_logs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.audit_logs + ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (uuid_audit_log); + + +-- +-- Name: categoriser categoriser_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.categoriser + ADD CONSTRAINT categoriser_pkey PRIMARY KEY (uuid_topic, uuid_thread); + + +-- +-- Name: eligibilite eligibilite_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.eligibilite + ADD CONSTRAINT eligibilite_pkey PRIMARY KEY (uuid_eligibilite); + + +-- +-- Name: est_eligible est_eligible_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.est_eligible + ADD CONSTRAINT est_eligible_pkey PRIMARY KEY (uuid_patient, uuid_eligibilite); + + +-- +-- Name: forum_post forum_post_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.forum_post + ADD CONSTRAINT forum_post_pkey PRIMARY KEY (uuid_post); + + +-- +-- Name: forum_report forum_report_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.forum_report + ADD CONSTRAINT forum_report_pkey PRIMARY KEY (uuid_report); + + +-- +-- Name: forum_thread forum_thread_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.forum_thread + ADD CONSTRAINT forum_thread_pkey PRIMARY KEY (uuid_thread); + + +-- +-- Name: forum_topic forum_topic_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.forum_topic + ADD CONSTRAINT forum_topic_pkey PRIMARY KEY (uuid_topic); + + +-- +-- Name: forum_topic forum_topic_topic_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.forum_topic + ADD CONSTRAINT forum_topic_topic_key UNIQUE (topic); + + +-- +-- Name: gerer gerer_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.gerer + ADD CONSTRAINT gerer_pkey PRIMARY KEY (uuid_aidant, uuid_dossier_mdph); + + +-- +-- Name: mdph_document mdph_document_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mdph_document + ADD CONSTRAINT mdph_document_pkey PRIMARY KEY (uuid_document_mdph); + + +-- +-- Name: mdph_dossier mdph_dossier_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mdph_dossier + ADD CONSTRAINT mdph_dossier_pkey PRIMARY KEY (uuid_dossier_mdph); + + +-- +-- Name: mdph_section mdph_section_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mdph_section + ADD CONSTRAINT mdph_section_pkey PRIMARY KEY (uuid_section_mdph); + + +-- +-- Name: patient patient_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.patient + ADD CONSTRAINT patient_pkey PRIMARY KEY (uuid_patient); + + +-- +-- Name: proposer_suggestion proposer_suggestion_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.proposer_suggestion + ADD CONSTRAINT proposer_suggestion_pkey PRIMARY KEY (uuid_patient, uuid_suggestion); + + +-- +-- Name: rendez_vous rendez_vous_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.rendez_vous + ADD CONSTRAINT rendez_vous_pkey PRIMARY KEY (uuid_rdv); + + +-- +-- Name: resource resource_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.resource + ADD CONSTRAINT resource_pkey PRIMARY KEY (uuid_resource); + + +-- +-- Name: suggestion suggestion_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.suggestion + ADD CONSTRAINT suggestion_pkey PRIMARY KEY (uuid_suggestion); + + +-- +-- Name: type_action type_action_nom_action_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.type_action + ADD CONSTRAINT type_action_nom_action_key UNIQUE (nom_action); + + +-- +-- Name: type_action type_action_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.type_action + ADD CONSTRAINT type_action_pkey PRIMARY KEY (uuid_type_action); + + +-- +-- Name: user_account user_account_email_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_account + ADD CONSTRAINT user_account_email_key UNIQUE (email); + + +-- +-- Name: user_account user_account_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_account + ADD CONSTRAINT user_account_pkey PRIMARY KEY (uuid_user); + + +-- +-- Name: user_security user_security_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_security + ADD CONSTRAINT user_security_pkey PRIMARY KEY (uuid_user); + + +-- +-- Name: idx_ai_message_session; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_ai_message_session ON public.ai_message USING btree (uuid_ai_session); + + +-- +-- Name: idx_ai_session_dossier; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_ai_session_dossier ON public.ai_session USING btree (uuid_dossier_mdph); + + +-- +-- Name: idx_forum_post_thread; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_forum_post_thread ON public.forum_post USING btree (uuid_thread); + + +-- +-- Name: idx_forum_post_user; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_forum_post_user ON public.forum_post USING btree (uuid_user); + + +-- +-- Name: idx_forum_report_post; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_forum_report_post ON public.forum_report USING btree (uuid_post); + + +-- +-- Name: idx_forum_report_status; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_forum_report_status ON public.forum_report USING btree (status); + + +-- +-- Name: idx_forum_thread_category; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_forum_thread_category ON public.forum_thread USING btree (category); + + +-- +-- Name: idx_forum_thread_owner; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_forum_thread_owner ON public.forum_thread USING btree (owner_id); + + +-- +-- Name: idx_forum_thread_user; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_forum_thread_user ON public.forum_thread USING btree (uuid_user); + + +-- +-- Name: idx_mdph_document_user; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_mdph_document_user ON public.mdph_document USING btree (user_id); + + +-- +-- Name: idx_mdph_section_dossier; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_mdph_section_dossier ON public.mdph_section USING btree (uuid_dossier_mdph); + + +-- +-- Name: idx_mdph_section_numero; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_mdph_section_numero ON public.mdph_section USING btree (numero_section); + + +-- +-- Name: idx_resource_type; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_resource_type ON public.resource USING btree (type); + + +-- +-- Name: idx_user_account_email_norm; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_user_account_email_norm ON public.user_account USING btree (email_normalized); + + +-- +-- Name: uq_ai_session_form; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uq_ai_session_form ON public.ai_session USING btree (uuid_section_mdph, sous_section, nom_formulaire) WHERE (sous_section IS NOT NULL); + + +-- +-- Name: uq_mdph_section_ai_session; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uq_mdph_section_ai_session ON public.mdph_section USING btree (uuid_ai_session) WHERE (uuid_ai_session IS NOT NULL); + + +-- +-- Name: uq_user_account_email_norm; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uq_user_account_email_norm ON public.user_account USING btree (email_normalized); + + +-- +-- Name: admin admin_uuid_admin_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.admin + ADD CONSTRAINT admin_uuid_admin_fkey FOREIGN KEY (uuid_admin) REFERENCES public.user_account(uuid_user); + + +-- +-- Name: ai_message ai_message_uuid_ai_session_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.ai_message + ADD CONSTRAINT ai_message_uuid_ai_session_fkey FOREIGN KEY (uuid_ai_session) REFERENCES public.ai_session(uuid_ai_session); + + +-- +-- Name: ai_session ai_session_uuid_patient_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.ai_session + ADD CONSTRAINT ai_session_uuid_patient_fkey FOREIGN KEY (uuid_patient) REFERENCES public.patient(uuid_patient); + + +-- +-- Name: ai_session ai_session_uuid_section_mdph_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.ai_session + ADD CONSTRAINT ai_session_uuid_section_mdph_fkey FOREIGN KEY (uuid_section_mdph) REFERENCES public.mdph_section(uuid_section_mdph); + + +-- +-- Name: aidant aidant_uuid_aidant_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aidant + ADD CONSTRAINT aidant_uuid_aidant_fkey FOREIGN KEY (uuid_aidant) REFERENCES public.user_account(uuid_user); + + +-- +-- Name: audit_logs audit_logs_uuid_type_action_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.audit_logs + ADD CONSTRAINT audit_logs_uuid_type_action_fkey FOREIGN KEY (uuid_type_action) REFERENCES public.type_action(uuid_type_action); + + +-- +-- Name: audit_logs audit_logs_uuid_user_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.audit_logs + ADD CONSTRAINT audit_logs_uuid_user_fkey FOREIGN KEY (uuid_user) REFERENCES public.user_account(uuid_user); + + +-- +-- Name: categoriser categoriser_uuid_thread_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.categoriser + ADD CONSTRAINT categoriser_uuid_thread_fkey FOREIGN KEY (uuid_thread) REFERENCES public.forum_thread(uuid_thread); + + +-- +-- Name: categoriser categoriser_uuid_topic_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.categoriser + ADD CONSTRAINT categoriser_uuid_topic_fkey FOREIGN KEY (uuid_topic) REFERENCES public.forum_topic(uuid_topic); + + +-- +-- Name: est_eligible est_eligible_uuid_eligibilite_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.est_eligible + ADD CONSTRAINT est_eligible_uuid_eligibilite_fkey FOREIGN KEY (uuid_eligibilite) REFERENCES public.eligibilite(uuid_eligibilite); + + +-- +-- Name: est_eligible est_eligible_uuid_patient_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.est_eligible + ADD CONSTRAINT est_eligible_uuid_patient_fkey FOREIGN KEY (uuid_patient) REFERENCES public.patient(uuid_patient); + + +-- +-- Name: forum_post forum_post_uuid_thread_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.forum_post + ADD CONSTRAINT forum_post_uuid_thread_fkey FOREIGN KEY (uuid_thread) REFERENCES public.forum_thread(uuid_thread); + + +-- +-- Name: forum_post forum_post_uuid_user_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.forum_post + ADD CONSTRAINT forum_post_uuid_user_fkey FOREIGN KEY (uuid_user) REFERENCES public.user_account(uuid_user); + + +-- +-- Name: gerer gerer_uuid_aidant_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.gerer + ADD CONSTRAINT gerer_uuid_aidant_fkey FOREIGN KEY (uuid_aidant) REFERENCES public.aidant(uuid_aidant); + + +-- +-- Name: gerer gerer_uuid_dossier_mdph_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.gerer + ADD CONSTRAINT gerer_uuid_dossier_mdph_fkey FOREIGN KEY (uuid_dossier_mdph) REFERENCES public.mdph_dossier(uuid_dossier_mdph); + + +-- +-- Name: mdph_document mdph_document_uuid_section_mdph_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mdph_document + ADD CONSTRAINT mdph_document_uuid_section_mdph_fkey FOREIGN KEY (uuid_section_mdph) REFERENCES public.mdph_section(uuid_section_mdph); + + +-- +-- Name: mdph_dossier mdph_dossier_uuid_patient_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mdph_dossier + ADD CONSTRAINT mdph_dossier_uuid_patient_fkey FOREIGN KEY (uuid_patient) REFERENCES public.patient(uuid_patient); + + +-- +-- Name: mdph_section mdph_section_uuid_dossier_mdph_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mdph_section + ADD CONSTRAINT mdph_section_uuid_dossier_mdph_fkey FOREIGN KEY (uuid_dossier_mdph) REFERENCES public.mdph_dossier(uuid_dossier_mdph); + + +-- +-- Name: patient patient_uuid_patient_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.patient + ADD CONSTRAINT patient_uuid_patient_fkey FOREIGN KEY (uuid_patient) REFERENCES public.user_account(uuid_user); + + +-- +-- Name: proposer_suggestion proposer_suggestion_uuid_patient_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.proposer_suggestion + ADD CONSTRAINT proposer_suggestion_uuid_patient_fkey FOREIGN KEY (uuid_patient) REFERENCES public.patient(uuid_patient); + + +-- +-- Name: proposer_suggestion proposer_suggestion_uuid_suggestion_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.proposer_suggestion + ADD CONSTRAINT proposer_suggestion_uuid_suggestion_fkey FOREIGN KEY (uuid_suggestion) REFERENCES public.suggestion(uuid_suggestion); + + +-- +-- Name: rendez_vous rendez_vous_uuid_aidant_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.rendez_vous + ADD CONSTRAINT rendez_vous_uuid_aidant_fkey FOREIGN KEY (uuid_aidant) REFERENCES public.aidant(uuid_aidant); + + +-- +-- Name: rendez_vous rendez_vous_uuid_patient_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.rendez_vous + ADD CONSTRAINT rendez_vous_uuid_patient_fkey FOREIGN KEY (uuid_patient) REFERENCES public.patient(uuid_patient); + + +-- +-- Name: user_security user_security_uuid_user_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_security + ADD CONSTRAINT user_security_uuid_user_fkey FOREIGN KEY (uuid_user) REFERENCES public.user_account(uuid_user); + + +-- +-- Name: SCHEMA public; Type: ACL; Schema: -; Owner: - +-- + +REVOKE USAGE ON SCHEMA public FROM PUBLIC; + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict HJcEFrNAxy2cnQz42G9JestDL3b0jOZZdXe63smWe1OPYKyuM2SxTsBdbVkkS5b + diff --git a/docs/rapport-compatibilite-bdd.md b/docs/rapport-compatibilite-bdd.md new file mode 100644 index 0000000..d11a420 --- /dev/null +++ b/docs/rapport-compatibilite-bdd.md @@ -0,0 +1,123 @@ +# Rapport de compatibilité — nouvelle BDD vs code backend + +**Date** : 26 mai 2026 +**Cible** : AlwaysData `projectlab_projectlabumhma` +**Backup** : `backups/alwaysdata_avant_test_nouvelle_bdd_*.sql` + +## Résumé + +| Verdict | Détail | +|---------|--------| +| **Schéma déployé** | OK (avec correctifs `umhma_script_fixes_post_creation.sql`) | +| **Données de test** | OK (`umhma_script_seed_test.sql`) | +| **Auth / admin / dossiers** | OK (requêtes SQL alignées sur le nouveau schéma) | +| **Rôles PostgreSQL + RLS** | **BLOQUÉ** sur AlwaysData (pas de droit `CREATEROLE`) | +| **Chatbot** | **KO** (`::int` dans le code vs `UUID` en base) | +| **Démarrage API complète** | **KO** (module manquant `src/services/dbInit.js`) | + +**Recommandation** : la nouvelle structure SQL est utilisable pour l’auth, les dossiers et l’admin **sans RLS** tant que les rôles PG ne sont pas créés par l’hébergeur. Pour la prod sécurisée (RLS), demander à AlwaysData la création des rôles `patient`, `aidant`, `gerant` ou migrer vers AWS RDS. Corriger le chatbot (`::int` → UUID) avant de valider cette partie. + +--- + +## Ce qui a été exécuté + +1. Sauvegarde `pg_dump` avant modification. +2. `DROP SCHEMA public CASCADE` + [`umhma_script_creation_bdd.sql`](../backend/scripts_SQL/umhma_script_creation_bdd.sql). +3. Correctifs : [`umhma_script_fixes_post_creation.sql`](../backend/scripts_SQL/umhma_script_fixes_post_creation.sql) (`admin`, `user_id`, `uuid_mdph_section` sur documents). +4. Tentative scripts rôles → **échec** `permission denied to create role`. +5. Seed : [`umhma_script_seed_test.sql`](../backend/scripts_SQL/umhma_script_seed_test.sql). +6. Tests : [`scripts/test-compat-bdd.js`](../backend/scripts/test-compat-bdd.js). + +### Erreur connue à la création + +``` +umhma_script_creation_bdd.sql:385 — ERROR: column "user_id" does not exist +``` + +Corrigé par `umhma_script_fixes_post_creation.sql`. + +--- + +## Comptes de test (mot de passe : `TestPass@1234`) + +| Email | Rôles métier | Admin UI | +|-------|---------------|----------| +| `patient.test@umhma.fr` | patient | non | +| `aidant.test@umhma.fr` | aidant | non | +| `gerant.test@umhma.fr` | gerant + admin | oui | + +--- + +## Matrice compatibilité + +| Zone | Statut | Cause | +|------|--------|-------| +| Tables / colonnes dossiers MDPH | OK | `uuid_mdph_dossier`, `uuid_mdph_section`, JSONB `contenu` | +| Table `admin` | OK | Ajoutée via fix (absente du script de création) | +| Login + `is_admin` | OK | Jointure `admin` fonctionne | +| `fetchUserRoles` (gerant/patient/aidant) | OK | Tables métier présentes | +| `GET /dossiers` (SQL équivalent) | OK | 1 dossier pour patient seed | +| `mdph_document.user_id` | OK | Colonne ajoutée via fix | +| Rôles PG `patient` / `aidant` / `gerant` | **KO** | AlwaysData : `permission denied to create role` | +| Policies RLS | **KO** | 0 politique (rôles non créés) | +| `SET ROLE` | **KO** | `role "patient" does not exist` | +| Chatbot (`::int` sur UUID) | **KO** | `operator does not exist: uuid = integer` | +| Smoke API Express | **KO** | `Cannot find module '../services/dbInit'` | +| `POST /signup` via app | Non testé | Bloqué par module manquant ; SQL signup OK avec paramètres typés | + +--- + +## Limitation AlwaysData + +L’utilisateur `projectlab` **ne possède pas** l’attribut `CREATEROLE`. Les scripts : + +- `umhma_script_role_gerant.sql` +- `umhma_script_role_aidant.sql` +- `umhma_script_role_patient.sql` + +ne peuvent pas être appliqués sans intervention AlwaysData (interface d’administration ou ticket support). + +**Impact** : le code [`roles.js`](../backend/src/middleware/roles.js) (`SET ROLE` + `app.current_user_uuid`) ne peut pas être validé sur cet hébergement tel quel. En tant que propriétaire des tables, `projectlab` **contourne** le RLS (pas de `FORCE ROW LEVEL SECURITY`), donc l’app peut sembler fonctionner sans isolation réelle entre utilisateurs. + +--- + +## Correctifs recommandés + +### SQL ([`umhma_script_creation_bdd.sql`](../backend/scripts_SQL/umhma_script_creation_bdd.sql)) + +- Intégrer dans le script principal : table `admin`, colonnes `mdph_document.user_id` et `uuid_mdph_section`. +- Supprimer ou conditionner l’index `idx_mdph_document_user` avant création de `user_id`. + +### Code backend + +1. **Chatbot** — [`chatbotController.js`](../backend/src/controllers/chatbotController.js) : retirer les casts `::int` ; traiter les UUID comme strings/UUID. +2. **dbInit** — créer [`src/services/dbInit.js`](../backend/src/services/dbInit.js) ou retirer l’import dans [`settingsController.js`](../backend/src/controllers/settingsController.js) pour permettre le démarrage de l’API. +3. **RLS** — une fois les rôles créés sur l’hébergeur : `GRANT patient, aidant, gerant TO projectlab` + parameter `app.current_user_uuid`. + +### Hébergement + +- Demander à AlwaysData la création des rôles NOLOGIN + exécution des scripts de policies, **ou** +- Tester la stack RLS complète sur AWS RDS (comme prévu dans [`database.js`](../backend/src/config/database.js)). + +--- + +## Rollback + +```bash +export DATABASE_URL='postgres://projectlab:***@postgresql-projectlab.alwaysdata.net:5432/projectlab_projectlabumhma' +export PGSSLMODE=require + +psql "$DATABASE_URL" -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO projectlab;" +psql "$DATABASE_URL" -f backups/alwaysdata_avant_test_nouvelle_bdd_YYYYMMDD_HHMM.sql +``` + +Remplacer le fichier backup par celui généré dans `backups/`. + +--- + +## Fichiers créés pendant le test + +- [`backend/scripts_SQL/umhma_script_fixes_post_creation.sql`](../backend/scripts_SQL/umhma_script_fixes_post_creation.sql) +- [`backend/scripts_SQL/umhma_script_seed_test.sql`](../backend/scripts_SQL/umhma_script_seed_test.sql) +- [`backend/scripts/test-compat-bdd.js`](../backend/scripts/test-compat-bdd.js) +- [`backend/scripts/test-api-alwaysdata.js`](../backend/scripts/test-api-alwaysdata.js) (nécessite correction `dbInit`) diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 0000000..a4fcaaf --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +test-results/ +playwright-report/ +blob-report/ +.playwright/ diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 0000000..72367c6 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,55 @@ +# Tests E2E (Playwright) + +Tests contre **l’API réelle** (AWS RDS) et, optionnellement, l’interface React. + +## Prérequis + +- `aws configure` (token IAM RDS pour `app_umhma`) +- `backend/.env` renseigné +- `cd e2e && npm install && npx playwright install chromium` + +## Lancer les tests API (recommandé) + +Démarre le backend automatiquement : + +```bash +cd e2e +npm run test:api +``` + +Parcours couverts : health, inscription, login, **upload document + OCR Mistral**. + +## Lancer les tests UI + +Dans **deux terminaux** : + +```bash +# Terminal 1 — backend (ou laisser Playwright le démarrer pour test:api) +cd backend && npm run dev + +# Terminal 2 — frontend (même hôte 127.0.0.1 que l’API pour les cookies CSRF) +cd frontend +HOST=127.0.0.1 BROWSER=none CI=true REACT_APP_API_URL=http://127.0.0.1:4000 npm start +``` + +Puis : + +```bash +cd e2e +npm run test:ui +``` + +## Variables utiles + +| Variable | Défaut | Rôle | +|----------|--------|------| +| `E2E_API_URL` | `http://127.0.0.1:4000` | URL API | +| `E2E_BASE_URL` | `http://127.0.0.1:3000` | URL frontend (tests UI) | +| `E2E_REQUIRE_OCR=1` | — | Échoue si l’OCR n’est pas `completed` | +| `CORS_ALLOWED_ORIGINS` | (auto en test) | Origines autorisées pour l’API | + +## Rapport + +```bash +npm run report +``` diff --git a/e2e/fixtures/tiny.png b/e2e/fixtures/tiny.png new file mode 100644 index 0000000000000000000000000000000000000000..f37764b1f7606623616dcdc169cc858273ea2d94 GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k92}1TpU9xZYBRYe;|OLfu)tPp=D){ QB2a?C)78&qol`;+0Lr!y6951J literal 0 HcmV?d00001 diff --git a/e2e/helpers/api-client.ts b/e2e/helpers/api-client.ts new file mode 100644 index 0000000..dfc39ba --- /dev/null +++ b/e2e/helpers/api-client.ts @@ -0,0 +1,42 @@ +import type { APIRequestContext, APIResponse } from '@playwright/test'; + +export const STRONG_PASSWORD = 'E2eTestPass@2026!'; + +/** Doit correspondre à CORS_ALLOWED_ORIGINS du backend pendant les tests E2E. */ +export const E2E_ORIGIN = process.env.E2E_ORIGIN || 'http://127.0.0.1:3000'; + +export async function getCsrfToken(request: APIRequestContext): Promise { + const res = await request.get('/csrf-token'); + if (!res.ok()) { + throw new Error(`Impossible d'obtenir le token CSRF (${res.status()})`); + } + const data = await res.json(); + if (!data?.csrfToken) { + throw new Error('Réponse /csrf-token sans csrfToken'); + } + return data.csrfToken as string; +} + +export async function apiPost( + request: APIRequestContext, + path: string, + body: Record, +): Promise { + const csrfToken = await getCsrfToken(request); + return request.post(path, { + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken, + Origin: E2E_ORIGIN, + }, + data: body, + }); +} + +export async function apiGet(request: APIRequestContext, path: string): Promise { + return request.get(path); +} + +export function uniqueEmail(prefix = 'e2e'): string { + return `${prefix}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}@umhma-e2e.test`; +} diff --git a/e2e/package-lock.json b/e2e/package-lock.json new file mode 100644 index 0000000..46179d7 --- /dev/null +++ b/e2e/package-lock.json @@ -0,0 +1,90 @@ +{ + "name": "umhma-e2e", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "umhma-e2e", + "devDependencies": { + "@playwright/test": "^1.52.0", + "dotenv": "^16.4.5" + } + }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 0000000..40a11f6 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,14 @@ +{ + "name": "umhma-e2e", + "private": true, + "scripts": { + "test": "playwright test", + "test:api": "playwright test --project=api", + "test:ui": "playwright test --project=chromium", + "report": "playwright show-report" + }, + "devDependencies": { + "@playwright/test": "^1.52.0", + "dotenv": "^16.4.5" + } +} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 0000000..f18b245 --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,50 @@ +import { defineConfig, devices } from '@playwright/test'; +import path from 'path'; +import dotenv from 'dotenv'; + +dotenv.config({ path: path.join(__dirname, '../backend/.env') }); + +// Même hôte (127.0.0.1) pour API + UI : cookies CSRF et CORS cohérents (localhost ≠ 127.0.0.1) +const API_URL = process.env.E2E_API_URL || 'http://127.0.0.1:4000'; +const WEB_URL = process.env.E2E_BASE_URL || 'http://127.0.0.1:3000'; +const E2E_CORS_ORIGINS = process.env.CORS_ALLOWED_ORIGINS || `${WEB_URL},http://localhost:3000`; + +export default defineConfig({ + testDir: './tests', + timeout: 120_000, + expect: { timeout: 20_000 }, + fullyParallel: false, + workers: 1, + retries: process.env.CI ? 1 : 0, + reporter: [['list'], ['html', { open: 'never' }]], + use: { + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + }, + projects: [ + { + name: 'api', + testMatch: /api-.*\.spec\.ts/, + use: { baseURL: API_URL }, + }, + { + name: 'chromium', + testMatch: /ui-.*\.spec\.ts/, + use: { + ...devices['Desktop Chrome'], + baseURL: WEB_URL, + }, + }, + ], + webServer: { + command: 'node src/server.js', + cwd: path.join(__dirname, '../backend'), + url: `${API_URL}/health`, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + env: { + ...process.env, + CORS_ALLOWED_ORIGINS: E2E_CORS_ORIGINS, + }, + }, +}); diff --git a/e2e/tests/api-auth-documents.spec.ts b/e2e/tests/api-auth-documents.spec.ts new file mode 100644 index 0000000..e578020 --- /dev/null +++ b/e2e/tests/api-auth-documents.spec.ts @@ -0,0 +1,91 @@ +import { test, expect } from '@playwright/test'; +import fs from 'fs'; +import path from 'path'; +import { + apiGet, + apiPost, + getCsrfToken, + STRONG_PASSWORD, + uniqueEmail, + E2E_ORIGIN, +} from '../helpers/api-client'; + +const FIXTURE_PNG = path.join(__dirname, '../fixtures/tiny.png'); + +test.describe('API E2E (AWS RDS)', () => { + test('health + inscription + login + document avec OCR', async ({ request }) => { + const health = await request.get('/health'); + expect(health.ok()).toBeTruthy(); + + const email = uniqueEmail(); + const name = 'Utilisateur E2E'; + + const signupRes = await apiPost(request, '/signup', { + name, + email, + password: STRONG_PASSWORD, + }); + expect(signupRes.status(), await signupRes.text()).toBe(201); + const signupBody = await signupRes.json(); + expect(signupBody.user?.email).toBeTruthy(); + + const meAfterSignup = await apiGet(request, '/me'); + expect(meAfterSignup.ok()).toBeTruthy(); + + const pngBuffer = fs.readFileSync(FIXTURE_PNG); + const fileBase64 = `data:image/png;base64,${pngBuffer.toString('base64')}`; + + const docRes = await apiPost(request, '/documents', { + titre: 'Document E2E OCR', + type: 'Autre', + chemin_vers_document: 'tiny.png', + taille: `${pngBuffer.length} o`, + status: 'pending', + file_mime_type: 'image/png', + file_base64: fileBase64, + }); + + expect(docRes.status(), await docRes.text()).toBe(201); + const docBody = await docRes.json(); + expect(docBody.document?.id).toBeTruthy(); + expect(['completed', 'failed', 'skipped']).toContain(docBody.ocr_status); + + if (process.env.E2E_REQUIRE_OCR === '1') { + expect(docBody.ocr_status).toBe('completed'); + expect(docBody.document?.contenu_ocr_json?.texte_complet ?? docBody.document?.contenu_ocr_json?.pages).toBeTruthy(); + } + + const listRes = await apiGet(request, '/documents'); + expect(listRes.ok()).toBeTruthy(); + const listBody = await listRes.json(); + expect(listBody.documents?.length).toBeGreaterThanOrEqual(1); + }); + + test('login avec compte existant', async ({ playwright }) => { + const baseURL = process.env.E2E_API_URL || 'http://127.0.0.1:4000'; + const email = uniqueEmail('login'); + + const setup = await playwright.request.newContext({ baseURL }); + const signupRes = await apiPost(setup, '/signup', { + name: 'Login E2E', + email, + password: STRONG_PASSWORD, + }); + expect(signupRes.status()).toBe(201); + await setup.dispose(); + + const client = await playwright.request.newContext({ baseURL }); + const csrf = await getCsrfToken(client); + const loginRes = await client.post('/login', { + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrf, + Origin: E2E_ORIGIN, + }, + data: { email, password: STRONG_PASSWORD }, + }); + expect(loginRes.status()).toBe(200); + expect((await client.get('/me')).ok()).toBeTruthy(); + await client.dispose(); + }); +}); diff --git a/e2e/tests/ui-auth-documents.spec.ts b/e2e/tests/ui-auth-documents.spec.ts new file mode 100644 index 0000000..1c30a3e --- /dev/null +++ b/e2e/tests/ui-auth-documents.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; +import { STRONG_PASSWORD, uniqueEmail } from '../helpers/api-client'; + +const FIXTURE_PNG = path.join(__dirname, '../fixtures/tiny.png'); + +test.describe('UI E2E', () => { + test.beforeAll(async () => { + const base = process.env.E2E_BASE_URL || 'http://127.0.0.1:3000'; + try { + const res = await fetch(base, { signal: AbortSignal.timeout(3000) }); + if (!res.ok) throw new Error(String(res.status)); + } catch { + test.skip( + true, + `Frontend indisponible sur ${base}. Lancez : cd frontend && HOST=127.0.0.1 BROWSER=none CI=true REACT_APP_API_URL=http://127.0.0.1:4000 npm start`, + ); + } + }); + + test('inscription, dashboard, ajout document', async ({ page }) => { + const email = uniqueEmail('ui'); + + await page.goto('/login'); + await page.getByRole('button', { name: "S'inscrire" }).click(); + + await page.locator('#name').fill('Patient UI E2E'); + await page.locator('#email').fill(email); + await page.locator('#password').fill(STRONG_PASSWORD); + await page.locator('#confirmPassword').fill(STRONG_PASSWORD); + await page.getByRole('button', { name: 'Créer mon compte' }).click(); + + await expect(page).toHaveURL(/\/dashboard/, { timeout: 30_000 }); + + await page.goto('/documents/ajouter'); + await expect(page.getByRole('heading', { name: 'Ajouter un document' })).toBeVisible(); + + await page.locator('#file-upload').setInputFiles(FIXTURE_PNG); + await expect(page.getByText('Fichiers sélectionnés (1)')).toBeVisible(); + + await page.locator('#name').fill('Certificat E2E'); + await page.locator('#type').selectOption('medical'); + await page.getByRole('button', { name: /Télécharger/ }).click(); + + await expect(page).toHaveURL(/\/documents/, { timeout: 120_000 }); + await expect(page.getByText(/Certificat E2E|tiny\.png/i).first()).toBeVisible({ timeout: 15_000 }); + }); +}); diff --git a/frontend/src/pages/AddDocument.js b/frontend/src/pages/AddDocument.js index 0d7333a..299ed4c 100644 --- a/frontend/src/pages/AddDocument.js +++ b/frontend/src/pages/AddDocument.js @@ -86,6 +86,14 @@ const AddDocument = () => { setFiles(files.filter((_, i) => i !== index)); }; + const readFileAsBase64 = (file) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = () => reject(new Error("Lecture du fichier impossible.")); + reader.readAsDataURL(file); + }); + const handleSubmit = async (e) => { e.preventDefault(); @@ -96,12 +104,15 @@ const AddDocument = () => { setError(""); const normalizedType = documentTypes.find((t) => t.id === formData.type)?.label || "Autre"; for (const file of files) { + const fileBase64 = file.file ? await readFileAsBase64(file.file) : null; await createDocument({ titre: formData.name || file.name, type: normalizedType, chemin_vers_document: file.name, taille: file.size, status: "pending", + file_mime_type: file.file?.type || null, + file_base64: fileBase64, }); } navigate("/documents"); From 708d8c9c2d006dc00a69fb0c55d8f9b173cbf685 Mon Sep 17 00:00:00 2001 From: Yanis MERABET <78480186+Yayabtw@users.noreply.github.com> Date: Tue, 26 May 2026 07:27:22 +0200 Subject: [PATCH 6/6] feat: enhance authentication and moderation features - Streamlined the signup process to automatically create a patient profile upon user registration. - Updated the login functionality to record the last connection date for users. - Introduced a new utility for managing report metadata, allowing for better handling of forum reports. - Added validation for report creation, ensuring proper UUID formats for posts and threads. - Enhanced the moderation controller to support JSON-based resolution notes for reports. - Implemented new routes for reporting content in the forum, improving user interaction with moderation features. - Updated tests to cover new functionalities and ensure robust integration with the database schema. --- backend/src/controllers/authController.js | 40 +-- .../src/controllers/moderationController.js | 137 ++++++++-- backend/src/routes/forum.js | 8 + backend/src/utils/forumReportMeta.js | 33 +++ backend/tests/auth.test.js | 106 ++++---- backend/tests/forumReportMeta.test.js | 14 + backend/tests/helpers/rdsMemSchema.js | 209 +++++++++++++++ backend/tests/helpers/testHttp.js | 59 ++++ backend/tests/solution.integration.test.js | 253 ++++++++++++++++++ 9 files changed, 756 insertions(+), 103 deletions(-) create mode 100644 backend/src/utils/forumReportMeta.js create mode 100644 backend/tests/forumReportMeta.test.js create mode 100644 backend/tests/helpers/rdsMemSchema.js create mode 100644 backend/tests/helpers/testHttp.js create mode 100644 backend/tests/solution.integration.test.js diff --git a/backend/src/controllers/authController.js b/backend/src/controllers/authController.js index 2f02e52..6410a6f 100644 --- a/backend/src/controllers/authController.js +++ b/backend/src/controllers/authController.js @@ -84,18 +84,12 @@ async function signup(pool, jwtSecret) { [newUserId, hashedPassword, '', false], ); - // Synchronise le profil métier patient si la table existe. - // Certaines fonctionnalités (dossiers, documents, chatbot) reposent dessus. - try { - await pool.query( - `INSERT INTO patient (uuid_patient) - VALUES ($1) - ON CONFLICT (uuid_patient) DO NOTHING`, - [newUserId], - ); - } catch (_patientSyncErr) { - // Ne bloque pas l'inscription si le schéma patient diffère localement. - } + await pool.query( + `INSERT INTO patient (uuid_patient) + VALUES ($1) + ON CONFLICT (uuid_patient) DO NOTHING`, + [newUserId], + ); const token = jwt.sign( { sub: newUserId, email: emailValue, name: nameValue, isAdmin: false }, @@ -158,6 +152,14 @@ async function login(pool, jwtSecret) { } registerSuccessfulLogin(normalizedEmail); + + await pool.query( + `UPDATE user_security + SET date_derniere_connection = CURRENT_TIMESTAMP + WHERE uuid_user = $1`, + [user.id], + ); + const isAdmin = Boolean(user.in_admin_table); const token = jwt.sign( @@ -187,12 +189,14 @@ function me(pool) { const { rows } = await pool.query( `SELECT - ua.uuid_user AS id, - ua.email, - ua.name, - EXISTS (SELECT 1 FROM admin a WHERE a.uuid_admin = ua.uuid_user) AS is_admin - FROM user_account ua - WHERE ua.uuid_user = $1 + uuid_user AS id, + email, + name, + EXISTS ( + SELECT 1 FROM admin WHERE uuid_admin = $1 + ) AS is_admin + FROM user_account + WHERE uuid_user = $1 LIMIT 1`, [req.user.id], ); diff --git a/backend/src/controllers/moderationController.js b/backend/src/controllers/moderationController.js index 2123216..c0e9c9a 100644 --- a/backend/src/controllers/moderationController.js +++ b/backend/src/controllers/moderationController.js @@ -1,11 +1,11 @@ /** * Modération : gestion des signalements de contenu (forum_report). - * - * Toutes les actions sont admin-only. Les signalements peuvent être - * créés par n'importe quel utilisateur authentifié, mais leur cycle de - * vie (résolution, masquage du contenu) est piloté par les admins. + * RDS : colonnes uuid_post/uuid_thread/uuid_reporter en INTEGER (legacy) ; + * les UUID réels sont stockés dans resolution_note (JSON, voir forumReportMeta). */ +const { buildReportMeta, parseReportMeta } = require('../utils/forumReportMeta'); + const ALLOWED_REASONS = new Set([ 'spam', 'harcelement', @@ -29,12 +29,18 @@ const PRIORITY_LABELS = { high: 'Haute', }; +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +function isUuid(value) { + return typeof value === 'string' && UUID_RE.test(value); +} + function decorate(rows) { return rows.map((row) => ({ id: row.id, target_type: row.target_type, - post_id: row.uuid_post, - thread_id: row.uuid_thread, + post_id: row.resolved_post_id || row.uuid_post, + thread_id: row.resolved_thread_id || row.uuid_thread, target_excerpt: (row.post_content || row.thread_title || '').slice(0, 140), thread_title: row.thread_title, reason_code: row.reason, @@ -76,6 +82,21 @@ function listReports(pool) { r.resolution_note, r.created_at, r.resolved_at, + CASE + WHEN r.resolution_note IS NOT NULL + AND r.resolution_note LIKE '{%' + THEN (r.resolution_note::jsonb->>'post_id') + ELSE NULL + END AS resolved_post_id, + CASE + WHEN r.resolution_note IS NOT NULL + AND r.resolution_note LIKE '{%' + THEN COALESCE( + r.resolution_note::jsonb->>'thread_id', + NULL + ) + ELSE NULL + END AS resolved_thread_id, p.message_forum AS post_content, p.is_hidden AS post_hidden, t.titre AS thread_title, @@ -83,9 +104,28 @@ function listReports(pool) { ua.email AS reporter_email, COALESCE(p.is_hidden, t.is_hidden) AS is_hidden FROM forum_report r - LEFT JOIN forum_post p ON p.uuid_post = r.uuid_post - LEFT JOIN forum_thread t ON t.uuid_thread = COALESCE(r.uuid_thread, p.uuid_thread) - LEFT JOIN user_account ua ON ua.uuid_user = r.uuid_reporter + LEFT JOIN forum_post p ON p.uuid_post = ( + CASE + WHEN r.resolution_note IS NOT NULL AND r.resolution_note LIKE '{%' + THEN (r.resolution_note::jsonb->>'post_id')::uuid + ELSE NULL + END + ) + LEFT JOIN forum_thread t ON t.uuid_thread = COALESCE( + CASE + WHEN r.resolution_note IS NOT NULL AND r.resolution_note LIKE '{%' + THEN (r.resolution_note::jsonb->>'thread_id')::uuid + ELSE NULL + END, + p.uuid_thread + ) + LEFT JOIN user_account ua ON ua.uuid_user = ( + CASE + WHEN r.resolution_note IS NOT NULL AND r.resolution_note LIKE '{%' + THEN (r.resolution_note::jsonb->>'reporter_id')::uuid + ELSE NULL + END + ) ${where} ORDER BY r.created_at DESC LIMIT 200`, @@ -139,26 +179,41 @@ function resolveReport(pool, action) { const report = await fetchReport(pool, id); if (!report) return res.status(404).json({ message: 'Signalement introuvable.' }); - const adminId = req.user?.id || null; const note = (req.body && req.body.note) ? String(req.body.note).slice(0, 500) : null; const status = action === 'dismiss' ? 'dismissed' : 'resolved'; + const meta = parseReportMeta(report.resolution_note); if (action === 'hide') { - if (report.uuid_post) { - await pool.query(`UPDATE forum_post SET is_hidden = TRUE WHERE uuid_post = $1`, [report.uuid_post]); - } else if (report.uuid_thread) { - await pool.query(`UPDATE forum_thread SET is_hidden = TRUE WHERE uuid_thread = $1`, [report.uuid_thread]); + if (meta.post_id && isUuid(meta.post_id)) { + await pool.query(`UPDATE forum_post SET is_hidden = TRUE WHERE uuid_post = $1`, [meta.post_id]); + } else if (meta.thread_id && isUuid(meta.thread_id)) { + await pool.query(`UPDATE forum_thread SET is_hidden = TRUE WHERE uuid_thread = $1`, [meta.thread_id]); + } + } + + let resolutionNote = report.resolution_note; + if (note) { + try { + const parsed = JSON.parse(report.resolution_note || '{}'); + if (parsed && parsed.v === 1) { + parsed.admin_note = note; + resolutionNote = JSON.stringify(parsed); + } else { + resolutionNote = note; + } + } catch (_parseErr) { + resolutionNote = note; } } await pool.query( `UPDATE forum_report SET status = $2, - resolution_note = COALESCE($3, resolution_note), - resolved_by = $4, + resolution_note = $3, + resolved_by = NULL, resolved_at = NOW() WHERE uuid_report = $1`, - [id, status, note, adminId], + [id, status, resolutionNote], ); return res.json({ message: 'Signalement mis à jour.', id, status }); @@ -184,24 +239,54 @@ function createReport(pool) { ? String(priority) : 'medium'; - const postIdInt = postId != null ? Number(postId) : null; - const threadIdInt = threadId != null ? Number(threadId) : null; - if (!postIdInt && !threadIdInt) { + const postUuid = postId != null ? String(postId) : null; + const threadUuid = threadId != null ? String(threadId) : null; + + if (!postUuid && !threadUuid) { return res.status(400).json({ message: 'Cible du signalement manquante.' }); } + if (postUuid && !isUuid(postUuid)) { + return res.status(400).json({ message: 'Identifiant de post invalide.' }); + } + if (threadUuid && !isUuid(threadUuid)) { + return res.status(400).json({ message: 'Identifiant de fil invalide.' }); + } + + if (postUuid) { + const check = await pool.query( + 'SELECT uuid_post FROM forum_post WHERE uuid_post = $1 LIMIT 1', + [postUuid], + ); + if (check.rows.length === 0) { + return res.status(404).json({ message: 'Publication introuvable.' }); + } + } + if (threadUuid) { + const check = await pool.query( + 'SELECT uuid_thread FROM forum_thread WHERE uuid_thread = $1 LIMIT 1', + [threadUuid], + ); + if (check.rows.length === 0) { + return res.status(404).json({ message: 'Fil de discussion introuvable.' }); + } + } + + const metaJson = buildReportMeta({ + postId: postUuid, + threadId: threadUuid, + reporterId: String(req.user.id), + }); const { rows } = await pool.query( `INSERT INTO forum_report - (uuid_post, uuid_thread, uuid_reporter, target_type, reason, priority) - VALUES ($1, $2, $3, $4, $5, $6) + (uuid_post, uuid_thread, uuid_reporter, target_type, reason, priority, resolution_note) + VALUES (NULL, NULL, NULL, $1, $2, $3, $4) RETURNING uuid_report AS id`, [ - postIdInt, - threadIdInt, - req.user.id, - postIdInt ? 'post' : 'thread', + postUuid ? 'post' : 'thread', reasonCode, priorityCode, + metaJson, ], ); diff --git a/backend/src/routes/forum.js b/backend/src/routes/forum.js index 67db198..baf7356 100644 --- a/backend/src/routes/forum.js +++ b/backend/src/routes/forum.js @@ -1,5 +1,6 @@ const { Router } = require('express'); const { makeForumController } = require('../controllers/forumController'); +const { createReport } = require('../controllers/moderationController'); const { requireAuth } = require('../middleware/auth'); const { validateFlexibleIdParam, validateBody } = require('../middleware/validation'); @@ -34,6 +35,13 @@ function forumRoutes(pool) { }), ctrl.updatePost); router.delete('/posts/:id', validateFlexibleIdParam('id'), ctrl.deletePost); + router.post('/reports', validateBody({ + post_id: { type: 'string', maxLength: 80 }, + thread_id: { type: 'string', maxLength: 80 }, + reason: { type: 'string', maxLength: 80 }, + priority: { type: 'string', maxLength: 20 }, + }), createReport(pool)); + return router; } diff --git a/backend/src/utils/forumReportMeta.js b/backend/src/utils/forumReportMeta.js new file mode 100644 index 0000000..1bbf174 --- /dev/null +++ b/backend/src/utils/forumReportMeta.js @@ -0,0 +1,33 @@ +/** + * forum_report sur RDS : uuid_post / uuid_thread / uuid_reporter sont en INTEGER + * alors que le forum utilise des UUID. Les cibles sont stockées en JSON dans resolution_note. + */ +function buildReportMeta({ postId, threadId, reporterId }) { + return JSON.stringify({ + v: 1, + post_id: postId || null, + thread_id: threadId || null, + reporter_id: reporterId || null, + }); +} + +function parseReportMeta(resolutionNote) { + if (!resolutionNote || typeof resolutionNote !== 'string') { + return { post_id: null, thread_id: null, reporter_id: null }; + } + try { + const data = JSON.parse(resolutionNote); + if (data && data.v === 1) { + return { + post_id: data.post_id || null, + thread_id: data.thread_id || null, + reporter_id: data.reporter_id || null, + }; + } + } catch (_err) { + // Ancien format texte libre + } + return { post_id: null, thread_id: null, reporter_id: null }; +} + +module.exports = { buildReportMeta, parseReportMeta }; diff --git a/backend/tests/auth.test.js b/backend/tests/auth.test.js index 84fd838..ba648d5 100644 --- a/backend/tests/auth.test.js +++ b/backend/tests/auth.test.js @@ -1,9 +1,9 @@ const request = require('supertest'); -const { newDb } = require('pg-mem'); const { Pool } = require('pg'); const { createApp } = require('../src/app'); +const { createRdsMemPool, truncateAll } = require('./helpers/rdsMemSchema'); +const { STRONG_PASSWORD } = require('./helpers/testHttp'); -const strongPassword = 'StrongPass@1234'; const useRealDb = Boolean(process.env.TEST_DB_URL); describe('Auth routes', () => { @@ -11,53 +11,20 @@ describe('Auth routes', () => { let pool; beforeAll(async () => { - process.env.JWT_SECRET = 'test-secret'; + process.env.NODE_ENV = 'test'; + process.env.JWT_SECRET = 'test-secret-key-with-enough-length-32'; if (useRealDb) { pool = new Pool({ connectionString: process.env.TEST_DB_URL, ssl: process.env.PGSSLMODE === 'require' ? { rejectUnauthorized: false } : false, }); - // Prépare l'extension requise pour gen_random_uuid() si le schéma n'a pas été appliqué await pool.query('CREATE EXTENSION IF NOT EXISTS pgcrypto;'); } else { - const db = newDb(); - const pg = db.adapters.createPg(); - pool = new pg.Pool(); - - // Créer les tables nécessaires pour les tests dans pg-mem - await pool.query(` - CREATE TABLE user_account ( - uuid_user SERIAL PRIMARY KEY, - email VARCHAR(511) UNIQUE NOT NULL, - email_normalized TEXT, - name TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - ); - `); - - await pool.query(` - CREATE TABLE user_security ( - uuid_user INT PRIMARY KEY REFERENCES user_account(uuid_user), - mdp_hash VARCHAR(511) NOT NULL, - mdp_sel VARCHAR(511) NOT NULL, - lockout BOOLEAN NOT NULL DEFAULT FALSE, - mfa TEXT, - date_creation DATE NOT NULL, - date_derniere_connection DATE - ); - `); - - // Table admin requise par les requêtes d'authentification - // (EXISTS SELECT FROM admin WHERE uuid_admin = ...) - await pool.query(` - CREATE TABLE admin ( - uuid_admin INT PRIMARY KEY REFERENCES user_account(uuid_user) - ); - `); + pool = await createRdsMemPool(); } - app = await createApp(pool, { jwtSecret: 'test-secret' }); + app = await createApp(pool, pool, { jwtSecret: process.env.JWT_SECRET }); }); afterAll(async () => { @@ -65,61 +32,71 @@ describe('Auth routes', () => { }); beforeEach(async () => { - // Nettoyage des données entre tests if (useRealDb) { - await pool.query('TRUNCATE admin, user_security, user_account RESTART IDENTITY CASCADE;'); + await pool.query(` + TRUNCATE forum_report, forum_post, forum_thread, ai_message, ai_session, + mdph_document, mdph_section, mdph_dossier, admin, aidant, patient, + user_security, user_account RESTART IDENTITY CASCADE; + `); } else { - // Pour pg-mem, on utilise DELETE qui est supporté - await pool.query('DELETE FROM admin;'); - await pool.query('DELETE FROM user_security;'); - await pool.query('DELETE FROM user_account;'); + await truncateAll(pool); } }); - test('signup crée un compte et positionne un cookie de session httpOnly', async () => { + test('signup crée un compte, patient et cookie httpOnly', async () => { const res = await request(app) .post('/signup') - .send({ name: 'Alice', email: 'alice@example.com', password: strongPassword }); + .send({ name: 'Alice', email: 'alice@example.com', password: STRONG_PASSWORD }); expect(res.status).toBe(201); expect(res.body.user).toMatchObject({ email: 'alice@example.com', name: 'Alice' }); + + const patient = await pool.query( + 'SELECT 1 FROM patient WHERE uuid_patient = $1', + [res.body.user.id], + ); + expect(patient.rows.length).toBe(1); + expect(res.body.token).toBeUndefined(); - expect(Array.isArray(res.headers['set-cookie'])).toBe(true); - expect(res.headers['set-cookie'].join(';')).toContain('access_token='); + expect(res.headers['set-cookie']?.join(';')).toContain('access_token='); }); test('signup refuse un email déjà utilisé', async () => { await request(app) .post('/signup') - .send({ name: 'Bob', email: 'dup@example.com', password: strongPassword }); + .send({ name: 'Bob', email: 'dup@example.com', password: STRONG_PASSWORD }); const res = await request(app) .post('/signup') - .send({ name: 'Bobby', email: 'dup@example.com', password: strongPassword }); + .send({ name: 'Bobby', email: 'dup@example.com', password: STRONG_PASSWORD }); expect(res.status).toBe(409); }); - test('login réussit avec des identifiants valides', async () => { - await request(app) + test('login réussit et met à jour date_derniere_connection', async () => { + const signup = await request(app) .post('/signup') - .send({ name: 'Charlie', email: 'charlie@example.com', password: strongPassword }); + .send({ name: 'Charlie', email: 'charlie@example.com', password: STRONG_PASSWORD }); + const userId = signup.body.user.id; const res = await request(app) .post('/login') - .send({ email: 'charlie@example.com', password: strongPassword }); + .send({ email: 'charlie@example.com', password: STRONG_PASSWORD }); expect(res.status).toBe(200); expect(res.body.user).toMatchObject({ email: 'charlie@example.com', name: 'Charlie' }); - expect(res.body.token).toBeUndefined(); - expect(Array.isArray(res.headers['set-cookie'])).toBe(true); - expect(res.headers['set-cookie'].join(';')).toContain('access_token='); + + const sec = await pool.query( + 'SELECT date_derniere_connection FROM user_security WHERE uuid_user = $1', + [userId], + ); + expect(sec.rows[0].date_derniere_connection).not.toBeNull(); }); test('login échoue avec un mot de passe invalide', async () => { await request(app) .post('/signup') - .send({ name: 'Dave', email: 'dave@example.com', password: strongPassword }); + .send({ name: 'Dave', email: 'dave@example.com', password: STRONG_PASSWORD }); const res = await request(app) .post('/login') @@ -135,4 +112,15 @@ describe('Auth routes', () => { expect(res.status).toBe(400); }); + + test('GET /me retourne le profil connecté', async () => { + const agent = request.agent(app); + await agent + .post('/signup') + .send({ name: 'Me User', email: 'me@example.com', password: STRONG_PASSWORD }); + + const me = await agent.get('/me'); + expect(me.status).toBe(200); + expect(me.body.user.email).toBe('me@example.com'); + }); }); diff --git a/backend/tests/forumReportMeta.test.js b/backend/tests/forumReportMeta.test.js new file mode 100644 index 0000000..8817150 --- /dev/null +++ b/backend/tests/forumReportMeta.test.js @@ -0,0 +1,14 @@ +const { buildReportMeta, parseReportMeta } = require('../src/utils/forumReportMeta'); + +describe('forumReportMeta', () => { + test('build et parse conservent les UUID', () => { + const raw = buildReportMeta({ + postId: '11111111-1111-4111-8111-111111111111', + threadId: null, + reporterId: '22222222-2222-4222-8222-222222222222', + }); + const parsed = parseReportMeta(raw); + expect(parsed.post_id).toBe('11111111-1111-4111-8111-111111111111'); + expect(parsed.reporter_id).toBe('22222222-2222-4222-8222-222222222222'); + }); +}); diff --git a/backend/tests/helpers/rdsMemSchema.js b/backend/tests/helpers/rdsMemSchema.js new file mode 100644 index 0000000..a7026fe --- /dev/null +++ b/backend/tests/helpers/rdsMemSchema.js @@ -0,0 +1,209 @@ +/** + * Schéma pg-mem aligné sur RDS AWS (lecture seule du 2026-05-26). + * Ne pas ajouter date_creation sur user_security (absent sur RDS). + */ +const { newDb } = require('pg-mem'); +const { v4: uuidv4 } = require('uuid'); + +function registerPgMemFunctions(db) { + db.public.registerFunction({ + name: 'gen_random_uuid', + returns: 'uuid', + impure: true, + implementation: () => uuidv4(), + }); +} + +async function applyRdsMemSchema(pool) { + await pool.query(` + CREATE TABLE user_account ( + uuid_user UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT, + email VARCHAR(511) UNIQUE NOT NULL, + email_normalized TEXT, + total_input_tokens_spent INTEGER DEFAULT 0, + total_output_tokens_spent INTEGER DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + + CREATE TABLE user_security ( + uuid_user UUID PRIMARY KEY REFERENCES user_account(uuid_user), + mdp_hash TEXT NOT NULL, + mdp_sel VARCHAR(511) NOT NULL DEFAULT '', + lockout BOOLEAN NOT NULL DEFAULT FALSE, + date_derniere_connection TIMESTAMP + ); + + CREATE TABLE admin ( + uuid_admin UUID PRIMARY KEY REFERENCES user_account(uuid_user) + ); + + CREATE TABLE patient ( + uuid_patient UUID PRIMARY KEY REFERENCES user_account(uuid_user), + date_derniere_modification_ai_session TIMESTAMP + ); + + CREATE TABLE aidant ( + uuid_aidant UUID PRIMARY KEY REFERENCES user_account(uuid_user) + ); + + CREATE TABLE mdph_dossier ( + uuid_mdph_dossier UUID PRIMARY KEY DEFAULT gen_random_uuid(), + numero_dossier TEXT, + titre TEXT, + description TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + uuid_patient UUID REFERENCES patient(uuid_patient) + ); + + CREATE TABLE mdph_section ( + uuid_mdph_section UUID PRIMARY KEY DEFAULT gen_random_uuid(), + numero_section VARCHAR(50), + titre TEXT, + contenu JSONB, + uuid_ai_session TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + uuid_mdph_dossier UUID REFERENCES mdph_dossier(uuid_mdph_dossier) + ); + + CREATE TABLE mdph_document ( + uuid_document_mdph UUID PRIMARY KEY DEFAULT gen_random_uuid(), + chemin_vers_document TEXT NOT NULL, + type TEXT, + titre_document TEXT NOT NULL, + titre TEXT, + taille TEXT, + contenu_ocr_json JSON NOT NULL, + status TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + user_id VARCHAR(42), + uuid_mdph_section UUID REFERENCES mdph_section(uuid_mdph_section) + ); + + CREATE TABLE ai_session ( + uuid_ai_session UUID PRIMARY KEY DEFAULT gen_random_uuid(), + uuid_mdph_dossier INTEGER, + section TEXT, + sous_section TEXT, + nom_formulaire TEXT, + date_creation_session TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + resultat_json JSON NOT NULL, + traites JSONB DEFAULT '[]'::jsonb, + est_termine BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + uuid_mdph_section UUID REFERENCES mdph_section(uuid_mdph_section), + uuid_patient UUID REFERENCES patient(uuid_patient) + ); + + CREATE TABLE ai_message ( + uuid_ai_message UUID PRIMARY KEY DEFAULT gen_random_uuid(), + uuid_ai_session UUID NOT NULL REFERENCES ai_session(uuid_ai_session), + message_role TEXT NOT NULL, + message_conv TEXT NOT NULL, + token_input INTEGER, + token_output INTEGER, + created_at TIMESTAMPTZ DEFAULT NOW() + ); + + CREATE TABLE forum_thread ( + uuid_thread UUID PRIMARY KEY DEFAULT gen_random_uuid(), + titre VARCHAR(255) NOT NULL, + owner_id TEXT, + uuid_user VARCHAR(255), + category TEXT, + excerpt TEXT, + is_pinned BOOLEAN DEFAULT FALSE, + views INTEGER DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + is_hidden BOOLEAN DEFAULT FALSE + ); + + CREATE TABLE forum_post ( + uuid_post UUID PRIMARY KEY DEFAULT gen_random_uuid(), + message_forum TEXT NOT NULL, + date_publication TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + uuid_thread UUID NOT NULL REFERENCES forum_thread(uuid_thread), + uuid_user UUID NOT NULL REFERENCES user_account(uuid_user), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + is_hidden BOOLEAN DEFAULT FALSE + ); + + CREATE SEQUENCE forum_report_uuid_report_seq; + + CREATE TABLE forum_report ( + uuid_report INTEGER PRIMARY KEY DEFAULT nextval('forum_report_uuid_report_seq'), + uuid_post INTEGER, + uuid_thread INTEGER, + uuid_reporter INTEGER, + target_type TEXT NOT NULL DEFAULT 'post', + reason TEXT NOT NULL, + priority TEXT NOT NULL DEFAULT 'medium', + status TEXT NOT NULL DEFAULT 'pending', + resolution_note TEXT, + resolved_by INTEGER, + resolved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + + CREATE TABLE app_setting ( + key TEXT PRIMARY KEY, + value TEXT, + updated_at TIMESTAMPTZ DEFAULT NOW() + ); + + CREATE TABLE resource ( + uuid_resource UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type TEXT NOT NULL, + icon TEXT, + category TEXT, + title TEXT NOT NULL, + description TEXT, + read_time TEXT, + keywords JSONB DEFAULT '[]'::jsonb, + created_by VARCHAR(255), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + `); +} + +async function createRdsMemPool() { + const db = newDb(); + registerPgMemFunctions(db); + const pg = db.adapters.createPg(); + const pool = new pg.Pool(); + await applyRdsMemSchema(pool); + return pool; +} + +async function truncateAll(pool) { + await pool.query(` + DELETE FROM forum_report; + DELETE FROM forum_post; + DELETE FROM forum_thread; + DELETE FROM ai_message; + DELETE FROM ai_session; + DELETE FROM mdph_document; + DELETE FROM mdph_section; + DELETE FROM mdph_dossier; + DELETE FROM resource; + DELETE FROM app_setting; + DELETE FROM admin; + DELETE FROM aidant; + DELETE FROM patient; + DELETE FROM user_security; + DELETE FROM user_account; + `); +} + +module.exports = { + createRdsMemPool, + applyRdsMemSchema, + truncateAll, +}; diff --git a/backend/tests/helpers/testHttp.js b/backend/tests/helpers/testHttp.js new file mode 100644 index 0000000..1faba16 --- /dev/null +++ b/backend/tests/helpers/testHttp.js @@ -0,0 +1,59 @@ +const request = require('supertest'); + +const STRONG_PASSWORD = 'StrongPass@1234'; + +async function signupUser(app, { name = 'Test User', email, password = STRONG_PASSWORD } = {}) { + const uniqueEmail = email || `user.${Date.now()}.${Math.random().toString(36).slice(2, 8)}@test.local`; + const res = await request(app) + .post('/signup') + .send({ name, email: uniqueEmail, password }); + return { res, email: uniqueEmail, password }; +} + +function authCookieAgent(app, cookieHeader) { + const agent = request.agent(app); + if (cookieHeader) { + const prev = agent.set.bind(agent); + agent.set = (field, val) => { + if (field === 'Cookie' || field === 'cookie') { + return prev('Cookie', val); + } + return prev(field, val); + }; + agent.set('Cookie', cookieHeader); + } + return agent; +} + +function extractCookies(res) { + const raw = res.headers['set-cookie']; + if (!raw) return ''; + return (Array.isArray(raw) ? raw : [raw]).map((c) => c.split(';')[0]).join('; '); +} + +async function signupAndGetAgent(app, opts) { + const { res, email, password } = await signupUser(app, opts); + const cookies = extractCookies(res); + return { + res, + email, + password, + agent: authCookieAgent(app, cookies), + cookies, + }; +} + +async function promoteAdmin(pool, userId) { + await pool.query( + `INSERT INTO admin (uuid_admin) VALUES ($1) ON CONFLICT DO NOTHING`, + [userId], + ); +} + +module.exports = { + STRONG_PASSWORD, + signupUser, + signupAndGetAgent, + extractCookies, + promoteAdmin, +}; diff --git a/backend/tests/solution.integration.test.js b/backend/tests/solution.integration.test.js new file mode 100644 index 0000000..497b283 --- /dev/null +++ b/backend/tests/solution.integration.test.js @@ -0,0 +1,253 @@ +/** + * Tests d'intégration alignés sur le schéma RDS (pg-mem ou TEST_DB_URL). + */ +jest.mock('../src/services/mistralOcrService', () => { + const actual = jest.requireActual('../src/services/mistralOcrService'); + return { + ...actual, + runMistralOcr: jest.fn(async () => ({ + type_extraction: 'mistral_ocr', + texte_complet: 'Texte OCR de test', + pages: [], + date_extraction: new Date().toISOString(), + })), + }; +}); + +jest.mock('../src/services/mailjet', () => ({ + sendContactEmail: jest.fn(async () => ({ ok: true })), +})); + +jest.mock('../src/services/chatbotService', () => ({ + loadRegroupementSections: jest.fn(() => ({})), + buildChatBot: jest.fn(() => ({ + traites: new Set(), + dataFields: { besoin: 'test' }, + async discuter() { + return { + answer: 'Réponse chatbot test', + options: [], + type_options: 'text', + est_termine: false, + input_tokens: 1, + output_tokens: 2, + formData: { besoin: 'test' }, + }; + }, + })), +})); + +const request = require('supertest'); +const { Pool } = require('pg'); +const { createApp } = require('../src/app'); +const { createRdsMemPool, truncateAll } = require('./helpers/rdsMemSchema'); +const { signupAndGetAgent, promoteAdmin } = require('./helpers/testHttp'); + +const useRealDb = Boolean(process.env.TEST_DB_URL); +const TINY_PNG_BASE64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='; + +describe('Solution UMHMA (schéma RDS)', () => { + let app; + let pool; + + beforeAll(async () => { + process.env.NODE_ENV = 'test'; + process.env.JWT_SECRET = 'test-secret-key-with-enough-length-32'; + + if (useRealDb) { + pool = new Pool({ + connectionString: process.env.TEST_DB_URL, + ssl: process.env.PGSSLMODE === 'require' ? { rejectUnauthorized: false } : false, + }); + await pool.query('CREATE EXTENSION IF NOT EXISTS pgcrypto;'); + } else { + pool = await createRdsMemPool(); + } + + app = await createApp(pool, pool, { jwtSecret: process.env.JWT_SECRET }); + }); + + afterAll(async () => { + await pool.end(); + }); + + beforeEach(async () => { + if (useRealDb) { + await pool.query(` + TRUNCATE forum_report, forum_post, forum_thread, ai_message, ai_session, + mdph_document, mdph_section, mdph_dossier, resource, app_setting, + admin, aidant, patient, user_security, user_account RESTART IDENTITY CASCADE; + `); + } else { + await truncateAll(pool); + } + }); + + test('healthcheck DB', async () => { + const res = await request(app).get('/health'); + expect(res.status).toBe(200); + expect(res.body.status).toBe('ok'); + }); + + test('parcours patient : dossier → section → document OCR', async () => { + const { agent, res: signupRes } = await signupAndGetAgent(app, { name: 'Patient MDPH' }); + expect(signupRes.status).toBe(201); + + const dossierRes = await agent + .post('/dossiers') + .send({ titre: 'Mon dossier', numero_dossier: 'MDPH-001', description: 'Test' }); + expect(dossierRes.status).toBe(201); + const dossierId = dossierRes.body.dossier.id; + + const sectionRes = await agent + .post(`/dossiers/${dossierId}/sections`) + .send({ numero_section: 'A', titre: 'Section A', contenu: { note: 'x' } }); + expect(sectionRes.status).toBe(201); + + const docRes = await agent + .post('/documents') + .send({ + titre: 'Pièce jointe', + type: 'Autre', + chemin_vers_document: 'test.png', + taille: '100 o', + status: 'pending', + file_mime_type: 'image/png', + file_base64: TINY_PNG_BASE64, + }); + expect(docRes.status).toBe(201); + expect(docRes.body.ocr_status).toBe('completed'); + expect(docRes.body.document.contenu_ocr_json).toBeTruthy(); + + const list = await agent.get('/documents'); + expect(list.body.documents.length).toBeGreaterThanOrEqual(1); + }); + + test('forum : fil, message, signalement (meta JSON)', async () => { + const { agent } = await signupAndGetAgent(app); + + const threadRes = await agent + .post('/forum/threads') + .send({ titre: 'Question MDPH', category: 'general', excerpt: 'Besoin aide' }); + expect(threadRes.status).toBe(201); + const threadId = threadRes.body.thread.id; + + const postRes = await agent + .post(`/forum/threads/${threadId}/posts`) + .send({ message: 'Premier message' }); + expect(postRes.status).toBe(201); + const postId = postRes.body.post.id; + + const reportRes = await agent + .post('/forum/reports') + .send({ post_id: postId, reason: 'spam', priority: 'high' }); + expect(reportRes.status).toBe(201); + + const { rows } = await pool.query( + 'SELECT resolution_note FROM forum_report WHERE uuid_report = $1', + [reportRes.body.id], + ); + const meta = JSON.parse(rows[0].resolution_note); + expect(meta.post_id).toBe(postId); + expect(meta.reporter_id).toBeTruthy(); + }); + + test('admin : stats et modération hide', async () => { + const { agent, res: signupRes } = await signupAndGetAgent(app, { name: 'Admin User' }); + await promoteAdmin(pool, signupRes.body.user.id); + + const threadRes = await agent + .post('/forum/threads') + .send({ titre: 'Contenu signalé', category: 'general' }); + const threadId = threadRes.body.thread.id; + + const reportRes = await agent + .post('/forum/reports') + .send({ thread_id: threadId, reason: 'contenu_inapproprie' }); + const reportId = reportRes.body.id; + + const statsRes = await agent.get('/admin/stats'); + expect(statsRes.status).toBe(200); + + const hideRes = await agent.post(`/admin/reports/${reportId}/hide`); + expect(hideRes.status).toBe(200); + + const hidden = await pool.query( + 'SELECT is_hidden FROM forum_thread WHERE uuid_thread = $1', + [threadId], + ); + expect(hidden.rows[0].is_hidden).toBe(true); + }); + + test('chatbot : démarrage de session sur dossier', async () => { + const { agent } = await signupAndGetAgent(app); + + const dossierRes = await agent.post('/dossiers').send({ titre: 'Dossier chatbot' }); + const dossierId = dossierRes.body.dossier.id; + + const startRes = await agent + .post('/chatbot/start') + .send({ + dossierId, + section: 'start', + sousSection: 'intro', + formulaire: 'accueil', + }); + expect(startRes.status).toBe(200); + expect(startRes.body.sessionId).toBeTruthy(); + expect(startRes.body.message).toContain('test'); + }); + + test('settings public', async () => { + const res = await request(app).get('/settings/public'); + expect(res.status).toBe(200); + }); + + test('ressources publiques et contact', async () => { + const resources = await request(app).get('/resources'); + expect(resources.status).toBe(200); + expect(Array.isArray(resources.body.resources)).toBe(true); + + const contact = await request(app) + .post('/contact') + .send({ + name: 'Jean Dupont', + email: 'jean@example.com', + subject: 'general', + message: 'Message de test pour le formulaire.', + }); + expect(contact.status).toBe(202); + }); + + test('logout invalide la session', async () => { + const agent = request.agent(app); + await agent + .post('/signup') + .send({ name: 'Logout User', email: 'logout@example.com', password: 'StrongPass@1234' }); + + const logoutRes = await agent.post('/logout'); + expect(logoutRes.status).toBe(200); + + const me = await agent.get('/me'); + expect(me.status).toBe(401); + }); + + test('admin crée une ressource', async () => { + const { agent, res: signupRes } = await signupAndGetAgent(app); + await promoteAdmin(pool, signupRes.body.user.id); + + const createRes = await agent + .post('/resources') + .send({ + type: 'guide', + title: 'Guide test', + category: 'mdph', + description: 'Description', + read_time: '5 min', + }); + expect(createRes.status).toBe(201); + + const list = await request(app).get('/resources'); + expect(list.body.resources.some((r) => r.title === 'Guide test')).toBe(true); + }); +});