--- description: Primary modules must use @RequirePrivilege on every non-public handler; privilege_keys seeded per module globs: "src/modules/**/*.ts,src/common/decorators/**/*.ts,src/common/guards/**/*.ts" alwaysApply: false --- # Privileges Authorization ## Mandatory Every **non-public** controller handler on a primary (CRUD) resource MUST use: ```typescript @RequirePrivilege('MODULE.RESOURCE', 'view' | 'create' | 'update' | 'delete' | 'import') ``` Map HTTP verbs to actions: | Handler | Action | | ------- | ------ | | `GET` list / detail | `view` | | `POST /` create | `create` | | `PATCH /:id`, status, bulk-status | `update` | | `DELETE /:id`, bulk-delete | `delete` | | `POST /import` | `import` | Key codes use dotted uppercase module levels (`PRIVILEGES`, `SALES.INVOICE`). New modules add a `privilege_keys` seed row via migration — do not invent a parallel permission helper. Seed an Administrator privilege only via SQL/ops after the first user exists (`created_by` requires a user). Documented bootstrap: insert privilege + details, then `UPDATE users SET privilege_id = …`. Do not auto-grant on register. ## Guard behavior `PrivilegesGuard` (global) allows when there is no metadata. When metadata is present, `users.is_superadmin === true` skips the matrix check. Otherwise the user’s assigned privilege must be **status `active`** and the matrix cell must be `value === true`, or the request is `403 Forbidden`. Missing privilege / draft / archived / missing cell / `false` → deny. Do not set `is_superadmin` via register/login. Default is `false`; promote via SQL/ops (`UPDATE users SET is_superadmin = true`). The flag is loaded from the database on each JWT validation (not from JWT claims). Primary tables still use `primaryEntityColumns(users)` (`status`, audit timestamps, `created_by` / `updated_by`).