backend phase 2: identity — phone-OTP auth, sessions & roles (REST)
- six REST endpoints (auth/request_otp, verify_otp, refresh, logout, me, me/select_role) wrapping the existing JWE/TOTP/RBAC engine - usr.UserSessions with refresh-token rotation + stolen-token (replay) detection → revoke-all + 401; logout rotates the security stamp - users extended: gender, national_id (enc, NULL until KYC), shahkar_verified_at (auto-reset on phone change), phone_hash UNIQUE, is_active, deleted_at + soft-delete filter; phone/email/national_id encrypted at rest via IFieldEncryptor value converter - user_roles grant/revoke audit trail + global revoked filter; 7 roles seeded; admin sub-roles never self-assignable (403) - ISmsSender seam (mock logs the OTP code) replaces the TODO log lines - OperationResult/BaseController learned enveloped 401/403 - auth knobs as platform_configs rows (resend/attempts/session TTL) - migration IdentitySessionsAndUserExtensions applied to the dev DB - 24 new tests incl. Baya.Test.Api (WebApplicationFactory over SQLite); 47 total green, zero new build warnings; swagger snapshot + contract (identity-auth.md), handoff, report, mocks-registry updated Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,27 @@ One block per completed backend phase. Newest at the top. Backend lane writes he
|
||||
- **Notes for frontend:** <anything load-bearing>
|
||||
-->
|
||||
|
||||
## backend-phase-2 — Identity: phone-OTP auth, sessions & roles (REST) — 2026-07-02
|
||||
- **Shipped:** the six-endpoint REST auth surface (`auth/request_otp`, `auth/verify_otp`,
|
||||
`auth/refresh`, `auth/logout`, `me`, `me/select_role`) wrapping the existing JWE/TOTP/RBAC engine;
|
||||
new `usr.UserSessions` (refresh-token rotation + revoke-all on replayed token); `usr.Users` extended
|
||||
(`Gender`, `NationalId` enc NULL, `ShahkarVerifiedAt` auto-reset on phone change, `PhoneHash`
|
||||
UNIQUE, `IsActive`, `DeletedAt` + soft-delete filter); phone/email/national-id **encrypted at rest**
|
||||
(EF value converter over `IFieldEncryptor`); `usr.UserRoles` grant/revoke audit trail + revoked
|
||||
filter; 7 roles seeded; `ISmsSender` seam (mock logs the code); 3 auth config keys;
|
||||
`OperationResult`/`BaseController` learned 401/403.
|
||||
- **Contracts:** dev/contracts/domains/identity-auth.md + openapi snapshot refreshed (yes — 22 paths).
|
||||
- **Mocked:** `ISmsSender` → 🟡 (see reports/mocks-registry.md).
|
||||
- **Gate:** build clean (0 new code warnings) / tests green (47 pass: 10 new `Baya.Test.Api`
|
||||
integration + 14 new handler unit tests). Migration `IdentitySessionsAndUserExtensions` applied to
|
||||
the dev DB; full §7 flow verified live (OTP in log, tokens, 401/403/429, rotation, replay-revoke,
|
||||
logout stamp-kill).
|
||||
- **Handoff:** backend/handoff/after-backend-phase-2.md
|
||||
- **Notes for frontend:** exact paths are `request_otp`/`verify_otp`/`select_role` (snake_case
|
||||
transformer — not the `otp/request` sketch). Bodies camelCase. Fresh users: `roles: []` → role
|
||||
router → `me/select_role` → **refresh tokens** to pick up role claims. `/me` phone is masked.
|
||||
SMS is mocked — read the OTP from the server log.
|
||||
|
||||
## backend-phase-1 — Config, reference & platform signals — 2026-07-02
|
||||
- **Shipped:** first marketplace migration baseline (`InitialMarketplaceBaseline`, new **`ops`** schema)
|
||||
with 6 tables (`PlatformConfigs`, `AuditLogs`, `SystemEvents`, `IranianHolidays`, `Notifications`,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user