Aller au contenu principal

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 :

  1. Code d'identification publique (ex: CL-AG-0001)
  2. Login (nom d'utilisateur)
  3. Téléphone
  4. Email

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ôlePermissions accordées
administrateurToutes les permissions
observateurorganization.manage, stats.exhaustive.read, user.manage, role.read
supportorganization.manage, user.read, role.read, ticket.read, log.read
Premier accès (Bootstrap)

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ôlePermissions
gestionnaireToutes les permissions de l'organisation
superviseurcard.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
caissiercard.read, card.recharge.cash, card.correction, card.create, card.holder.create, transaction.read, register.*
vendeurproduct.sell, product.read, transaction.read
porteurcard.read, card.recharge.mobile_money (+ child.* si fonctionnalités parent activées)

Permissions CardHolder​

PermissionDescription
card.readConsultation du solde, détails et historique de la carte
card.recharge.mobile_moneyRecharge 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.read
  • child.recharge
  • child.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..."
}
Rotation de tokens

À 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.

Détection de vol

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"]
}
ClaimDescription
subUUID de l'utilisateur (platform_user, tenant_user ou card_holder)
kind"Platform", "Tenant" ou "CardHolder"
display_nameNom affiché (pour l'UI)
organization_idUUID de l'organisation (null pour platform)
organization_slugSlug URL de l'organisation (null pour platform)
permissionsTableau des codes de permission résolus (Vec<String>)

Durées de validité​

TokenDuréeConfiguration
Access token12 heures (720 min / 43 200 s)CAFFETTE_JWT_ACCESS_TTL_MINUTES
Refresh token30 jours (2 592 000 s)CAFFETTE_JWT_REFRESH_TTL_DAYS

Erreurs d'authentification​

Code HTTPerror_codeSignification
401UNAUTHORIZEDToken manquant, invalide ou expiré ; identifiants incorrects
403FORBIDDENUtilisateur désactivé, organisation suspendue/archivée, ou libre-service désactivé
403ROLE_NOT_ALLOWEDLe rôle de l'utilisateur ne permet pas cette action
404NOT_FOUNDOrganisation, utilisateur ou entité introuvable
422VALIDATION_ERRORDonnées invalides (ex: mot de passe trop court)
500INTERNAL_ERRORErreur interne du serveur