Repository navigation
Conversation
…records AUDITED_ACTIONS is the one list of pairs the API writes, so the logger and the audit log filters can be derived from it instead of from the two enums, which also hold values nothing writes. Co-Authored-By: Claude Opus 5.5
A log call whose entity and action are not in AUDITED_ACTIONS no longer compiles, so a new event cannot be written without the audit log filters learning about it. Co-Authored-By: Claude Opus 5.5
The Entity and Action menus are derived from AUDITED_ACTIONS instead of listing every enum value, and each is narrowed to what the other's choice allows, so no filter combination can match an entry nothing ever writes. Session no longer appears, since nothing logs it. Co-Authored-By: Claude Opus 5.5
The architecture doc and the audit log spec both listed which calls write an entry, and both had gone stale. They now name the map instead. Co-Authored-By: Claude Opus 5.5
…e audit log An admin creating a user, editing one, or replacing their permissions, and a user editing their own account, now each write an entry. An update names the fields it changed and never their values, and one that changed nothing writes no entry. Archiving and unarchiving a user are recorded as ARCHIVE and UNARCHIVE instead of UPDATE, as they are for instruments. User creation is logged in the controller, because setup and demo seeding call the same service method with no one acting. Co-Authored-By: Claude Opus 5.5
Deleting assignments in bulk writes one DELETE entry per assignment that was actually removed, in that assignment's group, naming it. An assignment that failed to delete is not recorded. The route now forwards the request user, so the service can attribute the entries. Co-Authored-By: Claude Opus 5.5
Creating a series and deleting one each write an entry in the group that owns the series, naming it by id and title, as archiving one already did. A request that only reports a duplicate creates nothing and records nothing. Co-Authored-By: Claude Opus 5.5
Deleting a subject writes one entry, carrying whether the delete was forced and how many records it removed, rather than one per cascaded row. A subject can belong to several groups and the route names none, so the entry names no group. It is written in the controller once the service call returns, leaving the delete and its transaction as they were. Co-Authored-By: Claude Opus 5.5
…t log Updating a record writes an entry naming the data fields the update changed, never their values, in the record's group; an update that changed nothing writes none. Deleting a record writes an entry naming the record, its instrument and its subject. The delete is logged in the controller, because the gateway synchronizer calls the same service method to roll back. changedFields now refuses a value with no fields, since record data is typed as any JSON value. Co-Authored-By: Claude Opus 5.5
Creating a group writes an entry in the new group, and updating one writes an entry naming the fields the update changed. An update that changed nothing writes none, so a resubmitted settings form adds no entry. Creation is logged in the controller, because demo seeding calls the same service method with no one acting. Co-Authored-By: Claude Opus 5.5
This branch has not been deployed
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.
The audit log offered Entity and Action filters that nothing ever wrote, and most admin changes were never recorded. This makes the written pairs a single declared list, derives the filters from it, and records the admin activity the triage asked for. The log stays an admin activity log: an update names the fields it changed, never their values.
Reviews commit by commit: four Phase 1 commits, then one per entity.
Phase 1
AUDITED_ACTIONSinpackages/schemas/src/audit/audit.tsdeclares every (entity, action) pair the API writes.AuditLogger.logis typed against it, so a call with an undeclared pair does not compile..agents/docs/architecture/auth-and-permissions.mdand the comment inadmin-audit-logs.spec.tsnow point at the map instead of listing call sites.Phase 2
CREATE,UPDATE(admin edit, permissions, self-edit); archive and unarchive becomeARCHIVE/UNARCHIVEUsersController(setup and demo seeding share the service method), the rest inUsersServicetargetUserId, plusfieldson updatesDELETE, one entry per assignment actually deleted, in its groupAssignmentsService.deleteBulkassignmentIdCREATEandDELETE, in the owning groupInstrumentsServiceinstrumentId,titleDELETE, one entry,groupId: nullSubjectsController, after the service call returnssubjectId,force,recordCountUPDATE,DELETEInstrumentRecordsService.updateById; delete in the controller (the gateway rollback shares the service method)recordIdandfields; on delete alsoinstrumentId,subjectIdCREATE,UPDATEGroupsController(demo seeding shares the service method), update inGroupsServicefieldson updatesUpdates go through
AuditLogger.logUpdatewithchangedFields(apps/api/src/audit/audit.utils.ts). It compares the stored row with the written one, ignoresupdatedAt, and treats a reordered list of ids as unchanged, because the web forms resubmit every field and rebuild id lists in their own order. An update that changed nothing writes no entry, which matches how series archiving already behaves. Entries are still written after the change, not in its transaction. Sessions and record creation are not logged, as decided.Services that now receive the request user instead of the ability:
UsersService.updatePermissions,AssignmentsService.deleteBulk,GroupsService.updateByIdandInstrumentRecordsService.updateById. No route's path, guard, body or response changed.Judgment calls
actionkey, now that the action column saysARCHIVEorUNARCHIVE. Earlier rows stay as they were, underUPDATE.forceis set. An unforced delete removes no records, and a caller withdelete Subjectmay hold no rule on records at all.SubjectsService.countDeletableRecordsuses the same scope as the forced delete.deleteByIditself is untouched, for Force-deleting a subject who has a remote assignment fails with 500 and deletes nothing #1748.InstrumentRecordsService.updateByIdcopies the stored data before merging, because lodashmergeWithwrites into its target. Without the copy the diff would always be empty. A unit test pins this.changedFieldsthrows aTypeErroron a value with no fields. Record data is typed as any JSON value, and an empty diff would hide the update.UsersService.updateByIdnow always loads the user, where before it did so only to check a new password. A missing id is therefore a 404 before any write.updateSelfByIdreads by id alone, like its existingwhere: { id }write. A group manager in no group holds noread Userrule that matches themselves, so a scoped read would 404 their own profile save.Verification
audit.utils.test.tsandaudit.logger.test.ts(including a@ts-expect-errorcall with an undeclared pair); each service and controller test asserts its new log call and the refused-path no-op;logs.test.tsxcovers the derived Entity and Action options and the narrowing, withlogFixturepaired asSEND_EMAIL/ASSIGNMENT.testing/src/specs/admin-audit-logs.spec.tsfilters by an entity still offered and asserts Session is gone. A newadmin audit log coverageblock has one test per event: users, assignments, series, subjects, records and groups. Group creation is also found through the page's group, entity and action filters. Subject and record events are checked throughApiClient.findAuditLogs, which now takes anentity.Results:
pnpm lintgreen.pnpm test:coverage100% statements, branches, functions and lines, 450 of 450 files.pnpm test:e2eon chromium with one worker: 369 of 370 passed.Exceptions:
@smokeset.admin-instrument-repos.spec.ts:196("should fit both tables to the page without a horizontal scroll bar") measured an overflowing sync log table. That spec passed 25 of 25 when re-run on its own, and nothing on this branch touches that page.Closes #1473
Co-Authored-By: Claude Opus 5.5