**Motivations:** - Align master with current codebase (token from projects/<id>/.secrets/<env>/ia_token) - Id resolution by mail To or by API token; no slug **Root causes:** - Token moved from conf.json to .secrets/<env>/ia_token; env from directory name **Correctifs:** - Server and scripts resolve project+env by scanning all projects and envs **Evolutions:** - tickets-fetch-inbox routes by To address; notary-ai agents and API doc updated **Pages affectées:** - ai_working_help/server.js, docs, project_config.py, lib/project_config.sh - projects/README.md, lecoffreio/docs/API.md, gitea-issues/tickets-fetch-inbox.py
78 KiB
Architecture LeCoffre.io
Dernière mise à jour : 2026-01-28 Version : 2.1.0
Référence unique (checks de déploiement) : docs/DEPLOYMENT.md#cartographie-des-checks-de-déploiement-source-unique
Ce document décrit l'architecture complète de LeCoffre.io : infrastructure physique, architecture logicielle, configuration des serveurs, types d'utilisateurs, rôles et permissions, gestion multi-office, et ancrage blockchain.
📋 Table des Matières
- Architecture Physique
- Architecture Logicielle (Monorepo)
- Configuration Initiale des Serveurs
- Environnements
- Services Externes
- Types d'Utilisateurs
- Architecture Rôles & Règles
- Matrice des Permissions
- JWT Multi-Office
- Évolution planifiée - Documents unifiés
- Partage Inter-Études
- Gestion Multi-Office
- Ancrage Blockchain
Architecture Physique
Infrastructure Serveurs
LeCoffre.io est déployé sur une infrastructure privée (réseau 192.168.1.x) avec un proxy public en point d'entrée unique.
VM Environnements (Application)
| Environnement | IP | Domaine | Répertoire |
|---|---|---|---|
| test | 192.168.1.101 |
test.lecoffreio.4nkweb.com |
/srv/4NK/test.lecoffreio.4nkweb.com |
| pprod | 192.168.1.102 |
pprod.lecoffreio.4nkweb.com |
/srv/4NK/pprod.lecoffreio.4nkweb.com |
| prod | 192.168.1.103 |
prod.lecoffreio.4nkweb.com |
/srv/4NK/prod.lecoffreio.4nkweb.com |
Proxy (point d'entrée) :
- IP privée :
192.168.1.100 - Accès externe :
4nk.myftp.biz - Rôle : terminaison SSL + routage Nginx vers les ports applicatifs des serveurs
101/102/103.
Services déployés sur chaque serveur environnement (host-native, systemd) :
- PostgreSQL (service système)
- ClamAV (service système)
- Backend (systemd)
- Cron (systemd)
- Frontend (systemd)
- Routeur HTTP local (Node) sur le port "service" (systemd)
Important :
- Il n'y a pas de Nginx ni Certbot sur
101/102/103. - Les déploiements "v2" sont gérés par
deploy/scripts_v2/(orchestrés depuis le proxy).
VM Git (Repository)
Serveur Git : git.4nkweb.com
Repository : git@git.4nkweb.com:4nk/lecoffreio.git
Rôle :
- Stockage du code source (monorepo)
- Versioning Git
- Synchronisation entre machines locales et serveurs de déploiement
Workflow :
- Développement local → commit/push vers Git
- Script de déploiement → pull depuis Git sur VM environnement
- Build et déploiement sur VM environnement
VM Base de Données
PostgreSQL externe :
- Host : Variable selon environnement (
DATABASE_HOSTdans.env.<env>) - Port : 5432 (standard PostgreSQL en mode host-native)
- Utilisateurs :
lecoffre-user-{env}(un par environnement) - Bases :
bdd-{env}(une par environnement)
Accès :
- Depuis VM environnements sur l'hôte
- Depuis machines locales via SSH tunnel (optionnel)
VM API Ancrage Bitcoin (Séparée)
Serveur : dev3.4nkweb.com
Host SSH : <USER>@31.33.24.235
Répertoire : /home/<USER>/dev/lecoffre-anchor-api
Port : 3002
Health : http://31.33.24.235:3002/health
Rôle :
- API d'ancrage blockchain Bitcoin Signet
- Service séparé pour isolation et scalabilité
- Appelée par le backend principal pour les ancrages
Réseau et Communication
┌─────────────────────────────────────────────────────────────┐
│ Machine Dev (externe) │
│ │
│ ┌──────────────┐ │
│ │ Git Client │ ────SSH───> Proxy (4nk.myftp.biz) │
│ └──────────────┘ │
│ │
│ ┌──────────────┐ │
│ │ Deploy Script│ ────SSH───> Proxy (orchestrateur) │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
│ SSH (réseau privé)
▼
┌─────────────────────────────────────────────────────────────┐
│ Serveurs Environnements (101/102/103) │
│ │
│ ┌──────────────┐ │
│ │ Git Pull │ <───SSH─── Git (git.4nkweb.com) │
│ └──────────────┘ │
│ │
│ ┌──────────────┐ │
│ │ systemd │ - backend / cron / frontend / router │
│ └──────────────┘ │
│ │ │
│ │ PostgreSQL (port 5432) │
│ ▼ │
│ ┌──────────────┐ │
│ │ PostgreSQL │ (VM Base de Données externe) │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
Persistance des Données
| Type | Emplacement | Persistance |
|---|---|---|
| Code source | VM Git (git.4nkweb.com) | ✅ Git repository |
| Code déployé | VM Environnement (/home/debian/sites/...) |
✅ Host filesystem |
| Base de données | VM PostgreSQL externe | ✅ PostgreSQL |
| Certificats SSL | VM Environnement (deploy/nginx/certbot/) |
✅ Host filesystem (bind mount) |
| Configuration Nginx | VM Environnement (deploy/nginx/nginx-*.conf) |
✅ Host filesystem (bind mount) |
| Backups BDD | VM Environnement (deploy/backups/) |
✅ Host filesystem |
| Backups certificats | VM Environnement (deploy/certificats/) |
✅ Host filesystem |
| Logs | VM Environnement (logs/) |
✅ Host filesystem (collectés par Promtail) |
| Build backend/frontend | VM Environnement | ⚠️ Rebuild à chaque déploiement |
| node_modules | VM Environnement (dans le build) | ❌ Non persistant (rebuild) |
Architecture Logicielle (Monorepo)
Structure du Monorepo
LeCoffre.io est organisé en monorepo contenant 4 sous-projets principaux :
lecoffreio/ # Repository global (ce repo)
│
├── lecoffre-back-main/ # Backend API (Express + TypeScript)
│ ├── src/ # Code source TypeScript
│ │ ├── app/ # Controllers & Middlewares
│ │ │ ├── api/ # Routes API (notary, admin, customer, public)
│ │ │ └── middleware/ # Middlewares (auth, rate limiting, CORS)
│ │ ├── services/ # Business Logic
│ │ │ ├── admin/ # Services admin
│ │ │ ├── common/ # Services communs (IdNot, Annuaire, Stripe, Bitcoin)
│ │ │ ├── customer/ # Services clients
│ │ │ ├── notary/ # Services notaires
│ │ │ └── super-admin/ # Services super-admin
│ │ ├── common/ # Utilities & Repositories
│ │ │ ├── databases/ # Prisma schema & migrations
│ │ │ ├── dtos/ # DTOs validation
│ │ │ ├── emails/ # Services email
│ │ │ ├── repositories/ # Couche d'accès données
│ │ │ └── utils/ # Helpers centralisés
│ │ └── entries/ # Entry points
│ │ └── App.ts # Point d'entrée API
│ ├── dist/ # Build JavaScript (généré)
│ ├── prisma/ # Migrations base de données
│ └── package.json # Dependencies backend
│
├── lecoffre-front-main/ # Frontend Web (Next.js + React)
│ ├── src/
│ │ ├── pages/ # Pages Next.js (routes)
│ │ │ ├── folders/ # Routes dossiers
│ │ │ ├── admin/ # Routes admin
│ │ │ ├── subscription/ # Routes abonnements
│ │ │ └── ...
│ │ ├── front/ # Components React
│ │ │ ├── Components/ # UI Components
│ │ │ │ ├── DesignSystem/ # Design system (Header, Footer, etc.)
│ │ │ │ ├── Layouts/ # Layouts (Folder, Subscription, etc.)
│ │ │ │ └── Elements/ # Éléments UI (Buttons, Forms, etc.)
│ │ │ ├── Api/ # API Services (59 services)
│ │ │ ├── Hooks/ # Custom React Hooks
│ │ │ ├── Services/ # Frontend Services
│ │ │ ├── Stores/ # State management (MobX)
│ │ │ └── Utils/ # Utilitaires
│ │ └── proxy.ts # Next.js Proxy (auth)
│ ├── public/ # Assets statiques
│ └── package.json # Dependencies frontend
│
├── lecoffre-ressources-dev/ # Resources TypeScript partagées
│ ├── src/
│ │ ├── Admin/ # Types Admin (22 types)
│ │ ├── Notary/ # Types Notary (29 types)
│ │ ├── Customer/ # Types Customer (15 types)
│ │ └── SuperAdmin/ # Types SuperAdmin (20 types)
│ └── package.json # Dependencies resources
│
├── lecoffre-anchor-api/ # API d'ancrage Bitcoin (serveur séparé)
│ ├── src/
│ │ ├── config/ # Configuration logger
│ │ ├── controllers/ # Controllers API ancrage
│ │ ├── services/ # Bitcoin + Queue services
│ │ └── types/ # Types ancrage
│ └── package.json # Dependencies anchor API
│
├── docs/ # Documentation technique
├── todoFix/ # Corrections et fixes documentés
├── IA_agents/ # Règles pour IA
├── deploy/ # Déploiement et outils
│ ├── scripts/ # Scripts de déploiement
│ ├── nginx/ # Configuration Nginx
│ ├── monitoring/ # Configuration Grafana/Loki
│ └── scripts_v2/ # Scripts déploiement host-native
├── logs/ # Logs de déploiement
├── VERSION # Version actuelle
├── CHANGELOG.md # Historique des versions
└── README.md # Documentation principale
Rôles des Sous-Projets
| Sous-Projet | Rôle | Technologies | Port |
|---|---|---|---|
| lecoffre-back-main | API REST, authentification, business logic | Express, TypeScript, Prisma, PostgreSQL | 3001 |
| lecoffre-front-main | Interface web responsive, SSR | Next.js 14, React 18, TypeScript, SCSS | 3000 |
| lecoffre-ressources-dev | Types TypeScript partagés | TypeScript | - |
| lecoffre-anchor-api | API d'ancrage blockchain Bitcoin Signet | Express, TypeScript, Bitcoin RPC | 3002 |
Dépendances Inter-Projets
lecoffre-back-main
└── lecoffre-ressources-dev (types partagés)
lecoffre-front-main
└── lecoffre-ressources-dev (types partagés)
lecoffre-anchor-api
└── (indépendant)
Build order :
lecoffre-ressources-dev: Compilation TypeScript → 87 fichiers.jslecoffre-back-main: Utilise les types compiléslecoffre-front-main: Utilise les types compiléslecoffre-anchor-api: Build indépendant (serveur séparé)
Communication Inter-Services
Backend ↔ Frontend
- Protocole : HTTP/HTTPS
- Format : JSON (REST API)
- Authentification : JWT (Bearer token)
- Endpoints :
/api/v1/{notary|admin|customer|public}/...
Backend ↔ Anchor API
- Protocole : HTTP
- Format : JSON
- Authentification : API Key (
ANCHORE_API_KEY) - Endpoint :
http://<DOMAINE_BITCOIN>:3002/anchor
Backend ↔ Services Externes
- PostgreSQL : Prisma ORM (port 5432 en host-native)
- IdNot : OAuth2 (HTTPS)
- API Annuaire : Basic Auth (HTTPS)
- Stripe : API Key (HTTPS)
- Pinata (IPFS) : API Key (HTTPS)
- Mailchimp : API Key (HTTPS)
- OVH SMS : API Key (HTTPS)
- ClamAV : TCP (port 3310)
Configuration Initiale des Serveurs
Vue d'ensemble
Le script setup-all-servers.sh permet de configurer automatiquement toutes les machines distantes avec :
- Configuration de
/etc/hostspour la résolution de noms - Configuration sudo NOPASSWD pour l'utilisateur
ncantu - Installation des outils système (git, curl, wget)
- Installation et configuration des services (Node, PostgreSQL, etc.)
Serveurs configurés :
test(192.168.1.101) : Environnement de testpprod(192.168.1.102) : Pré-productionprod(192.168.1.103) : Production
Point d'entrée : Proxy (4nk.myftp.biz) via SSH ProxyJump
Prérequis
Sur la machine locale :
- Accès SSH au proxy (4nk.myftp.biz)
- Clé SSH configurée pour l'utilisateur
ncantu - Bash disponible (Git Bash sur Windows)
- Mot de passe sudo des serveurs distants :
picnic1280
Sur les serveurs distants :
- Accès SSH depuis le proxy
- Utilisateur
ncantuavec droits sudo - Connexion Internet pour télécharger les paquets
Script de configuration
Fichier : deploy/scripts/backup/setup-all-servers.sh
Description : Script bash qui se connecte à chaque serveur via SSH (ProxyJump) et exécute un script de configuration distant.
Fonctionnalités
1. Configuration de /etc/hosts
Ajoute les entrées suivantes dans /etc/hosts de chaque serveur :
192.168.1.100 proxy
192.168.1.101 test
192.168.1.102 pprod
192.168.1.103 prod
192.168.1.104 services
Avantages :
- Résolution de noms locale sans dépendre du DNS
- Accès rapide entre serveurs
- Indépendance du réseau externe
2. Configuration sudo NOPASSWD
Configure l'utilisateur ncantu pour utiliser sudo sans mot de passe :
ncantu ALL=(ALL) NOPASSWD: ALL
Fichier créé : /etc/sudoers.d/ncantu-nopasswd
Sécurité : Cette configuration permet à l'utilisateur ncantu d'exécuter toutes les commandes sudo sans mot de passe. À utiliser uniquement sur des serveurs d'infrastructure avec accès SSH sécurisé.
3. Installation des outils système
Installe les outils suivants via apt-get (Debian/Ubuntu) ou yum (CentOS/RHEL) :
- git : Gestion de versions
- curl : Téléchargement de fichiers
- wget : Téléchargement de fichiers (alternative)
4. Prérequis serveur
Le script vérifie et installe les prérequis selon la distribution (Debian/Ubuntu) : git, curl, wget, Node.js, PostgreSQL, etc. Voir DEPLOYMENT.md et section "Configuration Initiale des Serveurs" ci-dessus.
Utilisation
Exécution du script :
cd d:\code\lecoffreio
bash deploy/scripts/backup/setup-all-servers.sh
Configuration :
Le script contient les variables suivantes (modifiables si nécessaire) :
PROXY_HOST="ncantu@4nk.myftp.biz"
SUDO_PASSWORD="picnic1280"
SERVERS=(
"192.168.1.101:test"
"192.168.1.102:pprod"
"192.168.1.103:prod"
)
Architecture SSH
Le script utilise SSH ProxyJump pour se connecter aux serveurs backend via le proxy :
Client Local → Proxy (4nk.myftp.biz) → Serveur Backend (192.168.1.10x)
Commande SSH :
ssh -J ncantu@4nk.myftp.biz ncantu@192.168.1.101
Vérifications post-configuration
Vérification manuelle :
# Se connecter au serveur
ssh -J ncantu@4nk.myftp.biz ncantu@192.168.1.101
# Vérifier /etc/hosts
cat /etc/hosts | grep "192.168.1.10"
# Vérifier sudo NOPASSWD
sudo -n true && echo "OK" || echo "KO"
# Vérifier les outils
which git curl wget node
node --version
Notes importantes :
- ⚠️ Le script configure sudo NOPASSWD pour toutes les commandes
- ⚠️ À utiliser uniquement sur des serveurs d'infrastructure avec accès SSH sécurisé
- ⚠️ Le mot de passe sudo est codé en dur dans le script (à modifier si nécessaire)
- Le script est idempotent : peut être exécuté plusieurs fois sans problème
Environnements
Environnements Disponibles
| Environnement | Usage | Domaine | Base de Données |
|---|---|---|---|
| test | Tests et développement | test-lecoffreio.4nkweb.com |
bdd-test |
| pprod | Pré-production | pprod-lecoffreio.4nkweb.com |
bdd-pprod |
| prod | Production | prod-lecoffreio.4nkweb.com |
bdd-prod |
| demo | Démonstration | demo-lecoffreio.4nkweb.com |
bdd-demo |
Configuration par Environnement
Chaque environnement a sa propre configuration (legacy vs scripts_v2) :
# scripts_v2 (host-native, recommandé)
DEPLOY_SSH_USER=ncantu
DEPLOY_SSH_KEY=~/.ssh/id_ed25519
DEPLOY_SSH_PROXY_HOST=4nk.myftp.biz
# Base de données (souvent locale au host env)
DATABASE_HOST=127.0.0.1
DATABASE_PORT=5432
DATABASE_USERNAME=lecoffre-user-<env>
DATABASE_PASSWORD=***
DATABASE_NAME=bdd-<env>
Services Externes
Services Utilisés
| Service | Usage | Configuration |
|---|---|---|
| PostgreSQL | Base de données principale | Port 5432 (host-native) |
| Bitcoin Signet | Ancrage blockchain | RPC (port 38332) |
| IPFS Pinata | Stockage décentralisé fichiers | API Key |
| ClamAV | Scan antivirus uploads | Service système (TCP 3310) |
| Mailchimp | Emails transactionnels | API Key |
| OVH SMS | Notifications SMS | API Key |
| Stripe | Gestion paiements | API Key |
| IdNot | OAuth notarial | OAuth2 |
| API Annuaire | Synchronisation offices/personnes | Basic Auth |
Configuration Dynamique
Les configurations sensibles sont stockées dans la base de données (system_configuration) plutôt que dans les fichiers .env :
- 109 configurations disponibles
- Injection : Via
npm run config:import-env -- --env <env> - Lecture : Via
BackendVariables(backend) - Sécurité : Masquage des valeurs sensibles dans les logs
Voir DATABASE_COMPLETE.md pour plus de détails sur system_configuration.
Types d'Utilisateurs
Le système LeCoffre gère 4 types d'utilisateurs avec des mécanismes d'authentification et d'autorisation différents.
1. Notaires authentifiés IdNot (avec abonnement)
Caractéristiques :
- Authentification via IdNot OAuth
- Stockés dans table
users - Possèdent un
office_role_uid(lié à leur rôle dans l'office) - Abonnement actif requis pour accès complet
- Accès via JWT contenant :
userId,office_Id,role(global),rules(role + office_role),isSuperAdmin(boolean, siusers.is_super_admin === true)
Rôles possibles :
roleglobal :admin,super-admin,notary,defaultoffice_role:Notaire,Collaborateur(lié à l'office)
Règles d'accès :
- JWT contient toutes les règles combinées (
role.rules+office_role.rules) - Accès complet selon règles assignées
- Exemple :
POST rib,GET folders,DELETE documents, etc.
Création :
- Connexion IdNot OAuth
- Appel API IdNot
/api/pp/v2/rattachements/{profile_idn} - Création automatique de l'
officesi n'existe pas - Duplication
office_rolesdepuis Office Template (idNot "0000") - Assignment automatique
office_roleselontypeLien.nameIdNot :NOTAIRE_TITULAIRE,NOTAIRE_ASSOCIE,NOTAIRE_SALARIE→ "Notaire"COLLABORATEUR,SUPPLEANT,ADMINISTRATEUR,CURATEUR→ "Collaborateur"
À la connexion (mise à jour) : les données contact (nom, prénom, téléphones, civilité) sont synchronisées depuis IdNot ; l’email en base n’est pas mis à jour s’il existe déjà (les notaires conservent l’email stocké). Implémentation : IdNotService.updateContactFromRattachement (condition !normalizedCurrentEmail) et AffiliationSyncContactDataExtractionHelper.buildContactUpdateDataFromComparison (condition newEmail && !currentEmail).
2. Notaires visiteurs (authentifiés IdNot, SANS abonnement)
Caractéristiques :
- Identiques aux notaires authentifiés IdNot mais sans abonnement actif
hasActiveSubscription === falsedans le payload JWT- Nouveau flag JWT :
isVisitor = true - Le rôle d'office renvoyé au frontend est forcé à
Visiteur - L'UI affiche un badge
Visiteur(OfficeSelector) au lieu de(invité)
Règles d'accès :
- Filtrage serveur → seuls les endpoints
GET *et le parcours d'abonnement restent autorisés :GET /api/v1/admin/subscriptionsGET /api/v1/admin/subscription-plansPOST /api/v1/admin/stripePUT /api/v1/admin/subscriptionsGET /api/v1/admin/stripe/:uid/client-portal
- Toutes les autres règles (POST/PUT/DELETE dossiers, documents, etc.) sont retirées du JWT
- Les contrôles frontend reposent donc uniquement sur
jwt.rules(plus de bouton "Créer un dossier")
Effets frontend :
- Badge
Visiteurdans le sélecteur d'étude (OfficeSelector) - Absence du CTA "Créer un dossier" et des actions de modification
- Parcours abonnement toujours visible (composants conditionnés sur
jwt.rules) - Lorsqu'un notaire invité ouvre un dossier partagé, l'interface monte automatiquement le template client (
DefaultCustomerDashboard) pour éviter les états parasites issus des composants notaire (onglets, CTA, hooks permissions) et garantir une lecture seule stricte. - La liste complète des dossiers invités et la vue détail utilisent maintenant la même expérience que les clients (sections "Documents envoyés / reçus" basées sur
DocumentTables), sans onglets notaire ni actions d'édition.
But :
- Permettre à un notaire sans abonnement actif de naviguer en lecture seule et de souscrire
- Faciliter l'onboarding multi-office
3. Clients (Customers)
Caractéristiques :
- Authentification via email + password ou code 2FA
- Stockés dans table
customers(PAS dansusers) - Pas de
office_role_uid(n'existent pas dansusers) - JWT différent :
ICustomerJwtPayloadaveccustomerIdetemail
Règles d'accès :
- Endpoints dédiés préfixés
/api/v1/customer/* - Authentification via
customerAuthHandler(différent deauthHandler) - Accès limité à :
- Leurs propres dossiers
- Leurs propres documents
- Upload de documents demandés
Création :
- Notaire crée un client via
POST /api/v1/notary/customers - Client reçoit email avec code 2FA
- Client se connecte via
/customer-login - Définit son mot de passe (première connexion)
4. Tiers (Third Parties)
Caractéristiques :
- Quasiment identiques aux clients
- Authentification via email + code 2FA (pas de mot de passe permanent)
- Stockés dans table
folder_third_parties - Liés à un dossier spécifique
Règles d'accès :
- Endpoints dédiés :
/api/v1/third-party/* - Authentification via code 2FA à usage unique
- Accès limité à :
- Le dossier auquel ils sont rattachés
- Upload de documents demandés par le notaire
Création :
- Notaire ajoute un tiers via
POST /api/v1/notary/folders/:folderUid/third-parties - Tiers reçoit email avec code 2FA
- Tiers se connecte via
/third-party/login - Accès temporaire au dossier
Accès aux dossiers :
- Les tiers n'ont pas d'
activeOfficeUid(contrairement aux notaires) - L'accès au dossier est vérifié via le
folderUiddans le JWT - Les contrôleurs doivent gérer ce cas spécial : vérifier
folderUidau lieu deactiveOfficeUidpour les tiers - Exemple :
FolderThirdPartiesController.verifyFolderAccess()vérifiepermissionContext.folderUidpour les tiers
Différence avec clients :
- Clients : liés à l'office, multiples dossiers possibles
- Tiers : liés à UN dossier spécifique, accès temporaire
Résumé des Types d'Utilisateurs
| Type | Table | Auth | office_role_uid | Abonnement | Accès |
|---|---|---|---|---|---|
| Notaire avec abo | users |
IdNot OAuth | ✅ | ✅ Requis | Complet selon rules |
| Notaire invité | users |
IdNot OAuth | ✅ | ❌ Non | Limité + Gestion abo |
| Client | customers |
Email/2FA | ❌ | N/A | Ses dossiers seulement |
| Tiers | folder_third_parties |
2FA | ❌ | N/A | 1 dossier seulement |
| Template users | users (Office 0000) |
N/A | ❌ | N/A | Jamais connectés |
Architecture Rôles & Règles
Hiérarchie
┌─────────────────────────────────────────────────────────────┐
│ GLOBAL │
├─────────────────────────────────────────────────────────────┤
│ roles (table) │
│ ├─ admin │
│ ├─ super-admin │
│ ├─ notary │
│ └─ default │
│ │
│ _RolesHasRules (liaison) │
│ ├─ role.uid → rules.uid │
│ └─ Ex: super-admin → [ALL RULES] │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ PAR OFFICE │
├─────────────────────────────────────────────────────────────┤
│ office_roles (table) │
│ ├─ Notaire (office A) │
│ ├─ Collaborateur (office A) │
│ ├─ Notaire (office B) │
│ └─ Collaborateur (office B) │
│ │
│ _OfficeRolesHasRules (liaison) │
│ ├─ office_role.uid → rules.uid │
│ └─ Ex: Notaire (office A) → [POST rib, GET folders, ...] │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ UTILISATEUR │
├─────────────────────────────────────────────────────────────┤
│ users.roles_uid → role (global) │
│ users.office_role_uid → office_role (spécifique office) │
│ │
│ JWT contains: │
│ rules = role.rules + office_role.rules (MERGED) │
│ hasActiveSubscription (boolean) │
│ isVisitor (boolean) │
│ isSuperAdmin (boolean, si users.is_super_admin === true) │
└─────────────────────────────────────────────────────────────┘
Office Template (idNot "0000")
Rôle :
- Office de référence contenant les données par défaut
- Utilisé pour dupliquer
office_roles,deed_types,document_typesvers nouveaux offices
Contenu :
office_roles:Notaire,Collaborateuravec leurs règlesdeed_types: Types d'actes par défautdocument_types: Types de documents par défaut
Users dans Office Template :
- 4 users de test avec
idNotinvalides (ex:rflrefrjf) - N'ont PAS de
office_role_uid(c'est NORMAL) - Ne se connectent jamais à l'application
- Servent uniquement de données de référence
Flux d'autorisation
Notaires (avec/sans abonnement)
Request → authHandler → ActiveOfficeInjector → ruleHandler → Controller
↓ ↓ ↓
JWT verify office_uid inject check rule
req.user req.activeOfficeUid req.user.rules.includes(requiredRule)
Bypass ruleHandler :
- Super-admin : Accès à toutes les routes si
isSuperAdmin === true(champ séparé dans la tableusers.is_super_admin) OUrole === "super-admin"(rétrocompatibilité) - Les middlewares (
roleHandler,ruleHandler) et les controllers super-admin vérifient les deux conditions - Endpoints
/admin/subscriptions,/admin/stripe,/admin/subscription-plans→ pas deruleHandler
Clients
Request → customerAuthHandler → Customer endpoints only
↓
JWT verify (ICustomerJwtPayload)
req.body.customer
Tiers
Request → thirdPartyAuthHandler → Third Party endpoints only
↓
2FA code verify
req.body.thirdParty
Règles spéciales
Endpoints SANS ruleHandler
Abonnements (accessibles à TOUS les notaires authentifiés) :
GET /api/v1/admin/subscriptionsGET /api/v1/admin/subscription-plansPOST /api/v1/admin/stripe(création checkout)GET /api/v1/admin/stripe/:uid/client-portal
Raison : Permettre aux notaires invités (sans abonnement) de s'abonner.
Endpoints sensibles AVEC ruleHandler
RIB (données bancaires) :
GET /api/v1/notary/rib→ règleGET ribPOST /api/v1/notary/rib→ règlePOST ribDELETE /api/v1/notary/rib→ règleDELETE rib
Assignées par défaut à :
office_role"Notaire" et "Collaborateur" (selon configuration Office Template)
Téléchargement de fichiers :
POST /api/v1/notary/files/download-multiple→ règlePOST files
Assignées par défaut à :
office_role"Notaire" et "Collaborateur" (héritage des règles documents/fichiers)
Statut Super-Admin
Double mécanisme de vérification
Le système utilise deux mécanismes pour identifier un super-admin :
- Champ
is_super_admin(tableusers) : Statut séparé du rôle, permet de promouvoir un utilisateur en super-admin sans changer son rôle global - Rôle
super-admin(tableroles) : Rôle global classique (rétrocompatibilité)
Vérification dans le code :
- Les middlewares (
roleHandler,ruleHandler) vérifient :isSuperAdmin === true || role === "super-admin" - Les controllers super-admin (ex:
HealthTestsController,SiteTextsController) utilisent la même logique - Le JWT contient
isSuperAdmin: truesiusers.is_super_admin === true
Bypass super-admin :
ruleHandler: Les super-admins bypassent la vérification des règles de permissions (accès à toutes les routes)- Validation d'accès aux dossiers : Les super-admins sont soumis aux mêmes règles que les notaires et collaborateurs :
- Vérification de l'appartenance au dossier (stakeholder)
- Vérification des partages de dossier (folder_sharing)
- Validation de l'office propriétaire
- Raison : Le statut super-admin ne joue pas sur les fonctions de validation d'accès aux dossiers. Un super-admin doit être explicitement stakeholder ou avoir accès via
folder_sharingpour accéder à un dossier. - Correction appliquée (décembre 2025) : Le bypass super-admin dans
folderAccessHelpers.ensureFolderAccess()a été supprimé. Les super-admins passent maintenant par les mêmes validations que les autres utilisateurs.
Avantages :
- Permet d'avoir un super-admin avec un rôle
notaryouadmintout en conservant les permissions super-admin - Flexibilité pour la gestion des accès sans modifier la structure des rôles
- Sécurité renforcée : les super-admins ne peuvent pas contourner les validations d'accès aux dossiers
Page /offices et synchronisation IdNot
Sur la page super-admin /offices, les actions liées aux synchronisations historiques IdNot/Annuaire ne sont plus exposées. Les recherches de confrères et les résolutions par email utilisent des appels API (IdNot/Annuaire) au moment de la demande, en complément de la base du site (users/contacts/offices) et des partages (folder_sharing).
Promotion en super-admin
La promotion se fait via :
- Script :
lecoffre-back-main/src/scripts/promote-super-admin.ts - Configuration : Variable
SUPERADMIN_EMAILSdans la BDD (déploiement avec--promoteSuperAdmins)
Cas particuliers
Multer et authHandler
Problème : multer().single() écrase req.body lors du parsing de multipart/form-data.
Solution :
authHandlerstocke le user dans 2 emplacements :req.body.user(standard)req.user(backup, non écrasé par multer)
ActiveOfficeInjectorvérifie les 2 emplacements et restaurereq.body.usersi manquant
Token d'authentification : le JWT est lu depuis l'en-tête Authorization ou, pour les flux SSE uniquement (EventSource sans en-têtes personnalisés), depuis le paramètre de requête token. L'URL est nettoyée avant d'être loggée (paramètre token masqué). Voir docs/CODE_STANDARDS.md (section Sécurité du Code).
Multi-office
- Un user peut avoir plusieurs
user_office_affiliations - Une seule affiliation est
is_primary = true - JWT généré avec l'office PRIMARY
ActiveOfficeInjectorinjectereq.body.activeOfficeUid- Les routes utilisant
activeOfficeInjectordoivent utiliserreq.body.activeOfficeUidau lieu dereq.body.user.office_Idpour supporter les super-admins et le multi-office
Important - Ordre des routes Express : Les routes spécifiques (ex: /folders/blockchain, /folders/deleted) doivent être déclarées avant les routes paramétrées (ex: /folders/:uid) pour éviter les conflits de routage. Express matche les routes dans l'ordre de déclaration, donc /folders/:uid intercepterait /folders/blockchain si déclarée en premier.
Exemple :
// ✅ Correct : route spécifique avant route paramétrée
@Get("/v1/notary/folders/blockchain", [authHandler, activeOfficeInjector, ruleHandler])
@Get("/v1/notary/folders/deleted", [authHandler, ruleHandler])
@Get("/v1/notary/folders/:uid", [authHandler, ruleHandler, folderHandler])
// ❌ Incorrect : route paramétrée avant route spécifique
@Get("/v1/notary/folders/:uid", [authHandler, ruleHandler, folderHandler])
@Get("/v1/notary/folders/blockchain", [authHandler, activeOfficeInjector, ruleHandler]) // Ne sera jamais atteinte
Base de données - Tables clés
-- Utilisateurs notaires
users {
uid, idNot, roles_uid (global), office_role_uid (spécifique),
office_uid (deprecated, use affiliations), contact_uid,
is_super_admin (boolean, statut super-admin séparé du rôle)
}
-- Affiliations multi-office
user_office_affiliations {
uid, user_uid, office_uid, is_primary, role_in_office (string),
status, idnot_sync
}
-- Clients
customers {
uid, contact_uid, office_uid, status, password, deleted_at
}
-- Tiers
folder_third_parties {
uid, folder_uid, added_by_uid, role_type, role_description,
first_name, last_name, email, phone, auth_method, email_verified
}
-- Rôles globaux
roles {
uid, name (admin, super-admin, notary, default)
}
-- Rôles par office
office_roles {
uid, name (Notaire, Collaborateur), office_uid
}
-- Règles
rules {
uid, name (ex: "POST rib", "GET folders")
}
-- Liaisons
_RolesHasRules { A: role.uid, B: rule.uid }
_OfficeRolesHasRules { A: office_role.uid, B: rule.uid }
Matrice des Permissions
Vue d'Ensemble
Le système de permissions de LeCoffre.io utilise une matrice de permissions stockée en base de données (role_permissions_matrix) qui détermine les accès pour chaque combinaison :
- Ressource (ex:
documents,folders,rib) - Action (ex:
read,create,update,delete) - Rôle (ex:
super-admin,admin,notary,collaborator,guest_notary,client,third_party) - Scope (ex:
GLOBAL,OFFICE,ASSIGNMENT) - Scoped Entity (optionnel, UID d'une entité spécifique comme un office)
Architecture de la Matrice
Table role_permissions_matrix
Structure :
- uid (PK)
- resource (string) : nom de la ressource (ex: "documents", "folders")
- action (string) : action (ex: "read", "create", "update", "delete")
- role (string) : rôle utilisateur (ex: "super-admin", "notary")
- allowed (boolean) : true = autorisé, false = refusé
- scope (string) : "GLOBAL", "OFFICE", "ASSIGNMENT"
- scoped_entity (string | null) : UID d'une entité spécifique (ex: office_uid)
- pages (JSON) : pages frontend associées
- updated_by (string | null) : UID de l'utilisateur qui a modifié
- created_at, updated_at
Types de Scope
GLOBAL :
- Accès à toutes les ressources, tous offices confondus
- Utilisé pour
super-adminetadmin(dans certains cas)
OFFICE :
- Accès limité à l'office actif de l'utilisateur
- Utilisé pour
notaryetcollaborator - Vérifié via
activeOfficeInjectormiddleware
ASSIGNMENT :
- Accès limité aux ressources assignées à l'utilisateur
- Utilisé pour
guest_notary(dossiers partagés),client,third_party - Vérifié dans les controllers via filtres sur
depositor_uid,third_party_depositor_uid,shared_to_office_uid
Flux de Vérification des Permissions
Middleware ruleHandler
Ordre d'exécution :
Request → authHandler → activeOfficeInjector → ruleHandler → Controller
Étapes dans ruleHandler :
-
Extraction du rôle utilisateur :
resolveUserRole(user)détermine le rôle effectif- Priorité :
isSuperAdmin→roleglobal →officeRole→userType
-
Résolution de la ressource et action :
resolveResource(req, service): extrait la ressource depuis le pathresolveAction(req, resource): détermine l'action (read/create/update/delete)
-
Consultation de la matrice :
permissionsService.getDecision({ resource, action, role, scoped_entity })- Retourne :
"allow","deny", ou"pass"
-
Décision finale :
- Super-admin : bypass automatique (ligne 99-112)
- "deny" : refusé (avec exceptions pour certaines ressources)
- "allow" : autorisé
- "pass" : pas d'entrée dans la matrice → fallback sur règles JWT
Service RolePermissionsMatrixService
Méthode getDecision :
-
Construction de la clé :
- Format :
resource::action::role::scoped_entity - Exemple :
documents::read::notary::-(sans scope) - Exemple :
documents::read::notary::office-uid-123(avec scope)
- Format :
-
Recherche dans le cache :
- Cache en mémoire (Map) chargé depuis la BDD au démarrage
- Recherche exacte de la clé
-
Retour :
"allow"siallowed === true"deny"siallowed === false"pass"si aucune entrée trouvée
Permissions par Type d'Utilisateur
Super-Admin
- Bypass automatique dans
ruleHandler(ligne 99-112) - Accès à toutes les ressources avec scope
GLOBAL - Endpoints
/api/v1/super-admin/*accessibles - Peut modifier la matrice des permissions
Admin (Office)
- Scope généralement
OFFICE(limité à son office actif) - Peut gérer les utilisateurs de son office
- Accès aux ressources de son office
- Matrice : alignement des permissions
adminsurcollaboratorpour les ressources notariales (migration20260305120000_backfill_admin_permissions_from_collaborator+ scriptensure-role-permissions-matrix-folders.sql).
Notary
- Scope
OFFICE: accès aux ressources de son office actif - Peut créer/modifier/supprimer dossiers, documents, RIB
- Accès complet aux fonctionnalités notariales
Collaborator
- Scope
OFFICE: accès aux ressources de son office actif - Mêmes droits que Notaire pour documents, files, folder_customer, folder_third_parties, folder_sharing, document_reminder, document_request (read, create, update, delete selon ressource). Pour folders : archive autorisé, delete non (les collaborateurs peuvent archiver mais pas supprimer un dossier).
- Matrice : script
ensure-role-permissions-matrix-folderset migration20260204120000_align_collaborator_notary_folders_members. Après déploiement :run-ensure-role-permissions-matrix-folders.sh(test | pprod | prod).
Guest Notary (Notaire Invité)
- Scope
ASSIGNMENT: accès uniquement aux dossiers partagés - Peut lire/créer/modifier documents dans les dossiers partagés
- Accès limité aux ressources assignées via
shared_to_office_uid
Client
- Scope
ASSIGNMENT: accès uniquement à ses propres dossiers et documents - Endpoints
/api/v1/customer/* - Peut lire ses documents, uploader des documents demandés
Third Party (Tiers)
- Scope
ASSIGNMENT: accès uniquement au dossier auquel il est rattaché - Endpoints
/api/v1/third-party/* - Peut lire ses documents, uploader des documents demandés
- Consultation (Mark as Viewed) : La route
PUT .../mark-as-viewedest exclue du blocage de mise à jour des documentsSENT. Cela permet au tiers de consulter un document (passant de SENT à DOWNLOADED) sans erreur 403 ni déconnexion.
Points d'Attention
Super-Admin Bypass
Le super-admin bypass automatiquement la matrice des permissions. Cela signifie :
- ✅ Accès à toutes les routes
- ⚠️ Les controllers doivent quand même vérifier la logique métier
- ⚠️ Les filtres de scope (OFFICE, ASSIGNMENT) ne s'appliquent pas automatiquement
Fallback sur Règles JWT
Si la matrice retourne "pass" (pas d'entrée), le système fait un fallback sur les règles JWT :
- Vérifie si
user.rulescontient la règle requise (ex:"POST documents") - Si oui → autorisé
- Si non → refusé
Scoped Entity
Le paramètre scoped_entity permet de définir des permissions spécifiques pour une entité (ex: un office particulier) :
- Exemple :
documents::read::notary::office-uid-123 - Si présent, prioritaire sur l'entrée sans scope
- Utilisé pour des permissions granulaires
Modifications de la Matrice
Via Interface Super-Admin
- Endpoint :
PUT /api/v1/super-admin/role-permissions-matrix - Permet de modifier les permissions pour chaque combinaison rôle/ressource/action
- Met à jour le cache automatiquement
Via Migrations
- Les migrations peuvent créer/modifier des entrées dans
role_permissions_matrix - Exemple :
prisma/migrations/20251107_role_permissions_matrix/migration.sql
Pilotage dynamique (onglet Super Admin "Matrice des droits")
L'onglet Super Admin > Matrice des droits (/super-admin/role-permissions) permet désormais de piloter en temps réel la matrice.
Interface :
- Tableau double entrée (ressource × action × rôle) avec cases à cocher synchronisées avec la BDD.
- Colonne informative "Pages concernées" (liste des écrans impactés).
- Badges de portée (
Global,Étude active,Affectation,Personnel) pour chaque rôle afin de rappeler le niveau de permission. - Barre de recherche full-text (ressource, action, page, rôle, scope).
- Résumé des modifications en attente + bouton
Réinitialiser(revenir aux valeurs chargées) + boutonEnregistrer. - Feedbacks : loader centralisé, toasts de confirmation, surface d'erreur explicite.
API & persistance :
GET /api/v1/super-admin/role-permissions→ retourne la matrice structurée (ressource → action → [pages, rôles]).PUT /api/v1/super-admin/role-permissions→ accepte une liste d'updates{ resource, action, role, allowed, scope?, pages? }.- Table SQL
role_permissions_matrix: clé unique(resource, scoped_entity, action, role), colonnesallowed,scope,pages,updated_by,updated_at. - Service backend : cache mémoire (chargé au démarrage, invalidé après update) + logging (
SafeLogger.debug).
Middleware :
RulesHandlerinterroge la matrice (décisionallow|deny|pass) avant le fallback sur les règles JWT historiques.- Résolution automatique du trio
{resource, action, role}à partir du chemin, du verbe HTTP et du JWT (guest_notary,client,third_party, etc.). - Journalisation standardisée des décisions (
resource,action,role,decision,userId).
Exceptions et fallbacks dans RulesHandler
Le RulesHandler implémente des fallbacks spécifiques pour certaines ressources/actions lorsque la matrice retourne pass (pas de règle explicite) ou deny (refus explicite) :
1. folder_third_parties + action resend :
- Si décision =
passet aucun JWT rules : autoriser par défaut pour les rôlesnotary,collaborator,admin,super-admin. - Permet le renvoi de code de vérification aux tiers sans nécessiter une règle explicite dans la matrice.
2. folder_sharing + action delete (révocation de partage) :
- Si décision =
passet aucun JWT rules : autoriser par défaut pour les rôlesnotary,collaborator,admin,super-admin. - Si décision =
denymais que l'utilisateur a des permissions sur les dossiers (POST folders,DELETE folders, ouPUT folders) : autoriser pour les rôlesnotary,collaborator,admin,super-admin. - Sécurité : Le contrôleur
FolderSharingController.revokeShare()vérifie de toute façon que l'utilisateur est membre de l'office propriétaire du dossier (folder.office_uid === activeOffice.uid). Cette double vérification garantit que seuls les membres de l'office propriétaire peuvent révoquer un partage, même si la matrice de permissions ne contient pas de règle explicite.
Raison des fallbacks :
- Permettre aux membres de l'office propriétaire d'un dossier de gérer les partages même si la matrice de permissions n'a pas été configurée explicitement pour
folder_sharingdelete. - Le contrôleur effectue toujours une vérification finale basée sur la propriété du dossier, garantissant la sécurité.
La matrice devient ainsi la source de vérité opérationnelle. Les sections précédentes restent la documentation fonctionnelle de référence ; l'interface Super Admin permet de l'appliquer sans redéploiement.
JWT Multi-Office et Calcul Dynamique des Règles
Principe
Lorsqu'un utilisateur a plusieurs affiliations (user_office_affiliations), le JWT doit refléter :
- L'office actif (affiliation avec
is_primary = true) - Les règles du rôle dans cet office actif
Implémentation dans AuthService
Le processus de calcul des règles est plus complexe que la simple addition des règles globales et office. Voici le processus complet :
// AuthService.getUserJwtPayload()
// 1. Déterminer l'office actif via determineActiveOffice()
// - Gère les affiliations PRIMARY
// - Gère les dossiers partagés (GUEST_FOLDERS) si pas d'abonnement actif
// - Retourne { officeId, activeAffiliation, isGuestFolders }
const { officeId, activeAffiliation, isGuestFolders } = await this.determineActiveOffice(user);
// 2. Normaliser le nom du rôle office
// - Utilise normalizeIdNotOfficeRoleName() pour standardiser les noms de rôles IdNot
// - Gère les variations de noms (accents, casse, underscores)
const rawAffiliationRoleName = activeAffiliation?.role_in_office ?? null;
const normalizedAffiliationRoleName = normalizeIdNotOfficeRoleName(rawAffiliationRoleName);
// 3. Calculer les règles dans l'ordre suivant :
const rules = new Set<string>();
// 3.1. Ajouter les règles du rôle global
user.role.rules.forEach((rule) => {
if (rule?.name) {
rules.add(rule.name);
}
});
// 3.2. Ajouter les règles super-admin si applicable
// - Vérifie is_super_admin OU rôle = "super_admin"
// - Ajoute toutes les règles super-admin via addSuperAdminRules()
await this.addSuperAdminRules(rules, isSuperAdmin);
// 3.3. Ajouter les règles office ou guest notary
if (isGuestFolders) {
// Cas "Dossiers invités" : ajoute les règles de notaire invité
this.addGuestNotaryRules(rules);
} else if (activeAffiliation && normalizedAffiliationRoleName && officeId) {
// Cas normal : charge le rôle office et ajoute ses règles
await this.addOfficeRoleRules(rules, officeId, normalizedAffiliationRoleName, user);
} else if (user.office_role) {
// Fallback : utilise le rôle office de l'utilisateur
user.office_role.rules.forEach((rule) => {
if (rule?.name) {
rules.add(rule.name);
}
});
}
// 4. Filtrer les règles pour les visiteurs (si pas d'abonnement actif)
if (!hasActiveSubscription && !isGuestFolders) {
uniqueRules = this.filterVisitorRules(uniqueRules);
}
// 5. JWT contient : userId, office_Id, role, officeRole, rules, isVisitor, isGuestFolders
Points importants :
- GUEST_FOLDERS : Si l'utilisateur n'a pas d'abonnement actif mais a des dossiers partagés,
officeId = "GUEST_FOLDERS"et les règles de notaire invité sont appliquées. - Normalisation des rôles : Les noms de rôles IdNot sont normalisés pour correspondre à la matrice de permissions.
- Règles super-admin : Ajoutées séparément après les règles globales, pas fusionnées.
- Filtrage visiteurs : Les règles sont filtrées si l'utilisateur est un visiteur (pas d'abonnement actif et pas GUEST_FOLDERS).
Switch d'Office
Lors du switch d'office via PUT /api/v1/notary/users/:userUid/offices/active :
- ✅ Modification de l'affiliation PRIMARY en BDD
- ✅ Régénération du JWT avec :
- Nouvel
office_Id(l'office cible) - Nouvelles
rules(rôle dans l'office cible)
- Nouvel
- ✅ Frontend met à jour le JWT en localStorage
- ✅ Rechargement automatique des dossiers/permissions
Vérification Frontend Réactive
Le frontend vérifie désormais les règles via PermissionContext/useRolePermissions() :
import { useRolePermissions } from "@Front/Stores/RolePermissionsStore";
import { AppRuleActions, AppRuleNames } from "@Front/Api/Entities/rule";
const { can, loading: permissionsLoading } = useRolePermissions();
const canCreateFolder = can(AppRuleNames.officeFolders, AppRuleActions.create);
// Exemple : gérer le clic sur "Créer un dossier"
const handleCreateFolderClick = useCallback(() => {
if (permissionsLoading) {
return;
}
if (canCreateFolder) {
router.push("/folders/create");
} else {
subscriptionModal.open();
}
}, [canCreateFolder, permissionsLoading, router, subscriptionModal]);
Cohérence Matrice ↔ JWT et fallback frontend
Source de vérité et arbitrage
- La matrice
role_permissions_matrix(pilotée dans l'onglet Super Admin) est la cible de gouvernance. - Le middleware
RulesHandlerdoit prioriser la matrice; en son absence pour un couple (ressource, action, rôle), la décision retombe sur lesrulesincluses dans le JWT.
Fallback côté frontend (non super‑admin)
- Par défaut, seuls les super‑admins chargent la matrice côté front.
- Pour les autres rôles,
RolePermissionsStore.can(resource, action)s'appuie surjwt.rulesavec:- mapping actions → méthodes HTTP:
create→POST,read→GET,update→PUT|PATCH,delete→DELETE,add→POST|PUT,remove→DELETE,share→POST,remind→POST,resend→POST - normalisation singulier/pluriel de ressource (ex.
folder_customer⇄folder_customers) - heuristiques:
- clients dossier:
addacceptePOST/PUT folder_customersouPUT folders - tiers dossier:
addacceptePOST folder_third_partiesouPUT folders - partage dossier:
createacceptePOST folder_sharingsouPOST folders
- clients dossier:
- mapping actions → méthodes HTTP:
Recommandations d'alignement
- Garder cohérents: seed des
ruleset affectations (roles,office_roles), calculAuthService.getUserJwtPayload(), et configuration de la matrice.
Licence (hasActiveSubscription) et comportement d'accès
Calcul côté serveur
SubscriptionsService.isUserSubscribed(user.uid, officeId)pilotehasActiveSubscription.- En échec de vérification: journalisation
warnet fallbackhasActiveSubscription=false. - Si
false→isVisitor=true,officeRole="Visiteur",rulesréduites: lecture + parcours abonnement.
Affichages et contrôles UI
- L'UI masque/désactive les actions d'écriture si la règle correspondante manque dans
jwt.rules. - Carte « Gestionnaires du projet »: « Licence active » s'affiche si
subscription.status === "active"etsubscription.office.uid === folder.office.uid.
Gestion des Fichiers
Nommage des Fichiers (Préfixe Dépositaire)
Objectif : Identifier l'origine et le contexte de chaque fichier via un préfixe standardisé.
Format : folderNumber.[Prefix.]FirstName.LastName.TypeDocument.aplc.ext
Exception RIB : RIB.[Prefix.]officeName.aplc.ext
Préfixes (Type de dépositaire) :
- N : Notaire gestionnaire
- C : Client
- T : Tiers
- NI : Notaire invité (office partagé)
Composants :
- Numéro de dossier (ou "RIB") : Toujours en premier.
- Préfixe :
N,C,TouNI. - Prénom : Formaté et inséré dans le nom.
- Nom : Formaté en majuscules, espaces remplacés par tirets (ex.
DUPONT-MOREAU). - Type de document : Nom du type de document.
- Suffixe :
.aplc(ancrage), sans index de version.
Implémentation :
- FileNamingService : Centralise la génération avec
generateDisplayName. - DepositaryPrefixHelper : Gère l'ajout/retrait du préfixe.
- FileNamingParsingHelper : Parse le format pour extraction.
Statuts des Documents et Flux
Distinction Sémantique
- DEPOSITED : Document envoyé par un déposant (Client, Tiers, Notaire Invité) vers le Notaire en charge.
- Note : Un document envoyé par un Notaire Invité au Notaire en charge est DEPOSITED.
- SENT : Document envoyé par le Notaire en charge vers un destinataire (Client, Tiers, Notaire Invité).
- Note : Un document envoyé par le Notaire en charge au Notaire Invité est SENT.
- DOWNLOADED : Document SENT qui a été consulté/téléchargé par le destinataire.
- ASKED : Document demandé par le Notaire en charge.
- VALIDATED/REFUSED : Statuts de validation par le Notaire en charge.
Règles de Visibilité et Actions
- "Documents à envoyer" (Front) : Affiche uniquement
ASKED,REFUSED,VALIDATED.- Exclut
DEPOSITED,SENT,DOWNLOADED.
- Exclut
- Blocage API Customer :
- Modification/Suppression : Interdite si statut est
SENT(403). Autorisé siDOWNLOADED. - Passage en SENT : Interdit via PUT (réservé au
send).
- Modification/Suppression : Interdite si statut est
Évolution planifiée - Documents unifiés
Statut : 📋 Planifié | Version : cible
Objectif
Unifier le flux d'envoi de documents et les métadonnées d'émetteur pour tous les types de destinataires et de déposants.
Décisions validées
1. Tables (court terme)
- Conserver
documentsetdocuments_notarypour des raisons historiques/techniques. - Unifier le flux d'envoi côté API.
- Fusion des tables prévue à terme.
2. Destinataire (recipient)
Type union unique pour tous les envois :
{
type: 'customer' | 'third_party' | 'office' | 'invited_notary';
uid: string;
}
Types de destinataires possibles :
customer: client du dossierthird_party: tiers du dossieroffice: notaire admin de l'officeinvited_notary: notaire invité du dossier
3. Émetteur (emitter)
-
Champ :
emitter(équivalent anglais de « émetteur »). -
Structure :
{ uid, first_name, last_name, office_uid?, office_name? }. -
Règle : celui qui envoie le document ; absent pour documents ASKED (personne n'a encore envoyé).
-
Sources :
- Client dépose :
depositor.contact - Tiers dépose :
third_party_depositor - Notaire envoie (DocumentsNotary) :
depositor(User) - Notaire envoie (Documents shared_to_office) : notaire du
folder.office - RIB :
rib_anchor_proof_data.depositor
- Client dépose :
4. Documents du notaire invité
Les documents créés par le notaire invité sont traités comme ceux des tiers ou des clients : même modèle, pas de cas particulier. Le notaire invité est un membre du dossier comme les autres ; il ne joue pas le rôle de notaire gestionnaire dans ce dossier.
Implémentation : Concept de membre unifié (FolderMember) :
- Base :
BaseFolderMember(uid, display_name, email) — champs communs hérités - Types dérivés :
CustomerMember,ThirdPartyMember,InvitedNotaryMember(extends BaseFolderMember + type) - Recipient :
BaseRecipient(uid),TypedRecipient(extends BaseRecipient + type) - API : GET folder retourne
members(tableau unifié clients + tiers + notaires invités) - Backend :
buildFolderMembers()dansfolderMemberHelpers.ts - Frontend :
folder.membersdisponible ;buildRecipientsFromMembers()pour le flux d'envoi
5. Espaces et onglets membres (dérivation depuis cas parent member)
Les espaces (client/tiers/notaires invités) et les onglets des membres des dossiers dérivent d'un cas parent « member ».
Espaces (FolderInformationUserContext) :
- Type de base :
BaseMemberContextType="customer" | "third_party" | "invited_notary" - Constantes :
MEMBER_CONTEXT_THIRD_PARTY,MEMBER_CONTEXT_INVITED_NOTARY FolderInformationUserContext="notary" | MEMBER_CONTEXT_THIRD_PARTY | MEMBER_CONTEXT_INVITED_NOTARY
Onglets membres (ClientView) :
- Type de base :
BaseMemberTabValueavectabType: BaseMemberTabType BaseMemberTabType="customer" | "third_party" | "shared_notary"- Constantes :
TAB_TYPE_CUSTOMER,TAB_TYPE_THIRD_PARTY,TAB_TYPE_SHARED_NOTARY - Types dérivés :
CustomerTabValue,ThirdPartyTabValue,SharedNotaryTabValue(extends BaseMemberTabValue)
Fichiers : MemberTypes.ts, folderInformationUserContext.ts, ClientView/types.ts
6. Flux d'envoi unifié
Une seule fonction d'envoi API avec un destinataire de type union. Pas de distinction par type de destinataire : envoi au bon membre en tant que notaire gestionnaire.
Implémentation : L'API POST /v1/notary/documents_notary/send accepte désormais :
- Format unifié :
recipients: { type: 'customer' | 'third_party' | 'office' | 'invited_notary'; uid: string }[] - Format legacy :
recipientUidourecipientUids(résolution automatique du type)
Le service DocumentBatchService route vers Documents (third_party, office/invited_notary) ou DocumentsNotary (customer) selon le type.
7. Orchestrateurs de sortie API (plain)
Les réponses API documents sont normalisées via des orchestrateurs de post-hydratation pour garantir une sortie plain cohérente entre endpoints liste, mono-entité, transformation et suppression.
DocumentsNotary :
#Common/utils/documentsNotaryPostHydrationHelperbuildDocumentsNotaryPlainWithRelations: enrichit la sortie (emitter) et réinjectefilessi nécessairehydrateDocumentsNotaryToPlainWithRelations: pipeline listehydrateSingleDocumentNotaryToPlainWithRelations: pipeline mono-entité (get by uid, mark-as-viewed, suppression, transformations)logDocumentsNotaryPlainFilesGuard: garde-fou de cohérencefiles
Documents (non-notary) :
#Common/utils/documentsPostHydrationHelperbuildDocumentsPlainWithRelations: pipeline listebuildSingleDocumentPlainWithRelations: pipeline mono-entité (get by uid, création, mark-as-viewed, suppression)logDocumentsPlainFilesGuard: garde-fou de cohérence des fichiers téléchargeables
Règle d'architecture :
- Les controllers/helpers ne construisent plus la sortie JSON avec un
instanceToPlainlocal suivi de merges manuels. - Toute sortie document doit passer par l'orchestrateur correspondant (
DocumentsNotaryouDocuments).
Références pour implémentation
DocumentsEnrichmentHelper: enrichir les réponses avecemitterau lieu de limiter à{ uid }.DocumentsNotary: incluredepositor(aveccontact) dans les requêtes et exposeremitter.- RIB : exposer
emitterdepuisrib_anchor_proof_data.depositor(POST/GET office, GET office/:uid).
Partage Inter-Études
Statut : ✅ 100% | Version : 2.0.0
Distinction importante : Partage avec une personne spécifique
Concept fondamental : Le partage se fait avec une personne spécifique (notaire invité) qui reçoit un email et se connecte ensuite.
Sémantique du partage :
- Partage = Invitation d'une personne spécifique (IdNot + email de préférence pour les envois,
invited_notary_email) - Documents = Demandés à l'office de cette personne (
shared_to_office_uid) - Accès aux dossiers invités = Croisement sur l'id IdNot (
invited_notary_idnot) ; email en base = envois uniquement
Workflow :
- Le notaire principal partage un dossier avec un confrère (sélection dans l'annuaire + email de préférence pour les envois, stocké en
invited_notary_email) - Un email d'invitation est envoyé au confrère à l'adresse
invited_notary_email(après création, seul cet email en base est utilisé pour tous les envois) - Le confrère se connecte et accède aux dossiers partagés via "Dossiers invités" (GUEST_FOLDERS)
- Les documents demandés sont créés avec
shared_to_office_uid(l'office du confrère) - L'accès aux dossiers invités est déterminé par l'IdNot ; les envois d'email utilisent
invited_notary_emailen base
Gestion de l'affichage et de la sélection des dossiers :
is_primarypour GUEST_FOLDERS : Si un utilisateur n'a pas d'office avec licence active mais a des dossiers partagés, "Dossiers invités" est ajouté avecis_primary: truepour permettre la sélection automatique- Stabilisation de la liste : Les hooks frontend (
useInvitedCustomerFolders) utilisent des refs pour stabiliser la liste des dossiers et éviter les re-rendus lors de la navigation - Badge "Invité" : Affiché uniquement pour les dossiers partagés en contexte non-GUEST_FOLDERS (évite la redondance)
- Mise à jour IdNot : Les utilisateurs invités sont mis à jour via l'API IdNot même s'ils n'ont pas de
officeMembership.idNot(recherche par rattachements)
Contrainte du schéma : @@unique([folder_uid, shared_to_office_uid])
- Un dossier ne peut être partagé qu'une seule fois avec un office donné
- Cette contrainte garantit l'isolation des documents par notaire invité
- Si plusieurs notaires du même office doivent être invités, cela nécessiterait des partages distincts (non supporté actuellement)
Distinction avec un partage d'office complet :
Le système ne supporte pas le partage avec un office entier où tous les membres auraient accès. Le partage est toujours avec une personne spécifique identifiée par son IdNot et son email de préférence pour les envois. L'office (shared_to_office_uid) sert uniquement à :
- Identifier l'office du notaire invité pour la création des documents
- Permettre l'affichage dans l'interface "Dossiers invités"
L'accès aux dossiers invités est déterminé par l'IdNot du notaire connecté (invited_notary_idnot). L'email en base (invited_notary_email) sert uniquement aux envois (invitations, documents).
Architecture Backend
Service : FolderSharingService (~250 lignes)
Fichier : lecoffre-back-main/src/services/notary/FolderSharingService/FolderSharingService.ts
Controller : FolderSharingController (~280 lignes)
Fichier : lecoffre-back-main/src/app/api/notary/FolderSharingController.ts
Endpoints :
POST /api/v1/notary/folders/:uid/share: Partager dossier avec confrère- Paramètres :
invitedNotaryEmail(requis, email de préférence pour les envois),invitedNotaryIdnot(optionnel),invitedNotaryFirstName,invitedNotaryLastName(optionnels). CRPCEN optionnel. - Note : L'email stocké (
invited_notary_email) est utilisé pour tous les envois après création. L'IdNot (invited_notary_idnot) sert au croisement login / dossiers invités.
- Paramètres :
GET /api/v1/notary/folders/shared: Liste dossiers reçusGET /api/v1/notary/folders/:uid/shares: Liste partages d'un dossierPOST /api/v1/notary/shares/:uid/revoke: Révoquer partage
Base de Données
Table : folder_sharing
Champs :
folder_uid: Dossier partagéshared_from_office_uid: Office qui partageshared_to_office_uid: Office qui reçoit (office du notaire invité)invited_notary_email: Email de préférence pour les envois (invitations, documents). Seul champ email pour le notaire invité ; par défaut pré-rempli depuis IdNot à l'invitation, modifiable avant enregistrement. Après création, seul ce champ est utilisé pour tous les envois (aucune relecture IdNot).invited_notary_idnot: Identifiant IdNot du notaire invité. Utilisé pour le croisement login / dossiers invités : l'accès aux dossiers partagés est déterminé par cet IdNot (quand l'utilisateur connecté a un IdNot).invited_notary_first_name: Prénom du notaire invitéinvited_notary_last_name: Nom du notaire invitéshared_by_user_uid: Utilisateur qui a partagéshare_role: Rôle accordé (lecture seule)status: ACTIVE, REVOKEDexpires_at: Date expiration optionnellecan_view_other_members_documents: Permission pour voir les documents des autres membres
Email et IdNot du notaire invité :
- Un seul champ email en base :
invited_notary_email= email de préférence (et par défaut email IdNot à l'invitation). Il n'y a pas de champ séparé « email IdNot » ; l'email IdNot et l'email de préférence sont donc le même champ persisté. - Accès aux dossiers invités : croisement sur
invited_notary_idnot(voirbuildGuestNotaryShareWhere). L'email en base n'est pas utilisé pour l'accès. - Notaires invités sans seat : à l'invitation, le partage enregistre
invited_notary_idnot. Lors de la première connexion via IdNot, un profilUserest créé ou mis à jour avecusers.idNot. Les notaires invités ont donc en base un identifiant IdNot (sur le partage, et sur le User après connexion).
Contrainte unique : @@unique([folder_uid, shared_to_office_uid])
- Empêche de partager plusieurs fois le même dossier avec le même office
- Garantit l'isolation des documents par notaire invité
Frontend
Pages :
/folders/shared: Dossiers reçus- Modal :
ShareFolderModal(partager avec confrère)
API : FolderSharingApi (4 méthodes)
Fonctionnalités :
- Onglets notaires invités créés dans
ClientView - Badge "Lecture seule" pour notaires invités
- Query inclut
folder_sharingsavec relations - Demande de documents : Possibilité de demander des documents aux confrères invités (v2.0.1)
- Relance de documents : Possibilité de relancer les confrères pour les documents en attente (v2.0.1)
- Invitation simplifiée : Seul l'email est requis pour inviter un confrère. Le CRPCEN est automatiquement récupéré depuis IdNot lors de la connexion (v2.0.1)
Destinataire notaire invité : Tous les envois (invitation partage, relance, documents) utilisent uniquement l'email en base invited_notary_email. Aucune relecture de l'email côté IdNot après création du partage.
Templates :
FOLDER_SHARING_INVITATION: Invitation partage dossierDOCUMENT_REQUEST: Demande de document (utilisé aussi pour les confrères)- Documentation :
docs/MAILCHIMP_TEMPLATE_FOLDER_SHARING_INVITATION.md
Demande et Relance de Documents aux Confrères (v2.0.1)
Fonctionnalités :
- Demander un document : Bouton disponible sur l'onglet du confrère invité
- Relancer un confrère : Bouton disponible pour les documents en statut ASKED
- Affichage des documents : Table des documents avec filtrage par
shared_to_office_uid
Frontend :
ClientView: Gestion des onglets notaires invités avec boutonsAskDocuments: Support du formatshared-office:uiddans l'URLDocumentTables: Affichage des documents avec flagisSharedNotaryEmailReminder: Support des relances pour les confrères
Backend :
- Utilise le champ
shared_to_office_uidexistant dans la tabledocuments - Les documents sont demandés à l'office du notaire invité, mais l'accès est contrôlé par l'email
EmailBuilder.sendDocumentEmails(): Gère déjà les emails pour les confrèresDocumentsController.resendRequest(): Fonctionne pour tous les types de documents
Important : Les documents avec shared_to_office_uid sont techniquement visibles par tous les membres de cet office, mais la contrainte unique @@unique([folder_uid, shared_to_office_uid]) limite l'exposition car un dossier ne peut être partagé qu'une seule fois avec un office donné. L'accès individuel est contrôlé via invited_notary_email dans le contexte GUEST_FOLDERS.
Documentation détaillée : voir section « Partage Inter-Études » ci-dessus. Déploiement : exécuter les migrations Prisma puis deploy/scripts/build-and-deploy.sh. Analyse : vérifier en base invited_notary_idnot sur folder_sharing ; créer un partage depuis l’UI ; connexion notaire invité (IdNot) → dossiers partagés accessibles.
Accès et Téléchargement pour Notaire Invité
Problème résolu (v2.0.2) : Les notaires invités ne pouvaient pas voir/télécharger les documents envoyés par le notaire gestionnaire (table documents_notary).
Architecture d'accès :
- Vue Document : Permission
canViewDocumentsétendue siisGuestNotary(rôle ou contexte GUEST_FOLDERS). - API Customer :
- GET documents_notary : Support du filtre
where: { AND: [...] }pour extrairefolderUid. - Download :
FileNotaryAccessGuestNotaryHelpervérifie désormais dansdocuments_notary(en plus dedocuments) et valide l'accès viafolder_sharing.
- GET documents_notary : Support du filtre
- Fallback Téléchargement : Si le document est dans la table
documents(envoyé à l'invité), le front utilise l'API Customer (/customer/files/download/:uid) au lieu de Notary, car l'API Customer valide correctement l'accès viafolder_sharing.
Gestion Multi-Office
Statut : ✅ 100% | Version : 2.0.0
Architecture Backend (Multi-Office)
Service : UserOfficeAffiliationsService (~250 lignes)
Fichier : lecoffre-back-main/src/services/notary/UserOfficeAffiliationsService/UserOfficeAffiliationsService.ts
Middleware : ActiveOfficeInjector (injection activeOfficeUid dans req.body)
Fichier : lecoffre-back-main/src/app/middlewares/ActiveOfficeInjector.ts
Endpoints :
GET /api/v1/notary/users/:uid/offices: Liste offices utilisateurPOST /api/v1/notary/users/:uid/offices/active: Changer office actifPOST /api/v1/notary/users/:uid/offices/sync: Synchroniser depuis IdNot
Base de Données (Multi-Office)
Table : user_office_affiliations
Champs :
user_uid: Utilisateuroffice_uid: Officerole: Rôle dans cet officeis_primary: Office principal (booléen)
Unique : (user_uid, office_uid)
Frontend (Multi-Office)
Store : ActiveOfficeStore.ts (MobX, 150 lignes)
Composant : OfficeSelector (dropdown header, masqué si 1 seul office)
API : UserOffices (4 méthodes)
Fonctionnalités
- Sync automatique : Depuis IdNot API Annuaire
- Changement office : Sans déconnexion (reload page)
- Filtrage automatique : Par office actif
- Méthode
setPrimary(): Atomique (transaction) - Compatible : Tous les TODOs existants
Middleware Injection
Transparente : Ajoute activeOfficeUid dans tous les endpoints notary
Alias : officeUid pour compatibilité code existant
Switch d'Office : Régénération JWT et Permissions
Flux attendu :
-
PUT /api/v1/notary/users/:userUid/offices/active(auth requis, pas deruleHandler) -
UserOfficeAffiliationsService.setActiveOffice(...)vérifie l'affiliationACTIVEet basculeis_primary -
AuthService.getUserJwtPayload(...)régénère le JWT avec:- nouvel
office_Id rulesrecalculées (rôle d'office + règles globales)- filtrage licence (
hasActiveSubscription,isVisitor) pour le nouvel office
- nouvel
-
Front: remplace les tokens, vérifie le cookie, et relit les droits (journaux:
office_Id,rulesCount)
Points de contrôle :
- Le backend doit renvoyer de nouveaux tokens après chaque bascule.
- Le front doit vérifier la mise à jour effective du cookie
leCoffreAccessTokenpuis conditionner l'UI.
Ancrage Blockchain
LeCoffre.io utilise l'ancrage blockchain Bitcoin Signet pour garantir l'intégrité et la traçabilité des documents.
Évolution des Versions
| Version | Blockchain | Périmètre | Déclenchement | Filigrane | Fallback |
|---|---|---|---|---|---|
| V1 | Tezos | Dossiers uniquement | Manuel | ❌ Non | ⚠️ Lecture seule |
| V2 | Bitcoin Signet | Docs + Dossiers | Auto | ❌ Non | - |
| V3 | Bitcoin Signet | Docs + Dossiers | Auto | ✅ Oui | ✅ Tezos masqué |
Nouveautés V3
- Filigrane automatique : "lecoffre.io" sur tous documents
- Double version : Original + filigrané (conservés en BDD)
- ZIP enrichi : Originaux + filigranés + certificats + preuves JSON
- Preuves structurées : JSON standardisé pour vérification externe
- Ancrage dossier : Hash du ZIP complet (pas seulement Merkle tree)
- Fallback Tezos : Accès masqué aux anciens ancrages (lecture seule)
Principes Fondamentaux
- Ancrage immédiat : L'ancrage est déclenché automatiquement lors de la validation d'un document
- Hash filigrané : Seule la version filigranée est ancrée sur blockchain
- Double hash : Hash original et hash filigrané sont stockés pour vérification
- Preuve on-chain : Le
tx_id(transaction ID Bitcoin) est la seule preuve fiable d'ancrage
Services Backend
- WatermarkService : Conversion documents → PDF (hors tableurs : Excel/ODS exclus), ajout filigrane "lecoffre.io"
- DocumentAnchorsService : Ancrage automatique des documents validés
- OfficeFolderAnchorsService : Ancrage des dossiers complets (ZIP)
- BitcoinSignetService : Ancrage hash sur Bitcoin Signet via API externe
- ProofDataService : Génération JSON de preuve d'ancrage
Processus d'Ancrage
Séquence de traitement (Upload) :
- Calcul hash original (fichier original)
- Filigrane (sur fichier original non chiffré)
- Calcul hash filigrané et ancrage blockchain (hash filigrané, génération txid)
- Chiffrement du document filigrané
- Upload IPFS du document chiffré
- Stockage métadonnées fichier (hash original + hash filigrané)
- Stockage certificat BDD (document_anchor avec hash filigrané + hash original + txid)
Ancrage automatique : Déclenché lors de la validation d'un document via PUT /api/v1/notary/documents/:uid (asynchrone, non bloquant).
Certificats et ZIP
Structure ZIP dossier complet :
dossier_12345678/
├── _LISEZ-MOI.txt
├── 01_originaux/
├── 02_filigranes/
├── 03_certificats/
└── 04_preuves/
Certificats PDF : Générés automatiquement pour les documents validés et ancrés, contenant les hashs complets, le lien de transaction, et toutes les métadonnées d'ancrage.
Documentation complète : Voir docs/ANCRAGE_COMPLETE.md pour les détails complets de l'architecture V3, des processus d'ancrage, des certificats, et du troubleshooting.
Références
- Déploiement : DEPLOYMENT.md
- Monitoring : DEPLOYMENT.md
- Ancrage : ANCRAGE_COMPLETE.md
- Base de données : DATABASE_COMPLETE.md
- Consolidation opérationnelle : README.md
- Correctifs et dépannage : FRONTEND.md, DEPLOYMENT.md, ANCRAGE_COMPLETE.md
Dernière mise à jour : 2026-01-28