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:
@@ -910,6 +910,49 @@ export interface RolePermissionAuditList {
|
||||
nextOffset: number | null;
|
||||
}
|
||||
|
||||
export type PermissionAuditEntryTargetKind =
|
||||
(typeof PermissionAuditEntryTargetKind)[keyof typeof PermissionAuditEntryTargetKind];
|
||||
|
||||
export const PermissionAuditEntryTargetKind = {
|
||||
user: "user",
|
||||
group: "group",
|
||||
app: "app",
|
||||
} as const;
|
||||
|
||||
export type PermissionAuditEntryChangeKind =
|
||||
(typeof PermissionAuditEntryChangeKind)[keyof typeof PermissionAuditEntryChangeKind];
|
||||
|
||||
export const PermissionAuditEntryChangeKind = {
|
||||
userroles: "user.roles",
|
||||
usergroups: "user.groups",
|
||||
groupusers: "group.users",
|
||||
grouproles: "group.roles",
|
||||
groupapps: "group.apps",
|
||||
apppermissions: "app.permissions",
|
||||
} as const;
|
||||
|
||||
export interface PermissionAuditEntry {
|
||||
id: number;
|
||||
targetKind: PermissionAuditEntryTargetKind;
|
||||
targetId: number;
|
||||
changeKind: PermissionAuditEntryChangeKind;
|
||||
previousIds: number[];
|
||||
newIds: number[];
|
||||
addedIds: number[];
|
||||
removedIds: number[];
|
||||
createdAt: string;
|
||||
actor: AuditLogActor | null;
|
||||
}
|
||||
|
||||
export interface PermissionAuditList {
|
||||
entries: PermissionAuditEntry[];
|
||||
totalCount: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
/** @nullable */
|
||||
nextOffset: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
@@ -1028,6 +1071,60 @@ export type DeleteGroupParams = {
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
export type GetUserPermissionAuditParams = {
|
||||
/**
|
||||
* @minimum 1
|
||||
* @maximum 200
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @minimum 0
|
||||
*/
|
||||
offset?: number;
|
||||
/**
|
||||
* @minimum 0
|
||||
*/
|
||||
actorUserId?: number;
|
||||
from?: string;
|
||||
to?: string;
|
||||
};
|
||||
|
||||
export type GetGroupPermissionAuditParams = {
|
||||
/**
|
||||
* @minimum 1
|
||||
* @maximum 200
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @minimum 0
|
||||
*/
|
||||
offset?: number;
|
||||
/**
|
||||
* @minimum 0
|
||||
*/
|
||||
actorUserId?: number;
|
||||
from?: string;
|
||||
to?: string;
|
||||
};
|
||||
|
||||
export type GetAppPermissionAuditParams = {
|
||||
/**
|
||||
* @minimum 1
|
||||
* @maximum 200
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @minimum 0
|
||||
*/
|
||||
offset?: number;
|
||||
/**
|
||||
* @minimum 0
|
||||
*/
|
||||
actorUserId?: number;
|
||||
from?: string;
|
||||
to?: string;
|
||||
};
|
||||
|
||||
export type GetAdminStatsParams = {
|
||||
/**
|
||||
* Time range for trend stats. Use "custom" with from/to.
|
||||
|
||||
Reference in New Issue
Block a user