Skip to content

Feat: journalisation des accès au carnet et rotation du code porteur - #68

Merged
Zoubeir23 merged 7 commits into
mainfrom
Feature/068-journalisation-et-rotation-des-carnets
Aug 2, 2026
Merged

Feat: journalisation des accès au carnet et rotation du code porteur#68
Zoubeir23 merged 7 commits into
mainfrom
Feature/068-journalisation-et-rotation-des-carnets

Conversation

@Zoubeir23

@Zoubeir23 Zoubeir23 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Contexte

Deuxième des quatre points de dette relevés lors de l'audit signature / carnet / ordonnance.

Le code CAR-XXXXXXXXXXXXXXXX est un jeton porteur : quiconque le détient peut, via
importPatientCarnet, rattacher le patient à sa clinique et lire tout l'historique médical validé
inter-cliniques. Deux manques en découlaient :

  1. Aucune trace. Impossible de répondre à « quelles cliniques ont accédé à mon dossier, et
    quand ? » — question à laquelle un patient a le droit d'obtenir une réponse.
  2. Aucune révocation. Un code divulgué le restait définitivement.

Ce qui change

Migration 014

  • Table carnet_import_events : carnet_id, clinic_id, imported_by_user_id, patient_id,
    created_at. On journalise le carnet_id, pas le code — journaliser un jeton porteur
    reviendrait à le dupliquer dans une table de plus.
  • RLS en lecture seule : le patient voit qui a rattaché son carnet, une clinique voit ses propres
    imports. Aucune policy d'INSERT, UPDATE ou DELETE — l'écriture passe exclusivement par le
    client d'administration, car un journal que son sujet peut réécrire ne vaut rien.
  • Fonction rotate_patient_carnet_code(uuid) en SECURITY DEFINER, avec le même format et la même
    garantie d'unicité que le trigger de la migration 004. Droits révoqués pour public, anon et
    authenticated : seul le rôle de service l'exécute, après contrôle applicatif.

Actions

  • importPatientCarnet écrit désormais une entrée de journal. Si cette écriture échoue, l'import
    n'est pas annulé — le patient existe déjà, échouer reviendrait à mentir à l'appelant — mais
    l'anomalie est tracée via console.error("[carnet] …"), convention en place dans dix autres
    fichiers d'actions.
  • getCarnetImportHistory(patientId) — historique, réservé au rôle médical de la clinique du
    patient : la liste des cabinets fréquentés est elle-même une donnée de santé.
  • regenerateCarnetCode(patientId) — rotation, même rôle requis, avec rate limiting.

InterfaceCarnetAccessPanel sur la fiche patient : liste des rattachements (clinique, auteur,
date) et bouton de régénération. Affiché seulement si le patient a un carnet.

Décision assumée

La rotation invalide l'ancien code immédiatement, mais ne détache pas les cliniques déjà
rattachées
: leur accès repose sur le carnet_id, pas sur le code. Rompre un suivi médical en
cours serait plus dangereux que la fuite elle-même. Le journal permet d'identifier un rattachement
illégitime et de le traiter séparément. C'est écrit dans l'interface pour que le praticien ne s'y
trompe pas.

Tests

  • npx tsc --noEmit — propre
  • npm run lint — le nouveau composant n'ajoute aucune erreur ; les 7 erreurs de
    patients/[id]/page.tsx sont préexistantes (vérifié par comparaison avec la version stashée)
  • npx vitest run — 199 tests
  • npm run build — succès
  • Clé de cache ["patient", patientId] vérifiée : le code affiché se rafraîchit après rotation

Limite : les trois actions dépendent de Supabase et ne sont pas atteignables par le harnais
actuel (Vitest en environnement node, sans base). La migration n'a pas été appliquée — c'est
npm run db:migrate, votre décision.

Non traité

La rotation depuis le portail patient n'est pas incluse : le patient est pourtant le mieux
placé pour constater une fuite. Cela suppose un chemin d'autorisation différent
(auth_user_id plutôt que rôle de clinique) et une entrée dans l'interface du portail. À faire
dans une PR dédiée si vous le souhaitez.

