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.
Replit-Task-Id: 9b8886a2-6e76-4072-b345-a772421fa161
Original task: verify POST /api/apps/:id/open behaves correctly under slow
networks (the keepalive POST must survive the user navigating away),
unauthenticated callers (401 with no row inserted), and unknown app ids
(404 with no row inserted).
Changes
- New committed test file artifacts/api-server/tests/apps-open.test.mjs
using Node's built-in node:test runner (no new test framework added):
* happy path: authenticated POST returns 204 and inserts an app_opens row
* unauthenticated POST returns 401 and inserts no row
* authenticated POST to a non-existent app id (max(id)+100000) returns 404
and inserts no row
* slow-network simulation: opens a raw http.request to /api/apps/:id/open,
aborts the socket ~50 ms after sending so the client never reads the
response (mimicking a navigation-aborted keepalive POST), then asserts
the server still inserted the row. This proves the route's
`await db.insert(...)` runs to completion independently of whether the
client is still around to read the 204.
- The tests create a dedicated test user with a precomputed bcrypt hash for
"TestPass123!", assign the standard "user" role, log in via
POST /api/auth/login to obtain a connect.sid cookie, run the four cases,
and clean up (app_opens / user_roles / users) in an `after` hook.
- Added `pg` as a devDependency on @workspace/api-server (used by the
tests for direct DB assertions) and a `test` script:
`node --test 'tests/**/*.test.mjs'`.
- Also ran in-browser end-to-end coverage via the testing skill that
exercised the keepalive + wouter navigation flow against a live home
page with a 3 s route delay; that run also passed.
Schema drift fixed during the run
- The dev DB was missing the `app_opens` table and the `users.clock_style`
column referenced by the running schema. `pnpm --filter @workspace/db
push` blocked on an interactive rename/create prompt that could not be
answered non-interactively, so I brought the dev DB in line with the
Drizzle schema using idempotent SQL (CREATE TABLE IF NOT EXISTS for
app_opens with its two indexes; ALTER TABLE users ADD COLUMN IF NOT
EXISTS clock_style varchar(30)). No schema files were modified.
No production code changes were required — the existing route already
returns 401/404/204 correctly and the tests now lock that behavior in.
Replit-Task-Id: b7422abb-cc1b-4727-b70b-cde090f1a748