import { formatAuditValue, parseAuditDetails, resolveAuditActor, type AuditDetails, type CommonAuditLog, } from "../../../../common/core/audit"; import { t } from "../../lib/i18n"; import type { ClientSummary, DevAuditLog } from "../../lib/devApi"; export type RecentClientChange = { eventId: string; clientId: string; clientName: string; actorId: string; action: string; actionLabel: string; timestamp: string; detailLabels: Array<{ label: string; value: string }>; }; const recentClientActions = new Set([ "CREATE_CLIENT", "UPDATE_CLIENT", "UPDATE_CLIENT_STATUS", "ROTATE_SECRET", "ADD_RELATION", "REMOVE_RELATION", "DELETE_CLIENT", ]); function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } export function getRecentClientActionLabel(action: string) { switch (action) { case "CREATE_CLIENT": return t("ui.dev.clients.recent_changes.guide.create", "앱 생성"); case "UPDATE_CLIENT": return t("ui.dev.clients.recent_changes.guide.settings", "설정 변경"); case "UPDATE_CLIENT_STATUS": return t("ui.dev.clients.recent_changes.guide.status", "상태 변경"); case "ROTATE_SECRET": return t( "ui.dev.clients.recent_changes.guide.secret", "클라이언트 시크릿 재발급", ); case "ADD_RELATION": return t("ui.dev.clients.relationships.add_title", "관계 추가"); case "REMOVE_RELATION": return t("ui.common.remove", "Remove"); case "DELETE_CLIENT": return t("ui.dev.clients.recent_changes.guide.delete", "앱 삭제"); default: return action; } } function getRecentClientFieldLabel(key: string) { switch (key) { case "name": return t("ui.dev.clients.table.application", "Application"); case "type": return t("ui.dev.clients.table.type", "Type"); case "status": return t("ui.dev.clients.table.status", "Status"); case "relation": return t("ui.dev.clients.relationships.relation", "관계"); case "subject": return t("ui.dev.clients.relationships.subject", "대상"); case "client_secret": return t( "ui.dev.clients.details.credentials.client_secret", "클라이언트 시크릿", ); default: return key; } } export function buildRecentClientChangeDetails( action: string, details: AuditDetails, ) { const before = isRecord(details.before) ? details.before : {}; const after = isRecord(details.after) ? details.after : {}; if (action === "ROTATE_SECRET") { return [ { label: getRecentClientFieldLabel("client_secret"), value: t("msg.dev.clients.details.secret_rotated", "재발급"), }, ]; } if (action === "ADD_RELATION" || action === "REMOVE_RELATION") { const source = action === "ADD_RELATION" ? after : before; return [ ...(source.relation ? [ { label: getRecentClientFieldLabel("relation"), value: formatAuditValue(source.relation), }, ] : []), ...(source.subject ? [ { label: getRecentClientFieldLabel("subject"), value: formatAuditValue(source.subject), }, ] : []), ]; } const keys = Array.from( new Set([...Object.keys(before), ...Object.keys(after)]), ); const changes = keys .map((key) => { const beforeValue = before[key]; const afterValue = after[key]; if (action !== "CREATE_CLIENT" && action !== "DELETE_CLIENT") { if (JSON.stringify(beforeValue) === JSON.stringify(afterValue)) { return null; } } const label = getRecentClientFieldLabel(key); if (action === "CREATE_CLIENT") { if (afterValue === undefined) { return null; } return { label, value: formatAuditValue(afterValue) }; } if (action === "DELETE_CLIENT") { if (beforeValue === undefined) { return null; } return { label, value: formatAuditValue(beforeValue) }; } if (beforeValue === undefined && afterValue === undefined) { return null; } if (beforeValue === undefined) { return { label, value: formatAuditValue(afterValue) }; } if (afterValue === undefined) { return { label, value: formatAuditValue(beforeValue) }; } return { label, value: `${formatAuditValue(beforeValue)} → ${formatAuditValue(afterValue)}`, }; }) .filter((item): item is { label: string; value: string } => Boolean(item)); return changes.slice(0, 3); } export function buildRecentClientChanges( auditLogs: DevAuditLog[], clients: ClientSummary[], ) { const clientNameById = new Map( clients.map((client) => [client.id, client.name || client.id]), ); return auditLogs .map((item) => { const details = parseAuditDetails(item.details); const action = details.action || ""; const clientId = String(details.target_id || ""); if (!recentClientActions.has(action) || !clientId) { return null; } return { eventId: item.event_id, clientId, clientName: clientNameById.get(clientId) || clientId, actorId: resolveAuditActor( item as Pick, details, ), action, actionLabel: getRecentClientActionLabel(action), timestamp: item.timestamp, detailLabels: buildRecentClientChangeDetails(action, details), } satisfies RecentClientChange; }) .filter((item): item is RecentClientChange => Boolean(item)) .sort( (left, right) => new Date(right.timestamp).getTime() - new Date(left.timestamp).getTime(), ); }