Feat: journalisation des accès au carnet et rotation du code porteur - #68
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 20 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughLe 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. ChangesAccès et traçabilité des carnets
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
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
actions/patients.ts (1)
327-332: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueÉviter
as anysur le client d'administration.
adminDbest typéany, ce qui désactive la vérification de type suradminDb.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 typercreateAdminClient()plutôt que de recourir àany.As per coding guidelines: « Ne pas utiliser
anyet ne pas laisser deconsole.logen 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 | 🔵 TrivialVérifier la survie du journal en cas de suppression du carnet.
carnet_idutiliseON DELETE CASCADE. Sipatient_carnetssupprime 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.
EnvisagezON DELETE SET NULL(comme pourpatient_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
📒 Files selected for processing (4)
actions/patients.tsapp/(app)/app/patients/[id]/page.tsxcomponents/patients/carnet-access-panel.tsxsupabase/migrations/014_journalisation_et_rotation_carnets.sql
| -- 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; |
There was a problem hiding this comment.
🔒 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/migrationsRepository: 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()}")
PYRepository: 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.
Contexte
Deuxième des quatre points de dette relevés lors de l'audit signature / carnet / ordonnance.
Le code
CAR-XXXXXXXXXXXXXXXXest un jeton porteur : quiconque le détient peut, viaimportPatientCarnet, rattacher le patient à sa clinique et lire tout l'historique médical validéinter-cliniques. Deux manques en découlaient :
quand ? » — question à laquelle un patient a le droit d'obtenir une réponse.
Ce qui change
Migration 014
carnet_import_events:carnet_id,clinic_id,imported_by_user_id,patient_id,created_at. On journalise lecarnet_id, pas le code — journaliser un jeton porteurreviendrait à le dupliquer dans une table de plus.
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.
rotate_patient_carnet_code(uuid)enSECURITY DEFINER, avec le même format et la mêmegarantie d'unicité que le trigger de la migration 004. Droits révoqués pour
public,anonetauthenticated: 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'importn'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 autresfichiers d'actions.
getCarnetImportHistory(patientId)— historique, réservé au rôle médical de la clinique dupatient : 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.Interface —
CarnetAccessPanelsur 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 encours 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— proprenpm run lint— le nouveau composant n'ajoute aucune erreur ; les 7 erreurs depatients/[id]/page.tsxsont préexistantes (vérifié par comparaison avec la version stashée)npx vitest run— 199 testsnpm run build— succès["patient", patientId]vérifiée : le code affiché se rafraîchit après rotationLimite : 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'estnpm 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_idplutôt que rôle de clinique) et une entrée dans l'interface du portail. À fairedans une PR dédiée si vous le souhaitez.
Summary by CodeRabbit