EduShade
Audit Module

Audit Logs

Admin-visible trail of every CRUD mutation across the platform with before/after snapshots

Audit Logs

Audit logs answer the question "who changed what, and what did the data look like before and after?" Every mutation performed by an admin, instructor, or system actor is captured here with the context needed to reconstruct what happened.

Where to Find Them

Sign in as an admin with audit.read permission and go to Admin → Audit → Audit Logs (/admin/audit-logs).

What Each Row Captures

Every audit log row stores the actor, the resource, the operation, and (when available) the data on either side of the change.

FieldTypeNotes
idUUIDPrimary key
tenant_idUUIDNOT NULL — every row is tenant-scoped
actor_idUUIDThe user that performed the action (nullable for system actors)
actor_typestringOne of: user, admin, system (default user)
actionstringcreated, updated, deleted, or any custom verb published by a service
resource_typestringcourse, user, role, enrollment, quiz, category, tag, setting, api_call, etc.
resource_idstringUUID or other identifier of the affected resource
modulestringThe originating service: auth, learning, quiz, billing, notification, engagement, ecommerce, tenant, etc.
descriptiontextHuman-readable summary, set by the publishing service
before_valueJSONBPre-mutation snapshot (typically present for updated/deleted)
after_valueJSONBPost-mutation snapshot (typically present for created/updated)
ip_addressINETClient IP captured at publish time
user_agenttextClient user agent at publish time
metadataJSONBFree-form context (endpoint, method, status code, correlation IDs, etc.)
created_attimestamptzServer timestamp at insert (or the Timestamp field on the source event if set)

Actor enrichment. actor_id is stored as a bare UUID. After the page is fetched, loadActorsForAuditLogs (audit-service/api/handlers/audit_log_handler.go) collects every non-nil ActorID across the result set and issues a single UserAssociationService.GetUsersByIDs call to the RBAC database — so a page of 50 rows costs one lookup. Matches are attached to each row as actor (name, email, avatar). Rows whose actor was hard-deleted from edushade_auth still surface; only the actor block stays empty.

What the UI Shows

The list view (src/components/admin/audit/audit-logs-table.tsx) renders these columns:

ColumnBehavior
TimestampHuman-formatted, sortable
ActorName + email if the actor was loaded from RBAC; otherwise actor_type + truncated actor_id
ActionColoured badge — green for create, blue for update, red for delete, gray otherwise
ResourceCapitalised resource_type
Resource IDFirst 8 characters in monospace (full ID is in the detail drawer)
ModuleCapitalised module (or - if unset)
DescriptionTruncated, muted text

Click any row to open the detail drawer (audit-log-detail.tsx) which expands every field — including the full before_value, after_value, and metadata JSON blocks pretty-printed inside scrollable code panels.

Built-in Filters

The toolbar exposes three quick filters (the underlying API supports more — see Filters & Search):

FilterOptions
Actioncreate, update, delete
Resource Typecourse, user, role, enrollment, quiz, category, tag, setting
Moduleauth, learning, quiz, billing, notification, engagement, ecommerce, tenant

These dropdowns are a curated UI subset, not an exhaustive list of what the backend emits. Services publish other values — for example tag_association, category_association, subscription_plan, purchase, refund, installment_payment, manual_payment, scholarship, file, multipart_upload, media, directory, and the generic api_call produced by Audit Middleware. Modules also include misc, migration, and booking. The API's resource_types[] and modules[] query params accept any string, so you can filter by those values directly even when the UI dropdown does not list them — see Filters & Search.

A bulk Export button is available in the toolbar — see Export.

API

MethodPathPurpose
GET/v1/admin/audit/audit-logsPaginated list with filters
GET/v1/admin/audit/audit-logs/:idSingle audit log by ID
GET/v1/admin/audit/audit-logs/exportJSON file download

All three require a valid JWT and the audit.read permission. All three are scoped to the caller's tenant by audit-service/middlewares — the tenant ID comes from the request context and is used as the leading WHERE clause.

Pagination Defaults

  • Default page size: 50 rows
  • Default sort: created_at DESC
  • sort_by accepts any column name; if it doesn't already contain a ., the alias al. is automatically prepended

See API Reference for the full request/response shape.

How a Row Gets Created

Audit log rows are never inserted by application code directly. They are always the product of an event published to the audit.events topic and persisted by the AuditEventSubscriber in the audit-service. There are two main publishing paths:

  1. Explicit publishes — A handler builds an AuditEventPayload and calls events.PublishAuditEvent(...) after a mutation succeeds. Used when you want full control over before_value, after_value, and description.
  2. Auto-publishes via middleware — Mounting events.AuditMiddleware(publisher, "<module>") on an admin route group automatically publishes a row for every successful POST/PUT/PATCH/DELETE. Resource is recorded as api_call and endpoint goes into both resource_id and metadata.endpoint.

See Event Ingestion for the payload contract and Audit Middleware for the drop-in.

Indexes & Query Behaviour

The audit_logs table is indexed for the access patterns you actually use:

IndexPurpose
idx_audit_logs_tenant_idTenant-leading scans
idx_audit_logs_actor_id"What did user X do?"
idx_audit_logs_resource (resource_type, resource_id)"What happened to this row?"
idx_audit_logs_action"Show me all deletes"
idx_audit_logs_modulePer-module filtering
idx_audit_logs_created_atTime-based scans
idx_audit_logs_tenant_created (tenant_id, created_at DESC)The composite index that powers the default sort + tenant filter — the hot path for the UI

Filtering by a non-tenant column on its own (without a date range) on a multi-tenant table can fall back to a sequential scan; the UI never does this because tenant scoping is mandatory.

Troubleshooting

IssueSolution
Page is empty for a fresh tenantAudit logs accumulate as admins act — no logs is the expected initial state
Actor column shows only an IDThe user was hard-deleted from edushade_auth; the row is preserved but actor enrichment cannot resolve
Mutation happened but no audit row appearedCheck the publishing service's logs — Redis may have been down at publish time, or the service may not be wired with AuditMiddleware / a manual publish
Detail drawer JSON is hugeThe before_value/after_value snapshots are full payloads — large objects will render large blocks; this is intentional
Cannot see Audit Logs in the sidebarYou lack the audit.read permission; ask an admin to grant it

On this page