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, useRef, 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"; import { ClientDetailTabs } from "./ClientDetailTabs"; function ClientDetailsPage() { const params = useParams(); const queryClient = useQueryClient(); const clientId = params.id ?? ""; const { data, error, isLoading } = useQuery({ queryKey: ["client", clientId], queryFn: () => fetchClient(clientId), enabled: clientId.length > 0, }); const [redirectUris, setRedirectUris] = useState(""); const [showSecret, setShowSecret] = useState(false); const redirectUrisHydratedRef = useRef(false); useEffect(() => { if ( !redirectUrisHydratedRef.current && data?.client?.redirectUris && redirectUris === "" ) { setRedirectUris(data.client.redirectUris.join(", ")); redirectUrisHydratedRef.current = true; } }, [data, redirectUris]); 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) => { const axiosError = err as AxiosError<{ error?: string }>; if (axiosError.response?.status === 403) { toast( t( "msg.dev.clients.details.save_forbidden", "이 RP 설정을 수정할 권한이 없습니다.\n관리자에게 RP 일반 설정 또는 RP 관리자 관계 부여를 요청해 주세요.", ), "error", ); return; } toast( t("msg.dev.clients.details.save_error", "저장 실패: {{error}}", { error: axiosError.response?.data?.error ?? (err as Error).message ?? t("msg.common.unknown_error", "unknown error"), }), "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 (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") }, )}
); } if (isLoading && !data) { return (
{t("msg.dev.clients.details.loading", "Loading app details...")}
); } const client = data?.client; if (!client) { return null; } const endpointValues = data?.endpoints ?? { discovery: "-", issuer: "-", authorization: "-", token: "-", userinfo: "-", }; const endpoints = [ { labelKey: "ui.dev.clients.details.endpoint.discovery", labelFallback: "Discovery Endpoint", value: endpointValues.discovery, }, { labelKey: "ui.dev.clients.details.endpoint.issuer", labelFallback: "Issuer URL", value: endpointValues.issuer, }, { labelKey: "ui.dev.clients.details.endpoint.authorization", labelFallback: "Authorization Endpoint", value: endpointValues.authorization, }, { labelKey: "ui.dev.clients.details.endpoint.token", labelFallback: "Token Endpoint", value: endpointValues.token, }, { labelKey: "ui.dev.clients.details.endpoint.userinfo", labelFallback: "UserInfo Endpoint", value: endpointValues.userinfo, }, ]; const hasClientSecret = client.type === "private"; const secretPlaceholder = "SECRET_NOT_AVAILABLE"; const clientSecret = hasClientSecret ? client?.clientSecret || secretPlaceholder : t("ui.common.na", "N/A"); const displaySecret = !hasClientSecret ? t( "msg.dev.clients.details.secret_not_applicable", "PKCE 앱에는 Client Secret이 없습니다.", ) : clientSecret === secretPlaceholder ? t("msg.dev.clients.details.secret_unavailable", "SECRET_NOT_AVAILABLE") : clientSecret; return (

{client?.name || client?.id || clientId}

{t( "msg.dev.clients.details.subtitle", "Manage OIDC credentials and endpoints.", )}

{client?.status === "active" ? t("ui.common.status.active", "Active") : client?.status === "inactive" ? t("ui.common.status.inactive", "Inactive") : t("msg.common.loading", "Loading...")}

{t( "ui.dev.clients.details.credentials.title", "Client Credentials", )}

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

{client?.id || clientId}

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

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

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

{hasClientSecret ? ( <> toast( t( "msg.dev.clients.details.copy_client_secret", "Client Secret이 복사되었습니다.", ), ) } /> ) : null}
{!hasClientSecret ? (

{t( "msg.dev.clients.details.secret_not_applicable", "PKCE 앱에는 Client Secret이 없습니다.", )}

) : null}

{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 목록입니다. 콤마(,)로 구분하여 여러 개 입력할 수 있습니다.", )}