Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

12 — API Mapping (Users Module)

Exact endpoints per screen. Wire contract per 00-shared/07: base /api/v1, Bearer JWT, success {success:true,message:"OK",data,meta?,timestamp, requestId}, error envelope with codes. Only shapes in code are used. (planned)/(forward-looking) marked. Sources: users.controller.ts, users.service.ts, bulk.controller.ts, rbac.controller.ts, auth.controller.ts.


E1 — Create user

EndpointPOST /api/v1/users (users.controller.ts:36-40)
GuardJwtAuthGuard (:31); RBAC permission guard (planned)user.create exists (permissions.constants.ts:7) but is not enforced on this route (OQ-11)
RequestCreateUserDto (create-user.dto.ts:5-62) — firstName/lastName/email required; status enum default active (:44-47); tenantId from token only
Response201 envelope, data = user doc (displayName auto "<firstName> <lastName>"users.service.ts:62); no meta
Errors400 VALIDATION_ERROR; 409 DUPLICATE_RESOURCE email/phone (users.service.ts:50-60); 429 RATE_LIMITED; 5xx (incl. race dup-key, OQ-14)
Side effectsUserCreated event → in-app queue user-created-notification (event-queue-map.ts:10)
ClientSS2 create form; roles via E12 after success; cache invalidate users list
Realtimein-app notification to tenant (P9, 02_User_Personas.md)

E2 — List users

EndpointGET /api/v1/users?page&limit&sort&q (users.controller.ts:42-46)
Paramspage ≥1 default 1; limit 1–100 default 20; sort (-field desc); q$or regex on firstName/lastName/email/displayName, case-insensitive (users.service.ts:92-99); default sort -createdAt (:103)
Responsepaginated: data array + meta {page,limit,totalItems,totalPages,hasNext,hasPrevious} (pagination-query.dto.ts:32-55)
Errors400 (bad params); 429; 5xx
ClientSS1 list; debounced search; infinite scroll while hasNext; pull-to-refresh bypasses cache
FiltersNo status/role query params (users.service.ts:90-115) — status/role chips are client-side (planned) server params (OQ-9)
Cacheclient paginated cache sl:{tenant}:users:{query} TTL 5 min (volatile, 00-shared/06 §3.3)

E3 — Get user

EndpointGET /api/v1/users/:id (users.controller.ts:48-53)
Response200 envelope, data = user doc
Errors400 (bad ObjectId); 404 RESOURCE_NOT_FOUND (users.service.ts:86); cross-tenant/deleted ids → 404 (scoped repo — base.repository.ts:20-30; no existence leak)
ClientSS3 detail / SS7 self profile (:id = JWT sub, auth.service.ts:461; server self-guard (planned) OQ-3)

E4 — Update user

EndpointPATCH /api/v1/users/:id (users.controller.ts:55-60)
RequestUpdateUserDto (update-user.dto.ts:5-70) — partial; $set merge + version +1 (base.repository.ts:57-66)
Response200 updated doc; displayName recomputed when names change (users.service.ts:136-139)
Errors404; 409 email/phone change conflicts (:120-134); 400 enums invalid
Side effectsUserUpdated (changes list) → audit-write (users.service.ts:145-153; event-queue-map.ts:11)
ClientSS5 edit; SS6 status change; conflict → inline field error

E5 — Delete user (soft)

EndpointDELETE /api/v1/users/:id (users.controller.ts:62-67)
Behavioursoft delete: isDeleted:true, deletedAt, deletedBy + version (base.repository.ts:68-74); handler returns void → 200 data null; emits UserDeletedaudit-write (users.service.ts:176-183; event-queue-map.ts:12)
Errors404 (users.service.ts:175)
ClientSS8 typed-confirm; row removed; snackbar "purged after 30 days"
PurgeTENANT_PURGE worker hard-deletes isDeleted docs older than 30 days (tenant-purge.worker.ts:32-43); idempotent
Sessionsactive JWTs not revoked (OQ-8); user cannot re-login (users.repository.ts:21-25)

E6 — GDPR erasure

EndpointPOST /api/v1/users/:id/erasure (users.controller.ts:69-76)
Behaviouranonymize (Erased User, erased-<id>@anonymized.invalid), isDeleted:true (users.service.ts:187-197); enqueue gdpr-erasure on tenant-purge queue, attempts:3, exponential backoff 5000 ms (:200-209); worker hard-deletes user doc (tenant-purge.worker.ts:49-56)
Errors404 (users.service.ts:198)
ClientSS8 typed "ERASE" confirm; irreversible copy

E7 — Get preferences

EndpointGET /api/v1/users/:id/preferences (users.controller.ts:78-83)
Responsedata = user.preferences ?? {} (users.service.ts:168-171) — never null
Errors404 (via findById)
ClientSS6 load

E8 — Update preferences