Summary by CodeRabbit

  • Nouvelles fonctionnalités
    • Affichage de l’historique des rattachements inter-cliniques d’un carnet patient.
    • Présentation des établissements, utilisateurs concernés et dates des rattachements.
    • Possibilité de régénérer le code d’accès public du carnet depuis la fiche patient.
  • Améliorations
    • Les rattachements réussis sont désormais journalisés pour assurer un meilleur suivi.
    • La régénération du code protège l’accès en cas de besoin.
  • Gestion des erreurs
    • Les erreurs de régénération sont signalées clairement, sans bloquer l’import du carnet.

Copilot AI review requested due to automatic review settings August 2, 2026 05:12
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
doc-flow-ai Ready Ready Preview Aug 2, 2026 5:54am

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Zoubeir23, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 651d06af-056f-45f8-8484-57128f5b9e78

📥 Commits

Reviewing files that changed from the base of the PR and between 2251980 and 505f29d.

📒 Files selected for processing (4)
  • components/patients/carnet-access-panel.tsx
  • messages/en.json
  • messages/fr.json
  • supabase/migrations/014_journalisation_et_rotation_carnets.sql
📝 Walkthrough

Walkthrough

Le changement journalise les rattachements de carnets, ajoute la lecture de leur historique et permet de régénérer le code public. Un nouveau panneau patient présente ces données et contrôle la rotation du code.

Changes

Accès et traçabilité des carnets

Layer / File(s) Summary
Journalisation et rotation côté base
supabase/migrations/014_journalisation_et_rotation_carnets.sql
La migration ajoute carnet_import_events, ses index et ses politiques RLS. Elle ajoute rotate_patient_carnet_code avec génération unique, SECURITY DEFINER et privilèges d’exécution restreints.
Actions d’import et de gestion
actions/patients.ts
Les imports réussis créent un événement sans échouer si la journalisation échoue. Les actions lisent l’historique autorisé et régénèrent le code après les contrôles médicaux et de débit.
Panneau d’accès du patient
components/patients/carnet-access-panel.tsx, app/(app)/app/patients/[id]/page.tsx
La page affiche CarnetAccessPanel pour les patients ayant un code public. Le panneau présente l’historique, les états d’interface et l’avertissement de révocation, puis invalide les caches après une rotation réussie.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Utilisateur médical
  participant CarnetAccessPanel
  participant actions/patients.ts
  participant Supabase
  participant rotate_patient_carnet_code

  Utilisateur médical->>CarnetAccessPanel: consulte le panneau
  CarnetAccessPanel->>actions/patients.ts: demande l’historique
  actions/patients.ts->>Supabase: lit carnet_import_events
  Supabase-->>actions/patients.ts: retourne les événements
  actions/patients.ts-->>CarnetAccessPanel: affiche l’historique
  Utilisateur médical->>CarnetAccessPanel: demande la régénération
  CarnetAccessPanel->>actions/patients.ts: appelle regenerateCarnetCode
  actions/patients.ts->>rotate_patient_carnet_code: demande un nouveau code
  rotate_patient_carnet_code->>Supabase: met à jour le code public
  Supabase-->>rotate_patient_carnet_code: retourne le nouveau code
  rotate_patient_carnet_code-->>actions/patients.ts: retourne le code
  actions/patients.ts-->>CarnetAccessPanel: retourne le résultat
Loading

Suggested reviewers: copilot

Poem

Un lapin suit les carnets,
Chaque import est enregistré.
Le vieux code saute au terrier,
Le nouveau vient d’être généré.
Les cliniques gardent leur accès,
Et l’historique reste tracé.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Le titre décrit clairement les deux changements principaux : la journalisation des accès au carnet et la rotation du code porteur.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
actions/patients.ts (1)

327-332: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Éviter as any sur le client d'administration.

