import { useInfiniteQuery } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { ChevronDown, ChevronUp, Copy, ListChecks, RefreshCw, Search, Terminal, } from "lucide-react"; import * as React from "react"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "../../components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../../components/ui/table"; import type { AuditLog } from "../../lib/adminApi"; import { fetchAuditLogs } from "../../lib/adminApi"; const defaultAuditFilters = [ "method:POST path:/api/v1/*", "status:failure", "latency_ms:>1000", ]; type AuditDetails = { request_id?: string; method?: string; path?: string; status?: number; latency_ms?: number; error?: string; tenant_id?: string; actor_id?: string; action?: string; target?: string; before?: unknown; after?: unknown; }; function parseDetails(details?: string): AuditDetails { if (!details) { return {}; } try { const parsed = JSON.parse(details); if (parsed && typeof parsed === "object") { return parsed as AuditDetails; } } catch {} return {}; } function formatCellValue(value: unknown) { if (value === null || value === undefined || value === "") { return "-"; } if (typeof value === "string") { return value; } try { return JSON.stringify(value); } catch { return String(value); } } function formatIsoDateTime(value: string) { if (!value) { return { date: "-", time: "-" }; } const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) { return { date: value, time: "-" }; } const date = parsed.toISOString().slice(0, 10); const time = parsed.toLocaleTimeString("ko-KR", { hour12: false }); return { date, time }; } function AuditLogsPage() { const [filters, setFilters] = React.useState(defaultAuditFilters); const [filterDraft, setFilterDraft] = React.useState(""); const [expandedRows, setExpandedRows] = React.useState< Record >({}); const handleCopy = (value: string) => { if (!value) { return; } navigator.clipboard.writeText(value); }; const { data, isLoading, error, fetchNextPage, hasNextPage, isFetchingNextPage, isFetching, refetch, } = useInfiniteQuery({ queryKey: ["audit-logs"], queryFn: ({ pageParam }) => fetchAuditLogs(50, pageParam), initialPageParam: undefined as string | undefined, getNextPageParam: (lastPage) => lastPage.next_cursor || undefined, }); const logs = data?.pages?.flatMap( (page) => page?.items?.filter((item): item is AuditLog => Boolean(item), ) ?? [], ) ?? []; const handleAddFilter = () => { const trimmed = filterDraft.trim(); if (!trimmed) { return; } setFilters((prev) => prev.includes(trimmed) ? prev : [...prev, trimmed], ); setFilterDraft(""); }; if (isLoading) { return
Loading audit logs...
; } if (error) { const errMsg = (error as AxiosError<{ error?: string }>).response?.data?.error ?? (error as Error).message; return (
Error loading logs: {errMsg}
); } return (
Audit / Logs

감사 로그

Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다.

Audit registry 로드된 로그 {logs.length}건
Command only
setFilterDraft(event.target.value) } onKeyDown={(event) => { if (event.key === "Enter") { handleAddFilter(); } }} placeholder="필터 추가 (예: status:failure)" className="w-full bg-transparent text-sm text-foreground outline-none" />
{filters.length === 0 ? ( 필터 없음 ) : ( filters.map((filter) => ( {filter} )) )}
TIME ACTOR (ID) REQUEST PATH STATUS Action / Target {isLoading && ( 로딩 중... )} {!isLoading && logs.length === 0 && ( 아직 수집된 감사 로그가 없습니다. )} {logs.map((row, index) => { const details = parseDetails(row.details); const actionLabel = details.action || (details.method && details.path ? `${details.method} ${details.path}` : row.event_type); const rowKey = `${row.event_id}-${row.timestamp}-${index}`; const isExpanded = Boolean( expandedRows[rowKey], ); return ( {(() => { const { date, time } = formatIsoDateTime( row.timestamp, ); return (
{date}
{time}
); })()}
{row.user_id || details.actor_id || "-"} {(row.user_id || details.actor_id) && ( )}
{formatCellValue( details.request_id, )} {details.request_id && ( )}
{formatCellValue( details.method, )}
{formatCellValue( details.path, )}
{row.status}
{actionLabel}
{details.target && (
Target ·{" "} {details.target}
)}
{isExpanded && (
Request
Request ID ·{" "} {formatCellValue( details.request_id, )}
Event ID ·{" "} {formatCellValue( row.event_id, )}
IP ·{" "} {formatCellValue( row.ip_address, )}
Latency ·{" "} {details.latency_ms !== undefined ? `${details.latency_ms}ms` : "-"}
Actor
Actor ID ·{" "} {row.user_id || details.actor_id || "-"}
Tenant ·{" "} {formatCellValue( details.tenant_id, )}
Device ·{" "} {formatCellValue( row.device_id, )}
Result
Error ·{" "} {formatCellValue( details.error, )}
Before ·{" "} {formatCellValue( details.before, )}
After ·{" "} {formatCellValue( details.after, )}
)}
); })}
{hasNextPage ? ( ) : ( End of audit feed )}
); } export default AuditLogsPage;