# Users Backend ## Purpose `users` is the identity slice: it manages user accounts, their assigned role (`app_role`), per-user custom permissions, organization membership, optional avatar files, and the email-action tokens used for verification and password reset. The slice is hand-written: creating a user (or bulk-importing users) triggers an invitation email containing a password-reset link. ## Slice Files (by layer) - Route: `src/routes/users.ts` (CRUD plus `bulk-import`, `count`, `autocomplete`, `deleteByIds`; applies `checkCrudPermissions('users')` to every route). - Controller: `src/api/controllers/users.controller.ts` (resolves the UI host from the request `Referer` for invitation links; handles CSV export and file upload). - Service (BLL): `src/services/users.ts` (invitation + bulk-import workflow; duplicate-email and self-delete guards; delegates email sending to `AuthService`). - Repository (DAL): `src/db/api/users.ts` (also exposes auth helpers: `findBy`, `findProfileById`, `createFromAuth`, `updatePassword`, token generation/lookup, `markEmailVerified`). - Model: `src/db/models/users.ts`. - Shared used: `services/auth.ts` (`AuthService.sendPasswordResetEmail`), `db/api/shared/repository.ts`, `db/api/file.ts` (`replaceRelationFiles`), `db/utils.ts`, `shared/config.ts` (`config.roles`, `config.providers`, bcrypt settings), `shared/constants/roles.ts` (`ROLE_NAMES`), `shared/constants/auth.ts` (`EMAIL_ACTION_TOKEN_BYTES`, `EMAIL_ACTION_TOKEN_TTL_MS`), `shared/constants/database.ts` (`BULK_IMPORT_TIMESTAMP_STEP_MS`), `shared/constants/pagination.ts` (`resolvePagination`), `shared/csv.ts` (`toCsv`), `middlewares/upload.ts` (`processFile`), `middlewares/check-permissions.ts`, `shared/errors/validation.ts`. ## API All routes are mounted under `/api/users` and require JWT authentication (`src/index.ts`). Every route passes `checkCrudPermissions('users')`, requiring the permission `${METHOD}_USERS` (see `permissions.md`); the middleware's self-access bypass also lets a user act on their own record when `req.params.id`/`req.body.id` equals their own id. - `POST /api/users` -> `200` `true`. Request body: `{ data: }`. Creates the user then sends an invitation email. - `POST /api/users/bulk-import` -> `200` `true`. Multipart CSV upload; every row must carry an `email`. Missing file raises `ValidationError('importer.errors.invalidFileEmpty')`. - `PUT /api/users/:id` -> `200` `true`. The controller calls `Service.update(req.body.data, req.body.id, ...)` (reads `req.body.id`, not `req.params.id`). - `DELETE /api/users/:id` -> `200` `true`. - `POST /api/users/deleteByIds` -> `200` `true`. Request body: `{ data: string[] }`. - `GET /api/users` -> `200` `{ rows, count }`. When `?filetype=csv`, responds with a CSV attachment of fields `id, firstName, lastName, phoneNumber, email`. - `GET /api/users/count` -> `200` `{ rows: [], count }`. - `GET /api/users/autocomplete` -> `200` array of `{ id, label }`. - `GET /api/users/:id` -> `200`, a single eager-loaded user record (role + permissions, staff profile, custom permissions, organization). ## Access Rules - CRUD is gated by `checkCrudPermissions('users')` (`${METHOD}_USERS`), or a matching custom per-user permission, or the self-access bypass. - `remove` adds explicit service-level guards beyond the permission check: - A user cannot delete themselves: `currentUser.id === id` raises `ValidationError('iam.errors.deletingHimself')`. - Only roles named `config.roles.admin` or `config.roles.super_admin` may delete a user; otherwise `ValidationError('errors.forbidden.message')`. - `create` rejects a duplicate email (`iam.errors.userAlreadyExists`) and a missing email (`iam.errors.emailRequired`). ## Tenant Scope - `findAll` scopes to `currentUser.organizationId` only when the user has a loaded `organizations` association and an `organizationId`. `globalAccess` users (`currentUser.app_role.globalAccess`) have the org constraint removed and read across organizations. - On `create`, organization membership is set from `data.organizations` via `setOrganizations`. - On `update`, role/org/custom-permission associations are only changed when their respective fields are present in the input. ## Data Contract Model columns (`src/db/models/users.ts`): `id` (UUID PK), `firstName`, `lastName`, `phoneNumber` (text), `email` (text, `allowNull: false`), `disabled` (boolean, default false), `password` (text), `emailVerified` (boolean, default false), `emailVerificationToken` + `emailVerificationTokenExpiresAt`, `passwordResetToken` + `passwordResetTokenExpiresAt`, `provider` (text), `importHash` (unique), `organizationId`, `createdById`, `updatedById`, `createdAt`, `updatedAt`, `deletedAt` (paranoid soft-delete). A `beforeCreate`/`beforeUpdate` hook trims `email`/`firstName`/`lastName`; for non-local OAuth providers `beforeCreate` forces `emailVerified = true` and generates a random bcrypt password when none is supplied. Associations: `belongsToMany permissions` as `custom_permissions` (and `custom_permissions_filter` for list filtering) through `usersCustom_permissionsPermissions`; `hasMany staff` as `staff_user`; `hasMany messages` as `messages_sent_by`; `belongsTo roles` as `app_role`; `belongsTo organizations` as `organizations`; `hasMany file` as `avatar`; `belongsTo users` as `createdBy`/`updatedBy`. On `create`/`bulkImport` the repository sets `emailVerified` to `true` on single create and to `false` (unless supplied) on bulk import. A user created without an explicit `app_role` has no role and falls back to the `guest` role until one is assigned (roles are assigned explicitly by the provisioning flow). The service layer (`services/users.ts`) also enforces the relational role policy (`assertCanManageUserWithRole`), a same-organization guard (`assertSameTenant`) for non-global actors, and auto-creates the company when an `owner` is created (§3.3/§3.4). List filters (`findAll`): `id`, `firstName`, `lastName`, `phoneNumber`, `email`, `password`, `emailVerificationToken`, `passwordResetToken`, `provider` (ILIKE); `emailVerificationTokenExpiresAtRange`, `passwordResetTokenExpiresAtRange`, `createdAtRange`; `active`, `disabled`, `emailVerified`; `app_role` (join on id or name, `|`-separated); `organizations` (`|`-separated ids); `custom_permissions` (join on id or name); plus `field`/`sort` (default `createdAt desc`) and `limit`/`page`. ## Behavior / Notes - Invitation workflow: after a successful `create` commit, `AuthService.sendPasswordResetEmail` is called with type `'invitation'` and the host resolved from the request `Referer`, sending the new user a `/password-reset?token=...` link. The token is generated by `UsersDBApi.generatePasswordResetToken` with `EMAIL_ACTION_TOKEN_BYTES`/`EMAIL_ACTION_TOKEN_TTL_MS`. - Bulk import: parses the uploaded CSV (`csv-parser`); requires every row to carry an `email` (else `ValidationError('importer.errors.userEmailMissing')`); `bulkCreate`s with `ignoreDuplicates: true` and staggered `createdAt` (`BULK_IMPORT_TIMESTAMP_STEP_MS`); attaches avatar files per row. - Note (inconsistency in source): both `create` and `bulkImport` accept a `sendInvitationEmails` flag (controller passes `true`). In `create`, emails are sent only when `sendInvitationEmails` is truthy (`if (!sendInvitationEmails) return;`), but in `bulkImport` the email loop runs only when `!sendInvitationEmails`. With the controller always passing `true`, single-create users are emailed while bulk-imported users are not. - All mutations run inside a manual Sequelize transaction (commit on success, rollback on error). - `findBy` returns a per-request `AuthenticatedUser` (role + its permissions, staff profile, custom permissions, organization) used by authentication/authorization; `findProfileById` returns the trimmed profile DTO for `GET /me`. ## Tests None yet (no `users` unit/e2e test under `src/`). ## Related - Backend slices: `permissions.md` (the `${METHOD}_USERS` gate and the `custom_permissions` / `app_role.permissions` model consumed by `check-permissions.ts`); the `roles` entity (`app_role`; a user created without a role falls back to `guest`). - Frontend / auth: `auth-profile.md` (the profile DTO produced by `findProfileById`, plus the invitation/password-reset email flow shared with `AuthService`).