Aller au contenu

API — Introduction & authentification

SurfaceBase URLAuthUsage
API v1/v1/...JWT Bearer + MFA, API-key, OAuth2Fronts tenant/reception/mobile, intégrateurs
API admin/admin/...Cookie session cp_session ou header x-platform-admin-keyConsole Logmis control-plane uniquement

Les specs OpenAPI sont disponibles dans le monorepo :

  • apps/api/openapi.json — 128 paths, 141 opérations (API v1)
  • apps/api/openapi.admin.json — 9 paths (API admin)

Le flux standard pour les fronts (ADR-00D85 — résolution tenant globale) :

# Étape 1 — Login : email + mot de passe uniquement, sans x-tenant-id
POST /sessions
Content-Type: application/json
{ "email": "...", "password": "..." }
# Réponse si MFA requis :
{ "mfaRequired": true, "sessionToken": "<opaque>" }
# Étape 2 — Vérification TOTP
POST /sessions/mfa/verify
{ "sessionToken": "<opaque>", "code": "123456" }
# Réponse :
{ "accessToken": "eyJ...", "refreshToken": "<opaque>" }
# Étape 3 — Récupérer le contexte utilisateur (tenantId inclus)
GET /v1/me
Authorization: Bearer <accessToken>
# Réponse :
{ "data": { "userId": "...", "tenantId": "...", "sessionId": "...", "roles": [...], "establishments": [...] } }

Important — résolution tenant (ADR-00D85) : le login (POST /sessions) et les endpoints d’enrôlement MFA n’exigent pas de header x-tenant-id. Le serveur résout le tenant depuis l’email, qui est unique globalement. Le header x-tenant-id est requis uniquement sur POST /sessions/refresh — le front doit persister le tenantId retourné par GET /v1/me (champ data.tenantId) après le premier login.

Le accessToken est un JWT HS256 signé avec JWT_SECRET. Il contient :

  • sub — userId (UUID)
  • tid — tenantId (UUID)
  • scope — scope RBAC (OrgPath)
  • eid — establishmentId (résiduel : absent sur mobile-staff)
  • Durée de vie : 15 minutes

Le refreshToken est opaque, haché en DB (reuse-detection activée).

Si le compte n’a pas encore confirmé son TOTP, le login retourne MFA_ENROLLMENT_REQUIRED :

# Démarrer l'enrôlement (x-tenant-id optionnel — résolution depuis l'email)
POST /sessions/mfa/enroll/begin
{ "email": "...", "password": "..." }
# Réponse : URI TOTP + secret base32 à scanner ou saisir manuellement
{ "otpauthUri": "otpauth://totp/...", "secret": "<base32>" }
# Confirmer avec le premier code valide — ouvre la session
POST /sessions/mfa/enroll/confirm
{ "email": "...", "password": "...", "code": "123456" }
# Réponse : accessToken + refreshToken (session ouverte)
{ "accessToken": "eyJ...", "refreshToken": "<opaque>", "expiresAt": "..." }

Le secret TOTP n’est jamais persisté côté front — il est généré et chiffré AES-256-GCM en DB uniquement après confirmation du premier code valide. Il n’est exposé qu’une seule fois, à enroll/begin.

Format : pmsk_live_<base62> (préfixe pmsk_live_ vérifié côté serveur).

GET /v1/reservations
Authorization: pmsk_live_<key>

Les API-keys passent par le RBAC chemin unique (même PermissionResolver que les JWT). Rate-limit par tier (X-RateLimit-* headers).

POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=<id>&client_secret=<secret>&scope=<scope>
# Réponse :
{ "access_token": "eyJ...", "token_type": "bearer", "expires_in": 3600 }

L’access_token OAuth2 est un JWT — transporté en Authorization: Bearer sur toutes les requêtes suivantes. Le subject est synthétique (pas un userId humain).

Le flux OIDC (Authorization Code avec PKCE) permet l’account-linking. Endpoints : GET /v1/sso/authorize, GET /v1/sso/callback. JIT provisioning opt-in. Deny par défaut (aucun provider SSO = pas d’accès SSO).

Deux modes :

Session web (console admin) :

POST /admin/cp-sessions
{ "email": "admin@platform.internal", "password": "...", "totpCode": "123456" }
# → cookie httpOnly Secure SameSite=Strict cp_session
# Les requêtes suivantes transportent le cookie automatiquement

Clé API CLI/machine :

GET /admin/tenants
x-platform-admin-key: <clé opérateur>

La session web expire après 15 min d’inactivité ou 4h absolues. CSRF double-submit Origin fail-closed.

Toutes les réponses succès suivent la même structure :

// Ressource unique
{
"data": { ... }
}
// Liste paginée
{
"data": [ ... ],
"page": 1,
"pageSize": 20,
"total": 142
}
{
"code": "RESERVATION_NOT_FOUND",
"message": "Réservation introuvable",
"origin": {
"boundedContext": "reservation",
"module": "reservation"
},
"status": 404,
"traceId": "01HXYZ..."
}
Code HTTPSignification
400Validation Zod échouée (body/query malformés)
401Token absent, expiré ou révoqué
403Permission insuffisante (RBAC) ou IDOR (scope OrgPath)
404Ressource inexistante ou hors du scope tenant
409Conflit métier (ex: UNIT_TYPE_ALREADY_EXISTS)
422Erreur domaine (ex: RESERVATION_ALREADY_CONFIRMED)
429Rate limit dépassé
500Erreur interne (incluant le traceId pour le support)

Les codes métier (code) sont ceux de DomainError (ex: INVENTORY_INSUFFICIENT, FOLIO_CLOSED).

Pour les opérations non-idempotentes (POST), le header Idempotency-Key permet la déduplication :

POST /v1/reservations
Idempotency-Key: my-unique-key-2026-abc123

Si le même appel est rejoué avec la même clé, le serveur retourne la réponse originale sans créer de doublon. La clé est stockée dans Redis avec un TTL de 24h.

Contraintes : 8 à 200 caractères, opaque (pas de sémantique imposée côté client).

Les listes sont paginées par offset :

GET /v1/reservations?page=2&pageSize=50
ParamètreDéfautMaximum
page1-
pageSize20100

Chaque requête est vérifiée en 3 couches :

  1. RLS Postgres — filtre automatique par tenant_id
  2. ScopeAuthorizer (couche HTTP) — vérifie que la ressource appartient à l’établissement/mandat du JWT
  3. subject du JWT — jamais extrait du body, toujours du token

Un utilisateur ne peut accéder qu’aux ressources dans son home_scope OrgPath (ADR-00D82).

Les opérations sont groupées par tag dans la référence :

reservations · folios · deposits · guests · travelers · housekeeping · work-orders · inventory · out-of-service · rate-plans · allotments · availability · rack · reporting · organization · identity · users · long-stay-contracts · mice-events · mice-equipment · main-courante · inspections · meters · cashless · owner-revenue · storage