4.7 KiB
auth — phone OTP, sessions, /me, role selection
Client seam
client/src/services/auth/·USE_AUTH_MOCK = false(real) · 7 server ops Last verified: 2026-07-30 against commitd3ec723and swagger.v1.json (2026-07-29).
The only way into the platform. Phone + OTP, no passwords. The transport rules — bearer header, cookie storage, silent refresh, rotation, reuse detection — are in ../api-contract.md; this file is the endpoints and the payload semantics.
Endpoints
| Method | Path | Auth | Rate limit | Verdict |
|---|---|---|---|---|
| POST | /api/v1/auth/request_otp |
anonymous | otp 5/min |
wired |
| POST | /api/v1/auth/verify_otp |
anonymous | otp 5/min |
wired |
| POST | /api/v1/auth/refresh |
anonymous | auth 10/min |
wired · called by both services/auth and the fetch layer |
| POST | /api/v1/auth/logout |
[Authorize] |
— | wired |
| GET | /api/v1/me |
[Authorize] |
— | wired |
| POST | /api/v1/me/select_role |
[Authorize] |
— | wired |
| GET | /api/v1/dev/last_otp/{phone} |
anonymous | — | Development only — see below |
No phantoms.
AUTH_API_BASE is /api/v1; the routes above are exactly what the client sends.
dev/last_otp — live on the deployment
DevController is registered unconditionally, and the OTP-capture bridge behind it is wired only when
IsDevelopment() and the SMS provider is capture-safe (mock or telegram). The
balinyaar.ir deployment runs ASPNETCORE_ENVIRONMENT=Development with Seams:Sms:Provider = telegram,
so both conditions hold and the endpoint is reachable on api.balinyaar.ir. Anyone who knows a
registered phone number can read its login code. Recorded in DEPLOY.md as the
deployment's largest exposure; the fix is the environment switch, not a code change.
Selecting a real gateway (kavenegar, …) disables the bridge — the OTP must never be logged or captured
once real SMS ships.
Shape rules the JSON does not express
RequestOtpResultcarries the code length and expiry (REQ-002, delivered) so the client sizes the input and runs the countdown from server truth rather than a hardcoded constant.verify_otpfailures carry a machine-readablecodeon the envelope (REQ-003, delivered) — e.g.otp_locked— so the client branches on the state instead of matching a message string. This is thecodefield described in ../api-contract.md.refreshreturns a new pair and retires the old refresh token. Presenting a retired token is treated as theft: the session is killed, not merely refused. A 401 from/auth/refreshis therefore terminal and the client must not retry it./meis the only source of identity. The JWE is opaque; the client reads role, gender and profile-completeness from/me, never from a decoded claim.- Multi-role users:
/mereports the roles the caller holds.POST /me/select_rolecommits to one. REQ-004 was resolved as a client concern — the client owns the disambiguation and the "resolved vs. pending" role hydration; no backend change was needed. See docs/rules/client/auth.md. logoutreturns an empty envelope — nodata. The client awaits the revocation and must notunwrap()it.- Phone numbers are encrypted at rest. Login looks the user up by a deterministic HMAC hash
(
users.PhoneHash, derived fromSeams:FieldEncryption:HashKey), never by comparing the encrypted column. This is why that key is immutable — see ../config-matrix.md.
Enums
| Vocabulary | Values |
|---|---|
PublicRole |
customer nurse |
AdminRole |
admin support finance moderation super_admin |
Gender |
male female — load-bearing, drives same-gender caregiver matching |
NurseVerificationStatus (as surfaced on /me) |
not_started in_progress pending_review verified rejected |
The
/meverification summary uses a different vocabulary from the verification domain's own aggregate status (not_startedpendingin_reviewapprovedrejectedsuspended). They are two read models over the same source of truth, not a drift — but do not treat the strings as interchangeable. See verification.md.
Open REQs
| REQ | Status | Effect |
|---|---|---|
| REQ-038 | open | /me carries no signal that the caller administers a partner center, so partner auto-routing cannot be driven off it. See partner-center.md |
| REQ-039 | open | The OTP SMS template is not WebOTP-conformant, so the browser's one-tap autofill never fires. The client's WebOTP hook ships anyway and degrades silently |