import type { AdminOperationAuditItem, AdminOperationAuditResponse, PrivateContentAccessAuditItem, PrivateContentAccessAuditResponse, } from "@dada/shared-contracts"; import { useCallback, useEffect, useState } from "react"; import "./admin-audit.css"; type AuditTab = "operations" | "private-content"; interface AuditPageState { failed: boolean; generatedAt: string | null; items: Item[]; loading: boolean; nextCursor: string | null; } const emptyState = (): AuditPageState => ({ failed: false, generatedAt: null, items: [], loading: false, nextCursor: null, }); function formatTime(value: string) { return new Intl.DateTimeFormat("zh-CN", { day: "2-digit", hour: "2-digit", minute: "2-digit", month: "2-digit", second: "2-digit", }).format(new Date(value)); } function operationSummary(item: AdminOperationAuditItem) { if (item.after_summary) return item.after_summary; if (item.before_summary) return item.before_summary; return "无变更摘要"; } export function AdminAuditPage() { const [tab, setTab] = useState("operations"); const [operations, setOperations] = useState>(emptyState); const [privateAccess, setPrivateAccess] = useState>(emptyState); const loadOperations = useCallback(async (cursor?: string, append = false) => { setOperations((current) => ({ ...current, failed: false, loading: true })); try { const query = new URLSearchParams({ limit: "50" }); if (cursor) query.set("cursor", cursor); const response = await fetch(`/api/v1/admin/audit/operations?${query}`, { credentials: "same-origin" }); if (response.status === 401) { window.dispatchEvent(new Event("dada:session-invalid")); return; } if (!response.ok) throw new Error("admin_operation_audit_unavailable"); const body = await response.json() as AdminOperationAuditResponse; setOperations((current) => ({ failed: false, generatedAt: body.generated_at, items: append ? [...current.items, ...body.items] : body.items, loading: false, nextCursor: body.next_cursor, })); } catch { setOperations((current) => ({ ...current, failed: true, loading: false })); } }, []); const loadPrivateAccess = useCallback(async (cursor?: string, append = false) => { setPrivateAccess((current) => ({ ...current, failed: false, loading: true })); try { const query = new URLSearchParams({ limit: "50" }); if (cursor) query.set("cursor", cursor); const response = await fetch(`/api/v1/admin/audit/private-content?${query}`, { credentials: "same-origin" }); if (response.status === 401) { window.dispatchEvent(new Event("dada:session-invalid")); return; } if (!response.ok) throw new Error("private_content_audit_unavailable"); const body = await response.json() as PrivateContentAccessAuditResponse; setPrivateAccess((current) => ({ failed: false, generatedAt: body.generated_at, items: append ? [...current.items, ...body.items] : body.items, loading: false, nextCursor: body.next_cursor, })); } catch { setPrivateAccess((current) => ({ ...current, failed: true, loading: false })); } }, []); useEffect(() => { void loadOperations(); }, [loadOperations]); function selectTab(next: AuditTab) { setTab(next); if (next === "private-content" && !privateAccess.generatedAt && !privateAccess.loading) void loadPrivateAccess(); } const state = tab === "operations" ? operations : privateAccess; const reload = tab === "operations" ? loadOperations : loadPrivateAccess; return (

IMMUTABLE / 180 DAYS

审计

{state.generatedAt ? : null}
{state.failed ? (
审计记录暂时无法读取{state.generatedAt ? ",已保留上次结果" : ""}。
) : null} {tab === "operations" ? (
{operations.loading && operations.items.length === 0 ?

正在读取后台操作审计

: null} {!operations.loading && !operations.failed && operations.items.length === 0 ?

当前没有后台操作审计记录。

: null} {operations.items.length > 0 ? (
{operations.items.map((item) => ( ))}
时间管理员操作类型对象安全摘要结果Operation ID
{item.actor_ref}{item.actor_type} {item.operation_type} {item.target_type}:{item.target_ref}{operationSummary(item)} {item.result} {item.log_id}
) : null}
) : (
{privateAccess.loading && privateAccess.items.length === 0 ?

正在读取私有内容访问审计

: null} {!privateAccess.loading && !privateAccess.failed && privateAccess.items.length === 0 ?

当前没有私有内容访问审计记录。

: null} {privateAccess.items.length > 0 ? (
{privateAccess.items.map((item) => ( ))}
时间管理员安全目标标识内容类型到期时间Access ID
{item.actor_ref} {item.target_ref} {item.content_type} {item.log_id}
) : null}
)} {state.nextCursor ? (
) : null}

记录保留 180 天。此页面不提供编辑、删除或清空能力。

); }