Aller au contenu

Story AUTH-004: Operator activation flow (set password via invitation link)

Module: identity Slice: S3a reduced (arch §11) — V0 envelope AUTH-001 (arch §0.1; design decision D-v0.0.0-1) Side: [OPERATOR] Version target: V0 — v0.0.0 Priority: 4 Depends on: AUTH-003 (an invitation must exist), AUTH-001 Can develop concurrently with: AUTH-005 Merge order: After AUTH-003 Estimated complexity: S PRD User Stories: AUTH-OP-01 (operator first-time password setup) Wireframe: N/A — no prototype generated (design ref: docs/design/identity-design.md §5.5 / §7.7)


Objective

Let an invited CABINET_ADMIN turn their invitation link into a working account by choosing a password. In v0.0.0 the activation screen is password only — there is no MFA phone capture (MFA is V1, design decision D-v0.0.0-1). After activation the account becomes ACTIVE and the operator can log in (AUTH-002).


Backend Scope

Entities

Reads/updates user_invitations (state, consumed_at) and users (state → ACTIVE). Sets the password in Keycloak.

Service Layer

  • Resolve the invitation by token_hash (SHA-256 of the token in the URL). Validate: state = PENDING and expires_at > now().
  • On valid token + valid password: set the password in Keycloak for the user, mark the invitation CONSUMED, set users.state = ACTIVE, audit the activation.
  • Token error sub-states (design §5.5.bis):
  • Expired or already-consumed → the same generic "expired or already used" copy (anti-enumeration).
  • Already ACTIVE → "already active" copy + a link to sign in.
  • v0.0.0: no MFA phone block, no WhatsApp checkbox (those fields appear only in V1).

API Endpoints

Method Path Request Response Auth
GET /api/auth/operator/activation/{token} 200 {state:"PENDING", displayName, cabinetName} / 410 expired-or-consumed / 409 already-active none
POST /api/auth/operator/activation/{token} {password, passwordConfirm} 200 {redirectTo:"/login"} / 410 / 409 / 422 weak-password none

Validation Rules

  • Password min 8 characters (R-OP-013). No complexity rule, no rotation in V1.
  • password == passwordConfirm.
  • Token must be PENDING and unexpired.

Multi-Tenant Considerations

The invitation and user belong to one tenant; the activation only mutates that tenant's user. AUTH uses the platform datasource. No cross-tenant exposure of displayName/cabinetName (resolved from the invitation's own tenant).

Audit & Logging

  • operator_activateduser_id, activation_age_seconds.
  • operator_invitation_expiredinvited_user_id (when an expired token is hit, if not already marked).

Frontend Scope

Pages & Routes

  • Route /activate?token=…. Single screen (no wizard — design decision D12). Operator console. Design §5.5.

Components

  • Input type=password ×2 with eye-toggle, a live password-rule checklist, Button primary. (The Input type=tel MFA-phone field and WhatsApp Checkbox are V1 only — do not render in v0.0.0.)

UI States (ALL REQUIRED)

State Behavior French Copy
Loading Placeholder card while the token is resolved
Empty Blank form (nominal case) H1 « Bienvenue, {displayName} »; « Choisissez votre mot de passe »; rule « Au moins 8 caractères »; « Confirmez votre mot de passe »; button « Activer mon compte »
Error Dedicated screen per token state Expired/used: « Lien d'invitation expiré ou déjà utilisé. Demandez un nouvel accès à votre administrateur. » · Already active: « Votre compte est déjà actif. » + link « Se connecter → »
Offline / low network Warning banner; submission blocked (activation is a server mutation) « Connexion lente. Vérifiez votre réseau. »
Success Toast + redirect to /login with the identifiant pre-filled (success toast then redirect)

Responsive Behavior

  • Desktop-first; card centred max 420 px across 768 / 1024 / 1440 (design §8.2).

Interactions

  • Password rules shown before entry and validated live as the user types (reduces submit bounce).

Acceptance Criteria

AC-001: Valid token + valid password activates the account
Given an invited CABINET_ADMIN clicks a PENDING invitation link before its expiry
When they choose a password of at least 8 characters and submit
Then the password is set in Keycloak, the invitation is marked CONSUMED, and the user becomes ACTIVE
And they are redirected to /login with the identifiant pre-filled
And operator_activated is audit-logged
AC-002: Expired or already-used token shows the generic error
Given an invitation link that is expired or already consumed
When the user opens it
Then they see "Lien d'invitation expiré ou déjà utilisé. Demandez un nouvel accès à votre administrateur."
And no password can be set
AC-003: Already-active account is handled
Given the user is already ACTIVE
When they open an activation link
Then they see "Votre compte est déjà actif." with a "Se connecter →" link

Compliance Rules

  • RBAC + traçabilité = VÉRIFIÉ; activation must be audit-logged.
  • French strings only, fr-FR (AUTH-NFR-07). Min-8-char password policy (R-OP-013).

Standards & Conventions

  • docs/standards/react-ts-guidelines.md, docs/standards/design-system.md (Input, Button, checklist).
  • docs/standards/java-spring-guidelines.md, docs/standards/api-guidelines.md.
  • docs/standards/critical-rules.md (no cleartext token; SHA-256 lookup).

Testing Requirements

Unit Tests

  • Password < 8 chars → 422; mismatch → 422.
  • Token state transitions: PENDING→CONSUMED; expired→410; already-active→409.

Integration Tests

  • Happy path: GET activation resolves displayName + cabinetName; POST sets the Keycloak password and flips state to ACTIVE.
  • Expired/consumed token returns the generic error; replay of a consumed token does not re-activate.

What QA Will Validate

  • A fresh invitation activates with password only (no MFA field appears); expired/used/already-active screens show exact French copy; redirect to /login pre-fills the identifiant.

Out of Scope

  • MFA phone capture + WhatsApp opt-in on the activation screen — V1.
  • Password reset / forgot-password — V1.
  • Operator self-invite or admin-driven invite of further users — V1.

Definition of Done

  • [ ] Valid token + password activates the account and redirects to /login
  • [ ] Expired/used and already-active screens render with exact French copy
  • [ ] Password min-8 enforced; confirm-match enforced
  • [ ] Activation screen renders at 768 / 1024 / 1440 px; no MFA field in v0.0.0
  • [ ] All five UI states implemented with exact French copy
  • [ ] Audit entry operator_activated (and operator_invitation_expired where relevant)
  • [ ] Unit + integration tests written and passing
  • [ ] Tenant isolation verified (activation only mutates the invitation's own tenant)
  • [ ] No silent failures on network errors (submission blocked offline)