Authentification
La Caffette utilise des tokens JWT signés avec HMAC-SHA256 à l'aide de deux clés distinctes de 32 caractères (CAFFETTE_JWT_ACCESS_SECRET, CAFFETTE_JWT_REFRESH_SECRET). Deux types de tokens existent : les access tokens (720 min / 12 h) et les refresh tokens (30 jours).
Trois types d'utilisateurs sont pris en charge :
- Platform (administrateurs N2)
- Tenant (personnel d'organisation : caissiers, vendeurs, superviseurs, admin N1)
- CardHolder (porteurs de carte en libre-service)
Vue d'ensemble des flux
sequenceDiagram
participant Client
participant API
participant DB
Note over Client: Tous les types d'utilisateurs
Client->>API: POST /api/v1/auth/login<br/>{login, password} [+ tenant_slug si besoin]
alt Plateforme (admin N2)
API->>DB: SELECT platform_users WHERE login/email=?
API->>DB: Résoudre rôles + permissions
Note over API: Pas de contexte d'organisation
API->>API: Sign JWT (UserKind=Platform)
else Tenant (personnel)
API->>DB: Résoudre organisation (tenant_slug ou code)
API->>DB: SET search_path TO tenant.public
API->>DB: SELECT tenant_users WHERE code/login/phone/email=?
API->>DB: Résoudre rôles + permissions
API->>DB: RESET search_path
API->>API: Sign JWT (UserKind=Tenant)
else CardHolder (porteur)
API->>DB: Résoudre organisation (tenant_slug ou code)
API->>DB: SET search_path TO tenant.public
API->>DB: SELECT card_holders WHERE code/login/phone/email=?
API->>DB: Résoudre permissions
API->>DB: RESET search_path
API->>API: Sign JWT (UserKind=CardHolder)
end
API->>Client: {access_token, refresh_token, user, must_change_password}
Connexion (unifiée)
Le endpoint POST /api/v1/auth/login gère les trois types d'utilisateurs (platform, tenant, cardholder) dans une même requête. Le système distingue automatiquement le type en fonction de la présence de tenant_slug et de l'identité trouvée en base.
Un endpoint dédié POST /api/v1/auth/cardholder-login existe également pour le portail libre-service des porteurs de carte. Il est strictement limité à 3 req/minute et vérifie que le libre-service est activé pour l'organisation (settings.self_service_enabled).
POST /api/v1/auth/login
Body (Platform) :
{
"login": "admin@caffette.app",
"password": "••••••••"
}
Body (Tenant / CardHolder) :
{
"login": "jean.mbeki",
"password": "••••••••",
"tenant_slug": "cours-lumiere"
}
Le tenant_slug peut être omis si l'identifiant est un code d'identification publique (ex: CL-AG-0001, CL-EL-0008) — l'organisation est alors résolue automatiquement à partir du préfixe du code (short_code).
Cascade de résolution du login
Pour les utilisateurs Tenant et CardHolder, le champ login est résolu dans cet ordre :
- Code d'identification publique (ex:
CL-AG-0001) - Login (nom d'utilisateur)
- Téléphone
Cela permet de se connecter avec son code généré plutôt que son login ou email.
Réponse (200) — Platform :
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 43200,
"must_change_password": false,
"user": {
"id": "uuid",
"display_name": "Administrateur Système",
"kind": "platform",
"permissions": [
"organization.manage",
"stats.exhaustive.read",
"user.manage"
],
"organization_id": null,
"organization_slug": null,
"organization_name": null
}
}
Réponse (200) — Tenant :
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 43200,
"must_change_password": true,
"user": {
"id": "uuid",
"display_name": "Jean Mbeki",
"kind": "tenant",
"permissions": [
"card.read",
"card.recharge.cash"
],
"organization_id": "uuid-org",
"organization_slug": "cours-lumiere",
"organization_name": "Cours Lumière"
}
}
Réponse (200) — CardHolder :
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 43200,
"must_change_password": false,
"user": {
"id": "uuid-porteur",
"display_name": "Kouamé Alice",
"kind": "card_holder",
"permissions": [
"card.read",
"card.recharge.mobile_money"
],
"organization_id": "uuid-org",
"organization_slug": "cours-lumiere",
"organization_name": "Cours Lumière"
}
}
must_change_password
Le champ must_change_password: bool indique si l'utilisateur doit changer son mot de passe avant de poursuivre. Si true, le client doit rediriger vers le formulaire de changement de mot de passe.
Permissions Platform
| Rôle | Permissions accordées |
|---|---|
administrateur | Toutes les permissions |
observateur | organization.manage, stats.exhaustive.read, user.manage, role.read |
support | organization.manage, user.read, role.read, ticket.read, log.read |
Si aucun platform_user_role n'existe dans le système, le premier utilisateur qui se connecte obtient toutes les permissions. Cela permet l'initialisation du système.
Après la première assignation de rôle, ce mode bootstrap est désactivé.
Permissions Tenant
| Rôle | Permissions |
|---|---|
gestionnaire | Toutes les permissions de l'organisation |
superviseur | card.read, card.update, card.void, card.recharge.cash, card.recharge.mobile_money, card.correction, card.holder.*, card.link.*, transaction.read, reservation.*, menu.*, stock.read, stock.adjust, register.*, analytics.read, log.read |
caissier | card.read, card.recharge.cash, card.correction, card.create, card.holder.create, transaction.read, register.* |
vendeur | product.sell, product.read, transaction.read |
porteur | card.read, card.recharge.mobile_money (+ child.* si fonctionnalités parent activées) |
Permissions CardHolder
| Permission | Description |
|---|---|
card.read | Consultation du solde, détails et historique de la carte |
card.recharge.mobile_money | Recharge par mobile money |
Si le porteur a des enfants liés et que les fonctionnalités parent sont activées, des permissions supplémentaires sont accordées :
child.balance.readchild.rechargechild.history.read
Résolution de QR code
POST /api/v1/cards/resolve-qr
Ce endpoint permet de résoudre un QR code scanné. Il n'est pas lié à l'authentification, mais est utilisé après connexion pour identifier une carte.
Body :
{
"qr_token": "base64-encoded-qr-data"
}
Réponse (200) :
{
"card_id": "uuid-carte",
"code": "CL-EL-0008",
"card_number": "00042",
"status": "active",
"balance_cents": 4500,
"recent_transactions": []
}
Utilisation du token
Header Authorization
Tous les endpoints protégés requièrent le token dans le header :
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Contexte d'organisation
Le contexte d'organisation (tenant) est déterminé à partir des claims du JWT pour les utilisateurs Tenant et CardHolder.
Pour les utilisateurs Platform, le JWT n'a pas de contexte d'organisation. S'ils doivent agir dans un tenant spécifique, ils peuvent utiliser l'en-tête X-Tenant-Id :
X-Tenant-Id: uuid-org
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
La valeur de X-Tenant-Id est un UUID d'organisation. Le middleware résout le schéma depuis public.organizations WHERE id = $1 AND status = 'active'.
Rafraîchissement du token
POST /api/v1/auth/refresh
Les access tokens expirent après 12 heures. Utilisez le refresh token pour obtenir un nouvel access token sans ré-authentification.
Body :
{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Réponse (200) :
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
À chaque rafraîchissement, un nouveau refresh token est émis et l'ancien est invalidé. Les permissions sont re-résolues depuis la DB à chaque refresh. Pour les cardholders, les permissions parent sont également re-vérifiées.
Si un refresh token est utilisé alors qu'il n'existe pas en base (token volé), tous les refresh tokens de l'utilisateur sont révoqués immédiatement par mesure de sécurité.
Changement de mot de passe
POST /api/v1/auth/change-password
Permet de changer le mot de passe pour les utilisateurs Platform, Tenant et CardHolder. Nécessite le token d'accès dans le header (lu manuellement par le middleware).
Body :
{
"current_password": "••••••••",
"new_password": "••••••••"
}
Réponse (200) :
{
"message": "Mot de passe modifié"
}
Le nouveau mot de passe doit contenir au moins 6 caractères. Après un changement réussi, le flag must_change_password est remis à false en base de données.
Déconnexion
POST /api/v1/auth/logout
Body :
{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Invalide le refresh token côté serveur. L'access token reste valide jusqu'à expiration.
Structure du JWT
Les tokens JWT sont signés avec l'algorithme HMAC-SHA256 via deux clés distinctes de 32 caractères (CAFFETTE_JWT_ACCESS_SECRET pour l'access token, CAFFETTE_JWT_REFRESH_SECRET pour le refresh token).
Access Token Claims
{
"sub": "uuid-utilisateur",
"exp": 1750000000,
"iat": 1749999100,
"kind": "CardHolder",
"display_name": "Kouamé Alice",
"organization_id": "uuid-org",
"organization_slug": "cours-lumiere",
"permissions": ["card.read", "card.recharge.mobile_money"]
}
| Claim | Description |
|---|---|
sub | UUID de l'utilisateur (platform_user, tenant_user ou card_holder) |
kind | "Platform", "Tenant" ou "CardHolder" |
display_name | Nom affiché (pour l'UI) |
organization_id | UUID de l'organisation (null pour platform) |
organization_slug | Slug URL de l'organisation (null pour platform) |
permissions | Tableau des codes de permission résolus (Vec<String>) |
Durées de validité
| Token | Durée | Configuration |
|---|---|---|
| Access token | 12 heures (720 min / 43 200 s) | CAFFETTE_JWT_ACCESS_TTL_MINUTES |
| Refresh token | 30 jours (2 592 000 s) | CAFFETTE_JWT_REFRESH_TTL_DAYS |
Erreurs d'authentification
| Code HTTP | error_code | Signification |
|---|---|---|
401 | UNAUTHORIZED | Token manquant, invalide ou expiré ; identifiants incorrects |
403 | FORBIDDEN | Utilisateur désactivé, organisation suspendue/archivée, ou libre-service désactivé |
403 | ROLE_NOT_ALLOWED | Le rôle de l'utilisateur ne permet pas cette action |
404 | NOT_FOUND | Organisation, utilisateur ou entité introuvable |
422 | VALIDATION_ERROR | Données invalides (ex: mot de passe trop court) |
500 | INTERNAL_ERROR | Erreur interne du serveur |