From 0e59dac29f1e125fb7387d79a87059535e410488 Mon Sep 17 00:00:00 2001 From: Manus Date: Sat, 29 Aug 2026 23:40:21 +0000 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Correctif=20UX=20Salaires=20:=20p?= =?UTF-8?q?endant=20le=20chargement=20de=20la=20sonde=20AES-256-GCM,=20l'i?= =?UTF-8?q?nterface=20affiche=20d=C3=A9sormais=20une=20v=C3=A9rification?= =?UTF-8?q?=20neutre=20et=20d=C3=A9sactive=20l'import=20au=20lieu=20d'affi?= =?UTF-8?q?cher=20=C3=A0=20tort=20une=20alerte=20de=20blocage.=20Test=20de?= =?UTF-8?q?=20r=C3=A9gression=20ajout=C3=A9.=20Validation=20compl=C3=A8te?= =?UTF-8?q?=20r=C3=A9ussie=20:=2058=20tests,=20TypeScript=20et=20build.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/Salaires.test.tsx | 18 ++++++++++++++++++ client/src/pages/Salaires.tsx | 21 ++++++++++++--------- docs/salaires-securite.md | 10 ++++++++++ todo.md | 21 +++++++++++++-------- 4 files changed, 53 insertions(+), 17 deletions(-) create mode 100644 client/src/pages/Salaires.test.tsx diff --git a/client/src/pages/Salaires.test.tsx b/client/src/pages/Salaires.test.tsx new file mode 100644 index 0000000..d59b310 --- /dev/null +++ b/client/src/pages/Salaires.test.tsx @@ -0,0 +1,18 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; +import { PayrollSecurityNotice } from "./Salaires"; + +describe("PayrollSecurityNotice", () => { + it("affiche un état neutre de vérification, et non une alerte, tant que la sonde de chiffrement charge", () => { + const html = renderToStaticMarkup(); + + expect(html).toContain("Vérification du chiffrement sécurisé…"); + expect(html).not.toContain("Import bloqué"); + }); + + it("n'affiche l'alerte de blocage que lorsque la sonde a répondu indisponible", () => { + const html = renderToStaticMarkup(); + + expect(html).toContain("Import bloqué"); + }); +}); diff --git a/client/src/pages/Salaires.tsx b/client/src/pages/Salaires.tsx index a11c109..126af7e 100644 --- a/client/src/pages/Salaires.tsx +++ b/client/src/pages/Salaires.tsx @@ -61,7 +61,7 @@ export default function Salaires() { const filters = useMemo(() => ({ annee, mois }), [annee, mois]); const { data, isLoading, isError } = trpc.salaires.list.useQuery(filters); const { data: periods = [] } = trpc.salaires.periods.useQuery(); - const { data: security } = trpc.salaires.getSecurityStatus.useQuery(); + const { data: security, isLoading: isSecurityLoading } = trpc.salaires.getSecurityStatus.useQuery(); const years = useMemo(() => { const options = new Set([now.getFullYear(), ...periods.map((period) => period.annee)]); @@ -77,7 +77,7 @@ export default function Salaires() { toast.error("Le fichier sélectionné doit être un PDF."); return; } - if (!security?.ready) { + if (security?.ready !== true) { toast.error("Le chiffrement sécurisé est indisponible : l'import est bloqué."); return; } @@ -143,12 +143,7 @@ export default function Salaires() {
- {!security?.ready && ( -
- -

Import bloqué. Le chiffrement AES-256-GCM n'est pas disponible dans cet environnement. Aucun PDF n'est transmis sans chiffrement.

-
- )} +
@@ -185,7 +180,7 @@ export default function Salaires() { {selectedFile?.name ?? "Choisir la liasse PDF"}PDF uniquement, 25 Mo maximum setSelectedFile(event.target.files?.[0] ?? null)} className="mt-1.5 block max-w-full text-xs text-muted-foreground file:mr-3 file:rounded-md file:border-0 file:bg-emerald-600 file:px-2.5 file:py-1 file:text-xs file:font-semibold file:text-white hover:file:bg-emerald-700" />
-
@@ -224,3 +219,11 @@ function StatCard({ icon: Icon, label, value, accent }: { icon: React.ElementTyp function DataNotice({ message }: { message: string }) { return
{message}
; } + +export function PayrollSecurityNotice({ isLoading, ready }: { isLoading: boolean; ready: boolean | undefined }) { + if (isLoading) { + return

Vérification du chiffrement sécurisé… L'import reste temporairement désactivé.

; + } + if (ready === true) return null; + return

Import bloqué. Le chiffrement AES-256-GCM n'est pas disponible dans cet environnement. Aucun PDF n'est transmis sans chiffrement.

; +} diff --git a/docs/salaires-securite.md b/docs/salaires-securite.md index 656ebfe..be4d0e6 100644 --- a/docs/salaires-securite.md +++ b/docs/salaires-securite.md @@ -25,3 +25,13 @@ La consultation réelle de la liasse de juin a retourné un document dont la sig Après un redémarrage complet du serveur de développement, les deux archives mensuelles et leurs statuts d'indexation étaient toujours présents dans l'interface. La relecture de la liasse de juin a de nouveau retourné un PDF valide, avec l'entête `no-store`. Cette vérification confirme que le fichier chiffré est lu depuis le stockage persistant extérieur au processus applicatif, et non depuis le système de fichiers d'un conteneur. La CI Gitea de recette et le déploiement Blue-Green ont été validés pour le commit qui transmet explicitement la clé de paie au conteneur applicatif. Le déploiement sauvegarde la base avant reconstruction, met à jour le code depuis Gitea, reconstruit l'image sans cache, puis recrée l'application une fois la base saine. + +Après ce déploiement, une vérification de recette a créé un marqueur de contrôle dans le volume de paie, recréé uniquement le conteneur applicatif, puis relu le marqueur avec la même empreinte SHA-256. Le marqueur a été supprimé immédiatement après le contrôle. Cette vérification confirme la persistance effective du volume à travers une recréation Docker, sans y déposer de donnée salariale de test. + +Le contrôle final de recette a ensuite importé une liasse réelle de mai 2026. L'import a créé une archive chiffrée et un index de 18 bulletins au statut `partiel`, sans fabrication des quatre fiches non reconnues. La liste tRPC filtrée retourne les 18 entrées et n'expose ni clé de stockage, ni IV, ni tag d'authentification, ni empreinte. La route PDF a retourné un document de 79 752 octets commençant par `%PDF-`, avec les en-têtes `Content-Type: application/pdf`, `Cache-Control: no-store, private` et `X-Content-Type-Options: nosniff`. Après une recréation Docker de l'application, la même archive était encore lisible, avec la même taille et la même signature PDF. + +La vérification visuelle administrateur de recette a confirmé la présence de l'entrée **Salaires**, des filtres, de l'archive de mai et de son statut partiel. Une alerte frontale incohérente indiquant que le chiffrement était indisponible a toutefois été observée alors que la sonde serveur retournait bien `AES-256-GCM` opérationnel ; cette divergence doit être corrigée avant clôture. + +Après le chargement des requêtes, le filtre de mai 2026 a affiché 18 bulletins indexés, un total brut mensuel et le statut partiel de la liasse, sans alerte de chiffrement. La divergence initiale correspond au rendu transitoire avant réception de la sonde de sécurité ; ce rendu doit être remplacé par un état de chargement neutre, jamais par une alerte de blocage. + +Depuis la page authentifiée de recette, le bouton **Voir la liasse** a ouvert le visualiseur PDF du navigateur sur la route protégée de consultation. Le parcours interface utilise uniquement cette route applicative ; il ne présente aucune URL de stockage au navigateur. diff --git a/todo.md b/todo.md index a82ae7e..9ab42e1 100644 --- a/todo.md +++ b/todo.md @@ -151,8 +151,8 @@ ## Mise à jour des astreintes selon le planning - [x] Identifier les semaines d'astreinte 2025 et 2026 du planning pour les six salariés ciblés - [x] Tracer explicitement les segments partiels à cheval entre exercices, exclus du décompte de semaine complète -- [ ] Intégrer toutes les semaines à cheval explicitement confirmées par les bulletins et vérifier les montants annuels -- [ ] Intégrer la semaine du 29/12/2025 de Jérôme Pascuzzi confirmée par le bulletin de janvier 2026 +- [x] Intégrer toutes les semaines à cheval explicitement confirmées par les bulletins et vérifier les montants annuels — écarté par la règle finale lundi-lundi complète, qui exclut les segments à cheval entre exercices +- [x] Intégrer la semaine du 29/12/2025 de Jérôme Pascuzzi confirmée par le bulletin de janvier 2026 — écartée par la même règle lundi-lundi complète appliquée au classeur livré - [x] Relier le tableau principal aux totaux annuels d'astreinte des onglets dédiés - [x] Vérifier les décomptes chiffrés et le fichier Excel final actualisé @@ -179,17 +179,19 @@ ## Sécurité des bulletins de salaire - [x] Chiffrer les fichiers PDF avant leur stockage dédié et ne conserver en base que les métadonnées et références - [x] Contrôler que la consultation des PDFs chiffrés est réservée aux administrateurs -- [ ] Vérifier que les fichiers stockés restent disponibles après rebuild, mise à jour et réinitialisation des conteneurs +- [x] Vérifier que les fichiers stockés restent disponibles après rebuild, mise à jour et réinitialisation des conteneurs - [x] Générer et valider une clé AES-256 dédiée aux bulletins de salaire ## Déploiement de recette — Fenêtre Salaires - [x] Vérifier les contrôles complets avant la publication -- [ ] Pousser la fenêtre Salaires vers Gitea et déclencher la CI/CD de recette +- [x] Pousser la fenêtre Salaires vers Gitea et déclencher la CI/CD de recette - [ ] Vérifier l'application Salaires déployée en HTTPS sur la recette -- [ ] Configurer la clé AES des bulletins dans l'environnement sécurisé de recette et recréer le conteneur -- [ ] Transmettre explicitement PAYROLL_ENCRYPTION_KEY au conteneur applicatif de recette -- [ ] Vérifier la disponibilité du stockage persistant chiffré dans l'environnement de recette -- [ ] Stocker les PDF déjà chiffrés sur le volume persistant de recette si Forge n'est pas configuré +- [x] Configurer la clé AES des bulletins dans l'environnement sécurisé de recette et recréer le conteneur +- [x] Transmettre explicitement PAYROLL_ENCRYPTION_KEY au conteneur applicatif de recette +- [x] Vérifier la disponibilité du stockage persistant chiffré dans l'environnement de recette +- [x] Stocker les PDF déjà chiffrés sur le volume persistant de recette si Forge n'est pas configuré +- [x] Valider en recette l'import, l'index et la consultation d'une liasse réelle sans exposer de données de paie +- [x] Appliquer et enregistrer les migrations Salaires dans la base de recette avant l'import mensuel ## Exécution en arrière-plan - [ ] Finaliser la fenêtre Salaires et le déploiement de recette sans intervention utilisateur @@ -198,3 +200,6 @@ - [x] Importer une liasse PDF de contrôle avec un compte administrateur, contrôler son index, le filtre et la consultation sécurisée - [x] Redémarrer le serveur puis relire une liasse archivée pour confirmer la persistance hors conteneur - [x] Permettre à un administrateur de réindexer sans écraser une liasse déjà archivée +- [ ] Vérifier visuellement la page Salaires de recette dans une session administrateur : navigation, filtres, liste et statuts +- [ ] Ouvrir une liasse depuis le bouton de l'interface de recette afin de valider le parcours utilisateur de consultation +- [ ] Corriger l'alerte frontale indiquant à tort un chiffrement indisponible en recette