Acheteur Registration & Email Verification

Summary

An acheteur creates an account by submitting their name, email, and password. The system sends a verification email with a 48-hour deadline. Until the email is verified, the account is in a limited state — login is permitted but certain features require emailVerified: true. If the 48-hour deadline passes without verification, the account is considered stale and can be overwritten by a new registration attempt for the same email. On success, an access token and a refresh token cookie are issued immediately, and a Slack notification is fired.

State Diagram

stateDiagram-v2
    state "Compte non vérifié" as Unverified
    state "Vérification en attente" as PendingVerification
    state "Compte vérifié" as Verified
    state "Délai expiré" as Expired
    [*] --> Unverified: acheteur registers (new email)
    [*] --> Unverified: acheteur registers (stale email — overwrite)
    Unverified --> PendingVerification: verification email sent
    PendingVerification --> Verified: acheteur clicks verify link (valid, within 48h)
    PendingVerification --> Expired: 48h deadline passes
    Expired --> Unverified: new registration attempt overwrites row
    Verified --> [*]: account fully active

Steps

1. Submit Registration (Actor: acheteur)

Client POSTs { email, password, firstName, lastName, phone } to POST /acheteur/auth/register.

Rate limit: 3 requests per hour per IP.

Outcome: Request reaches the handler.

2. Duplicate / Stale Account Check (Actor: system)

The system looks up the email:

  • If a verified account exists → 409 CONFLICT.
  • If an unverified account within deadline exists → 409 VERIFICATION_PENDING.
  • If an expired unverified account exists → the row is atomically overwritten (refresh tokens deleted, account fields reset).
  • If no account exists → a new row is created.

The emailVerifyDeadline is set to now + 48h.

Outcome: Acheteur row exists (new or recycled).

3. Send Verification Email (Actor: system)

A signed JWT (purpose: "email_verify", expires in 48h) is embedded in the verify URL via buildVerifyUrl(). The email is sent via Resend using the acheteur-verify template.

On email send failure:

  • The acheteur row is kept (analytics value).
  • A Slack alert is posted to the sentry-notif channel.
  • The API returns 503 EMAIL_SEND_FAILED.

Outcome: Verification email delivered, or 503 returned.

4. Issue Tokens (Actor: system)

A 15-minute JWT access token is signed with ACHETEUR_JWT_SECRET. A 7-day opaque refresh token is generated, SHA-256 hashed, stored in acheteur_refresh_tokens, and set as an httpOnly cookie (acheteurRefreshToken).

A Slack notification is fired via notifyAcheteurRegistered (fire-and-forget, errors swallowed).

Outcome: 201 response with { acheteur, accessToken }, refresh token cookie set.

Acheteur clicks the link from their email. The browser hits GET /acheteur/auth/verify-email?token=....

The system verifies the JWT:

  • Expired → redirect to /verify-email?status=expired.
  • Invalid / wrong purpose → redirect to /verify-email?status=invalid.
  • Valid → emailVerified set to true → redirect to /verify-email?status=success.

Outcome: Acheteur row has emailVerified: true.

6. Resend Verification (Actor: acheteur)

If the acheteur did not receive the email, they can request a resend via POST /acheteur/auth/resend-verification (requires Bearer token). Rate limited to 1 request per 5 minutes. Re-uses the existing emailVerifyDeadline or sets a new 48h deadline if none exists.

Outcome: Verification email resent, or 503 on email failure.

Error States

  • Email already verified → 409 CONFLICT — “Cet email est déjà utilisé.”
  • Unverified account within deadline → 409 VERIFICATION_PENDING — “Une inscription est déjà en cours pour cet email.”
  • Email send failure → 503 EMAIL_SEND_FAILED — instructs user to contact support.
  • Token expired → redirect ?status=expired
  • Token invalid / wrong purpose → redirect ?status=invalid
  • Resend on already-verified account (no pendingEmail) → 400 ALREADY_VERIFIED