Admin Logs¶
Purpose¶
The Admin Logs screen is the read-only audit feed for everything an admin does. Every PermissionGuard / RolesGuard-protected request writes a row to AdminActionLog (Postgres table admin_action_log). This is the single source of truth for "who approved that withdrawal", "when did this player get banned", "did anyone touch the deposit-bonus config last night".
Audience¶
Compliance (primary), risk (post-incident forensics), engineering (post-mortem timelines), legal (regulatory data requests).
Path in admin-fe¶
| Screen | URL | Page |
|---|---|---|
| Logs list | /admin-logs |
ebit-admin-fe/src/app/(dashboard)/admin-logs/page.tsx |
| One log entry detail | /admin-logs/[id] |
ebit-admin-fe/src/app/(dashboard)/admin-logs/[id]/page.tsx |
Backing API endpoints¶
| Endpoint | Source |
|---|---|
GET /admin/user/admin-audit (paginated) |
apps/api/src/user/admin.user.controller.ts:99 |
| (per-row drill-in fetches the same row by id) | same controller |
GET /admin/logger (current log levels) |
apps/api/src/system/logger/admin.logger.controller.ts:14 |
PUT /admin/logger (toggle log level) |
apps/api/src/system/logger/admin.logger.controller.ts:19 |
Frontend wiring: ebit-admin-fe/src/queries/admin-logs/index.tsx (calls /admin/user/admin-audit).
Backing data model¶
AdminActionLog (api.prisma:1479-1497)
id Int @id
createdAt DateTime
method String -- GET / POST / PATCH / PUT / DELETE
url String -- canonical pattern; not query string
userId Int? -- the admin actor
userAgent String?
ipAddress String?
status Int -- HTTP status returned
response Json? -- (truncated) body on error or sensitive ops
durationMs Float?
requestBody Json? -- captured for write methods
Index on userId (idx_admin_action_log_user_id). For audit queries by target user, the URL pattern carries the target id (e.g. /admin/user/123/ban).
Key actions¶
| Action | Required permission | API call | DB tables touched | Audit-logged? |
|---|---|---|---|---|
| List audit rows | admin-users.view-admin-audit |
GET /admin/user/admin-audit |
AdminActionLog (joined User for actor) |
n/a (read) |
| View one log detail | admin-users.view-admin-audit |
same with id filter | same | n/a |
| View current log levels | system.logger |
GET /admin/logger |
in-memory EvoLogger config |
yes |
| Toggle log level (debug / info / warn / error) | system.logger |
PUT /admin/logger |
in-memory EvoLogger config (process-local) |
yes |
Filters and views¶
UserAdminAuditRequestDto (the query shape) supports:
userId— filter by target user (URL pattern/admin/user/<userId>/...).adminUserId— filter by actor (which admin did it).method—GET,POST,PATCH,PUT,DELETE.status— HTTP status (filter for failed actions).urlContains— substring match (e.g.,withdraw/approve).dateFrom/dateTo— ISO range.page/take— pagination.
Common workflows¶
- "Who approved withdrawal X?" Open
/admin-logs?urlContains=/admin/payments/withdraw/approve&dateFrom=.... Find the row matching the withdrawal id inrequestBody. TheuserIdcolumn is the admin actor. - "When was this user banned?" Filter by
userId=123,urlContains=/ban. Expect aPATCH /admin/user/123/banrow. - Daily compliance review. Filter
method != GETfor last 24h. Skim. Anything unexpected (e.g.,/admin/user-notesdeletions outside business hours) flags for follow-up. - Post-incident timeline. Engineering correlates with Loki logs (each row carries
durationMsfor end-to-end perf). The OTel trace for any audit row is fetchable by timestamp from Jaeger — seee2e-trace-demo.md. - Toggle debug logging. Engineering opens
/admin-logs(logger sub-section), bumpsapps/apitodebugfor 15 minutes during a live incident. Backend updates EvoLogger in-memory config. Process-local — only one instance flips. For multi-instance, set the env var and roll the deployment.
Edge cases / gotchas¶
- Read-only / immutable. No update / delete on
AdminActionLog. Compliance can rely on append-only semantics. requestBodymay include sensitive payload. Withdrawal addresses, KYC document IDs, etc. Treat the audit table as restricted-access. Permissionadmin-users.view-admin-auditcontrols who can view.requestBodyis captured for write methods only.GETrequests record the URL but not query body (rare anyway). Some destructive endpoints intentionally redact (e.g., balance edits store the delta but not the OTP code).- Per-process logger state.
PUT /admin/loggerflips the in-memory level for one Node instance. If the api app runs multiple replicas, you must call it on each. There is no broadcast pattern. Tracked as a known issue. - No retention policy in code. Table grows unbounded. Engineering periodically partitions / archives via a one-off script. Plan for partitioning before > 50 GB.
durationMsincludes time spent in BullMQ enqueue but NOT the job execution itself. A "fast" approve here can still be slow end-to-end if the BullMQ job lags.- OTel traces correlate by timestamp only — there's no
trace_idcolumn onAdminActionLog. Seeproject_e2e_trace_capture_decisionfor the rationale. - Old rows lack
requestBody. Capture was added later; rows from before that ship date have NULLrequestBody.
Cross-links¶
- Action source for every other admin page: see "Audit-logged?" column on each.
- Trace correlation flow:
e2e-trace-demo.md - Trace correlation memory:
project_evologger_trace_correlation,project_e2e_trace_capture_decision - Audit table data model:
data-model/→AdminActionLog - Permission key:
libs/auth/src/permissions/const.ts:168-174 - Logger toggle (debug-level rotation):
runbooks/→ "increase log verbosity"
Sequence — looking up "who approved withdrawal X"¶
sequenceDiagram
actor compliance
participant admin-fe
participant api
participant pg as Postgres
compliance->>admin-fe: open /admin-logs, filter url~="withdraw/approve", dateFrom=incident_start
admin-fe->>api: GET /admin/user/admin-audit?urlContains=...&dateFrom=...
api->>api: PermissionGuard('admin-users.view-admin-audit')
api->>pg: SELECT * FROM admin_action_log WHERE url LIKE '%/withdraw/approve%' AND createdAt >= ...
pg-->>api: rows JOIN User AS adminActor
api-->>admin-fe: PaginatedDto<AdminLogResponseDto>
compliance->>admin-fe: open the row matching withdrawal id (in requestBody)
admin-fe->>api: GET /admin/user/admin-audit?id=...
api-->>admin-fe: full row with requestBody, response, durationMs
admin-fe-->>compliance: actor revealed: admin user 42, durationMs=412ms, status=200