Aller au contenu principal

Client Authentication System

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

PhaseDescriptionStatutProgression
Phase 1MVP Authentification✅ Complétée100%
Phase 2Expérience Client✅ Complétée100%
Phase 3Messagerie✅ Complétée100%
Phase 4Fichiers et Préférences✅ Complétée100%
Phase 5Côté Photographe✅ Complétée100%

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 defaultValue du 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 : uploadErrordownloadError dans ClientPortalFilesPage
  • ✅ 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 (onError callback)
  • ✅ 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 que ContactHero

Fichiers modifiés :

  • frontend/src/i18n/locales/en/translation.json - Clés nav, messages, files, contacts
  • frontend/src/i18n/locales/fr/translation.json - Clés nav, messages, files, contacts
  • frontend/src/pages/client-portal/messages/ClientPortalMessagesPage/ClientPortalMessagesPage.tsx - i18n, toasts, hauteur responsive
  • frontend/src/pages/client-portal/files/ClientPortalFilesPage/ClientPortalFilesPage.tsx - Fix downloadError, validation 10 MB
  • frontend/src/pages/contacts/ContactsListPage/components/ContactCard.tsx - Badge pending invitation
  • frontend/src/pages/contacts/ContactViewPage/components/ContactClientMessagesSection.tsx - Toast erreur envoi
  • frontend/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 /login user
  • ✅ 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.ts
  • backend/src/client-auth/client-auth.service.ts
  • backend/src/client-auth/client-auth.repository.ts
  • backend/src/client-accounts/client-accounts.repository.ts
  • frontend/src/components/layout/ClientPortalLayout.tsx
  • frontend/src/pages/client-portal/auth/ClientPortalLoginPage/ClientPortalLoginPage.tsx
  • frontend/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() dans useContacts.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 URL invite, auto-switch register, bannière invitation
  • ClientAuthProvider + api.ts : Support invitationToken dans 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 tokens
  • backend/src/client-auth/client-auth.service.ts - Gestion invitationToken au register
  • backend/src/client-auth/client-auth.module.ts - Imports CommunicationModule, ConfigModule, exports InvitationService
  • backend/src/contacts/contacts.controller.ts - Endpoints invite + client-account-status
  • backend/src/contacts/contacts.module.ts - Import ClientAuthModule
  • frontend/src/client/contacts/useContacts.ts - Hooks et API client-account-status + invitation
  • frontend/src/pages/contacts/ContactViewPage/ContactViewPage.tsx - Dialog invitation + section messages
  • frontend/src/pages/contacts/ContactViewPage/components/ContactHero.tsx - Badges + action invitation
  • frontend/src/pages/contacts/ContactsListPage/components/ContactCard.tsx - Badge portail
  • frontend/src/pages/client-portal/auth/ClientPortalLoginPage/ClientPortalLoginPage.tsx - Support invite token
  • frontend/src/client-auth/ClientAuthProvider.tsx - invitationToken dans register
  • frontend/src/client-auth/api.ts - invitationToken dans register
  • frontend/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_files avec 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_files
  • backend/src/api/api-platform.module.ts - Import ClientFilesModule
  • frontend/src/client-auth/index.ts - Export nouveaux hooks et API
  • frontend/src/routes/client-portal.routes.tsx - Routes fichiers + settings
  • frontend/src/router.tsx - Intégration route fichiers
  • frontend/src/components/layout/ClientPortalLayout.tsx - Navigation fichiers
  • frontend/src/i18n/locales/*/translation.json - Traductions fichiers et paramètres
  • openapi/index.yaml - Paths client-files
  • openapi/components/schemas.yaml - Références schemas client-files
  • infra/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_messages avec enum client_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 messages
  • backend/src/api/api-platform.module.ts - Import ClientMessagesModule
  • backend/src/contacts/contacts.controller.ts - Endpoints messages photographe
  • backend/src/contacts/contacts.module.ts - Import ClientMessagesModule
  • frontend/src/client-auth/index.ts - Export nouveaux hooks
  • frontend/src/routes/client-portal.routes.tsx - Routes messages
  • frontend/src/router.tsx - Intégration routes messages
  • frontend/src/components/layout/ClientPortalLayout.tsx - Navigation messages
  • frontend/src/i18n/locales/*/translation.json - Traductions messages
  • openapi/index.yaml - Paths messages
  • openapi/components/schemas.yaml - Références schemas messages
  • infra/liquibase/changelog-master.yaml - Changeset 0162

2026-01-29 - Phase 2 Complétée

Objectif : Pages projets et documents pour le client

Réalisations :

  • ClientPortalProjectsPage : Liste des projets avec recherche, filtres (tous/à venir/passés/confirmés/en attente), groupement par mois
  • ClientPortalProjectViewPage : Détail projet 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/projects, /portal/projects/:sessionId, /portal/documents
  • ✅ Navigation simplifiée (Dashboard, Projets, Documents)

Fichiers créés :

frontend/src/pages/client-portal/projects/ClientPortalProjectsPage/
frontend/src/pages/client-portal/projects/ClientPortalProjectViewPage/
frontend/src/pages/client-portal/documents/ClientPortalDocumentsPage/

