Admin actions (audit trail)¶
Persisted audit log for back-office mutations. Every state-changing admin route should create a row in admin_action before the handler runs, then mark it completed: true after success.
Mechanism¶
| Piece | Location |
|---|---|
| Interceptor factory | CreateAdminActionInterceptor |
| Service | AdminActionService |
| Action name enum | AdminActionName |
| Table | admin_action (TypeORM entity, migration Kysely en cours) |
Lifecycle¶
- Before handler —
AdminActionService.create({ name, payload, adminId?, source? })inserts a row withcompleted = false. Payload ={ body, params }(cloned). Multer uploads onreq.fileare not persisted. - After success —
updateAsComplete(id)setscompleted = true. Failure to mark complete is logged but does not fail the HTTP response. - On handler error — row stays
completed = false(failed attempt still auditable).
Actor invariant¶
At least one of adminId or source must be set:
| Context | adminId |
source |
Guard |
|---|---|---|---|
| BO session (cookie) | req.user.id from AdminAuthGuard |
optional string | AdminAuthGuard |
| M2M / automation | NULL |
e.g. espace-financement |
ApiKeyAuthGuard on route |
Wiring a new mutation¶
@Patch('/:id')
@UseGuards(AdminAuthGuard)
@UseInterceptors(CreateAdminActionInterceptor(AdminActionName.MY_ACTION))
async myMutation(@Body() body: unknown) { /* … */ }
Options:
| Option | When |
|---|---|
{ source: 'espace-financement' } |
M2M route without req.user |
{ pickBodyKeys: ['id', 'amount'] } |
Body contains PII (identity, banking) — persist only audit-relevant keys |
Exemptions (do not add interceptor)¶
- Compute-only simulations (no DB write)
- Raw S3 presigned upload endpoints (bytes never touch the API body)
Convention also documented in api-conventions.mdc (section DO).
Coverage map (administration)¶
Mutations under /administration/* are covered unless listed as exempt above. Per-domain docs reference the action name inline — examples:
| Domain | Doc | Sample actions |
|---|---|---|
| Properties | properties.md | property.edit-contract-budget, property.end-funding, property.resale.start |
| Funding | property-funding.md | property.transfer-funds-to-project-owner, property.transfer-funding-fees |
| Échéancier | property-payment-schedule.md | property.payment-schedule.create, property.payment-schedule.assign-wire-to-echeance |
| Construction budget | property-construction-budget.md | project.construction-budget.approve-or-decline-request |
| Customers | customers.md | customer.freeze, customer.adminLogin, customer.bulkUpdate |
| SPV | special-purpose-vehicule.md | spv.edit, spv.create-technical-account |
| Espace Financement (M2M) | espace-financement.md | source: espace-financement on project patch routes |
Full enum: grep AdminActionName in admin-action.entity.ts.
Querying audit data¶
-- Recent incomplete actions (handler threw or mark-complete failed)
SELECT id, name, "adminId", source, "createdAt"
FROM admin_action
WHERE completed = false
ORDER BY "createdAt" DESC
LIMIT 50;
-- Actions by admin on a given day
SELECT name, payload, completed, "createdAt"
FROM admin_action
WHERE "adminId" = '<uuid>'
AND "createdAt"::date = CURRENT_DATE
ORDER BY "createdAt" DESC;
Payload is JSONB — use payload->'params'->>'propertyId' etc. for incident triage.
Troubleshooting¶
| Symptom | Likely cause |
|---|---|
500 admin-action.missing-actor on M2M route |
Forgot { source: '…' } on interceptor |
Row completed = false after apparent success |
updateAsComplete failed — check API logs for failed to mark admin_action as complete |
| Oversized / sensitive payload in DB | Add pickBodyKeys or strip uploads before audit |
Liens¶
- Admin auth : admins.md
- Better Auth admin instance : better-auth/README.md