Exact wire contract for every screen → endpoint. Base /api/v1; envelope per
00-shared/07 . All endpoints from src/modules/auth/auth.controller.ts; business rules
from auth.service.ts. Bearer JWT; global guards: RateLimitGuard→JwtAuthGuard→RbacGuard (app.module.ts:129-133). Public endpoints: register, login, refresh,
logout, verify-email, forgot-password, reset-password (@Public()`).
Aspect Contract
Base https://api.<domain>/api/v1
Headers Authorization: Bearer <accessToken>; x-request-id client-generated; Content-Type: application/json
response {success,message,data,meta?,timestamp,requestId}
error {success,message,error:{code,details?},timestamp,requestId}
Tenancy tenantId from JWT claim; never in body (except register.tenantId bootstrap)
Caching none (auth state) — client caches nothing server-sided
Offline pre-auth writes blocked; reads (sessions) cached last-good
Retry backoff on 5xx/network; no auto-retry on 429
Endpoint POST /auth/login
Request {email, password} (login.dto.ts)
Success 200 data:{accessToken,refreshToken}
Client flow validate → header set → persist token pair (secure storage) → navigate home
Auth @Public, rate auth 10/min (auth.controller.ts:38-44)
Errors 400 validation; 401 UNAUTHENTICATED (“Invalid email or password.”); 429 RATE_LIMITED; 5xx
2FA (planned) client challenge — no server support today (OQ-1)
Offline blocked, banner
—
POST /auth/register
Request: {firstName,lastName,email,password(≥8),tenantId,phone?}
200 data:{accessToken,refreshToken} (tokens in register response)
@Public() rate auth 5/min (auth.controller.ts:30-36)
Flag: tenantId in body = tenant bootstrap exception (AGENTS.md:38 note)
—
POST /auth/verify-email {token} → 200 {message:"Email verified successfully."} or 400/404
@Public() rate auth 10/min
POST /auth/resend-verification (JWT) → 200 {message:"Verification email resent."}; rate 5/120s.
—
POST /auth/forgot-password {email} → 200 {message:"If that email exists, a reset link has been sent."} (both branches)
rate auth 3/min
—
POST /auth/reset-password {token,password(≥8)} → 200 {message:"Password reset successfully."}
400 invalid/expired token; side-effect: sessions deleted
rate auth 5/min
—
POST /auth/2fa/enable (JWT) → 200 {secret, qrCodeUri}
POST /auth/2fa/verify {token} → 200 {message:"2FA enabled successfully."}
POST /auth/2fa/disable {token} → 200 {message:"2FA disabled successfully."}
errors: 400 not-enabled / already-enabled / no-secret; 401 invalid code (auth.service.ts:326-384)
—
GET /auth/sessions (JWT) → 200 data:[sessionDoc…]
DELETE /auth/sessions/:id (JWT) → 200 {message:"Session revoked."}
POST /auth/logout-all (JWT) → 200 {message:"Logged out from all devices."}
POST /auth/logout {refreshToken} (@Public) → 200 {message:"Logged out successfully."}
—
GET /auth/api-keys (JWT) → 200 data:[{id,name,prefix,scopes,createdAt,lastUsedAt}]
POST /auth/api-keys (JWT) {name,scopes?} → 200 data:{id,prefix,key} (key once)
DELETE /auth/api-keys/:id (JWT) → 200 {message:"API key revoked."}
Screen Loading Streaming Realtime
login button — —
register button — —
sessions AppSkeleton rows— (planned) WS session-change event
api-keys AppSkeleton— —
2FA spinner — —
Screen code UI
login 401 「 credentials incorrect」
login 429 countdown
verify-email 400 “Verify link expired/invalid” + resend
reset 400 expired-token banner
sessions 404 (revoke id) treat as removed
api-key create 400/409 inline; 401→refresh; 5xx generic+requestId
any 401 (during auth action) silent refresh; fail→sessionExpired
GET /auth/sessions & GET /auth/api-keys are non-paginated arrays (listX returns
full docs, user-session.repository.ts:19, api-key.repository.ts:20); client renders all
(no infinite scroll). Sort: api-keys by createdAt desc server-side; sessions insertion
order — client groups current-first.
Sessions/key listings are read+refresh (Refresh), revoke=server-confirm (no optimistic)
because irreversible; disable 2FA = server-first (code needed). Exception: row “revoke”
shows immediate removal only on server 200.