Fichiers modifiés :

  • frontend/src/routes/client-portal.routes.tsx - Nouvelles routes
  • frontend/src/router.tsx - Intégration routes
  • frontend/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, useClientData hooks
  • ✅ Pages auth : Login/Register combiné, ForgotPassword, ResetPassword
  • ✅ Layout : ClientPortalLayout avec 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 Kysely
  • backend/src/api/api-platform.module.ts - Import modules
  • frontend/src/main.tsx - ClientAuthProvider
  • frontend/src/router.tsx - Routes client portal
  • frontend/src/routes/types.ts - Type RouteConfig
  • frontend/src/i18n/locales/*/translation.json - Traductions
  • openapi/index.yaml - Nouveaux paths et security scheme
  • openapi/components/schemas.yaml - Références schemas
  • openapi/components/securitySchemes.yaml - clientBearerAuth
  • infra/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é

  • ClientAccount est global (un email = un compte, système entier)
  • Contact reste local au photographe (owner_id)
  • La table client_account_contacts fait 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éthodeEndpointDescriptionAuth
POST/registerInscription client (email/password)-
POST/loginConnexion-
POST/logoutDéconnexion (révoque refresh token)Client
POST/forgot-passwordDemande de reset-
POST/reset-passwordReset password avec token-
POST/refreshRefresh access token-
GET/meProfil client authentifiéClient
POST/verify-emailVérification email-

Compte Client (/api/client-accounts/)

MéthodeEndpointDescriptionAuth
GET/meProfil completClient
PUT/meMise à jour profilClient
PUT/me/passwordChangement mot de passeClient
GET/me/photographersListe des photographes liésClient
GET/me/projectsTous les projets (sessions)Client
GET/me/projects/:sessionIdDétail d'un projetClient
GET/me/documentsTous les documentsClient
DELETE/meSuppression compte (GDPR)Client

Messages (/api/client-messages/)

MéthodeEndpointDescriptionAuth
GET/conversationsListe conversationsClient
GET/conversations/:contactIdMessages avec un photographeClient
POST/conversations/:contactIdEnvoyer messageClient
PUT/messages/:messageId/readMarquer comme luClient

Fichiers (/api/client-files/)

MéthodeEndpointDescriptionAuth
GET/Liste fichiers personnelsClient
POST/presigned-urlObtenir URL d'upload présignéeClient
POST/Enregistrer fichier après uploadClient
GET/:fileIdURL de téléchargement signéeClient
DELETE/:fileIdSupprimer fichierClient

Côté Photographe (enrichissement /api/contacts/)

MéthodeEndpointDescriptionAuth
POST/:contactId/invite-to-create-accountInviter client à créer compteUser
GET/:contactId/client-account-statusStatut compte client completUser
GET/:contactId/messagesVoir messages avec clientUser
POST/:contactId/messagesEnvoyer message au clientUser

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 ClientAuthGuard vs AuthGuard

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, projets récents)
/portal/projects // Liste projets avec filtres
/portal/projects/:sessionId // Détail projet 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

  1. Pas de migration forcée : Les Contacts existants restent tels quels
  2. Liaison optionnelle : Quand un ClientAccount est créé avec un email existant dans Contacts, proposer liaison
  3. Tokens restent valides : L'accès token continue de fonctionner indépendamment
  4. 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 ClientAuthGuard et 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 useClientAuth avec ClientAuthProvider
  • Client API : clientAuthApi, clientAccountsApi
  • Layout ClientPortalLayout avec navigation
  • Dashboard /portal/dashboard avec 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 :

RouteDescription
/portal/loginLogin/Register combiné
/portal/forgot-passwordDemande reset password
/portal/reset-password?token=...Reset password
/portal/dashboardDashboard 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 :

RouteDescription
/portal/projectsListe des projets avec filtres
/portal/projects/:sessionIdDétail d'un projet
/portal/documentsDocuments unifiés (quotes, invoices, contracts)

Livrables :

  • ✅ Client voit tous ses projets 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_messages avec enum client_message_direction
  • Module client-messages : Repository, Service, Controller avec CRUD complet
  • Enrichissement /api/contacts cô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_files avec 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 invitationToken dans le register (liaison contact source INVITATION)
  • Modules mis à jour (ClientAuthModule, ContactsModule)

Frontend :

  • Bouton "Inviter à créer un compte" dans ContactViewPage (avec dialog confirmation)
  • Badge "Compte actif" sur ContactHero et ContactCard
  • Badge "Invitation en attente" sur ContactHero
  • Section messages client dans ContactViewPage (tab Communications)
  • Login page : support paramètre invite avec 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


📂 Index des Fichiers Créés

Backend

CheminDescription
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.yamlSchemas OpenAPI auth
openapi/components/client-messages.yamlSchemas OpenAPI messages
openapi/components/client-files.yamlSchemas OpenAPI fichiers
openapi/paths/client-auth.*.yamlEndpoints auth
openapi/paths/client-accounts.*.yamlEndpoints compte
openapi/paths/client-messages.*.yamlEndpoints messages
openapi/paths/client-files.*.yamlEndpoints fichiers

Frontend

CheminDescription
frontend/src/client-auth/Module auth React
frontend/src/components/layout/ClientPortalLayout.tsxLayout portail
frontend/src/pages/client-portal/auth/Pages authentification
frontend/src/pages/client-portal/dashboard/Page dashboard
frontend/src/pages/client-portal/projects/Pages projets
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.tsxSection messages dans fiche contact
frontend/src/routes/client-portal.routes.tsxConfiguration routes

Routes Disponibles

RouteAuthDescription
/portal/loginNonLogin/Register
/portal/forgot-passwordNonDemande reset
/portal/reset-passwordNonReset password
/portal/dashboardOuiTableau de bord
/portal/projectsOuiListe projets
/portal/projects/:idOuiDétail projet
/portal/documentsOuiDocuments unifiés
/portal/messagesOuiMessagerie
/portal/filesOuiFichiers personnels
/portal/settingsOuiParamètres