Aller au contenu

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

  1. Before handlerAdminActionService.create({ name, payload, adminId?, source? }) inserts a row with completed = false. Payload = { body, params } (cloned). Multer uploads on req.file are not persisted.
  2. After successupdateAsComplete(id) sets completed = true. Failure to mark complete is logged but does not fail the HTTP response.
  3. 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