# After backend-phase-2 — auth is live over REST

The marketplace has its front door: **phone-OTP login, revocable sessions with refresh-token
rotation + stolen-token detection, `/me`, and public role selection** — all over REST, wrapping the
pre-existing JWE/TOTP/RBAC engine (nothing was rebuilt). Contract:
[`dev/contracts/domains/identity-auth.md`](../../../contracts/domains/identity-auth.md); machine
schema: `dev/contracts/openapi/swagger.v1.json` (refreshed — 22 paths).

## What the frontend (f1-b2) can now build
- **Login flow:** `POST api/v1/auth/request_otp` → `POST api/v1/auth/verify_otp` (camelCase bodies;
  exact snake_case paths per the contract — note `request_otp`, **not** `otp/request`).
- **Session handling:** store the token pair; `POST api/v1/auth/refresh` rotates it (never reuse an
  old refresh token — replay = 401 + logout-everywhere); `POST api/v1/auth/logout` (send `{}`).
- **`AuthContext` roles + role router:** `GET api/v1/me` returns masked phone, `roles[]`,
  profile-completion flags (false until b3) and `nurseVerificationStatus` (`not_started` until b6).
  Fresh users have `roles: []` → route to `POST api/v1/me/select_role` (`customer`/`nurse`, both
  allowed, 403 for anything else). **Refresh tokens after role selection** — role claims are baked
  into the access token.
- **Errors:** 400 invalid phone/code (safe, non-enumerating message), 401 with the standard
  envelope (also written by the auth stack itself), 403 admin self-assign, 429 over the OTP/auth
  per-IP limits. The envelope is unchanged (camelCase body, snake_case URLs).

## What's mocked
- **SMS delivery (`ISmsSender` → 🟡).** The OTP code is written to the server log instead of a SIM.
  Local testing: call `request_otp`, read the code from the API console log, `verify_otp` with it.

## Rules baked into the API (don't fight them client-side)
- Phone is the only public credential; email is optional and never a login key.
- One resend per `auth_otp_resend_seconds` (response says `otpSent: false` + wait time).
- After `auth_otp_max_attempts` wrong codes, verification refuses until a fresh OTP is requested.
- Logout rotates the security stamp: **all** devices' access tokens die; they recover via refresh.

## Schema / migration
Migration **`20260701222425_IdentitySessionsAndUserExtensions`** applied to the dev DB on top of
b1's baseline: `usr.Users` gains `Gender`, `NationalId` (enc, NULL until b6 KYC),
`NationalIdVerifiedAt`, `ShahkarVerifiedAt` (auto-reset on phone change), `PhoneHash` (UNIQUE),
`PhoneVerifiedAt`, `IsActive`, `DeletedAt` (+ soft-delete filter); new `usr.UserSessions`;
`usr.UserRoles` gains `GrantedById`/`GrantedAt`/`RevokedAt` (revoked grants filtered out globally).
`PhoneNumber`/`Email`/`NationalId` are now **encrypted at rest** — never query them by equality;
use `PhoneHash`. Roles seeded: `customer`, `nurse`, `admin`, `support`, `finance`, `moderation`,
`super_admin`. Config keys added: `auth_otp_resend_seconds` (120), `auth_otp_max_attempts` (5),
`auth_session_ttl_days` (30).

## Follow-ups later phases must close
- **b3:** profiles/patients/addresses/bank accounts; `gender` + names become settable; the `/me`
  profile-completion flags start reading real tables.
- **b6:** Shahkar + KYC set `NationalId`/`ShahkarVerifiedAt`; `nurseVerificationStatus` becomes real.
- **Legacy `UserRefreshTokens`** still backs the gRPC path only; retire it when gRPC moves to
  sessions (or gRPC is dropped).
- **`ISmsSender` → real gateway** (see mocks-registry row).
