Task #147: Add structured permission-change audit (users, groups, apps)

Mirrors the existing role-permission audit pattern with a unified
`permission_audit` table capturing actor, target, prev/new id sets, and
timestamp written in the same transaction as the change.

Schema & API
- New `permission_audit` table (target_kind, target_id, change_kind,
  actor_user_id, previous_ids[], new_ids[], created_at) with index on
  (target_kind, target_id, created_at).
- Transactional audit writes in routes/users.ts (POST/DELETE roles,
  PATCH groupIds), routes/groups.ts (PATCH + add/remove members for
  users/roles/apps), routes/apps.ts (POST/DELETE permissions).
- Cross-entity mirroring: when group membership changes via a group
  endpoint, a user.groups row is also written for each affected user
  (and vice versa via PATCH /users), so each entity's history is
  exhaustive regardless of which editor was used.
- Admin-only GET /users/:id/audit, /groups/:id/audit, /apps/:id/audit
  with limit/offset/actorUserId/from/to filters and the same response
  shape as role audit.
- OpenAPI types + codegen regenerated.

UI
- Reusable PermissionAuditHistory component in admin.tsx wired into
  UserGroupsEditor, GroupDetailEditor (new "history" tab), and the
  editing-app dialog. App history correctly resolves permission ids
  (NOT roles) via useListPermissions.
- Bilingual i18n keys added under admin.{users,groups,apps}.history*
  in en.json + ar.json.

Tests
- New backend tests: user-permission-audit, group-permission-audit,
  app-permission-audit (14 cases — transactional capture, GET filters
  & pagination, admin-only, 404 on unknown id, plus 2 new mirror
  tests covering cross-entity audit visibility). All pass; 35
  adjacent role/groups/users/audit-coverage tests still pass.

Notes
- replit.md updated to list `permission_audit` table.
- Restored opengraph.jpg (an unrelated stray binary diff).
- Code-review comments addressed: cross-entity asymmetry fixed via
  mirroring; opengraph.jpg restored.
- Follow-ups proposed: timeline UI improvements, cascade audit on
  delete/bulk paths, e2e UI test for History sections.

Replit-Task-Id: 4ba8c8de-8dfd-42c7-a3bc-964a1ed3a1a3
This commit is contained in:
riyadhafraa
2026-04-30 11:58:18 +00:00
parent 8880d247e8
commit 2bf2f3cd3a
17 changed files with 3453 additions and 79 deletions
+282
View File
@@ -1846,6 +1846,212 @@ paths:
schema:
$ref: "#/components/schemas/GroupDeletionConflict"
/users/{id}/audit:
get:
operationId: getUserPermissionAudit
tags: [users]
summary: List recent permission-change audit entries for a user (admin)
description: |
Returns permission-change audit records for the given user, newest
first. Mirrors the role audit endpoint shape and supports the same
`limit`/`offset`/`actorUserId`/`from`/`to` filters. Each entry
captures the actor (if known), the previous and new id sets, and
the change kind (`user.roles` or `user.groups`).
parameters:
- name: id
in: path
required: true
schema:
type: integer
- in: query
name: limit
required: false
schema:
type: integer
minimum: 1
maximum: 200
default: 10
- in: query
name: offset
required: false
schema:
type: integer
minimum: 0
default: 0
- in: query
name: actorUserId
required: false
schema:
type: integer
minimum: 0
- in: query
name: from
required: false
schema:
type: string
format: date
- in: query
name: to
required: false
schema:
type: string
format: date
responses:
"200":
description: A page of permission-change audit entries for the user
content:
application/json:
schema:
$ref: "#/components/schemas/PermissionAuditList"
"400":
description: Invalid filter parameters
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"404":
description: User not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/groups/{id}/audit:
get:
operationId: getGroupPermissionAudit
tags: [users]
summary: List recent permission-change audit entries for a group (admin)
description: |
Returns permission-change audit records for the given group, newest
first. Captures changes to the group's user, role and app sets via
the discriminator field `changeKind`.
parameters:
- name: id
in: path
required: true
schema:
type: integer
- in: query
name: limit
required: false
schema:
type: integer
minimum: 1
maximum: 200
default: 10
- in: query
name: offset
required: false
schema:
type: integer
minimum: 0
default: 0
- in: query
name: actorUserId
required: false
schema:
type: integer
minimum: 0
- in: query
name: from
required: false
schema:
type: string
format: date
- in: query
name: to
required: false
schema:
type: string
format: date
responses:
"200":
description: A page of permission-change audit entries for the group
content:
application/json:
schema:
$ref: "#/components/schemas/PermissionAuditList"
"400":
description: Invalid filter parameters
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"404":
description: Group not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/apps/{id}/audit:
get:
operationId: getAppPermissionAudit
tags: [apps]
summary: List recent permission-change audit entries for an app (admin)
description: |
Returns permission-change audit records for the given app, newest
first. Captures changes to the set of permissions required to open
the app (`changeKind: app.permissions`).
parameters:
- name: id
in: path
required: true
schema:
type: integer
- in: query
name: limit
required: false
schema:
type: integer
minimum: 1
maximum: 200
default: 10
- in: query
name: offset
required: false
schema:
type: integer
minimum: 0
default: 0
- in: query
name: actorUserId
required: false
schema:
type: integer
minimum: 0
- in: query
name: from
required: false
schema:
type: string
format: date
- in: query
name: to
required: false
schema:
type: string
format: date
responses:
"200":
description: A page of permission-change audit entries for the app
content:
application/json:
schema:
$ref: "#/components/schemas/PermissionAuditList"
"400":
description: Invalid filter parameters
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"404":
description: App not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
# Stats
/stats/home:
get:
@@ -3864,6 +4070,82 @@ components:
- createdAt
- actor
PermissionAuditList:
type: object
properties:
entries:
type: array
items:
$ref: "#/components/schemas/PermissionAuditEntry"
totalCount:
type: integer
limit:
type: integer
offset:
type: integer
nextOffset:
type: ["integer", "null"]
required:
- entries
- totalCount
- limit
- offset
- nextOffset
PermissionAuditEntry:
type: object
properties:
id:
type: integer
targetKind:
type: string
enum: [user, group, app]
targetId:
type: integer
changeKind:
type: string
enum:
- user.roles
- user.groups
- group.users
- group.roles
- group.apps
- app.permissions
previousIds:
type: array
items:
type: integer
newIds:
type: array
items:
type: integer
addedIds:
type: array
items:
type: integer
removedIds:
type: array
items:
type: integer
createdAt:
type: string
format: date-time
actor:
oneOf:
- $ref: "#/components/schemas/AuditLogActor"
- type: "null"
required:
- id
- targetKind
- targetId
- changeKind
- previousIds
- newIds
- addedIds
- removedIds
- createdAt
- actor
AuditLogEntry:
type: object
properties: