feat(audit-log): add system-wide audit trail - #123
Merged
Conversation
Audit logging was limited to per-ticket ticket_activities; there was no system-wide trail for admin/config/security/user actions that happen outside a ticket. Port the Laravel reference (AuditLog model, Auditable trait, Admin\AuditLogController). - AuditLog Lucid model + escalated_audit_logs migration: actor (user_id), action, polymorphic target (auditable_type/id), old/new values JSON, ip_address, user_agent, created_at (append-only, no updated_at) - AuditService: reusable seam centralizing every AuditLog write, pulling the actor/ip/user-agent from the request; never throws so an audit failure can't break the mutation that triggered it - Wire records at admin CRUD + security sites: settings + public-tickets, user role grant/revoke, webhooks (create/update/delete), API tokens (create/update/revoke), automations (create/update/delete), and two-factor enable/disable/recovery-code regeneration - AdminAuditLogsController + route: filtered (user/action/type/date range), paginated, newest-first list with actor-name resolution - Framework-free helpers in support/audit_events.ts (row build, attribute diff, list filter/sort) + support/pagination.ts, with 16 unit tests
mpge
added a commit
that referenced
this pull request
Aug 2, 2026
Audit logging was limited to per-ticket ticket_activities; there was no system-wide trail for admin/config/security/user actions that happen outside a ticket. Port the Laravel reference (AuditLog model, Auditable trait, Admin\AuditLogController). - AuditLog Lucid model + escalated_audit_logs migration: actor (user_id), action, polymorphic target (auditable_type/id), old/new values JSON, ip_address, user_agent, created_at (append-only, no updated_at) - AuditService: reusable seam centralizing every AuditLog write, pulling the actor/ip/user-agent from the request; never throws so an audit failure can't break the mutation that triggered it - Wire records at admin CRUD + security sites: settings + public-tickets, user role grant/revoke, webhooks (create/update/delete), API tokens (create/update/revoke), automations (create/update/delete), and two-factor enable/disable/recovery-code regeneration - AdminAuditLogsController + route: filtered (user/action/type/date range), paginated, newest-first list with actor-name resolution - Framework-free helpers in support/audit_events.ts (row build, attribute diff, list filter/sort) + support/pagination.ts, with 16 unit tests Co-authored-by: Matt Gros <mpge@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Audit logging was limited to per-ticket
ticket_activities; there was no system-wide audit trail. Admin/config/security/user actions that happen outside a ticket were not recorded.What this does
Ports the Laravel reference (
AuditLogmodel,Auditabletrait,Admin\AuditLogController) to the AdonisJS backend.Storage
AuditLogLucid model +escalated_audit_logsmigration (0063): actor (user_id),action, polymorphic target (auditable_type/auditable_id),old_values/new_valuesJSON,ip_address,user_agent,created_at. Append-only — noupdated_at. Indexed on the morph pair,user_id,action, andcreated_at, matching the Laravel schema.auditable_idis a string so int/uuid/string host keys round-trip.The reusable seam
AuditServicecentralizes every write (mirrors how theAuditabletrait centralizesAuditLog::create), pulling the actor fromauth.userand IP + user-agent from the request. It never throws — a failed audit insert cannot break the mutation that triggered it (same contract asWebhookDispatcher).support/audit_events.ts(row build, attribute diff, list filter/sort) so it is unit-testable without Lucid/HTTP, mirroringsupport/webhook_events.ts.Audited actions (wired at the mutation sites)
settings.updated(secrets redacted),settings.public_tickets_updateduser.role_updated(old/new admin+agent flags)webhook.created/updated/deletedapi_token.created/updated/revokedautomation.created/updated/deletedtwo_factor.enabled/disabled/recovery_codes_regeneratedAdmin surface
AdminAuditLogsController+GET /support/admin/audit-logs(admin-gated): newest-first, paginated list filtered byuser_id,action,auditable_type, anddate_from/date_to, with actor-name resolution and filter-dropdown data. Matches the Laravel controller and returns the shared Laravel-paginator shape (extracted into a reusablesupport/pagination.ts).Tests
TDD-first. New
tests/unit/audit_log.spec.ts(16 assertions) proves recorded actions produce a row with the right actor/action/target, the attribute diff excludes timestamp noise, and the admin list filters + sorts correctly. Full suite green: 595 node-test + 21 Japa, 0 failures, no regressions.eslint+prettier@3.9.6(CI-locked) clean on all changed files.