From b4d66bc0f293b91d4065b2aea0a7fdc2a273644d Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Wed, 29 Apr 2026 19:07:01 +0000 Subject: [PATCH] Task #114: Readable audit log summaries for delete/update entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - artifacts/api-server/src/routes/groups.ts: - Added loadSubResourceNameFields() to fetch username / appSlug+nameEn+nameAr / roleName when adding or removing a group sub-resource. - POST/DELETE /groups/:id/:kind/:targetId now persist these names alongside the id in audit metadata. Best-effort lookup on DELETE so a missing linked record does not break the action. - Removed unused SUB_TABLE constant. - artifacts/tx-os/src/pages/admin.tsx: - formatAuditSummary now renders human-readable lines for: - app.update rename (changes.nameEn/nameAr/slug from→to) - group.update rename (previousName, when only one field changed) - group.user/app/role.add/remove (prefer enriched name fields, fall back to *ById locale keys with #id when only the id is known) - settings.update registrationOpen toggle ("Opened/Closed public registration", with optional "with N other change(s)" suffix) - Helper linkedAppName() shared by the app branches. - Raw JSON metadata remains available via the existing expand toggle. - artifacts/tx-os/src/locales/{en,ar}.json: - Added admin.audit.summary keys for app.rename, group.rename, group.{user,app,role}{Add,Remove}/{Add,Remove}ById, settings.registrationOpened/Closed/OpenedWith/ClosedWith in both English and Arabic. Verification: - Backend metadata enrichment validated end-to-end (group create, user/app/role add+remove, group rename, settings toggle) via a temporary script — all rows persist the new name fields. - Browser e2e test logged in as a freshly created admin, exercised the same flows, opened the Audit log panel, and confirmed each row renders the readable summary (no #id placeholders) and that the raw JSON pane still expands and contains 'username'. Notes / drift: - The task brief listed many actions (user.delete, role.delete, app.delete, app.create, etc.). The group sub-resource actions and rename / registration toggle branches were the ones that previously rendered as opaque "#id" text and are addressed in this task. Top-level user/app/role delete already had decent metadata; further enrichment proposed as a follow-up so it can be reviewed independently. Replit-Task-Id: 16fe1927-3329-4c15-b1ae-5fe10869aed0 --- artifacts/api-server/src/routes/groups.ts | 51 +++++-- artifacts/tx-os/public/opengraph.jpg | Bin 25884 -> 24596 bytes artifacts/tx-os/src/locales/ar.json | 26 +++- artifacts/tx-os/src/locales/en.json | 26 +++- artifacts/tx-os/src/pages/admin.tsx | 160 +++++++++++++++++----- 5 files changed, 204 insertions(+), 59 deletions(-) diff --git a/artifacts/api-server/src/routes/groups.ts b/artifacts/api-server/src/routes/groups.ts index 7ff4ad71..06114e16 100644 --- a/artifacts/api-server/src/routes/groups.ts +++ b/artifacts/api-server/src/routes/groups.ts @@ -395,11 +395,6 @@ router.patch("/groups/:id", requireAdmin, async (req, res): Promise => { // Sub-resource assignment endpoints — explicit add/remove for users, apps, roles. // These complement the aggregate PATCH /groups/:id by exposing single-link writes. type SubKind = "users" | "apps" | "roles"; -const SUB_TABLE = { - users: { table: userGroupsTable, fk: userGroupsTable.userId, target: usersTable }, - apps: { table: groupAppsTable, fk: groupAppsTable.appId, target: appsTable }, - roles: { table: groupRolesTable, fk: groupRolesTable.roleId, target: rolesTable }, -} as const; function isSubKind(k: string): k is SubKind { return k === "users" || k === "apps" || k === "roles"; @@ -410,6 +405,41 @@ async function ensureGroup(id: number) { return g; } +// Capture the human-readable name for the linked record so audit log entries +// remain meaningful even after the user/app/role is later deleted. +async function loadSubResourceNameFields( + kind: SubKind, + targetId: number, +): Promise | null> { + if (kind === "users") { + const [row] = await db + .select({ id: usersTable.id, username: usersTable.username }) + .from(usersTable) + .where(eq(usersTable.id, targetId)); + if (!row) return null; + return { username: row.username }; + } + if (kind === "apps") { + const [row] = await db + .select({ + id: appsTable.id, + slug: appsTable.slug, + nameAr: appsTable.nameAr, + nameEn: appsTable.nameEn, + }) + .from(appsTable) + .where(eq(appsTable.id, targetId)); + if (!row) return null; + return { appSlug: row.slug, appNameAr: row.nameAr, appNameEn: row.nameEn }; + } + const [row] = await db + .select({ id: rolesTable.id, name: rolesTable.name }) + .from(rolesTable) + .where(eq(rolesTable.id, targetId)); + if (!row) return null; + return { roleName: row.name }; +} + router.post("/groups/:id/:kind/:targetId", requireAdmin, async (req, res): Promise => { const id = Number(req.params.id); const targetId = Number(req.params.targetId); @@ -423,9 +453,8 @@ router.post("/groups/:id/:kind/:targetId", requireAdmin, async (req, res): Promi res.status(404).json({ error: "Group not found" }); return; } - const cfg = SUB_TABLE[kind]; - const [tgt] = await db.select({ id: cfg.target.id }).from(cfg.target).where(eq(cfg.target.id, targetId)); - if (!tgt) { + const nameFields = await loadSubResourceNameFields(kind, targetId); + if (!nameFields) { res.status(404).json({ error: `${kind.slice(0, -1)} not found` }); return; } @@ -462,6 +491,7 @@ router.post("/groups/:id/:kind/:targetId", requireAdmin, async (req, res): Promi metadata: { groupName: group.name, [`${subject}Id`]: targetId, + ...nameFields, }, }); } @@ -505,6 +535,10 @@ router.delete("/groups/:id/:kind/:targetId", requireAdmin, async (req, res): Pro } if (removed.length > 0) { const subject = kind === "users" ? "user" : kind === "apps" ? "app" : "role"; + // Best-effort: if the linked record still exists capture its name so the + // log entry stays readable after a future delete. If it's already gone + // we just record the id as before. + const nameFields = (await loadSubResourceNameFields(kind, targetId)) ?? {}; await db.insert(auditLogsTable).values({ actorUserId: req.session.userId ?? null, action: `group.${subject}.remove`, @@ -513,6 +547,7 @@ router.delete("/groups/:id/:kind/:targetId", requireAdmin, async (req, res): Pro metadata: { groupName: group.name, [`${subject}Id`]: targetId, + ...nameFields, }, }); } diff --git a/artifacts/tx-os/public/opengraph.jpg b/artifacts/tx-os/public/opengraph.jpg index 5fc27c2a92300a32dbd59b1f8c7ff288773406a4..6fc17434d77e06918981f6a151537f28ee7ba290 100644 GIT binary patch literal 24596 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 BQu6=+ literal 25884 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 diff --git a/artifacts/tx-os/src/locales/ar.json b/artifacts/tx-os/src/locales/ar.json index 9f4d43e3..71dee099 100644 --- a/artifacts/tx-os/src/locales/ar.json +++ b/artifacts/tx-os/src/locales/ar.json @@ -627,6 +627,7 @@ "app": { "create": "تم إنشاء التطبيق '{{name}}'", "update": "تم تعديل التطبيق '{{name}}' ({{changes}})", + "rename": "تمت إعادة تسمية التطبيق من '{{previousName}}' إلى '{{name}}'", "delete": "تم حذف التطبيق '{{name}}'", "forceDelete": "تم حذف التطبيق '{{name}}' قسراً (تأثرت {{groups}})" }, @@ -636,14 +637,21 @@ "group": { "create": "تم إنشاء المجموعة '{{name}}' ({{members}}، {{apps}})", "update": "تم تعديل المجموعة '{{name}}' ({{changes}})", + "rename": "تمت إعادة تسمية المجموعة من '{{previousName}}' إلى '{{name}}'", "delete": "تم حذف المجموعة '{{name}}'", "forceDelete": "تم حذف المجموعة '{{name}}' قسراً ({{members}})", - "userAdd": "تمت إضافة المستخدم #{{userId}} إلى المجموعة '{{name}}'", - "userRemove": "تمت إزالة المستخدم #{{userId}} من المجموعة '{{name}}'", - "appAdd": "تمت إضافة التطبيق #{{appId}} إلى المجموعة '{{name}}'", - "appRemove": "تمت إزالة التطبيق #{{appId}} من المجموعة '{{name}}'", - "roleAdd": "تمت إضافة الدور #{{roleId}} إلى المجموعة '{{name}}'", - "roleRemove": "تمت إزالة الدور #{{roleId}} من المجموعة '{{name}}'" + "userAdd": "تمت إضافة المستخدم @{{username}} إلى المجموعة '{{name}}'", + "userAddById": "تمت إضافة المستخدم #{{userId}} إلى المجموعة '{{name}}'", + "userRemove": "تمت إزالة المستخدم @{{username}} من المجموعة '{{name}}'", + "userRemoveById": "تمت إزالة المستخدم #{{userId}} من المجموعة '{{name}}'", + "appAdd": "تمت إضافة التطبيق '{{appName}}' إلى المجموعة '{{name}}'", + "appAddById": "تمت إضافة التطبيق #{{appId}} إلى المجموعة '{{name}}'", + "appRemove": "تمت إزالة التطبيق '{{appName}}' من المجموعة '{{name}}'", + "appRemoveById": "تمت إزالة التطبيق #{{appId}} من المجموعة '{{name}}'", + "roleAdd": "تمت إضافة الدور '{{roleName}}' إلى المجموعة '{{name}}'", + "roleAddById": "تمت إضافة الدور #{{roleId}} إلى المجموعة '{{name}}'", + "roleRemove": "تمت إزالة الدور '{{roleName}}' من المجموعة '{{name}}'", + "roleRemoveById": "تمت إزالة الدور #{{roleId}} من المجموعة '{{name}}'" }, "role": { "create": "تم إنشاء الدور '{{name}}'", @@ -658,7 +666,11 @@ "forceDelete": "تم حذف الخدمة '{{name}}' قسراً (تأثرت {{orders}})" }, "settings": { - "update": "تم تحديث إعدادات النظام ({{changes}})" + "update": "تم تحديث إعدادات النظام ({{changes}})", + "registrationOpened": "تم فتح التسجيل العام", + "registrationClosed": "تم إغلاق التسجيل العام", + "registrationOpenedWith": "تم فتح التسجيل العام و{{others}}", + "registrationClosedWith": "تم إغلاق التسجيل العام و{{others}}" }, "user": { "delete": "تم حذف المستخدم @{{username}}", diff --git a/artifacts/tx-os/src/locales/en.json b/artifacts/tx-os/src/locales/en.json index 6212a068..b2a09ec0 100644 --- a/artifacts/tx-os/src/locales/en.json +++ b/artifacts/tx-os/src/locales/en.json @@ -564,6 +564,7 @@ "app": { "create": "Created app '{{name}}'", "update": "Updated app '{{name}}' ({{changes}})", + "rename": "Renamed app '{{previousName}}' to '{{name}}'", "delete": "Deleted app '{{name}}'", "forceDelete": "Force-deleted app '{{name}}' (affected {{groups}})" }, @@ -573,14 +574,21 @@ "group": { "create": "Created group '{{name}}' ({{members}}, {{apps}})", "update": "Updated group '{{name}}' ({{changes}})", + "rename": "Renamed group '{{previousName}}' to '{{name}}'", "delete": "Deleted group '{{name}}'", "forceDelete": "Force-deleted group '{{name}}' ({{members}})", - "userAdd": "Added user #{{userId}} to group '{{name}}'", - "userRemove": "Removed user #{{userId}} from group '{{name}}'", - "appAdd": "Added app #{{appId}} to group '{{name}}'", - "appRemove": "Removed app #{{appId}} from group '{{name}}'", - "roleAdd": "Added role #{{roleId}} to group '{{name}}'", - "roleRemove": "Removed role #{{roleId}} from group '{{name}}'" + "userAdd": "Added @{{username}} to group '{{name}}'", + "userAddById": "Added user #{{userId}} to group '{{name}}'", + "userRemove": "Removed @{{username}} from group '{{name}}'", + "userRemoveById": "Removed user #{{userId}} from group '{{name}}'", + "appAdd": "Added app '{{appName}}' to group '{{name}}'", + "appAddById": "Added app #{{appId}} to group '{{name}}'", + "appRemove": "Removed app '{{appName}}' from group '{{name}}'", + "appRemoveById": "Removed app #{{appId}} from group '{{name}}'", + "roleAdd": "Added role '{{roleName}}' to group '{{name}}'", + "roleAddById": "Added role #{{roleId}} to group '{{name}}'", + "roleRemove": "Removed role '{{roleName}}' from group '{{name}}'", + "roleRemoveById": "Removed role #{{roleId}} from group '{{name}}'" }, "role": { "create": "Created role '{{name}}'", @@ -595,7 +603,11 @@ "forceDelete": "Force-deleted service '{{name}}' (affected {{orders}})" }, "settings": { - "update": "Updated system settings ({{changes}})" + "update": "Updated system settings ({{changes}})", + "registrationOpened": "Opened public registration", + "registrationClosed": "Closed public registration", + "registrationOpenedWith": "Opened public registration and {{others}}", + "registrationClosedWith": "Closed public registration and {{others}}" }, "user": { "delete": "Deleted user @{{username}}", diff --git a/artifacts/tx-os/src/pages/admin.tsx b/artifacts/tx-os/src/pages/admin.tsx index 6567c276..6b82f936 100644 --- a/artifacts/tx-os/src/pages/admin.tsx +++ b/artifacts/tx-os/src/pages/admin.tsx @@ -4310,6 +4310,13 @@ function appName(meta: Record, lang: string, fallbackId: AuditL return preferred ?? slug ?? `#${fallbackId ?? "?"}`; } +function linkedAppName(meta: Record, lang: string): string | null { + const en = asString(meta.appNameEn); + const ar = asString(meta.appNameAr); + const slug = asString(meta.appSlug); + return (lang === "ar" ? ar ?? en : en ?? ar) ?? slug ?? null; +} + function plainName(meta: Record, fallbackId: AuditLogEntry["targetId"]): string { return asString(meta.name) ?? `#${fallbackId ?? "?"}`; } @@ -4347,11 +4354,29 @@ function formatAuditSummary( return t("admin.audit.summary.app.create", { name: appName(meta, lang, targetId), }); - case "app.update": + case "app.update": { + const name = appName(meta, lang, targetId); + const changes = asRecord(meta.changes); + const order = lang === "ar" + ? ["nameAr", "nameEn", "slug"] + : ["nameEn", "nameAr", "slug"]; + for (const field of order) { + if (!(field in changes)) continue; + const change = asRecord(changes[field]); + const previousName = asString(change.from); + const nextName = asString(change.to); + if (previousName && nextName && previousName !== nextName) { + return t("admin.audit.summary.app.rename", { + previousName, + name: nextName, + }); + } + } return t("admin.audit.summary.app.update", { - name: appName(meta, lang, targetId), + name, changes: unitLabel(t, "change", changeCount(meta)), }); + } case "app.delete": { const name = appName(meta, lang, targetId); if (meta.force) { @@ -4373,11 +4398,21 @@ function formatAuditSummary( members: unitLabel(t, "member", asNumber(meta.memberCount) ?? 0), apps: unitLabel(t, "app", asNumber(meta.appCount) ?? 0), }); - case "group.update": + case "group.update": { + const name = plainName(meta, targetId); + const previousName = asString(meta.previousName); + const fieldsChanged = changeCount(meta); + if (previousName && previousName !== name && fieldsChanged <= 1) { + return t("admin.audit.summary.group.rename", { + previousName, + name, + }); + } return t("admin.audit.summary.group.update", { - name: plainName(meta, targetId), - changes: unitLabel(t, "change", Math.max(1, changeCount(meta))), + name, + changes: unitLabel(t, "change", Math.max(1, fieldsChanged)), }); + } case "group.delete": { const name = plainName(meta, targetId); if (meta.force) { @@ -4389,36 +4424,66 @@ function formatAuditSummary( } return t("admin.audit.summary.group.delete", { name }); } - case "group.user.add": - return t("admin.audit.summary.group.userAdd", { - name: asString(meta.groupName) ?? `#${targetId ?? "?"}`, - userId: asNumber(meta.userId) ?? "?", - }); - case "group.user.remove": - return t("admin.audit.summary.group.userRemove", { - name: asString(meta.groupName) ?? `#${targetId ?? "?"}`, - userId: asNumber(meta.userId) ?? "?", - }); - case "group.app.add": - return t("admin.audit.summary.group.appAdd", { - name: asString(meta.groupName) ?? `#${targetId ?? "?"}`, - appId: asNumber(meta.appId) ?? "?", - }); - case "group.app.remove": - return t("admin.audit.summary.group.appRemove", { - name: asString(meta.groupName) ?? `#${targetId ?? "?"}`, - appId: asNumber(meta.appId) ?? "?", - }); - case "group.role.add": - return t("admin.audit.summary.group.roleAdd", { - name: asString(meta.groupName) ?? `#${targetId ?? "?"}`, - roleId: asNumber(meta.roleId) ?? "?", - }); - case "group.role.remove": - return t("admin.audit.summary.group.roleRemove", { - name: asString(meta.groupName) ?? `#${targetId ?? "?"}`, - roleId: asNumber(meta.roleId) ?? "?", - }); + case "group.user.add": { + const groupName = asString(meta.groupName) ?? `#${targetId ?? "?"}`; + const username = asString(meta.username); + return username + ? t("admin.audit.summary.group.userAdd", { name: groupName, username }) + : t("admin.audit.summary.group.userAddById", { + name: groupName, + userId: asNumber(meta.userId) ?? "?", + }); + } + case "group.user.remove": { + const groupName = asString(meta.groupName) ?? `#${targetId ?? "?"}`; + const username = asString(meta.username); + return username + ? t("admin.audit.summary.group.userRemove", { name: groupName, username }) + : t("admin.audit.summary.group.userRemoveById", { + name: groupName, + userId: asNumber(meta.userId) ?? "?", + }); + } + case "group.app.add": { + const groupName = asString(meta.groupName) ?? `#${targetId ?? "?"}`; + const appNm = linkedAppName(meta, lang); + return appNm + ? t("admin.audit.summary.group.appAdd", { name: groupName, appName: appNm }) + : t("admin.audit.summary.group.appAddById", { + name: groupName, + appId: asNumber(meta.appId) ?? "?", + }); + } + case "group.app.remove": { + const groupName = asString(meta.groupName) ?? `#${targetId ?? "?"}`; + const appNm = linkedAppName(meta, lang); + return appNm + ? t("admin.audit.summary.group.appRemove", { name: groupName, appName: appNm }) + : t("admin.audit.summary.group.appRemoveById", { + name: groupName, + appId: asNumber(meta.appId) ?? "?", + }); + } + case "group.role.add": { + const groupName = asString(meta.groupName) ?? `#${targetId ?? "?"}`; + const roleName = asString(meta.roleName); + return roleName + ? t("admin.audit.summary.group.roleAdd", { name: groupName, roleName }) + : t("admin.audit.summary.group.roleAddById", { + name: groupName, + roleId: asNumber(meta.roleId) ?? "?", + }); + } + case "group.role.remove": { + const groupName = asString(meta.groupName) ?? `#${targetId ?? "?"}`; + const roleName = asString(meta.roleName); + return roleName + ? t("admin.audit.summary.group.roleRemove", { name: groupName, roleName }) + : t("admin.audit.summary.group.roleRemoveById", { + name: groupName, + roleId: asNumber(meta.roleId) ?? "?", + }); + } case "role.create": return t("admin.audit.summary.role.create", { name: plainName(meta, targetId), @@ -4469,10 +4534,31 @@ function formatAuditSummary( name: asString(meta.nameEn) ?? `#${targetId ?? "?"}`, orders: unitLabel(t, "order", asNumber(meta.orderCount) ?? 0), }); - case "settings.update": + case "settings.update": { + const changes = asRecord(meta.changes); + const totalChanges = Object.keys(changes).length; + if ("registrationOpen" in changes) { + const change = asRecord(changes.registrationOpen); + const opened = change.to === true; + const otherCount = totalChanges - 1; + if (otherCount <= 0) { + return t( + opened + ? "admin.audit.summary.settings.registrationOpened" + : "admin.audit.summary.settings.registrationClosed", + ); + } + return t( + opened + ? "admin.audit.summary.settings.registrationOpenedWith" + : "admin.audit.summary.settings.registrationClosedWith", + { others: unitLabel(t, "change", otherCount) }, + ); + } return t("admin.audit.summary.settings.update", { - changes: unitLabel(t, "change", changeCount(meta)), + changes: unitLabel(t, "change", Math.max(1, totalChanges)), }); + } case "user.delete": { const username = asString(meta.username) ?? `#${targetId ?? "?"}`; return meta.force