# #196 — Partage dirigé entre utilisateurs > **Statut :** livré | **Ouvert :** 2026-10-10 | **Type :** feature (sharing + auth) ## Problème Le seul partage existant (#85) était un lien **public** : quiconque possède l'URL lit le document, sans compte ni session. Il n'existait aucun moyen de partager un document *à un utilisateur précis* de la plateforme — un utilisateur ne pouvait ni adresser un document à un autre, ni consulter les documents qu'on lui adresse, ni (côté créateur) retirer ce partage. ## Décision d'architecture Deux formes étaient possibles : 1. **Réutiliser le partage par token existant, dirigé** (retenu) — le record `data/shares.json` gagne `shared_with: [usernames]`. Si la liste est non vide, les pages `/s/{token}` (et `/pdf`, `/raw`) exigent une session et n'acceptent que créateur + admin + destinataires. Vide/absent : le comportement public historique est inchangé (rétrocompatibilité totale, zéro migration — les shares existants n'ont pas le champ). 2. Octroyer le vault entier au destinataire (`update_user {"vaults": …}`) (écartée) — accès lecture-**écriture** au vault complet au lieu d'un document en lecture seule ; c'est le modèle « collaborateur de vault », pas « partager un document ». Reste la bonne réponse pour ce besoin-là. Points de sécurité délibérés : - **404, jamais 403/401, sur la gate** — un visiteur sans session (ou un utilisateur non destinataire) apprend le même « Share not found or expired » qu'un token inventé : l'existence d'un token dirigé n'est pas confirmable de l'extérieur. - **`GET /api/shares` scopé** (correctif au passage) — un non-admin ne voit que les partages qu'il a créés **ou reçus`** (avant : tout le monde voyait tous les partages, fuite préexistante devenue intenable avec du user-to-user). Un admin voit tout. - **Révocation réservée au créateur/admin** — un destinataire ne peut pas casser l'accès des autres. - Les **destinataires sont validés** à la création (`get_user`) : pas de lien mort vers un compte inexistant. ## Mise en place | Élément | Où | |---|---| | Store : champ `shared_with`, param de `create_share` | `backend/share.py` | | Gate `_gate_share()` + branchement `/s/{token}`, `/s/{token}/pdf`, `/s/{token}/raw` | `backend/routers/sharing.py` | | Scope de `GET /api/shares` (non-admin = créés + reçus) | `backend/routers/sharing.py`, `backend/share.py::list_shares(user=…)` | | Révocabilité créateur/admin uniquement | `backend/routers/sharing.py::api_share_revoke` | | Validation des destinataires à la création | `backend/routers/sharing.py::api_share_create` | | `ShareModel.shared_with` | `backend/schemas.py` | | Dialogue : radio public/dirigé + champ destinataires (virgules) | `frontend/js/config.js::openShareDialog` | | Destinataires affichés dans la vue « déjà partagé » | `frontend/js/config.js` | | Dashboard : cartes reçues (« Partagé par X », ouverture `/s/`, pas de bouton révoquer) | `frontend/js/dashboard.js::DashboardSharedWidget` | | i18n FR/EN (6 clés config.* + dashboard.shared_by) | `frontend/locales/{fr,en}.json` | Ce qui est gratuit par construction : lecture seule (le destinataire ne passe jamais par l'éditeur), expiration, compteur d'accès, suivi des renommages (`update_shares_after_rename`), masquage des secrets (`redact_file_content` déjà dans les 3 routes `/s/*`), isolation des dossiers perso #194 (le destinataire ne voit jamais l'arbre du vault de l'auteur, seulement le document rendu). ## Tests `tests/test_directed_shares.py` (9) : création dirigée, destinataire inconnu → 400, anonyme → 404 / destinataire + créateur → 200, non-destinataire → 404, lien public toujours anonyme, scope de `/api/shares`, destinataire ne peut pas révoquer (403), créateur révoque, la révocation coupe l'accès. ## Limites connues - `shared_with` est une liste plate d'usernames — pas de groupes. Ajouter des groupes quand le besoin sera réel. - Pas de notification push : le destinataire découvre les partages reçus au rechargement du dashboard (badge/rafraîchissement suffisent ; SSE existe si un vrai push est demandé). - La date d'« expiration » reste globale au partage (pas par destinataire).