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

01 — Product Overview (Users Module)

Canonical identity and profile management. Derived from studylyon-blueprint/04-Modules/Users.md, src/modules/users/**, the auth module (auth-account.schema.ts, auth.service.ts), RBAC (organization-member.schema.ts), the bulk module (bulk-import.service.ts), email.worker.ts, event-queue-map.ts, PLAN.md, and studylyon-blueprint/03-Database/COLLECTIONS.md. Nothing in this doc is invented; plan-only capability is marked (planned), client-only or roadmap-only capability (forward-looking), analytics (proposed).


1. What the module is

The Users module owns canonical identity for every person in a tenant: users stores profile data (name, contact, avatar, gender, DOB, language, timezone), notification/theme preferences, and a lifecycle status. It does not own authentication credentials (those live in auth_accounts), does not own business profiles (teacher/student/parent/staff profile rows reference users._id), and does not own role assignments (those live on organization_members).

"users stores identity only — never passwords, attendance, or academic data." (studylyon-blueprint/04-Modules/Users.md:63)

Key facts from source:

  • users schema: user.schema.ts:14-79 — status enum active | inactive | suspended | invited (user.schema.ts:7-12, default active :46-47); unique (tenantId, email) (:83) and unique partial (tenantId, phone) (:84-90); indexes on (tenantId, status) and (tenantId, displayName) (:91-92).
  • Every business document (including users) carries tenantId, soft-delete flags isDeleted/deletedAt/deletedBy, audit authors, and an optimistic-lock version (base.schema.ts:9-31). All queries are auto-scoped (base.repository.ts:20-30).
  • Events: UserCreated, UserUpdated, UserDeleted (events/user-events.ts:1-27); routing event-queue-map.ts:10-12 (UserCreated → in-app notification; UserUpdated/UserDeleted → audit-write).
  • Permissions exist in permissions.constants.ts:6-10: user.read, user.create, user.update, user.delete, user.import (plus rbac.member.* :15-18 for membership management).

2. Scope in / scope out

In scope (implemented)Out of scope (owned elsewhere)
User CRUD (create, list, get, patch, soft-delete)Passwords / auth credentials → auth_accounts (auth-account.schema.ts:10-56)
Preferences (notifications email/push/sms, theme light/dark/system + language)Roles & memberships → organization_members (organization-member.schema.ts:13-44)
Avatar upload via StorageProviderTeacher/student/staff/parent profile data → their modules (RELATIONSHIPS.md:13,22-32)
GDPR erasure endpoint + scheduled hard purgeMulti-channel notifications → Notifications module
Bulk CSV import (inline /users/import)Invite acceptance / set-password flow → (planned) (no endpoint in code)
RBAC member management (used as the "invite" wiring)Analytics → (proposed) (no SDK chosen, 00-shared/12 A4)

3. Status / lifecycle model

3.1 User status (user.schema.ts:7-12)

StatusMeaningSet by
activeDefault on create (user.schema.ts:46-47; also CreateUserDto default create-user.dto.ts:44-47)create / PATCH status
inactiveDeactivated, can be re-activated via PATCHPATCH status
suspendedSuspended (e.g. discipline); re-activatablePATCH status
invitedAwaiting acceptance — enum exists, no endpoint sets it today(planned) invite flow

3.2 Soft-delete lifecycle

  • DELETE /users/:idsoftDelete() sets isDeleted:true, deletedAt, deletedBy (base.repository.ts:68-74; users.service.ts:173-184) and emits UserDeletedaudit-write queue (event-queue-map.ts:12).
  • Soft-deleted users are invisible to every scoped query (base.repository.ts:20-30) and cannot log in — login uses findByEmailUnscoped which filters isDeleted:false (users.repository.ts:21-25, auth.service.ts:124).
  • Hard purge: TENANT_PURGE worker deletes all soft-deleted docs older than 30 days (tenant-purge.worker.ts:15,32-43) — idempotent (only docs already isDeleted before the cutoff).
  • GDPR erasure (POST /users/:id/erasure, users.controller.ts:69-76): anonymizes PII in place (firstName=Erased, lastName=User, displayName=Erased User, email=erased-<id>@anonymized.invalid, isDeleted=trueusers.service.ts:187-197) and enqueues a gdpr-erasure job on the tenant-purge queue (attempts: 3, exponential backoff 5000 ms — users.service.ts:200-209); the worker hard-deletes the user doc (tenant-purge.worker.ts:49-56).

3.3 "Invited" status — what exists vs what is missing

Exists in code:

  • UserStatus.INVITED enum (user.schema.ts:11).
  • MemberStatus.INVITED + invitedBy + acceptedAt on organization_members (organization-member.schema.ts:7-11,33-38).
  • auth_accounts.emailVerificationToken + resend-verification endpoint (auth-account.schema.ts:39-40; auth.controller.ts:83-89) — the closest live mechanism: a created user gets a verification email with a token (email.worker.ts:26-32), but only on self-registration (auth.service.ts:101-118), not on admin-created users.

Missing in code (hence (planned)):

  • No endpoint sets status=invited; UsersService.create always leaves the DTO default (create-user.dto.ts:44-47).
  • No invite email (email worker handles only UserRegistered and PasswordResetRequestedemail.worker.ts:25-42; no UserInvited handler, no event-queue-map route).
  • No accept-invite / set-password endpoint.

Product conclusion: the "invite" journey is designed below as the create-user + POST /rbac/members combination, with the acceptance e-mail explicitly (planned); UI must not promise an email that the backend does not send yet (see 12_API_Mapping.md E5 note and 14_QA_Checklist.md).


4. Bulk import — two real paths

There are two distinct CSV import implementations in source. Both are synchronous; there is no async job and no result-polling endpoint today:

PathEndpointParserReport shapeErrors shape
A. Users inlinePOST /api/v1/users/import (multipart field file) — users.controller.ts:104-110naive split(',') line parser (users.service.ts:236-252){imported, errors}flat string[]Row N: message (users.service.ts:281)
B. Bulk module adapterPOST /api/v1/bulk/import/:entitybulk.controller.ts:35-48csv-parse/sync (quoted fields, trim, skip empty) — bulk-import.service.ts:26-30ImportReport {entity, totalRows, imported, failed, errors}{rowNumber, errors[]} (import-adapter.interface.ts:14-25)
  • Path B today ships one adapter: students (students-import.adapter.ts:15), which internally creates users via UsersService.create for each row (students-import.adapter.ts:66-71). There is no users adapter — the service throws No import adapter for entity "users" (bulk-import.service.ts:17-20). (planned): register a users adapter in the bulk module (the service explicitly notes the registry is deferred: bulk-import.service.ts:15-16).
  • Path A rules (exact, users.service.ts:250-279): header row required + ≥ 1 data row (:238-243); headers lowercased/trimmed (:244-247); per row — missing email → error; email already exists → error; create with firstName=firstname|first_name|'Unknown', lastName=lastname|last_name|'Unknown', phone, gender, language (default en), timezone (default UTC).
  • Path A imports do not emit UserCreated events (rows call repo.create directly — users.service.ts:264-272), unlike single create. PLAN.md 2.7 plans a queue/worker path (POST /api/v1/files/upload-csvUserCreated per row) — (planned).

5. Invites, roles, and the RBAC bridge

"Inviting" someone into the tenant is a two-document operation today:

  1. POST /api/v1/users creates the identity (status active by default).
  2. POST /api/v1/rbac/members {userId, roles[]} creates the membership (rbac.controller.ts:63-67; rbac.service.ts:113-127 — status hard-set ACTIVE, joinedAt now; MemberStatus.INVITED exists in schema but is not used by the service).

Membership is what grants roles; roles carry permissions (rbac.service.ts:59-65; permission cache in Redis TTL 300 s :44-73). The users screen must therefore be built as a user + membership composite — roles shown on user rows come from GET /rbac/members (rbac.controller.ts:57-61), not from GET /users.


6. GDPR & data-retention posture

  • PII (email, phone, avatar) lives in users (studylyon-blueprint/03-Database/DATA_RETENTION.md:69); retention: life of tenancy, soft-delete (DATA_RETENTION.md:22).
  • Erasure: anonymize + soft-delete immediately, hard-delete via purge job (users.service.ts:186-210).
  • Blueprint: "PII masked in audit snapshots" (Users.md:65) and "deleting a user cascades (async) to dependent profiles" (Users.md:67) — cascade is (planned) (no cascade code; the purge worker deletes the user doc only, tenant-purge.worker.ts:49-56).

7. Platform & client scope notes

  • Mobile client is a forward-looking spec. The PRD explicitly excludes native mobile apps from Phase 1 ("Native mobile apps (web-first)", PRODUCT_REQUIREMENTS_DOCUMENT.md:144); the shared ledger flags the whole Flutter design set as forward-looking (00-shared/12 A1). All screens, routes, and Flutter implementation guidance in this doc set target the web-first responsive client (web/desktop/tablet/phone layouts) and are (forward-looking) by extension.
  • API is v1, Bearer JWT, tenant from token only (00-shared/07 §1,§6); wire contract and error codes per 00-shared/07 §2-§3 (success {success:true,message:"OK",data,meta?,timestamp,requestId}; codes 400/401/ 403/404/409/422/429/5xx).

8. Goals (product)

  1. Admin can see and manage every person in the tenant — searchable, paginated list; detail; edit; deactivate; erase (GDPR).
  2. Bulk onboarding — CSV upload with per-row error reporting so a 1,000-row file can be fixed and re-imported row-by-row without data loss.
  3. Self-service profile & preferences — user edits own display data, notification toggles, theme.
  4. Status lifecycle clarityactive / inactive / suspended / invited surfaced with unambiguous copy; soft-delete as the safe default removal.
  5. Invite path — create identity + membership, then invite acceptance (planned).

9. Non-goals (per source)

  • No passwords / auth flows in this module (auth module owns them).
  • No profile photos stored in MongoDB — delegated to StorageProvider (users.service.ts:217-222).
  • No role/permission editing here — RBAC module.
  • No async import job yet — import is request-synchronous.
  • No analytics instrumentation in this module (proposed).