12 — API Mapping (Users Module)
- E1 — Create user
- E2 — List users
- E3 — Get user
- E4 — Update user
- E5 — Delete user (soft)
- E6 — GDPR erasure
- E7 — Get preferences
- E8 — Update preferences
- E9 — Upload avatar
- E10 — Bulk import users (inline, synchronous)
- E11 — Bulk module (adapters) & export
- E12 — RBAC bridge (roles & membership)
- E13 — Auth module touchpoints (self-service context)
- Rate limits (client-relevant, 00-shared/07 §4)
- Client contract summary (all screens)
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
| Endpoint | POST /api/v1/users (users.controller.ts:36-40) |
| Guard | JwtAuthGuard (:31); RBAC permission guard (planned) — user.create exists (permissions.constants.ts:7) but is not enforced on this route (OQ-11) |
| Request | CreateUserDto (create-user.dto.ts:5-62) — firstName/lastName/email required; status enum default active (:44-47); tenantId from token only |
| Response | 201 envelope, data = user doc (displayName auto "<firstName> <lastName>" — users.service.ts:62); no meta |
| Errors | 400 VALIDATION_ERROR; 409 DUPLICATE_RESOURCE email/phone (users.service.ts:50-60); 429 RATE_LIMITED; 5xx (incl. race dup-key, OQ-14) |
| Side effects | UserCreated event → in-app queue user-created-notification (event-queue-map.ts:10) |
| Client | SS2 create form; roles via E12 after success; cache invalidate users list |
| Realtime | in-app notification to tenant (P9, 02_User_Personas.md) |
E2 — List users
| Endpoint | GET /api/v1/users?page&limit&sort&q (users.controller.ts:42-46) |
| Params | page ≥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) |
| Response | paginated: data array + meta {page,limit,totalItems,totalPages,hasNext,hasPrevious} (pagination-query.dto.ts:32-55) |
| Errors | 400 (bad params); 429; 5xx |
| Client | SS1 list; debounced search; infinite scroll while hasNext; pull-to-refresh bypasses cache |
| Filters | No status/role query params (users.service.ts:90-115) — status/role chips are client-side (planned) server params (OQ-9) |
| Cache | client paginated cache sl:{tenant}:users:{query} TTL 5 min (volatile, 00-shared/06 §3.3) |
E3 — Get user
| Endpoint | GET /api/v1/users/:id (users.controller.ts:48-53) |
| Response | 200 envelope, data = user doc |
| Errors | 400 (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) |
| Client | SS3 detail / SS7 self profile (:id = JWT sub, auth.service.ts:461; server self-guard (planned) OQ-3) |
E4 — Update user
| Endpoint | PATCH /api/v1/users/:id (users.controller.ts:55-60) |
| Request | UpdateUserDto (update-user.dto.ts:5-70) — partial; $set merge + version +1 (base.repository.ts:57-66) |
| Response | 200 updated doc; displayName recomputed when names change (users.service.ts:136-139) |
| Errors | 404; 409 email/phone change conflicts (:120-134); 400 enums invalid |
| Side effects | UserUpdated (changes list) → audit-write (users.service.ts:145-153; event-queue-map.ts:11) |
| Client | SS5 edit; SS6 status change; conflict → inline field error |
E5 — Delete user (soft)
| Endpoint | DELETE /api/v1/users/:id (users.controller.ts:62-67) |
| Behaviour | soft delete: isDeleted:true, deletedAt, deletedBy + version (base.repository.ts:68-74); handler returns void → 200 data null; emits UserDeleted → audit-write (users.service.ts:176-183; event-queue-map.ts:12) |
| Errors | 404 (users.service.ts:175) |
| Client | SS8 typed-confirm; row removed; snackbar "purged after 30 days" |
| Purge | TENANT_PURGE worker hard-deletes isDeleted docs older than 30 days (tenant-purge.worker.ts:32-43); idempotent |
| Sessions | active JWTs not revoked (OQ-8); user cannot re-login (users.repository.ts:21-25) |
E6 — GDPR erasure
| Endpoint | POST /api/v1/users/:id/erasure (users.controller.ts:69-76) |
| Behaviour | anonymize (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) |
| Errors | 404 (users.service.ts:198) |
| Client | SS8 typed "ERASE" confirm; irreversible copy |
E7 — Get preferences
| Endpoint | GET /api/v1/users/:id/preferences (users.controller.ts:78-83) |
| Response | data = user.preferences ?? {} (users.service.ts:168-171) — never null |
| Errors | 404 (via findById) |
| Client | SS6 load |
E8 — Update preferences
| Endpoint | PATCH /api/v1/users/:id/preferences (users.controller.ts:85-93) |
| Request | UpdateUserPreferencesDto (update-user-preferences.dto.ts:4-21) — notifications{email,push,sms}, `theme{mode: light |
| Behaviour | full replace $set:{preferences:dto} (users.service.ts:161-163) — client sends merged complete object |
| Response | 200 updated doc |
| Errors | 404; 400 (non-object groups) |
| Client | SS6; switches optimistic but submit full object |
E9 — Upload avatar
| Endpoint | POST /api/v1/users/:id/avatar (users.controller.ts:95-102) — multipart, field file (FileInterceptor('file') :96) |
| Request | image buffer; server prefixes filename ${id}- (users.service.ts:219); no size/mime validation in code (OQ-10) |
| Response | data {avatarFileId} (:230); previous avatar deleted best-effort (:227-229) |
| Errors | 404 (:216); 400 missing file (multer); 5xx storage failures |
| Client | SS7 AvatarUploader; display URL resolution (planned) (endpoint returns id only) |
E10 — Bulk import users (inline, synchronous)
| Endpoint | POST /api/v1/users/import (users.controller.ts:104-110) — multipart, field file |
| Behaviour | sync 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) |
| Response | 200 envelope, data = {imported: number, errors: string[]} (:281) — errors "Row N: message" |
| Errors | 429; 5xx. Malformed/empty CSV is not an error envelope — returns {imported:0, errors:[…]} (:238-243) |
| Async/polling | none exists — result returned in the same request. Async queue + polling (planned) per PLAN.md 2.7 (OQ-6) |
| Client | SS4 wizard; progress UI per 10_Interaction_Specification.md §6 |
E11 — Bulk module (adapters) & export
| Import | POST /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) |
| Entities | students 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 |
| Export | GET /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) |
| Client | SS4 alternate path when users adapter lands; template download |
E12 — RBAC bridge (roles & membership)
| Roles | GET /api/v1/rbac/roles (rbac.controller.ts:27-31) — role slugs for chips/selectors |
| Members | GET /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) |
| Guard | org_admin role required (rbac.controller.ts:21-22) — members API unavailable to HR-only roles (OQ-15) |
| Cache | permissions cached Redis sl:{tenantId}:perm:{userId} TTL 300 s (rbac.service.ts:44-73) |
E13 — Auth module touchpoints (self-service context)
| Register | POST /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/resend | POST /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) |
| Sessions | GET /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)
| Tier | Limit | Notes |
|---|---|---|
| auth | 10/min (5 register, 10 login, 20 refresh, 5 resend/120 s) | countdown copy, no auto-retry |
| api | 100/min | users CRUD/import default tier |
| admin | 500/min | (planned) if admin endpoints get throttled |
Client contract summary (all screens)
| Concern | Rule |
|---|---|
| Auth | Bearer JWT; 401 → single-flight refresh → replay; fail → session expiry (00-shared/06 §3.6) |
| Optimistic | only safe toggles (status, preferences switches) with rollback; create/import/erase never optimistic |
| Idempotency | DELETE/PATCH retry-safe; Idempotency-Key support unconfirmed (B6) — import retry safe by dedup (users.service.ts:260-263) |
| Offline | reads from last-good cache + banner; writes blocked (no module offline queue) |
| Pagination | page/limit/sort/q + meta exact (00-shared/07 §5) |
| Realtime | WS topics n/a for users today; (forward-looking) user.updated, user.deleted |
| Error mapping | 00-shared/06 §5: 400 field, 403 hide/deny, 404 empty, 409 inline conflict, 429 backoff, 5xx generic + requestId |