Show readable names in audit log for top-level deletions
Task: #177 — Make user/app/role deletion audit rows render readable names ("Deleted user @alice (Alice Smith)", "Deleted app 'Notes'", "Deleted role 'Editor'") instead of relying on whatever the route happened to capture. Backend metadata changes: - artifacts/api-server/src/routes/users.ts (user.delete): now also persists displayNameEn and displayNameAr alongside the existing username/email. - artifacts/api-server/src/routes/apps.ts (app.delete): renamed the metadata keys slug/nameAr/nameEn → appSlug/appNameAr/appNameEn so app sub-resource events and top-level deletes share one prefix. - artifacts/api-server/src/routes/roles.ts (role.delete): renamed the metadata key name → roleName, matching group.role.add/remove. Frontend formatter (artifacts/tx-os/src/pages/admin.tsx): - appName helper now reads both legacy (slug/nameEn/nameAr) and new (appSlug/appNameEn/appNameAr) keys so old rows still render. - role.delete case prefers roleName, falls back to legacy name. - user.delete case picks the user's localized display name and uses new locale strings user.deleteWithName / user.forceDeleteWithName when present; falls back to the existing username-only strings. - forceDeletedEntityName also accepts appNameEn/appNameAr/appSlug so force-deleted apps still get their inline name chip. Locales: - artifacts/tx-os/src/locales/{en,ar}.json: added admin.audit.summary.user.deleteWithName and forceDeleteWithName. Test updates: - artifacts/api-server/tests/audit-log-coverage.test.mjs: updated the role.delete and app.delete (no-deps) assertions to read the new metadata key names. The user.delete assertions kept working as-is since username/email/force are unchanged. No DB migration was required — audit_logs.metadata is already JSON. Legacy rows continue to render via the formatter fallbacks called out in the task description.
This commit is contained in:
@@ -914,9 +914,9 @@ router.delete("/apps/:id", requireAdmin, async (req, res): Promise<void> => {
|
||||
targetType: "app",
|
||||
targetId: appId,
|
||||
metadata: {
|
||||
slug: existing.slug,
|
||||
nameAr: existing.nameAr,
|
||||
nameEn: existing.nameEn,
|
||||
appSlug: existing.slug,
|
||||
appNameAr: existing.nameAr,
|
||||
appNameEn: existing.nameEn,
|
||||
force,
|
||||
groupCount,
|
||||
restrictionCount,
|
||||
|
||||
@@ -414,7 +414,7 @@ router.delete("/roles/:id", requireAdmin, async (req, res): Promise<void> => {
|
||||
targetType: "role",
|
||||
targetId: id,
|
||||
metadata: {
|
||||
name: existing.name,
|
||||
roleName: existing.name,
|
||||
descriptionAr: existing.descriptionAr,
|
||||
descriptionEn: existing.descriptionEn,
|
||||
},
|
||||
|
||||
@@ -720,6 +720,8 @@ router.delete("/users/:id", requireAdmin, async (req, res): Promise<void> => {
|
||||
targetId: userId,
|
||||
metadata: {
|
||||
username: existing.username,
|
||||
displayNameEn: existing.displayNameEn,
|
||||
displayNameAr: existing.displayNameAr,
|
||||
email: existing.email,
|
||||
force,
|
||||
...(hasDeps
|
||||
|
||||
@@ -276,6 +276,8 @@ test("DELETE /api/users/:id (no-force, no deps) writes one user.delete row with
|
||||
assert.equal(row.actor_user_id, adminId);
|
||||
assert.equal(row.metadata.username, username);
|
||||
assert.equal(row.metadata.email, email);
|
||||
assert.equal(row.metadata.displayNameEn, "Audit Coverage");
|
||||
assert.equal(row.metadata.displayNameAr, null);
|
||||
assert.equal(row.metadata.force, false);
|
||||
// No-deps path must NOT include the dependency counts.
|
||||
assert.equal(row.metadata.noteCount, undefined);
|
||||
@@ -447,7 +449,7 @@ test("DELETE /api/roles/:id (no users/groups) writes one role.delete row", async
|
||||
});
|
||||
assert.equal(rows.length, 1);
|
||||
assert.equal(rows[0].actor_user_id, adminId);
|
||||
assert.equal(rows[0].metadata.name, role.name);
|
||||
assert.equal(rows[0].metadata.roleName, role.name);
|
||||
});
|
||||
|
||||
// ---------- groups ----------
|
||||
@@ -844,7 +846,9 @@ test("DELETE /api/apps/:id (no deps) writes app.delete with force=false and zero
|
||||
targetType: "app",
|
||||
});
|
||||
assert.equal(rows.length, 1);
|
||||
assert.equal(rows[0].metadata.slug, app.slug);
|
||||
assert.equal(rows[0].metadata.appSlug, app.slug);
|
||||
assert.equal(rows[0].metadata.appNameEn, "Audit App");
|
||||
assert.equal(rows[0].metadata.appNameAr, "تطبيق تدقيق");
|
||||
assert.equal(rows[0].metadata.force, false);
|
||||
assert.equal(rows[0].metadata.groupCount, 0);
|
||||
assert.equal(rows[0].metadata.restrictionCount, 0);
|
||||
|
||||
@@ -814,7 +814,9 @@
|
||||
},
|
||||
"user": {
|
||||
"delete": "تم حذف المستخدم @{{username}}",
|
||||
"forceDelete": "تم حذف المستخدم @{{username}} قسراً"
|
||||
"deleteWithName": "تم حذف المستخدم @{{username}} ({{displayName}})",
|
||||
"forceDelete": "تم حذف المستخدم @{{username}} قسراً",
|
||||
"forceDeleteWithName": "تم حذف المستخدم @{{username}} ({{displayName}}) قسراً"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -715,7 +715,9 @@
|
||||
},
|
||||
"user": {
|
||||
"delete": "Deleted user @{{username}}",
|
||||
"forceDelete": "Force-deleted user @{{username}}"
|
||||
"deleteWithName": "Deleted user @{{username}} ({{displayName}})",
|
||||
"forceDelete": "Force-deleted user @{{username}}",
|
||||
"forceDeleteWithName": "Force-deleted user @{{username}} ({{displayName}})"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5693,9 +5693,9 @@ function unitLabel(t: AuditTFunction, kind: string, count: number): string {
|
||||
}
|
||||
|
||||
function appName(meta: Record<string, unknown>, lang: string, fallbackId: AuditLogEntry["targetId"]): string {
|
||||
const en = asString(meta.nameEn);
|
||||
const ar = asString(meta.nameAr);
|
||||
const slug = asString(meta.slug);
|
||||
const en = asString(meta.nameEn) ?? asString(meta.appNameEn);
|
||||
const ar = asString(meta.nameAr) ?? asString(meta.appNameAr);
|
||||
const slug = asString(meta.slug) ?? asString(meta.appSlug);
|
||||
const preferred = lang === "ar" ? ar ?? en : en ?? ar;
|
||||
return preferred ?? slug ?? `#${fallbackId ?? "?"}`;
|
||||
}
|
||||
@@ -5908,7 +5908,10 @@ function formatAuditSummary(
|
||||
}
|
||||
case "role.delete":
|
||||
return t("admin.audit.summary.role.delete", {
|
||||
name: plainName(meta, targetId),
|
||||
name:
|
||||
asString(meta.roleName) ??
|
||||
asString(meta.name) ??
|
||||
`#${targetId ?? "?"}`,
|
||||
});
|
||||
case "role.permissions.replace": {
|
||||
const added = Array.isArray(meta.added) ? meta.added.length : 0;
|
||||
@@ -5965,6 +5968,22 @@ function formatAuditSummary(
|
||||
}
|
||||
case "user.delete": {
|
||||
const username = asString(meta.username) ?? `#${targetId ?? "?"}`;
|
||||
const displayEn = asString(meta.displayNameEn);
|
||||
const displayAr = asString(meta.displayNameAr);
|
||||
const displayName = lang === "ar"
|
||||
? displayAr ?? displayEn
|
||||
: displayEn ?? displayAr;
|
||||
if (displayName) {
|
||||
return meta.force
|
||||
? t("admin.audit.summary.user.forceDeleteWithName", {
|
||||
username,
|
||||
displayName,
|
||||
})
|
||||
: t("admin.audit.summary.user.deleteWithName", {
|
||||
username,
|
||||
displayName,
|
||||
});
|
||||
}
|
||||
return meta.force
|
||||
? t("admin.audit.summary.user.forceDelete", { username })
|
||||
: t("admin.audit.summary.user.delete", { username });
|
||||
@@ -6025,11 +6044,17 @@ function forceDeletedEntityName(
|
||||
): string | null {
|
||||
if (!isForceDeleteEntry(entry)) return null;
|
||||
const meta = asRecord(entry.metadata);
|
||||
const en = asString(meta.nameEn) ?? asString(meta.displayNameEn);
|
||||
const ar = asString(meta.nameAr) ?? asString(meta.displayNameAr);
|
||||
const en =
|
||||
asString(meta.nameEn) ??
|
||||
asString(meta.appNameEn) ??
|
||||
asString(meta.displayNameEn);
|
||||
const ar =
|
||||
asString(meta.nameAr) ??
|
||||
asString(meta.appNameAr) ??
|
||||
asString(meta.displayNameAr);
|
||||
const localized = lang === "ar" ? ar ?? en : en ?? ar;
|
||||
if (localized) return localized;
|
||||
const plain = asString(meta.name);
|
||||
const plain = asString(meta.name) ?? asString(meta.appSlug);
|
||||
if (plain) return plain;
|
||||
const username = asString(meta.username);
|
||||
if (username) return `@${username}`;
|
||||
|
||||
Reference in New Issue
Block a user