01 — Product Overview (Users Module)
- 1. What the module is
- 2. Scope in / scope out
- 3. Status / lifecycle model
- 4. Bulk import — two real paths
- 5. Invites, roles, and the RBAC bridge
- 6. GDPR & data-retention posture
- 7. Platform & client scope notes
- 8. Goals (product)
- 9. Non-goals (per source)
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, andstudylyon-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).
"
usersstores identity only — never passwords, attendance, or academic data." (studylyon-blueprint/04-Modules/Users.md:63)
Key facts from source:
usersschema:user.schema.ts:14-79— status enumactive | inactive | suspended | invited(user.schema.ts:7-12, defaultactive: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 flagsisDeleted/deletedAt/deletedBy, audit authors, and an optimistic-lockversion(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); routingevent-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(plusrbac.member.*:15-18for 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 StorageProvider | Teacher/student/staff/parent profile data → their modules (RELATIONSHIPS.md:13,22-32) |
| GDPR erasure endpoint + scheduled hard purge | Multi-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)
| Status | Meaning | Set by |
|---|---|---|
active | Default on create (user.schema.ts:46-47; also CreateUserDto default create-user.dto.ts:44-47) | create / PATCH status |
inactive | Deactivated, can be re-activated via PATCH | PATCH status |
suspended | Suspended (e.g. discipline); re-activatable | PATCH status |
invited | Awaiting acceptance — enum exists, no endpoint sets it today | (planned) invite flow |
3.2 Soft-delete lifecycle
DELETE /users/:id→softDelete()setsisDeleted:true, deletedAt, deletedBy(base.repository.ts:68-74;users.service.ts:173-184) and emitsUserDeleted→audit-writequeue (event-queue-map.ts:12).- Soft-deleted users are invisible to every scoped query
(
base.repository.ts:20-30) and cannot log in — login usesfindByEmailUnscopedwhich filtersisDeleted:false(users.repository.ts:21-25,auth.service.ts:124). - Hard purge:
TENANT_PURGEworker deletes all soft-deleted docs older than 30 days (tenant-purge.worker.ts:15,32-43) — idempotent (only docs alreadyisDeletedbefore 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=true—users.service.ts:187-197) and enqueues agdpr-erasurejob on thetenant-purgequeue (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.INVITEDenum (user.schema.ts:11).MemberStatus.INVITED+invitedBy+acceptedAtonorganization_members(organization-member.schema.ts:7-11,33-38).auth_accounts.emailVerificationToken+resend-verificationendpoint (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.createalways leaves the DTO default (create-user.dto.ts:44-47). - No invite email (email worker handles only
UserRegisteredandPasswordResetRequested—email.worker.ts:25-42; noUserInvitedhandler, noevent-queue-maproute). - 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:
| Path | Endpoint | Parser | Report shape | Errors shape |
|---|---|---|---|---|
| A. Users inline | POST /api/v1/users/import (multipart field file) — users.controller.ts:104-110 | naive split(',') line parser (users.service.ts:236-252) | {imported, errors} | flat string[] — Row N: message (users.service.ts:281) |
| B. Bulk module adapter | POST /api/v1/bulk/import/:entity — bulk.controller.ts:35-48 | csv-parse/sync (quoted fields, trim, skip empty) — bulk-import.service.ts:26-30 | ImportReport {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 viaUsersService.createfor each row (students-import.adapter.ts:66-71). There is nousersadapter — the service throwsNo 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 withfirstName=firstname|first_name|'Unknown',lastName=lastname|last_name|'Unknown',phone,gender,language(defaulten),timezone(defaultUTC). - Path A imports do not emit
UserCreatedevents (rows callrepo.createdirectly —users.service.ts:264-272), unlike single create.PLAN.md 2.7plans a queue/worker path (POST /api/v1/files/upload-csv→UserCreatedper row) —(planned).
5. Invites, roles, and the RBAC bridge
"Inviting" someone into the tenant is a two-document operation today:
POST /api/v1/userscreates the identity (statusactiveby default).POST /api/v1/rbac/members{userId, roles[]}creates the membership (rbac.controller.ts:63-67;rbac.service.ts:113-127— status hard-setACTIVE,joinedAtnow;MemberStatus.INVITEDexists 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)
- Admin can see and manage every person in the tenant — searchable, paginated list; detail; edit; deactivate; erase (GDPR).
- 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.
- Self-service profile & preferences — user edits own display data, notification toggles, theme.
- Status lifecycle clarity —
active / inactive / suspended / invitedsurfaced with unambiguous copy; soft-delete as the safe default removal. - 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).