adminDb est typé any, ce qui désactive la vérification de type sur adminDb.rpc(...) et sur la valeur retournée. Ce contournement existait déjà ailleurs dans le fichier, mais cette ligne l'introduit dans du code nouveau, ce qui contrevient à la règle du projet.

Si un type généré pour le schéma Supabase existe (Database), utilisez-le pour typer createAdminClient() plutôt que de recourir à any.

As per coding guidelines: « Ne pas utiliser any et ne pas laisser de console.log en production. »

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@actions/patients.ts` around lines 327 - 332, Remove the `as any` cast from
`adminDb` in the `createAdminClient` flow and type the client with the generated
`Database` schema type, preserving type checking for the
`rotate_patient_carnet_code` RPC call and its returned `data` and `error`
values.

Source: Coding guidelines

supabase/migrations/014_journalisation_et_rotation_carnets.sql (1)

22-29: 🗄️ Data Integrity & Integration | 🔵 Trivial

Vérifier la survie du journal en cas de suppression du carnet.

carnet_id utilise ON DELETE CASCADE. Si patient_carnets supprime une ligne, tout l'historique d'accès associé disparaît. Le commentaire d'en-tête du fichier justifie cette table par un droit RGPD d'accès aux destinataires. Une suppression en cascade efface ce droit rétroactivement.
Envisagez ON DELETE SET NULL (comme pour patient_id) si le journal doit rester consultable après suppression du carnet.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/014_journalisation_et_rotation_carnets.sql` around lines
22 - 29, Update the carnet_id foreign key in carnet_import_events to preserve
journal rows when the referenced patient_carnets record is deleted, using the
existing nullable-reference pattern with ON DELETE SET NULL; adjust the column
definition to allow NULL while keeping the other constraints unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/patients/carnet-access-panel.tsx`:
- Around line 51-118: Localize all visible strings in the carnet access panel by
adding matching French and English keys in messages/fr.json and
messages/en.json, then use useTranslations in the client component for the
title, regenerate button, empty state, default rotation error, clinic fallback,
importer label, date format text, and explanatory footer. Replace every
hard-coded user-facing string while preserving the existing behavior and
formatting.
- Around line 34-49: Encapsule l’appel à regenerateCarnetCode dans rotateCode
avec un try/finally afin que setIsRotating(false) soit toujours exécuté, y
compris en cas de rejet ou d’exception. Conserve le traitement actuel des
résultats réussis et échoués, et ajoute la gestion de l’exception pour
renseigner rotationError avec un message utilisateur approprié.

In `@supabase/migrations/014_journalisation_et_rotation_carnets.sql`:
- Around line 55-73: Update rotate_patient_carnet_code to set success only when
the UPDATE actually affects a row by checking FOUND immediately after the
UPDATE. Keep retrying on unique_violation or an unsuccessful update, and return
the generated code only after patient_carnets was successfully modified.
- Around line 75-79: Accordez explicitement le privilège EXECUTE sur la fonction
rotate_patient_carnet_code(UUID) au rôle service_role après les révocations
existantes, afin que les appels serveur via adminDb.rpc(...) restent autorisés.
- Around line 44-45: Mettre à jour la policy RLS
carnet_import_events_select_clinic pour restreindre SELECT aux utilisateurs de
la clinique dont le rôle est owner ou super_admin, en reprenant la même
condition de rôle que getCarnetImportHistory, tout en conservant la vérification
clinic_id liée à auth.uid().

---

Nitpick comments:
In `@actions/patients.ts`:
- Around line 327-332: Remove the `as any` cast from `adminDb` in the
`createAdminClient` flow and type the client with the generated `Database`
schema type, preserving type checking for the `rotate_patient_carnet_code` RPC
call and its returned `data` and `error` values.

In `@supabase/migrations/014_journalisation_et_rotation_carnets.sql`:
- Around line 22-29: Update the carnet_id foreign key in carnet_import_events to
preserve journal rows when the referenced patient_carnets record is deleted,
using the existing nullable-reference pattern with ON DELETE SET NULL; adjust
the column definition to allow NULL while keeping the other constraints
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 073849f1-47a3-458d-be9b-288103537c4f

📥 Commits

Reviewing files that changed from the base of the PR and between ded1dfe and 2251980.

📒 Files selected for processing (4)
  • actions/patients.ts
  • app/(app)/app/patients/[id]/page.tsx
  • components/patients/carnet-access-panel.tsx
  • supabase/migrations/014_journalisation_et_rotation_carnets.sql

Comment thread components/patients/carnet-access-panel.tsx
Comment thread components/patients/carnet-access-panel.tsx
Comment thread supabase/migrations/014_journalisation_et_rotation_carnets.sql Outdated
Comment thread supabase/migrations/014_journalisation_et_rotation_carnets.sql
Comment on lines +75 to +79
-- La fonction n'est jamais appelée directement depuis le navigateur : seul le
-- rôle de service l'exécute, après contrôle applicatif.
REVOKE ALL ON FUNCTION rotate_patient_carnet_code(UUID) FROM PUBLIC;
REVOKE ALL ON FUNCTION rotate_patient_carnet_code(UUID) FROM anon;
REVOKE ALL ON FUNCTION rotate_patient_carnet_code(UUID) FROM authenticated;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Chercher un ALTER DEFAULT PRIVILEGES ou GRANT EXECUTE existant pour service_role sur les routines.
rg -n -i 'default privileges|grant.*execute.*service_role|grant all on all routines' supabase/migrations

Repository: Zoubeir23/DocFlowAI

Length of output: 667


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate migration files =="
git ls-files 'supabase/migrations/*.sql' | sort -V | sed -n '1,40p'

echo
echo "== rotation function references =="
rg -n 'rotate_patient_carnet_code|REVOKE ALL ON FUNCTION|grant[[:space:]]+execute[[:space:]]+on[[:space:]]+function|alter default privileges' supabase/migrations -i

echo
echo "== target migration header and security lines =="
sed -n '1,120p' supabase/migrations/014_journalisation_et_rotation_carnets.sql | cat -n

echo
echo "== policy/call path =="
fd -a 'patients\.(ts|tsx)$' . | sed 's#^\./##' | while read -r f; do
  echo "--- $f"
  rg -n 'rotate_patient_carnet_code|rpc\(' "$f"
done

echo
echo "== security hardening relevant lines =="
sed -n '60,105p' supabase/migrations/005_security_hardening.sql | cat -n

echo
echo "== service_role grants across migrations =="
python3 - <<'PY'
from pathlib import Path
import re
for p in sorted(Path("supabase/migrations").glob("*.sql")):
    txt=p.read_text(errors="replace")
    lines=[(n,l) for n,l in enumerate(txt.splitlines(),1) if re.search(r'grant\s+execute\s+on\s+function\s+[^;\n]+service_role', l.strip(), re.I)]
    if lines:
        print(f"{p}")
        for n,l in lines:
            print(f"  {n}: {l.strip()}")
PY

Repository: Zoubeir23/DocFlowAI

Length of output: 10212


Accordez EXECUTE à service_role pour rotate_patient_carnet_code.

Ce fichier crée la fonction puis révoque PUBLIC, anon et authenticated, mais il ne restaure pas le droit pour service_role. Appelée côté serveur via adminDb.rpc(...), cette fonction doit explicitement accorder GRANT EXECUTE ON FUNCTION rotate_patient_carnet_code(UUID) TO service_role, comme pour les autres fonctions sécurisées.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/014_journalisation_et_rotation_carnets.sql` around lines
75 - 79, Accordez explicitement le privilège EXECUTE sur la fonction
rotate_patient_carnet_code(UUID) au rôle service_role après les révocations
existantes, afin que les appels serveur via adminDb.rpc(...) restent autorisés.

@Zoubeir23
Zoubeir23 merged commit bbd59a7 into main Aug 2, 2026
5 checks passed
@Zoubeir23
Zoubeir23 deleted the Feature/068-journalisation-et-rotation-des-carnets branch August 2, 2026 06:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants