import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { 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.missing_id", "Client ID가 필요합니다.")}
); } if (isLoading) { return (
{t("msg.dev.clients.details.loading", "Loading app...")}
); } if (error || !data) { const errMsg = (error as AxiosError<{ error?: string }>).response?.data?.error ?? (error as Error)?.message; return (
{t( "msg.dev.clients.details.load_error", "Error loading app: {{error}}", { error: errMsg || t("msg.common.unknown_error", "unknown error") }, )}
); } const endpoints = [ { labelKey: "ui.dev.clients.details.endpoint.discovery", labelFallback: "Discovery Endpoint", value: data.endpoints.discovery, }, { labelKey: "ui.dev.clients.details.endpoint.issuer", labelFallback: "Issuer URL", value: data.endpoints.issuer, }, { labelKey: "ui.dev.clients.details.endpoint.authorization", labelFallback: "Authorization Endpoint", value: data.endpoints.authorization, }, { labelKey: "ui.dev.clients.details.endpoint.token", labelFallback: "Token Endpoint", value: data.endpoints.token, }, { labelKey: "ui.dev.clients.details.endpoint.userinfo", labelFallback: "UserInfo Endpoint", value: data.endpoints.userinfo, }, ]; // Client Secret from API const secretPlaceholder = "SECRET_NOT_AVAILABLE"; const clientSecret = data.client.clientSecret || secretPlaceholder; const displaySecret = clientSecret === secretPlaceholder ? t("msg.dev.clients.details.secret_unavailable", "SECRET_NOT_AVAILABLE") : clientSecret; return (
{t("ui.dev.clients.details.breadcrumb.section", "Apps")} / {t("ui.dev.clients.details.breadcrumb.current", "클라이언트 상세")}

{data.client.name || data.client.id}

{t( "msg.dev.clients.details.subtitle", "OIDC 자격 증명과 엔드포인트를 관리합니다.", )}

{data.client.status === "active" ? t("ui.common.status.active", "Active") : t("ui.common.status.inactive", "Inactive")}
{t("ui.dev.clients.details.tab.connection", "Connection")} {t("ui.dev.clients.details.tab.consents", "Consent & Users")} {t("ui.dev.clients.details.tab.settings", "Settings")}

{t( "ui.dev.clients.details.credentials.title", "클라이언트 자격 증명", )}

{t( "ui.dev.clients.details.credentials.client_id", "Client ID", )}

{data.client.id}

toast( t( "msg.dev.clients.details.copy_client_id", "Client ID가 복사되었습니다.", ), ) } />

{t( "ui.dev.clients.details.credentials.client_secret", "Client Secret", )}

{showSecret ? displaySecret : "••••••••••••••••"}

toast( t( "msg.dev.clients.details.copy_client_secret", "Client Secret이 복사되었습니다.", ), ) } />

{t("ui.dev.clients.details.endpoints.title", "OIDC 엔드포인트")}

{t("ui.dev.clients.details.endpoints.read_only", "읽기 전용")}
{endpoints.map((endpoint) => (

{t(endpoint.labelKey, endpoint.labelFallback)}

{endpoint.value} toast( t( "msg.dev.clients.details.copy_endpoint", "{{label}}가 복사되었습니다.", { label: t( endpoint.labelKey, endpoint.labelFallback, ), }, ), ) } />
))}

{t("ui.dev.clients.details.redirect.title", "리디렉션 URI 설정")}

{t("ui.dev.clients.details.redirect.label", "Redirect URIs")} {t( "msg.dev.clients.details.redirect.description", "인증 성공 후 사용자를 리다이렉트할 허용된 URL 목록입니다. 콤마(,)로 구분하여 여러 개 입력할 수 있습니다.", )}