import { useMutation, useQuery } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { Copy, Edit3, Key, Plus, RefreshCw, RotateCcw, Save, Trash2, } from "lucide-react"; import * as React from "react"; import { Link } from "react-router-dom"; import { PageHeader } from "../../../../common/core/components/page"; import { commonStickyTableHeaderClass } from "../../../../common/ui/table"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "../../components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../components/ui/dialog"; import { Input } from "../../components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../../components/ui/table"; import { type ApiKeySummary, deleteApiKey, fetchApiKeys, rotateApiKeySecret, updateApiKeyScopes, } from "../../lib/adminApi"; import { t } from "../../lib/i18n"; import { cn } from "../../lib/utils"; import { AVAILABLE_API_KEY_SCOPES } from "./apiKeyScopes"; function ApiKeyListPage() { const [editingKey, setEditingKey] = React.useState( null, ); const [draftScopes, setDraftScopes] = React.useState([]); const [rotatedSecret, setRotatedSecret] = React.useState<{ key: ApiKeySummary; clientSecret: string; } | null>(null); const query = useQuery({ queryKey: ["api-keys", { limit: 50, offset: 0 }], queryFn: () => fetchApiKeys(50, 0), }); const deleteMutation = useMutation({ mutationFn: (id: string) => deleteApiKey(id), onSuccess: () => { query.refetch(); }, }); const updateScopesMutation = useMutation({ mutationFn: ({ id, scopes }: { id: string; scopes: string[] }) => updateApiKeyScopes(id, { scopes }), onSuccess: () => { setEditingKey(null); setDraftScopes([]); query.refetch(); }, }); const rotateSecretMutation = useMutation({ mutationFn: (id: string) => rotateApiKeySecret(id), onSuccess: (data) => { setRotatedSecret({ key: data.apiKey, clientSecret: data.clientSecret, }); query.refetch(); }, }); const errorMsg = (query.error as AxiosError<{ error?: string }>)?.response ?.data?.error; const fallbackError = !errorMsg && query.isError ? t( "msg.admin.api_keys.list.fetch_error", "API 키 목록 조회에 실패했습니다.", ) : null; const items = query.data?.items ?? []; const handleDelete = (id: string, name: string) => { if ( !window.confirm( t( "msg.admin.api_keys.list.delete_confirm", 'API 키 "{{name}}"를 삭제할까요?', { name }, ), ) ) { return; } deleteMutation.mutate(id); }; const openScopeEditor = (key: ApiKeySummary) => { setEditingKey(key); setDraftScopes(key.scopes); }; const toggleDraftScope = (scopeId: string) => { setDraftScopes((current) => current.includes(scopeId) ? current.filter((scope) => scope !== scopeId) : [...current, scopeId], ); }; const saveScopes = () => { if (!editingKey || draftScopes.length === 0) return; updateScopesMutation.mutate({ id: editingKey.id, scopes: draftScopes }); }; const handleRotateSecret = (key: ApiKeySummary) => { if ( !window.confirm( t( "msg.admin.api_keys.list.rotate_confirm", 'API 키 "{{name}}"의 Secret을 재발급할까요? 기존 Secret은 더 이상 사용할 수 없습니다.', { name: key.name }, ), ) ) { return; } rotateSecretMutation.mutate(key.id); }; const copyRotatedSecret = () => { if (!rotatedSecret) return; navigator.clipboard.writeText(rotatedSecret.clientSecret); }; return (
} />
{t("ui.admin.apikeys.registry.title", "API Key Registry")} {t( "msg.admin.apikeys.registry.count", "총 {{count}}개의 활성 키가 등록되어 있습니다.", { count: query.data?.items?.length ?? 0 }, )}
{(errorMsg || fallbackError) && (
{errorMsg ?? fallbackError}
)}
{t("ui.admin.api_keys.list.table.name", "NAME")} {t("ui.admin.api_keys.list.table.client_id", "CLIENT ID")} {t("ui.admin.api_keys.list.table.scopes", "SCOPES")} {t("ui.admin.api_keys.list.table.last_used", "LAST USED")} {t("ui.admin.api_keys.list.table.actions", "ACTIONS")} {query.isLoading && ( {t("msg.common.loading", "로딩 중...")} )} {!query.isLoading && items.length === 0 && ( {t( "msg.admin.api_keys.list.empty", "등록된 API 키가 없습니다.", )} )} {items.map((key) => (
{key.name}
{key.client_id}
{key.scopes.map((scope) => ( {scope} ))}
{key.lastUsedAt ? new Date(key.lastUsedAt).toLocaleString("ko-KR") : t("ui.common.never", "Never")}
))}
setEditingKey(null)} > {t("ui.admin.api_keys.list.edit_scopes", "권한 수정")} {editingKey ? t( "msg.admin.api_keys.list.edit_scopes_desc", "{{clientId}}의 CLIENT_ID는 유지하고 권한만 변경합니다.", { clientId: editingKey.client_id }, ) : null}
{AVAILABLE_API_KEY_SCOPES.map((scope) => { const isSelected = draftScopes.includes(scope.id); return ( ); })}
{draftScopes.length === 0 && (

{t( "msg.admin.api_keys.create.scope_required", "최소 하나 이상의 권한을 선택해야 합니다.", )}

)}
setRotatedSecret(null)} > {t( "ui.admin.api_keys.list.rotate_secret_done", "Secret 재발급 완료", )} {t( "msg.admin.api_keys.list.rotate_secret_notice", "새 Secret은 지금 한 번만 표시됩니다. CLIENT_ID는 변경되지 않았습니다.", )} {rotatedSecret && (

CLIENT ID

{rotatedSecret.key.client_id}

X-Baron-Key-Secret

)}
); } export default ApiKeyListPage;