feat: add paginated audit-log retrieval for group administrators - #134
Merged
Conversation
Add GET /groups/:id/audit-log, restricted to a group's admins via the existing requireAdmin check. Non-members and non-admins get the repository's standard 403 FORBIDDEN response; unknown groups still 404 through requireMembership. Results are ordered newest first and paginated with an opaque cursor bounded to a validated page size (1-100, default 50), with optional server-validated filters for action, actor, and a created-at date range. Query and pagination logic lives in src/services/audit-log.ts rather than the route so it stays testable independent of Fastify, and the query is scoped by an indexed group_id column added to audit_logs via migration (previously audit events had no direct group association). Response metadata is redacted of known-sensitive keys (signed XDR, tokens, secrets) before serialization as a defensive measure, even though no current write path stores them there. The audit_logs.group_id column is populated going forward by threading groupId through the existing audit() call sites in group, expense, settlement, and treasury routes and the settlement worker; past events remain ungrouped until backfilled. Closes mergepay#113
|
@AdaBebe0 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
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.
Summary
Closes #113
Adds
GET /groups/:id/audit-log, an admin-only endpoint that surfaces the audit trail (AuditLogrows) already being written for membership, expense, treasury, and settlement changes, so group administrators have an auditable history without loading an unbounded collection.requireAdminhelper. Non-admin members get the repository's standard 403FORBIDDEN; non-members/unknown groups still resolve throughrequireMembership's existing 403/404 behavior. No new authorization pattern introduced.AuditLogpreviously had no direct link to a group (onlyentityType/entityId, which vary per action and aren't uniformly joinable back to a group). Added a nullablegroup_idcolumn via migration, backed by an index on(group_id, created_at), and threadedgroupIdthrough the existingaudit()call sites in the group, expense, settlement, and treasury routes plus the settlement worker and reconciliation loop. Historical rows written before this change remain ungrouped (group_id IS NULL) and won't appear in this endpoint — there's no reliable way to backfill them without guessing at group membership fromentityType/entityId, so filtering, not backfill, was the pragmatic choice here.(createdAt desc, id desc), bounded page size (limit, 1-100, default 50, validated via Zod), returning an opaquenextCursor(the last row's id) ornullwhen there's no more data. Query/pagination logic lives insrc/services/audit-log.ts, not the route, per the "keep Horizon/DB access in services" convention, and queries only ever load one page (take: limit + 1) rather than the full collection.action(exact match),actorUserId, andfrom/to(ISO-8601 date-time) are validated server-side and applied in the Prismawhereclause; an invalid range (fromafterto) returns 400.signedXdr,transactionXdr,xdr,token,jwt,secret,password,privateKey,secretKey,authorization) before the response is built. No current write path stores these inAuditLog.metadata, but this makes the guarantee explicit rather than implicit.{ events: [{ id, createdAt, actorUserId, actorDisplayName, action, entityType, entityId, metadata }], nextCursor }.action/entityType/entityIdcarry the existing action-taxonomy convention (e.g.settlement.confirmedvssettlement.failed) which already encodes outcome — no separateoutcomecolumn was added since the schema doesn't need one to represent it.mergepay-web/src/lib/types.tsisn't in this repository, so the contract above isn't mirrored there in this PR — flagging so the corresponding web-side type can be added when this ships.Testing
tests/audit-log.test.ts(new): non-admin 403, non-member 403, successful admin listing with metadata redaction, cursor/nextCursorbehavior (including thetake: limit + 1lookahead), filter parameters reaching the query, invalid date range, over-maxlimit, and an empty-page response.groupIdfield but retain theirtry/catch-swallowed, best-effort semantics).npm run test:integrationrequires a live server + database/testnet connection not available in this sandbox, so it wasn't run end-to-end here.