EndpointPATCH /api/v1/users/:id/preferences (users.controller.ts:85-93)
RequestUpdateUserPreferencesDto (update-user-preferences.dto.ts:4-21) — notifications{email,push,sms}, `theme{mode: light
Behaviourfull replace $set:{preferences:dto} (users.service.ts:161-163) — client sends merged complete object
Response200 updated doc
Errors404; 400 (non-object groups)
ClientSS6; switches optimistic but submit full object

E9 — Upload avatar

EndpointPOST /api/v1/users/:id/avatar (users.controller.ts:95-102) — multipart, field file (FileInterceptor('file') :96)
Requestimage buffer; server prefixes filename ${id}- (users.service.ts:219); no size/mime validation in code (OQ-10)
Responsedata {avatarFileId} (:230); previous avatar deleted best-effort (:227-229)
Errors404 (:216); 400 missing file (multer); 5xx storage failures
ClientSS7 AvatarUploader; display URL resolution (planned) (endpoint returns id only)

E10 — Bulk import users (inline, synchronous)

EndpointPOST /api/v1/users/import (users.controller.ts:104-110) — multipart, field file
Behavioursync parse + per-row create (users.service.ts:233-282); header required + ≥1 row (:238-243); headers lowercased (:244-247); required email (:256-259); dup email → row error (:260-263); defaults Unknown/en/UTC (:265-271); no UserCreated events for rows (:264-272 direct repo.create)
Response200 envelope, data = {imported: number, errors: string[]} (:281) — errors "Row N: message"
Errors429; 5xx. Malformed/empty CSV is not an error envelope — returns {imported:0, errors:[…]} (:238-243)
Async/pollingnone exists — result returned in the same request. Async queue + polling (planned) per PLAN.md 2.7 (OQ-6)
ClientSS4 wizard; progress UI per 10_Interaction_Specification.md §6

E11 — Bulk module (adapters) & export

ImportPOST /api/v1/bulk/import/:entity (bulk.controller.ts:35-48) — multipart file required (:43-46); parser csv-parse/sync (bulk-import.service.ts:26-30); malformed → 400 VALIDATION_ERROR (:31-33); report {entity,totalRows,imported,failed,errors[{rowNumber,errors[]}]} (import-adapter.interface.ts:14-25); rowNumber = physical line (:46)
Entitiesstudents only (students-import.adapter.ts:15) — creates users via UsersService.create per row (:66-71); users entity → 404 RESOURCE_NOT_FOUND "No import adapter" (bulk-import.service.ts:17-20) (planned) users adapter
ExportGET /api/v1/bulk/export/:entity (bulk.controller.ts:50-60) — text/csv, Content-Disposition: attachment; filename="<entity>.csv" (:55-58); students rows = admissionNumber/rollNumber/status/admissionDate (students-import.adapter.ts:87-98)
ClientSS4 alternate path when users adapter lands; template download

E12 — RBAC bridge (roles & membership)

RolesGET /api/v1/rbac/roles (rbac.controller.ts:27-31) — role slugs for chips/selectors
MembersGET /api/v1/rbac/members (:57-61) — join for role chips; POST /api/v1/rbac/members {userId, roles[]} (:63-67; creates ACTIVE member — rbac.service.ts:113-127); PATCH /api/v1/rbac/members/:id roles (:69-73); DELETE /api/v1/rbac/members/:id (:75-79)
Guardorg_admin role required (rbac.controller.ts:21-22) — members API unavailable to HR-only roles (OQ-15)
Cachepermissions cached Redis sl:{tenantId}:perm:{userId} TTL 300 s (rbac.service.ts:44-73)

E13 — Auth module touchpoints (self-service context)

RegisterPOST /api/v1/auth/register (rate 5/min — auth.controller.ts:30-36): creates user + auth_account + org_admin membership + welcome email (auth.service.ts:54-121; event-queue-map.ts:7)
Verify/resendPOST /auth/verify-email (:74-81); POST /auth/resend-verification (5/120 s, :83-89) — verification email is the only email an admin-created user can get today (via manual resend, needs auth) — invite email (planned)
SessionsGET /auth/sessions, DELETE /auth/sessions/:id, POST /auth/logout-all (:66-72,130-142) — logout-all is the only session revocation tool (not called on delete/erase — OQ-8)

Rate limits (client-relevant, 00-shared/07 §4)

TierLimitNotes
auth10/min (5 register, 10 login, 20 refresh, 5 resend/120 s)countdown copy, no auto-retry
api100/minusers CRUD/import default tier
admin500/min(planned) if admin endpoints get throttled

Client contract summary (all screens)

ConcernRule
AuthBearer JWT; 401 → single-flight refresh → replay; fail → session expiry (00-shared/06 §3.6)
Optimisticonly safe toggles (status, preferences switches) with rollback; create/import/erase never optimistic
IdempotencyDELETE/PATCH retry-safe; Idempotency-Key support unconfirmed (B6) — import retry safe by dedup (users.service.ts:260-263)
Offlinereads from last-good cache + banner; writes blocked (no module offline queue)
Paginationpage/limit/sort/q + meta exact (00-shared/07 §5)
RealtimeWS topics n/a for users today; (forward-looking) user.updated, user.deleted
Error mapping00-shared/06 §5: 400 field, 403 hide/deny, 404 empty, 409 inline conflict, 429 backoff, 5xx generic + requestId