Skip to content

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).
  • methodGET, 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

  1. "Who approved withdrawal X?" Open /admin-logs?urlContains=/admin/payments/withdraw/approve&dateFrom=.... Find the row matching the withdrawal id in requestBody. The userId column is the admin actor.
  2. "When was this user banned?" Filter by userId=123, urlContains=/ban. Expect a PATCH /admin/user/123/ban row.
  3. Daily compliance review. Filter method != GET for last 24h. Skim. Anything unexpected (e.g., /admin/user-notes deletions outside business hours) flags for follow-up.
  4. Post-incident timeline. Engineering correlates with Loki logs (each row carries durationMs for end-to-end perf). The OTel trace for any audit row is fetchable by timestamp from Jaeger — see e2e-trace-demo.md.
  5. Toggle debug logging. Engineering opens /admin-logs (logger sub-section), bumps apps/api to debug for 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.
  • requestBody may include sensitive payload. Withdrawal addresses, KYC document IDs, etc. Treat the audit table as restricted-access. Permission admin-users.view-admin-audit controls who can view.
  • requestBody is captured for write methods only. GET requests 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/logger flips 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.
  • durationMs includes 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_id column on AdminActionLog. See project_e2e_trace_capture_decision for the rationale.
  • Old rows lack requestBody. Capture was added later; rows from before that ship date have NULL requestBody.

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