From eb7d15ca7eed8e3ee1a129c83f0fd378a47d5ca7 Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Thu, 30 Apr 2026 20:38:02 +0000 Subject: [PATCH] Show readable names in audit log for top-level deletions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Replit-Task-Id: a25d35dd-5005-4e57-96a5-580016f35e46 --- artifacts/api-server/src/routes/apps.ts | 6 +-- artifacts/api-server/src/routes/roles.ts | 2 +- artifacts/api-server/src/routes/users.ts | 2 + .../tests/audit-log-coverage.test.mjs | 8 +++- artifacts/tx-os/src/locales/ar.json | 4 +- artifacts/tx-os/src/locales/en.json | 4 +- artifacts/tx-os/src/pages/admin.tsx | 39 +++++++++++++++---- 7 files changed, 50 insertions(+), 15 deletions(-) diff --git a/artifacts/api-server/src/routes/apps.ts b/artifacts/api-server/src/routes/apps.ts index b5ce451c..02ddc1da 100644 --- a/artifacts/api-server/src/routes/apps.ts +++ b/artifacts/api-server/src/routes/apps.ts @@ -914,9 +914,9 @@ router.delete("/apps/:id", requireAdmin, async (req, res): Promise => { 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, diff --git a/artifacts/api-server/src/routes/roles.ts b/artifacts/api-server/src/routes/roles.ts index b169939b..4992cdc1 100644 --- a/artifacts/api-server/src/routes/roles.ts +++ b/artifacts/api-server/src/routes/roles.ts @@ -414,7 +414,7 @@ router.delete("/roles/:id", requireAdmin, async (req, res): Promise => { targetType: "role", targetId: id, metadata: { - name: existing.name, + roleName: existing.name, descriptionAr: existing.descriptionAr, descriptionEn: existing.descriptionEn, }, diff --git a/artifacts/api-server/src/routes/users.ts b/artifacts/api-server/src/routes/users.ts index 6d50a1b6..3d399da9 100644 --- a/artifacts/api-server/src/routes/users.ts +++ b/artifacts/api-server/src/routes/users.ts @@ -720,6 +720,8 @@ router.delete("/users/:id", requireAdmin, async (req, res): Promise => { targetId: userId, metadata: { username: existing.username, + displayNameEn: existing.displayNameEn, + displayNameAr: existing.displayNameAr, email: existing.email, force, ...(hasDeps diff --git a/artifacts/api-server/tests/audit-log-coverage.test.mjs b/artifacts/api-server/tests/audit-log-coverage.test.mjs index c62b9620..9fd42a7b 100644 --- a/artifacts/api-server/tests/audit-log-coverage.test.mjs +++ b/artifacts/api-server/tests/audit-log-coverage.test.mjs @@ -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); diff --git a/artifacts/tx-os/src/locales/ar.json b/artifacts/tx-os/src/locales/ar.json index 2e746f10..4bbca0eb 100644 --- a/artifacts/tx-os/src/locales/ar.json +++ b/artifacts/tx-os/src/locales/ar.json @@ -814,7 +814,9 @@ }, "user": { "delete": "تم حذف المستخدم @{{username}}", - "forceDelete": "تم حذف المستخدم @{{username}} قسراً" + "deleteWithName": "تم حذف المستخدم @{{username}} ({{displayName}})", + "forceDelete": "تم حذف المستخدم @{{username}} قسراً", + "forceDeleteWithName": "تم حذف المستخدم @{{username}} ({{displayName}}) قسراً" } } }, diff --git a/artifacts/tx-os/src/locales/en.json b/artifacts/tx-os/src/locales/en.json index bd72a67f..a9249aee 100644 --- a/artifacts/tx-os/src/locales/en.json +++ b/artifacts/tx-os/src/locales/en.json @@ -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}})" } } }, diff --git a/artifacts/tx-os/src/pages/admin.tsx b/artifacts/tx-os/src/pages/admin.tsx index 4c44bc47..edb094f3 100644 --- a/artifacts/tx-os/src/pages/admin.tsx +++ b/artifacts/tx-os/src/pages/admin.tsx @@ -5693,9 +5693,9 @@ function unitLabel(t: AuditTFunction, kind: string, count: number): string { } function appName(meta: Record, 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}`;