Client Authentication System
Historical implementation journal: this document is kept for traceability. Active implementation guidance now lives in
docs/FRONTEND.md,docs/BACKEND.md,docs/AGENTS_FRONTEND.md, anddocs/AGENTS_BACKEND.md. Statut : ✅ TERMINÉ - Phase 5 Complétée Priorité : Haute Estimation : 8-10 semaines (5 phases) Dernière mise à jour : 2026-01-31
📊 Tableau de Bord d'Avancement
| Phase | Description | Statut | Progression |
|---|---|---|---|
| Phase 1 | MVP Authentification | ✅ Complétée | 100% |
| Phase 2 | Expérience Client | ✅ Complétée | 100% |
| Phase 3 | Messagerie | ✅ Complétée | 100% |
| Phase 4 | Fichiers et Préférences | ✅ Complétée | 100% |
| Phase 5 | Côté Photographe | ✅ Complétée | 100% |
Progression globale : 100% (5/5 phases)
📝 Journal d'Implémentation
2026-01-31 - Revue complète Portail Client (Phases 3, 4, 5)
Objectif : Corriger les bugs i18n, UX et robustesse identifiés lors de la revue des phases Messagerie, Fichiers et Côté Photographe.
Corrections i18n :
- ✅ Suppression des chaînes françaises hardcodées dans
ClientPortalMessagesPage:"fr-FR"→ locale dynamique (i18n.language),"Hier"→ cléclientPortal.messages.yesterday,"Vous: "→ cléclientPortal.messages.you - ✅ Tous les
defaultValuedu layout et de la messagerie passés en anglais (fallback FR → EN) - ✅ Ajout des clés de traduction manquantes dans
clientPortal.nav:galleries,profile,logout - ✅ Ajout des clés
clientPortal.messages.yesterday,you,markReadError(EN + FR) - ✅ Ajout des clés
clientPortal.files.downloadError,fileTooLarge(EN + FR) - ✅ Ajout de la clé
contacts.clientPortal.messages.sendError(EN + FR)
Corrections UX / robustesse :
- ✅ Fix clé d'erreur download :
uploadError→downloadErrordansClientPortalFilesPage - ✅ Validation taille 10 MB côté frontend avant upload (toast d'erreur + reset input)
- ✅ Toast d'erreur à l'envoi de message côté client (
ClientPortalMessagesPage) - ✅ Toast d'erreur sur
markAsRead(onErrorcallback) - ✅ Toast d'erreur à l'envoi de message côté photographe (
ContactClientMessagesSection) - ✅ Hauteur responsive page messages :
h-[600px]→h-[calc(100vh-12rem)] min-h-[500px] - ✅ Badge "Invitation en attente" ajouté dans
ContactCard(liste contacts), même pattern queContactHero
Fichiers modifiés :
frontend/src/i18n/locales/en/translation.json- Clés nav, messages, files, contactsfrontend/src/i18n/locales/fr/translation.json- Clés nav, messages, files, contactsfrontend/src/pages/client-portal/messages/ClientPortalMessagesPage/ClientPortalMessagesPage.tsx- i18n, toasts, hauteur responsivefrontend/src/pages/client-portal/files/ClientPortalFilesPage/ClientPortalFilesPage.tsx- Fix downloadError, validation 10 MBfrontend/src/pages/contacts/ContactsListPage/components/ContactCard.tsx- Badge pending invitationfrontend/src/pages/contacts/ContactViewPage/components/ContactClientMessagesSection.tsx- Toast erreur envoifrontend/src/components/layout/ClientPortalLayout.tsx- defaultValues EN, clés nav
2026-01-30 - Durcissement accès portail & thème
Objectif : Sécuriser le parcours d'invitation et appliquer réellement les préférences d'apparence.
Réalisations Backend :
- ✅ Endpoint
POST /client-auth/validate-invitation: validation dédiée des tokens d'invitation - ✅ Ré-invitation : suppression/invalidation des anciens tokens avant création d'un nouveau
- ✅ Fix documents client : évite les requêtes
WHERE id IN ()quand aucun owner n'est disponible
Réalisations Frontend :
- ✅ Portail login : inscription affichée uniquement si le token est valide (sinon login)
- ✅ Bannière d'invitation + message d'erreur si lien invalide/expiré
- ✅ Portail public : accès
/portal/*sans redirection vers/loginuser - ✅ Apparence : thème client (light/dark/system) appliqué sur tout le portail
- ✅ UI : liens légaux sous le bloc de connexion + lien photographe repositionné
Fichiers modifiés (extraits) :
backend/src/client-auth/client-auth.controller.tsbackend/src/client-auth/client-auth.service.tsbackend/src/client-auth/client-auth.repository.tsbackend/src/client-accounts/client-accounts.repository.tsfrontend/src/components/layout/ClientPortalLayout.tsxfrontend/src/pages/client-portal/auth/ClientPortalLoginPage/ClientPortalLoginPage.tsxfrontend/src/components/layout/AppLayout.tsx
2026-01-30 - Phase 5 Complétée
Objectif : Côté photographe - Invitation clients, badges, messages dans fiche contact
Réalisations Backend :
- ✅ Repository : 6 méthodes invitation tokens (
createInvitationToken,findInvitationTokenByHash,markInvitationTokenUsed,findPendingInvitationForContact,deleteExpiredInvitationsForContact,getClientAccountStatusForContact) - ✅ Service
ClientAuthInvitationService: création/envoi invitation, validation/consommation token, statut compte contact - ✅ Endpoint
POST /contacts/:id/invite-to-create-account: invitation avec email contenant lien portail - ✅ Endpoint
GET /contacts/:id/client-account-status: statut compte (hasAccount, accountStatus, hasPendingInvitation, invitationSentAt) - ✅ Gestion invitation token au register : validation, consommation, liaison contact avec source
INVITATION - ✅ Modules mis à jour :
ClientAuthModule(imports CommunicationModule/ConfigModule, exports InvitationService),ContactsModule(import ClientAuthModule)
Réalisations Frontend :
- ✅ Hooks :
useClientAccountStatus(contactId),useInviteToCreateAccount()dansuseContacts.ts - ✅
ContactHero: Badge vert "Portail Client" (compte actif), badge orange "Invitation en attente" - ✅
ContactHero: Action "Inviter au Portail Client" dans le menu actions - ✅
ContactViewPage: Dialog de confirmation invitation avec toast succès/erreur - ✅
ContactCard: Badge "Portail" si compte client actif - ✅
ContactClientMessagesSection: Section messages dans la fiche contact (tab Communications) - ✅
ClientPortalLoginPage: Support paramètre URLinvite, auto-switch register, bannière invitation - ✅
ClientAuthProvider+api.ts: SupportinvitationTokendans le register - ✅ i18n FR/EN : traductions invitation, badges, messages
Fichiers créés :
# Backend
backend/src/client-auth/client-auth-invitation.service.ts
# Frontend
frontend/src/pages/contacts/ContactViewPage/components/ContactClientMessagesSection.tsx
Fichiers modifiés :
backend/src/client-auth/client-auth.repository.ts- Méthodes invitation tokensbackend/src/client-auth/client-auth.service.ts- Gestion invitationToken au registerbackend/src/client-auth/client-auth.module.ts- Imports CommunicationModule, ConfigModule, exports InvitationServicebackend/src/contacts/contacts.controller.ts- Endpoints invite + client-account-statusbackend/src/contacts/contacts.module.ts- Import ClientAuthModulefrontend/src/client/contacts/useContacts.ts- Hooks et API client-account-status + invitationfrontend/src/pages/contacts/ContactViewPage/ContactViewPage.tsx- Dialog invitation + section messagesfrontend/src/pages/contacts/ContactViewPage/components/ContactHero.tsx- Badges + action invitationfrontend/src/pages/contacts/ContactsListPage/components/ContactCard.tsx- Badge portailfrontend/src/pages/client-portal/auth/ClientPortalLoginPage/ClientPortalLoginPage.tsx- Support invite tokenfrontend/src/client-auth/ClientAuthProvider.tsx- invitationToken dans registerfrontend/src/client-auth/api.ts- invitationToken dans registerfrontend/src/i18n/locales/*/translation.json- Traductions Phase 5
2026-01-29 - Phase 4 Complétée
Objectif : Stockage de fichiers personnels pour clients + Page paramètres complète
Réalisations Backend :
- ✅ Migration 0163 : Table
client_filesavec indexes - ✅ Module
client-files: Repository, Service, Controller avec 5 endpoints - ✅ Endpoints : list, presigned-url, create record, download, delete
- ✅ Validation types fichiers réutilisée depuis
contact-files/file-validation.ts - ✅ Clé storage :
client-files/{clientAccountId}/{uuid}.{ext} - ✅ Max 10MB par fichier
- ✅ OpenAPI : schemas et paths dans
client-files.yaml - ✅ Types Kysely :
ClientFileTable
Réalisations Frontend - Page Fichiers :
- ✅
ClientPortalFilesPage: Liste fichiers avec icônes par type - ✅ Upload via presigned URL (workflow complet)
- ✅ Téléchargement via URL signée
- ✅ Suppression avec confirmation (AlertDialog)
- ✅ État vide avec illustration
- ✅ Loading skeleton
- ✅ Vue responsive (table desktop, cards mobile)
Réalisations Frontend - Page Settings :
- ✅
ClientPortalSettingsPage: 5 sections empilées en Cards - ✅
ProfileSection: displayName, phone, locale (TanStack Form) - ✅
PasswordSection: currentPassword, newPassword, confirmPassword - ✅
NotificationsSection: Toggles email/SMS/marketing (Switch) - ✅
ThemeSection: Radio light/dark/system + timezone input - ✅
DangerZoneSection: Suppression compte avec confirmation texte - ✅ Backend existant réutilisé (PUT /client-accounts/me, etc.)
Fichiers créés :
# Backend
infra/liquibase/changes/0163_create_client_files/up.sql
infra/liquibase/changes/0163_create_client_files/down.sql
backend/src/client-files/ (7 fichiers)
openapi/components/client-files.yaml
openapi/paths/client-files.*.yaml (3 fichiers)
# Frontend
frontend/src/client-auth/client-files-api.ts
frontend/src/client-auth/useClientFiles.ts
frontend/src/pages/client-portal/files/ClientPortalFilesPage/ (2 fichiers)
frontend/src/pages/client-portal/settings/ClientPortalSettingsPage/ (8 fichiers)
Fichiers modifiés :
backend/src/db/database.types.ts- Types Kysely client_filesbackend/src/api/api-platform.module.ts- Import ClientFilesModulefrontend/src/client-auth/index.ts- Export nouveaux hooks et APIfrontend/src/routes/client-portal.routes.tsx- Routes fichiers + settingsfrontend/src/router.tsx- Intégration route fichiersfrontend/src/components/layout/ClientPortalLayout.tsx- Navigation fichiersfrontend/src/i18n/locales/*/translation.json- Traductions fichiers et paramètresopenapi/index.yaml- Paths client-filesopenapi/components/schemas.yaml- Références schemas client-filesinfra/liquibase/changelog-master.yaml- Changeset 0163
2026-01-29 - Phase 3 Complétée
Objectif : Messagerie client-photographe
Réalisations Backend :
- ✅ Migration 0162 : Table
client_messagesavec enumclient_message_direction - ✅ Module
client-messages: Repository, Service, Controller avec CRUD complet - ✅ Endpoints client : conversations, messages, send, mark-as-read, unread-count
- ✅ Endpoints photographe : messages par contact, envoi depuis contacts controller
- ✅ OpenAPI : schemas et paths dans
client-messages.yaml - ✅ Types Kysely :
ClientMessageTable,ClientMessageDirection
Réalisations Frontend :
- ✅
ClientPortalMessagesPage: Liste conversations + fil de discussion combinés - ✅ Interface responsive : Liste sur desktop, toggle mobile
- ✅ Conversations avec avatar, dernière message, compteur non-lus
- ✅ Fil de discussion avec bulles messages, indicateurs lu/non-lu
- ✅ Envoi de messages avec optimistic update
- ✅ Auto-refresh des conversations et messages
- ✅ Hooks TanStack Query :
useClientConversations,useClientMessages,useSendClientMessage,useMarkConversationAsRead - ✅ Navigation mise à jour avec icône Messages
- ✅ i18n FR/EN pour la messagerie
Fichiers créés :
# Backend
infra/liquibase/changes/0162_create_client_messages/up.sql
infra/liquibase/changes/0162_create_client_messages/down.sql
backend/src/client-messages/ (6 fichiers)
openapi/components/client-messages.yaml
openapi/paths/client-messages.*.yaml (4 fichiers)
# Frontend
frontend/src/client-auth/client-messages-api.ts
frontend/src/client-auth/useClientMessages.ts
frontend/src/pages/client-portal/messages/ClientPortalMessagesPage/
Fichiers modifiés :
backend/src/db/database.types.ts- Types Kysely messagesbackend/src/api/api-platform.module.ts- Import ClientMessagesModulebackend/src/contacts/contacts.controller.ts- Endpoints messages photographebackend/src/contacts/contacts.module.ts- Import ClientMessagesModulefrontend/src/client-auth/index.ts- Export nouveaux hooksfrontend/src/routes/client-portal.routes.tsx- Routes messagesfrontend/src/router.tsx- Intégration routes messagesfrontend/src/components/layout/ClientPortalLayout.tsx- Navigation messagesfrontend/src/i18n/locales/*/translation.json- Traductions messagesopenapi/index.yaml- Paths messagesopenapi/components/schemas.yaml- Références schemas messagesinfra/liquibase/changelog-master.yaml- Changeset 0162
2026-01-29 - Phase 2 Complétée
Objectif : Pages projets et documents pour le client
Réalisations :
- ✅
ClientPortalSessionsPage: Liste des séances avec recherche, filtres (tous/à venir/passés/confirmés/en attente), groupement par mois - ✅
ClientPortalSessionViewPage: Détail séance avec timeline, documents liés, card photographe - ✅
ClientPortalDocumentsPage: Vue unifiée avec tabs (tous/devis/factures/contrats), recherche, filtre par photographe - ✅ Routes mises à jour :
/portal/sessions,/portal/sessions/:sessionId,/portal/documents - ✅ Navigation simplifiée (Dashboard, Séances, Documents)
Fichiers créés :
frontend/src/pages/client-portal/sessions/ClientPortalSessionsPage/
frontend/src/pages/client-portal/sessions/ClientPortalSessionViewPage/
frontend/src/pages/client-portal/documents/ClientPortalDocumentsPage/
Fichiers modifiés :
frontend/src/routes/client-portal.routes.tsx- Nouvelles routesfrontend/src/router.tsx- Intégration routesfrontend/src/components/layout/ClientPortalLayout.tsx- Navigation mise à jour
2026-01-29 - Phase 1 Complétée
Objectif : MVP Authentification client
Réalisations Backend :
- ✅ Migration 0161 : 7 tables créées (
client_accounts,client_account_contacts,client_password_reset_tokens,client_refresh_tokens,client_preferences,client_email_verification_tokens,client_invitation_tokens) - ✅ Module
client-auth: register, login, logout, refresh, forgot-password, reset-password, verify-email, me - ✅ Module
client-accounts: profil CRUD, photographers, projects, documents, preferences - ✅ Guard
ClientAuthGuard+ClientJwtStrategy - ✅ OpenAPI : schemas dans
client-auth.yaml, paths pour tous les endpoints - ✅ Types Kysely dans
database.types.ts
Réalisations Frontend :
- ✅ Module
client-auth:ClientAuthProvider,useClientAuth,clientAuthApi,clientAccountsApi,useClientDatahooks - ✅ Pages auth : Login/Register combiné, ForgotPassword, ResetPassword
- ✅ Layout :
ClientPortalLayoutavec navigation responsive - ✅ Dashboard : Stats, projets récents, photographes, actions rapides
- ✅ Routes : Configuration TanStack Router complète
- ✅ i18n : Traductions FR/EN pour toutes les pages
Fichiers créés :
# Backend
infra/liquibase/changes/0161_create_client_accounts/up.sql
infra/liquibase/changes/0161_create_client_accounts/down.sql
backend/src/client-auth/ (11 fichiers)
backend/src/client-accounts/ (6 fichiers)
openapi/components/client-auth.yaml
openapi/paths/client-auth.*.yaml (8 fichiers)
openapi/paths/client-accounts.*.yaml (6 fichiers)
# Frontend
frontend/src/client-auth/ (7 fichiers)
frontend/src/pages/client-portal/auth/ (3 dossiers)
frontend/src/pages/client-portal/dashboard/
frontend/src/components/layout/ClientPortalLayout.tsx
frontend/src/routes/client-portal.routes.tsx
Fichiers modifiés :
backend/src/db/database.types.ts- Types Kyselybackend/src/api/api-platform.module.ts- Import modulesfrontend/src/main.tsx- ClientAuthProviderfrontend/src/router.tsx- Routes client portalfrontend/src/routes/types.ts- Type RouteConfigfrontend/src/i18n/locales/*/translation.json- Traductionsopenapi/index.yaml- Nouveaux paths et security schemeopenapi/components/schemas.yaml- Références schemasopenapi/components/securitySchemes.yaml- clientBearerAuthinfra/liquibase/changelog-master.yaml- Changeset 0161
🎯 Prochaines Étapes
Toutes les 5 phases sont complétées. Améliorations futures possibles :
- Notifications push / WebSocket pour messages en temps réel
- Export données client (RGPD portabilité)
- Galeries photos partagées via le portail
- Signature électronique de documents intégrée
- Statistiques d'utilisation du portail pour le photographe
Vision
Transformer l'accès client de token-only vers un système hybride :
- Accès Token (existant) : Accès rapide sans compte pour voir/signer un document spécifique
- Compte Client (nouveau) : Compte authentifié avec login/mot de passe pour une expérience complète
Un client avec un compte verra tous ses projets avec tous les photographes depuis une interface unifiée.
Objectifs Métier
Pour le Client
- Vue unifiée : Tous ses projets avec tous les photographes au même endroit
- Historique persistant : Retrouver ses documents, messages, fichiers même après des années
- Communication directe : Messagerie avec chaque photographe
- Espace personnel : Stockage de fichiers (photos reçues, documents importants)
- Préférences : Notifications, thème, timezone personnalisés
Pour le Photographe
- Clients engagés : Meilleure conversion et fidélisation
- Communication centralisée : Historique des échanges avec chaque client
- Professionnalisme : Image "studio premium" avec portail client dédié
- Réduction des emails : Moins de "Où en est-on ?" grâce au portail
Architecture
Modèle de Données
erDiagram
CLIENT_ACCOUNTS ||--o{ CLIENT_ACCOUNT_CONTACTS : has
CLIENT_ACCOUNTS ||--o{ CLIENT_MESSAGES : sends
CLIENT_ACCOUNTS ||--o{ CLIENT_FILES : owns
CLIENT_ACCOUNTS ||--|| CLIENT_PREFERENCES : has
CONTACTS ||--o{ CLIENT_ACCOUNT_CONTACTS : linked_to
CONTACTS ||--o{ SESSIONS : has
CONTACTS ||--o{ QUOTES : has
CONTACTS ||--o{ INVOICES : has
CONTACTS ||--o{ CONTRACTS : has
CONTACTS }|--|| USERS : owned_by
CLIENT_MESSAGES }|--|| CONTACTS : via
CLIENT_MESSAGES }|--|| USERS : with
Principe Clé
ClientAccountest global (un email = un compte, système entier)Contactreste local au photographe (owner_id)- La table
client_account_contactsfait la liaison
graph TB
subgraph Global[Système Global]
CA[ClientAccount<br/>email unique global]
end
subgraph Photographer1[Photographe A]
U1[User A]
C1[Contact A1<br/>owner_id = A]
S1[Sessions de A]
end
subgraph Photographer2[Photographe B]
U2[User B]
C2[Contact A2<br/>owner_id = B]
S2[Sessions de B]
end
CA -->|linked to| C1
CA -->|linked to| C2
C1 -->|contact_id| S1
C2 -->|contact_id| S2
Schéma Base de Données
Table client_accounts
CREATE TABLE client_accounts (
id VARCHAR(30) PRIMARY KEY, -- CUID
email VARCHAR(255) NOT NULL UNIQUE,
email_verified BOOLEAN DEFAULT false,
password_hash VARCHAR(255),
display_name VARCHAR(255),
avatar_url TEXT,
phone VARCHAR(50),
locale VARCHAR(10) DEFAULT 'fr-FR',
status VARCHAR(20) DEFAULT 'PENDING', -- PENDING, ACTIVE, DISABLED
last_login_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_client_accounts_email ON client_accounts(email);
CREATE INDEX idx_client_accounts_status ON client_accounts(status);
Table client_account_contacts (liaison)
CREATE TABLE client_account_contacts (
id VARCHAR(30) PRIMARY KEY,
client_account_id VARCHAR(30) NOT NULL REFERENCES client_accounts(id) ON DELETE CASCADE,
contact_id VARCHAR(30) NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
linked_at TIMESTAMPTZ DEFAULT NOW(),
linked_by VARCHAR(20) NOT NULL, -- 'CLIENT' | 'PHOTOGRAPHER' | 'AUTO'
UNIQUE(client_account_id, contact_id)
);
CREATE INDEX idx_cac_client_account ON client_account_contacts(client_account_id);
CREATE INDEX idx_cac_contact ON client_account_contacts(contact_id);
Table client_messages
CREATE TABLE client_messages (
id VARCHAR(30) PRIMARY KEY,
client_account_id VARCHAR(30) NOT NULL REFERENCES client_accounts(id) ON DELETE CASCADE,
contact_id VARCHAR(30) NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
owner_id VARCHAR(30) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
direction VARCHAR(15) NOT NULL, -- 'TO_CLIENT' | 'FROM_CLIENT'
content TEXT NOT NULL,
read_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_cm_client_account ON client_messages(client_account_id);
CREATE INDEX idx_cm_contact ON client_messages(contact_id);
CREATE INDEX idx_cm_owner ON client_messages(owner_id);
CREATE INDEX idx_cm_created_at ON client_messages(created_at DESC);
Table client_files
CREATE TABLE client_files (
id VARCHAR(30) PRIMARY KEY,
client_account_id VARCHAR(30) NOT NULL REFERENCES client_accounts(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
mime_type VARCHAR(100),
size_bytes BIGINT,
storage_key TEXT NOT NULL,
folder VARCHAR(255) DEFAULT '/',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_cf_client_account ON client_files(client_account_id);
CREATE INDEX idx_cf_folder ON client_files(client_account_id, folder);
Table client_preferences
CREATE TABLE client_preferences (
id VARCHAR(30) PRIMARY KEY,
client_account_id VARCHAR(30) NOT NULL UNIQUE REFERENCES client_accounts(id) ON DELETE CASCADE,
notification_email BOOLEAN DEFAULT true,
notification_sms BOOLEAN DEFAULT false,
marketing_opt_in BOOLEAN DEFAULT false,
theme VARCHAR(20) DEFAULT 'system', -- 'light' | 'dark' | 'system'
timezone VARCHAR(50),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
Table client_password_reset_tokens
CREATE TABLE client_password_reset_tokens (
id VARCHAR(30) PRIMARY KEY,
client_account_id VARCHAR(30) NOT NULL REFERENCES client_accounts(id) ON DELETE CASCADE,
token_hash VARCHAR(64) NOT NULL, -- SHA-256 hash
encrypted_token TEXT NOT NULL, -- AES-256-GCM encrypted
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_cprt_token_hash ON client_password_reset_tokens(token_hash);
CREATE INDEX idx_cprt_client_account ON client_password_reset_tokens(client_account_id);
Table client_refresh_tokens
CREATE TABLE client_refresh_tokens (
id VARCHAR(30) PRIMARY KEY,
client_account_id VARCHAR(30) NOT NULL REFERENCES client_accounts(id) ON DELETE CASCADE,
token_hash VARCHAR(64) NOT NULL UNIQUE, -- SHA-256 hash
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_crt_token_hash ON client_refresh_tokens(token_hash);
CREATE INDEX idx_crt_client_account ON client_refresh_tokens(client_account_id);
Backend NestJS
Structure des Modules
backend/src/client-auth/
├── client-auth.module.ts
├── client-auth.controller.ts
├── client-auth.service.ts
├── client-auth-tokens.service.ts
├── client-auth-password.service.ts
├── client-auth-validation.service.ts
├── client-auth-invitation.service.ts # Phase 5
├── client-jwt.strategy.ts
├── client-auth.guard.ts
├── dto/
│ ├── client-register.dto.ts
│ ├── client-login.dto.ts
│ ├── client-forgot-password.dto.ts
│ └── client-reset-password.dto.ts
└── types/
└── authenticated-client.ts
backend/src/client-accounts/
├── client-accounts.module.ts
├── client-accounts.controller.ts
├── client-accounts.service.ts
├── client-accounts.repository.ts
├── client-account-linking.service.ts
└── dto/
├── update-client-profile.dto.ts
└── change-client-password.dto.ts
backend/src/client-messages/
├── client-messages.module.ts
├── client-messages.controller.ts
├── client-messages.service.ts
├── client-messages.repository.ts
└── dto/
├── send-message.dto.ts
└── list-messages.dto.ts
backend/src/client-files/
├── client-files.module.ts
├── client-files.controller.ts
├── client-files.service.ts
├── client-files.repository.ts
└── dto/
└── upload-file.dto.ts
Endpoints API
Authentification Client (/api/client-auth/)
| Méthode | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /register | Inscription client (email/password) | - |
| POST | /login | Connexion | - |
| POST | /logout | Déconnexion (révoque refresh token) | Client |
| POST | /forgot-password | Demande de reset | - |
| POST | /reset-password | Reset password avec token | - |
| POST | /refresh | Refresh access token | - |
| GET | /me | Profil client authentifié | Client |
| POST | /verify-email | Vérification email | - |
Compte Client (/api/client-accounts/)
| Méthode | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /me | Profil complet | Client |
| PUT | /me | Mise à jour profil | Client |
| PUT | /me/password | Changement mot de passe | Client |
| GET | /me/photographers | Liste des photographes liés | Client |
| GET | /me/projects | Tous les projets (sessions) | Client |
| GET | /me/projects/:sessionId | Détail d'un projet | Client |
| GET | /me/documents | Tous les documents | Client |
| DELETE | /me | Suppression compte (GDPR) | Client |
Messages (/api/client-messages/)
| Méthode | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /conversations | Liste conversations | Client |
| GET | /conversations/:contactId | Messages avec un photographe | Client |
| POST | /conversations/:contactId | Envoyer message | Client |
| PUT | /messages/:messageId/read | Marquer comme lu | Client |
Fichiers (/api/client-files/)
| Méthode | Endpoint | Description | Auth |
|---|---|---|---|
| GET | / | Liste fichiers personnels | Client |
| POST | /presigned-url | Obtenir URL d'upload présignée | Client |
| POST | / | Enregistrer fichier après upload | Client |
| GET | /:fileId | URL de téléchargement signée | Client |
| DELETE | /:fileId | Supprimer fichier | Client |
Côté Photographe (enrichissement /api/contacts/)
| Méthode | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /:contactId/invite-to-create-account | Inviter client à créer compte | User |
| GET | /:contactId/client-account-status | Statut compte client complet | User |
| GET | /:contactId/messages | Voir messages avec client | User |
| POST | /:contactId/messages | Envoyer message au client | User |
Sécurité
Tokens JWT Client
// Payload du token client (différent du user)
interface ClientTokenPayload {
sub: string; // client_account_id
email: string;
type: "client"; // Distingue des tokens user
iat: number;
exp: number;
}
- Access token : 15 minutes
- Refresh token : 30 jours, stocké en DB (révocable)
- Séparation : Guards distincts
ClientAuthGuardvsAuthGuard
Rate Limiting
@Throttle({ default: { limit: 5, ttl: 900000 } }) // 5 tentatives / 15 min
async login() {}
@Throttle({ default: { limit: 3, ttl: 3600000 } }) // 3 comptes / heure / IP
async register() {}
@Throttle({ default: { limit: 3, ttl: 3600000 } }) // 3 demandes / heure / email
async forgotPassword() {}
Vérification de Liaison
async linkContactToAccount(clientAccountId: string, contactId: string, linkedBy: string) {
const contact = await this.contactsRepository.findById(contactId);
const account = await this.clientAccountsRepository.findById(clientAccountId);
// Vérifier que l'email correspond
if (linkedBy === 'AUTO' || linkedBy === 'CLIENT') {
if (contact.email?.toLowerCase() !== account.email.toLowerCase()) {
throw new ForbiddenException('Email mismatch - cannot auto-link');
}
}
// Si PHOTOGRAPHER, vérifier qu'il est bien le owner
if (linkedBy === 'PHOTOGRAPHER') {
// La vérification se fait via le guard AuthGuard + ownership check
}
return this.clientAccountContactsRepository.create({
client_account_id: clientAccountId,
contact_id: contactId,
linked_by: linkedBy,
});
}
Frontend
Structure des Fichiers
frontend/src/client-auth/
├── ClientAuthProvider.tsx # Context provider auth client
├── useClientAuth.ts # Hook auth (login, logout, status)
├── api.ts # API auth (register, login, refresh)
├── client-accounts-api.ts # API compte (profil, password, préférences)
├── client-messages-api.ts # API messages (conversations, envoi)
├── client-files-api.ts # API fichiers (upload, download, delete)
├── useClientData.ts # Hook données client (projets, documents)
├── useClientMessages.ts # Hooks TanStack Query messages
├── useClientFiles.ts # Hooks TanStack Query fichiers
├── types.ts # Types partagés
├── token.ts # Gestion tokens JWT
└── index.ts # Barrel exports
frontend/src/pages/client-portal/
├── auth/
│ ├── ClientPortalLoginPage/
│ ├── ClientPortalForgotPasswordPage/
│ └── ClientPortalResetPasswordPage/
├── dashboard/
│ └── ClientPortalDashboardPage/
├── projects/
│ ├── ClientPortalProjectsPage/
│ └── ClientPortalProjectViewPage/
├── documents/
│ └── ClientPortalDocumentsPage/
├── messages/
│ └── ClientPortalMessagesPage/
├── files/
│ └── ClientPortalFilesPage/
└── settings/
└── ClientPortalSettingsPage/
└── components/
├── ProfileSection.tsx
├── PasswordSection.tsx
├── NotificationsSection.tsx
├── ThemeSection.tsx
└── DangerZoneSection.tsx
frontend/src/components/layout/
└── ClientPortalLayout.tsx # Layout avec navigation responsive
Routes
// Routes publiques client (pas de layout authentifié)
/portal/login // Login/Register combiné (tabs)
/portal/forgot-password // Demande reset password
/portal/reset-password?token=... // Reset password
// Routes authentifiées client (ClientPortalLayout)
/portal/dashboard // Vue d'ensemble (stats, séances récentes)
/portal/sessions // Liste séances avec filtres
/portal/sessions/:sessionId // Détail séance avec timeline
/portal/documents // Documents unifiés (tous/devis/factures/contrats)
/portal/messages // Messagerie (conversations + fil)
/portal/files // Espace fichiers personnel
/portal/settings // Paramètres (profil, mot de passe, notifications, thème, suppression)
// Token access (existant - garde la compatibilité)
/client-access?token=...
Hook useClientAuth
export function useClientAuth() {
const [clientAccount, setClientAccount] = useState<ClientAccount | null>(
null,
);
const [isLoading, setIsLoading] = useState(true);
// Stockage tokens
const getAccessToken = () =>
localStorage.getItem("aaperture:client-auth-token");
const getRefreshToken = () =>
localStorage.getItem("aaperture:client-refresh-token");
// Refresh automatique
useEffect(() => {
const token = getAccessToken();
if (token && isTokenExpired(token)) {
refreshAccessToken();
}
}, []);
return {
clientAccount,
isAuthenticated: !!clientAccount,
isLoading,
login,
logout,
register,
refreshAccessToken,
};
}
Liaison Contact-ClientAccount
Scénarios de Liaison
1. Invitation par Photographe
sequenceDiagram
participant P as Photographe
participant B as Backend
participant E as Email
participant C as Client
P->>B: POST /contacts/:id/invite-to-create-account
B->>B: Génère invitation token
B->>E: Envoie email d'invitation
E->>C: Email avec lien /client/register?invite=TOKEN
C->>B: POST /client-auth/register (avec invite token)
B->>B: Crée ClientAccount + Liaison automatique
B->>C: 201 Created + JWT tokens
2. Auto-détection à l'Inscription
sequenceDiagram
participant C as Client
participant B as Backend
C->>B: POST /client-auth/register (email: x@y.com)
B->>B: Cherche Contacts avec email x@y.com
alt Contacts trouvés
B->>B: Propose liaison (retourne liste)
C->>B: POST /client-accounts/link-contacts
B->>B: Crée liaisons
else Pas de contacts
B->>B: Crée compte sans liaison
end
B->>C: 201 Created
3. Liaison via Token d'Accès
sequenceDiagram
participant C as Client
participant B as Backend
C->>B: GET /client-access?token=XXX (non authentifié)
B->>C: 200 OK + payload + "Créer un compte pour sauvegarder"
C->>B: POST /client-auth/register (avec access_token en body)
B->>B: Valide token, récupère contact_id
B->>B: Crée ClientAccount + Liaison automatique
B->>C: 201 Created + JWT tokens
Migration et Compatibilité
Coexistence des Systèmes
flowchart TD
A[Client accède au portail] --> B{A un compte?}
B -->|Oui| C[Login avec email/password]
B -->|Non| D{A un token d'accès?}
D -->|Oui| E[Accès token existant]
D -->|Non| F[Page login/register]
E --> G{Veut créer compte?}
G -->|Oui| H[Register avec pré-remplissage]
G -->|Non| I[Continue en mode token]
C --> J[Dashboard multi-photographes]
H --> J
Règles de Migration
- Pas de migration forcée : Les Contacts existants restent tels quels
- Liaison optionnelle : Quand un ClientAccount est créé avec un email existant dans Contacts, proposer liaison
- Tokens restent valides : L'accès token continue de fonctionner indépendamment
- Progressif : Le photographe peut inviter ses clients à créer un compte quand il le souhaite
RGPD / Conformité
Droits du Client
- Accès : Export de toutes ses données (projets, messages, fichiers)
- Rectification : Modification de son profil
- Effacement : Suppression du compte avec anonymisation des données liées
- Portabilité : Export JSON/CSV de ses données
Consentements
- Notifications email : Opt-in par défaut (transactionnel), opt-out possible
- Marketing : Opt-in explicite requis
- SMS : Opt-in explicite requis
Rétention des Données
- Messages : Conservés tant que le compte existe
- Fichiers : Conservés tant que le compte existe
- Logs de connexion : 90 jours
- Après suppression : Anonymisation immédiate, suppression complète après 30 jours
Phases d'Implémentation
Phase 1 : MVP Authentification (2-3 semaines) ✅ COMPLÉTÉE
Backend :
- Migration 0161 : tables
client_accounts,client_account_contacts,client_password_reset_tokens,client_refresh_tokens,client_preferences,client_email_verification_tokens,client_invitation_tokens - Module
client-auth: register, login, logout, forgot-password, reset-password, refresh, verify-email, me - Module
client-accounts: CRUD profil, liaison contacts, liste projets, documents, préférences - Guard
ClientAuthGuardet stratégie JWT (ClientJwtStrategy) - OpenAPI schemas et paths (client-auth.yaml, client-accounts paths)
- Types TypeScript générés (backend + frontend)
Frontend :
- Pages auth :
/portal/login(login + register tabs),/portal/forgot-password,/portal/reset-password - Hook
useClientAuthavecClientAuthProvider - Client API :
clientAuthApi,clientAccountsApi - Layout
ClientPortalLayoutavec navigation - Dashboard
/portal/dashboardavec stats, projets récents, photographes
Fichiers créés :
backend/src/client-auth/
├── client-auth.module.ts
├── client-auth.controller.ts
├── client-auth.service.ts
├── client-auth.repository.ts
├── client-auth-tokens.service.ts
├── client-auth-password.service.ts
├── client-jwt.strategy.ts
├── client-auth.guard.ts
├── dto/*.ts
├── types/authenticated-client.ts
└── index.ts
backend/src/client-accounts/
├── client-accounts.module.ts
├── client-accounts.controller.ts
├── client-accounts.service.ts
├── client-accounts.repository.ts
├── dto/*.ts
└── index.ts
frontend/src/client-auth/
├── ClientAuthProvider.tsx
├── useClientAuth.ts
├── api.ts
├── client-accounts-api.ts
├── useClientData.ts
├── types.ts
├── token.ts
└── index.ts
frontend/src/pages/client-portal/
├── auth/
│ ├── ClientPortalLoginPage/
│ ├── ClientPortalForgotPasswordPage/
│ └── ClientPortalResetPasswordPage/
└── dashboard/
└── ClientPortalDashboardPage/
frontend/src/components/layout/
└── ClientPortalLayout.tsx
frontend/src/routes/
└── client-portal.routes.tsx
Routes :
| Route | Description |
|---|---|
/portal/login | Login/Register combiné |
/portal/forgot-password | Demande reset password |
/portal/reset-password?token=... | Reset password |
/portal/dashboard | Dashboard authentifié |
Livrables :
- ✅ Client peut s'inscrire, se connecter, réinitialiser son mot de passe
- ✅ Dashboard basique avec liste des projets liés
- ✅ Navigation complète entre les sections
Phase 2 : Expérience Client (2 semaines) ✅ COMPLÉTÉE
Backend :
- Endpoints :
/me/projects,/me/documents(déjà implémentés en Phase 1) - Récupération des sessions/quotes/invoices/contracts via liaisons
- Timeline des projets (événements de base)
Frontend :
-
ClientPortalProjectsPage: Liste des projets avec recherche, filtres, et groupement par mois -
ClientPortalProjectViewPage: Détail avec timeline, documents liés, info photographe -
ClientPortalDocumentsPage: Vue unifiée avec tabs (tous/devis/factures/contrats), recherche, filtre photographe - Composants intégrés : Cards photographe, Timeline basique
Fichiers créés :
frontend/src/pages/client-portal/
├── projects/
│ ├── ClientPortalProjectsPage/
│ │ ├── ClientPortalProjectsPage.tsx
│ │ └── index.ts
│ └── ClientPortalProjectViewPage/
│ ├── ClientPortalProjectViewPage.tsx
│ └── index.ts
└── documents/
└── ClientPortalDocumentsPage/
├── ClientPortalDocumentsPage.tsx
└── index.ts
Routes :
| Route | Description |
|---|---|
/portal/sessions | Liste des séances avec filtres |
/portal/sessions/:sessionId | Détail d'une séance |
/portal/documents | Documents unifiés (quotes, invoices, contracts) |
Livrables :
- ✅ Client voit toutes ses séances avec tous ses photographes
- ✅ Navigation fluide entre projets et documents
- ✅ Recherche et filtres avancés
Phase 3 : Messagerie (1-2 semaines) ✅ COMPLÉTÉE
Backend :
- Migration 0162 : table
client_messagesavec enumclient_message_direction - Module
client-messages: Repository, Service, Controller avec CRUD complet - Enrichissement
/api/contactscôté photographe (messages par contact, envoi)
Frontend :
-
ClientPortalMessagesPage: Liste conversations + fil de discussion combinés - Interface responsive avec toggle mobile
- Hooks TanStack Query pour conversations, messages, envoi, mark-as-read
Livrables :
- ✅ Client peut envoyer/recevoir des messages avec ses photographes
- ✅ Photographe voit les messages dans la fiche Contact
Phase 4 : Fichiers et Préférences (1-2 semaines) ✅ COMPLÉTÉE
Backend :
- Migration 0163 : table
client_filesavec indexes - Module
client-files: presigned URL upload, download, delete (5 endpoints) - Préférences via extension
client-accounts(déjà en Phase 1)
Frontend :
-
ClientPortalFilesPage: Espace fichiers avec upload presigned URL -
ClientPortalSettingsPage: 5 sections (profil, mot de passe, notifications, thème, zone danger) - Vue responsive (table desktop, cards mobile)
Livrables :
- ✅ Client peut stocker ses fichiers personnels (max 10MB, types validés)
- ✅ Préférences sauvegardées (notifications, thème, timezone)
Phase 5 : Côté Photographe (1 semaine) ✅ COMPLÉTÉE
Backend :
- Endpoint :
POST /contacts/:id/invite-to-create-account - Endpoint :
GET /contacts/:id/client-account-status - Service invitation : génération token SHA-256, email via EmailService, validation/consommation
- Repository : 6 méthodes gestion invitation tokens
- Gestion
invitationTokendans le register (liaison contact sourceINVITATION) - Modules mis à jour (ClientAuthModule, ContactsModule)
Frontend :
- Bouton "Inviter à créer un compte" dans
ContactViewPage(avec dialog confirmation) - Badge "Compte actif" sur
ContactHeroetContactCard - Badge "Invitation en attente" sur
ContactHero - Section messages client dans
ContactViewPage(tab Communications) - Login page : support paramètre
inviteavec auto-switch register + bannière - Hooks :
useClientAccountStatus,useInviteToCreateAccount - i18n FR/EN complet
Livrables :
- ✅ Photographe peut inviter ses clients à créer un compte via email
- ✅ Badges visuels "Portail Client" et "Invitation en attente" sur les contacts
- ✅ Visualisation et envoi de messages dans la fiche Contact
- ✅ Client reçoit email d'invitation avec lien direct vers l'inscription
🧪 Tests et Validation
Phase 1 - Tests ✅
- Inscription nouveau client
- Connexion avec email/mot de passe
- Réinitialisation de mot de passe (demande + reset)
- Refresh token automatique
- Déconnexion
- Accès dashboard authentifié
- Redirection si non authentifié
Phase 2 - Tests ✅
- Liste des projets avec filtres
- Détail d'un projet
- Vue des documents liés
- Recherche et filtres
- Navigation entre pages
Phase 3 - Tests ✅
- Liste des conversations
- Messages avec un photographe
- Envoi de message avec optimistic update
- Marquer conversation comme lue
- Navigation mobile
Phase 4 - Tests ✅
- Liste des fichiers
- Upload via presigned URL
- Téléchargement via URL signée
- Suppression avec confirmation
- Page paramètres : profil, mot de passe, notifications, thème
- Suppression de compte avec confirmation
Phase 5 - Tests ✅
- Invitation contact à créer un compte (envoi email)
- Lien invitation ouvre page register avec bannière
- Inscription via invitation lie le contact automatiquement
- Badge "Portail Client" visible sur fiche contact et liste
- Badge "Invitation en attente" visible sur fiche contact
- Section messages dans fiche contact (affichage + envoi)
- Gestion cas limites (pas d'email, compte déjà existant, invitation déjà envoyée)
À tester en intégration
- Liaison automatique contact-account à l'inscription (même email)
- Affichage multi-photographes
- Tokens JWT expiration et refresh
- Rate limiting endpoints sensibles
🚀 Déploiement
Checklist pré-déploiement
- Migration 0161 appliquée (tables auth + comptes)
- Migration 0162 appliquée (table messages)
- Migration 0163 appliquée (table fichiers)
- Tests manuels Phase 1-5 validés
- Variables d'environnement JWT client configurées
- OpenAPI regeneré et types vérifiés
- Traductions complètes (FR/EN)
Variables d'environnement requises
# JWT pour clients (à ajouter si différent des users)
JWT_CLIENT_SECRET=<secret>
JWT_CLIENT_EXPIRES_IN=15m
JWT_CLIENT_REFRESH_EXPIRES_IN=30d
Commandes de déploiement
# Backend
npm run db:migrate # Applique migrations 0161, 0162, 0163
npm run build
npm run start:prod
# Frontend
npm run build
📚 Références
- PRIORITES.md - Priorités de développement
- PLAN_AMELIORATIONS_CRM.md - Plan détaillé CRM
- AGENTS_BACKEND.md (documentation interne) - Guidelines backend
- AGENTS_FRONTEND.md (documentation interne) - Guidelines frontend
- SECURITY_OPERATIONAL_PROCEDURES.md - Guidelines sécurité
📂 Index des Fichiers Créés
Backend
| Chemin | Description |
|---|---|
infra/liquibase/changes/0161_create_client_accounts/ | Migration DB (auth + comptes) |
infra/liquibase/changes/0162_create_client_messages/ | Migration DB (messagerie) |
infra/liquibase/changes/0163_create_client_files/ | Migration DB (fichiers) |
backend/src/client-auth/ | Module authentification client + invitation |
backend/src/client-accounts/ | Module gestion compte client |
backend/src/client-messages/ | Module messagerie client |
backend/src/client-files/ | Module fichiers client |
openapi/components/client-auth.yaml | Schemas OpenAPI auth |
openapi/components/client-messages.yaml | Schemas OpenAPI messages |
openapi/components/client-files.yaml | Schemas OpenAPI fichiers |
openapi/paths/client-auth.*.yaml | Endpoints auth |
openapi/paths/client-accounts.*.yaml | Endpoints compte |
openapi/paths/client-messages.*.yaml | Endpoints messages |
openapi/paths/client-files.*.yaml | Endpoints fichiers |
Frontend
| Chemin | Description |
|---|---|
frontend/src/client-auth/ | Module auth React |
frontend/src/components/layout/ClientPortalLayout.tsx | Layout portail |
frontend/src/pages/client-portal/auth/ | Pages authentification |
frontend/src/pages/client-portal/dashboard/ | Page dashboard |
frontend/src/pages/client-portal/sessions/ | Pages séances |
frontend/src/pages/client-portal/documents/ | Page documents |
frontend/src/pages/client-portal/messages/ | Page messagerie |
frontend/src/pages/client-portal/files/ | Page fichiers |
frontend/src/pages/client-portal/settings/ | Page paramètres |
frontend/src/pages/contacts/ContactViewPage/components/ContactClientMessagesSection.tsx | Section messages dans fiche contact |
frontend/src/routes/client-portal.routes.tsx | Configuration routes |
Routes Disponibles
| Route | Auth | Description |
|---|---|---|
/portal/login | Non | Login/Register |
/portal/forgot-password | Non | Demande reset |
/portal/reset-password | Non | Reset password |
/portal/dashboard | Oui | Tableau de bord |
/portal/sessions | Oui | Liste séances |
/portal/sessions/:id | Oui | Détail séance |
/portal/documents | Oui | Documents unifiés |
/portal/messages | Oui | Messagerie |
/portal/files | Oui | Fichiers personnels |
/portal/settings | Oui | Paramètres |