Record an audit trail every time a role's permissions change and surface
recent history inside the role edit dialog.
Schema (lib/db):
- New `role_permission_audit` table: id, roleId (FK roles), actorUserId
(FK users, nullable on delete), previousPermissionIds int[],
newPermissionIds int[], createdAt. Created via raw SQL because
`drizzle push` currently fails on a pre-existing app_permissions
duplicate (followup #148 tracks fixing this).
API (artifacts/api-server):
- PUT /api/roles/:id/permissions now wraps the permission delete/insert,
the legacy audit_logs row, and the new role_permission_audit row in
a *single* transaction so the permission set and its audit trail
always commit (or roll back) together. Previous IDs are also read
inside the transaction to avoid races.
- A role_permission_audit row is written on EVERY PUT call (per task
spec), including no-op saves; the GET handler computes
addedPermissionIds/removedPermissionIds so the History UI can
distinguish meaningful changes from no-op saves.
- New GET /api/roles/:id/audit (admin-only, ?limit=1..50, default 10)
returns recent entries newest-first with computed
added/removedPermissionIds and joined actor info.
- New tests in tests/role-permission-audit.test.mjs cover write,
every-call write (incl. no-op), GET ordering+diff+actor, no-op diff
reporting, 404, and limit.
Drive-by fixes:
- Fixed pre-existing syntax corruption in tests/apps-open.test.mjs
(stray `ccaPassa,` line from a prior bad merge that prevented the
whole api-server test workflow from running).
- Made tests/roles-crud.test.mjs "rejects an invalid name" robust to
parallel test execution by scoping the leak check to the bad name
instead of relying on a global row count.
API spec / codegen (lib/api-spec, lib/api-client-react):
- Added `getRolePermissionAudit` operation and
`RolePermissionAuditEntry` schema; ran orval codegen.
Frontend (artifacts/tx-os):
- New RolePermissionHistory component renders inside the role edit
dialog (between permissions and Save/Cancel) using
useGetRolePermissionAudit. Shows timestamp, actor, added/removed
permission names (resolved via permissionsById), and total count.
- Save invalidates the audit query so the new entry appears immediately
on the next open.
- Bilingual i18n strings (en + ar with full Arabic plural variants:
zero/one/two/few/many/other) under admin.roles.history*.
Verification:
- tx-os typecheck passes.
- All 5 new audit tests + 13 existing roles tests pass.
- E2E (testing skill) verified the full flow: empty state on a fresh
role, save adds a permission, reopened dialog shows the new entry
with actor name in Arabic.
Docs:
- replit.md updated to list `role_permission_audit` in Database Tables.
Follow-ups proposed: #146 paginate/filter history, #147 audit other
admin permission changes, #148 fix drizzle push duplicate.
Original task
- Cover the new /api/roles create, update, and delete handlers with
automated tests so regressions in role validation, system-role
protection, and the in-use conflict response can't slip in unnoticed.
- Tests must run as part of the standard api-server test command.
What was added
- artifacts/api-server/tests/roles-crud.test.mjs with 7 tests:
* POST /api/roles -> 201 (also checks DB row + serialized fields)
* POST /api/roles duplicate name -> 409 (no second row inserted)
* POST /api/roles invalid name format -> 400 (no row leaked)
* PATCH /api/roles/:id description-only update -> 200, name unchanged
* DELETE /api/roles/:id on system role 'admin' -> 400, row preserved
* DELETE /api/roles/:id on in-use role -> 409 with userCount=1,
groupCount=1; row preserved
* DELETE /api/roles/:id on unused role -> 204, row gone
- The file follows the same pattern as groups-crud.test.mjs:
pg.Pool seed via DATABASE_URL, login -> connect.sid cookie,
thorough teardown of users / groups / roles / link tables.
Test infra
- No new test setup needed; the package's existing
"test": "node --test 'tests/**/*.test.mjs'" picks the file up
automatically. All 7 new tests pass.
Notes / drift
- Two pre-existing test failures (apps-open.test.mjs and one assertion
in groups-crud.test.mjs that depends on global group counts) are
unrelated to this task and were left untouched.