API — Authentication
Base path: /api/v1/auth. Auth is OTP-first with an optional password login. New users are created as Light Users on OTP verification and become Active Users on profile completion.
Endpoints
| Method | Path | Notes |
|---|---|---|
| POST | /auth/send-otp | Send OTP. Rate limited 5/min per phone. |
| POST | /auth/verify-otp | Create-or-find User; issue JWT access + refresh pair. |
| POST | /auth/firebase-verify | Firebase phone-auth fallback. |
| PATCH | /auth/complete-profile | { name, state, district, role, password? } → profileComplete=true, assigns membership number. |
| POST | /auth/set-password | Post-completion password set/change. |
| POST | /auth/reset-password | OTP-gated password reset. |
| POST | /auth/login-password | Password login (only if a passwordHash is set). Runs phone → device → ip limiters. |
| POST | /auth/refresh | { refreshToken, deviceId } → new pair (rotates the refresh token). |
| POST | /auth/logout | Invalidate the device's refresh token. |
| GET | /auth/me | Current user context. |
User states
| State | profileComplete | Can do | Cannot do |
|---|---|---|---|
| Light User | false | Browse all leads, view products/profiles, dashboard | Post leads, offer/bid, manage products, inquiries |
| Active User | true | Everything | — |
JWT payload
interface JWTPayload {
userId: string
profileComplete: boolean
activeRole: string | null // "farmer" | "trader" | null
deviceId: string
}- Access token: 15 minutes.
- Refresh token: 30 days, device-bound, rotated on every refresh (single-use with a grace window — see below).
- Password: bcrypt, work factor 12, stored in
User.passwordHash, never logged or returned.
Refresh & rotation
Each refresh issues a new refresh token and keeps the previous one valid for REFRESH_TOKEN_GRACE_MS (default 60s), so a client that rotated but lost the response (flaky network) can retry without being logged out. A successful fresh login clears the grace field.
Client behaviour (web axios interceptor):
- Transient refresh failure (5xx / network / timeout) → keep the session, fail only that one request.
- Terminal refresh failure (401 on
/auth/refresh) → log out. - Cross-tab refresh is serialised with the Web Locks API so peers reuse the freshly-stored token instead of re-rotating.
Deactivated users
The authenticate middleware rejects a user whose kycStatus === DEACTIVATED with 401.
QA fast-path (non-production only)
When ENABLE_TEST_ENDPOINTS=true + TEST_ENDPOINT_SECRET are set, /api/v1/test/* lets automation read the current OTP, expire OTP/JWT, mint an instant session, and reset a fixture phone (guarded to TEST_OTP_PHONES only). Numbers in TEST_OTP_PHONES get the fixed OTP 123456, no SMS, and are rate-limit-exempt. Both locks must be unset in production.
