Acheteur Profile Management

Summary

Authenticated acheteurs can read and update their profile (name, phone), initiate an email change (which requires password verification and a new email verification step), and change their password (which invalidates all sessions). All routes are behind authenticateAcheteur — the auth middleware enforces emailVerified status and the 48h deadline. Sensitive fields (passwordHash) are never returned in responses via an explicit SAFE_SELECT projection.

State Diagram

stateDiagram-v2
    state "Authentifié" as Authenticated
    state "Profil consulté" as ProfileRead
    state "Profil mis à jour" as ProfileUpdated
    state "Changement email en attente" as EmailChangePending
    state "Email changé et vérifié" as EmailChangeVerified
    state "Échec envoi email" as EmailChangeFailed
    state "Mot de passe changé" as PasswordChanged
    state "Sessions révoquées" as SessionsRevoked
    [*] --> Authenticated: Bearer token valid
    Authenticated --> ProfileRead: acheteur reads profile
    Authenticated --> ProfileUpdated: acheteur updates profile fields
    Authenticated --> EmailChangePending: acheteur requests email change
    EmailChangePending --> EmailChangeVerified: acheteur clicks email-change verify link
    EmailChangePending --> EmailChangeFailed: email send failure (503)
    Authenticated --> PasswordChanged: acheteur changes password
    PasswordChanged --> SessionsRevoked: all refresh tokens deleted
    ProfileRead --> [*]
    ProfileUpdated --> [*]
    EmailChangeVerified --> [*]: email updated, pendingEmail cleared
    SessionsRevoked --> [*]: must re-authenticate

Steps

1. Read Profile (Actor: acheteur)

GET /acheteur/profile returns the acheteur’s safe fields: id, email, firstName, lastName, phone, emailVerified, pendingEmail, createdAt. No sensitive fields.

Outcome: { data: acheteur }.

2. Update Profile Fields (Actor: acheteur)

PUT /acheteur/profile accepts { firstName?, lastName?, phone? }. Validated against acheteurProfileUpdateSchema. Prisma update with SAFE_SELECT projection.

Outcome: { data: updatedAcheteur }.

3. Initiate Email Change (Actor: acheteur)

PUT /acheteur/profile/email accepts { newEmail, password }.

Rate limit: 3 requests per hour, keyed on acheteurId (not IP).

Validations:

  • Current password must match passwordHash (Google-only accounts — passwordHash: null — are blocked with 401).
  • newEmail must differ from current email (400 SAME_EMAIL).
  • newEmail must not be taken by another acheteur (409 CONFLICT).

On success:

  • pendingEmail is set to newEmail.
  • A verification email is sent via Resend using the acheteur-verify template with a purpose: "email_change" JWT (48h expiry) via buildEmailChangeUrl().
  • On email send failure: Slack sentry-notif + 503 EMAIL_SEND_FAILED.

Outcome: { data: { pendingEmail } } or 503.

4. Confirm Email Change (Actor: acheteur)

Acheteur clicks the link in the email. The browser hits GET /acheteur/auth/verify-email?token=.... The handler detects purpose === "email_change" and validates that acheteur.pendingEmail === payload.newEmail. On match: email is updated to newEmail, pendingEmail is cleared, emailVerified remains true. Redirect to /verify-email?status=success.

Outcome: Acheteur’s email address permanently changed.

5. Change Password (Actor: acheteur)

PUT /acheteur/profile/password accepts { currentPassword, newPassword }.

Rate limit: 10 requests per minute, keyed on acheteurId.

Validates current password against passwordHash (Google-only accounts blocked with 401). In a Prisma transaction:

  1. New bcrypt hash stored.
  2. All acheteur_refresh_tokens for this acheteur deleted.

Outcome: { data: { message: "Mot de passe modifié. Veuillez vous reconnecter." } }. Acheteur must re-authenticate.

Error States

  • Google-only account (no passwordHash) tries email or password change → 401 UNAUTHORIZED — “Mot de passe incorrect.” / “Mot de passe actuel incorrect.”
  • New email same as current → 400 SAME_EMAIL
  • New email already taken → 409 CONFLICT
  • Email send failure on email change → 503 EMAIL_SEND_FAILED
  • Email change token: pending email mismatch → redirect ?status=invalid
  • Account not found (race condition / deleted) → 404 NOT_FOUND