import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { ArrowLeft, Eye, EyeOff, Link2, RefreshCw, Save, Shield, } from "lucide-react"; import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "../../components/ui/card"; import { CopyButton } from "../../components/ui/copy-button"; import { Label } from "../../components/ui/label"; import { Separator } from "../../components/ui/separator"; import { Table, TableBody, TableCell, TableRow, } from "../../components/ui/table"; import { Textarea } from "../../components/ui/textarea"; import { toast } from "../../components/ui/use-toast"; import { fetchClient, rotateClientSecret, updateClient, } from "../../lib/devApi"; import { t } from "../../lib/i18n"; import { cn } from "../../lib/utils"; function ClientDetailsPage() { const params = useParams(); const queryClient = useQueryClient(); const clientId = params.id ?? ""; const { data, isLoading, error } = useQuery({ queryKey: ["client", clientId], queryFn: () => fetchClient(clientId), enabled: clientId.length > 0, }); const [redirectUris, setRedirectUris] = useState(""); const [showSecret, setShowSecret] = useState(false); useEffect(() => { if (data?.client?.redirectUris) { setRedirectUris(data.client.redirectUris.join(", ")); } }, [data]); const mutation = useMutation({ mutationFn: () => { const uriList = redirectUris .split(",") .map((u) => u.trim()) .filter(Boolean); return updateClient(clientId, { redirectUris: uriList }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["client", clientId] }); toast( t( "msg.dev.clients.details.redirect_saved", "Redirect URIs가 저장되었습니다.", ), ); }, onError: (err) => { toast( t("msg.dev.clients.details.save_error", "저장 실패: {{error}}", { error: (err as Error).message, }), "error", ); }, }); const rotateMutation = useMutation({ mutationFn: () => rotateClientSecret(clientId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["client", clientId] }); toast( t( "msg.dev.clients.details.secret_rotated", "Client Secret이 재발급되었습니다.", ), ); setShowSecret(true); // 재발급 후 바로 보여줌 }, onError: (err) => { toast( t("msg.dev.clients.details.rotate_error", "재발급 실패: {{error}}", { error: (err as Error).message, }), "error", ); }, }); const handleRotateSecret = () => { if ( window.confirm( t( "msg.dev.clients.details.rotate_confirm", "경고: Client Secret을 재발급하면 기존 시크릿은 즉시 무효화됩니다.\n연동된 애플리케이션이 중단될 수 있습니다. 계속하시겠습니까?", ), ) ) { rotateMutation.mutate(); } }; if (!clientId) { return (
{t( "msg.dev.clients.details.subtitle", "Manage OIDC credentials and endpoints.", )}
{t( "ui.dev.clients.details.credentials.client_id", "Client ID", )}
{data.client.id}
{t( "ui.dev.clients.details.credentials.client_secret", "Client Secret", )}
{showSecret ? displaySecret : "••••••••••••••••"}
{t("ui.dev.clients.details.security.title", "보안 메모")}
{t( "msg.dev.clients.details.security.note", "엔드포인트는 읽기 전용으로 유지하고, 비밀키 재발행/복사는 감사 로그와 연계하세요.", )}
{t( "msg.dev.clients.details.security.footer", "비밀키 재발행 작업에는 관리자 세션 TTL 확인과 레이트리밋, 알림 연동을 권장합니다.", )}