import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { ArrowLeft, Link2, Plus, Trash2 } from "lucide-react"; import { useMemo, 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 { Input } from "../../components/ui/input"; import { Label } from "../../components/ui/label"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../../components/ui/table"; import { toast } from "../../components/ui/use-toast"; import { addClientRelation, fetchClient, fetchClientRelations, removeClientRelation, } from "../../lib/devApi"; import { t } from "../../lib/i18n"; const relationOptions = [ "admins", "creator", "config_editor", "secret_rotator", "jwks_viewer", "jwks_operator", "consent_viewer", "consent_revoker", "relationship_viewer", "status_operator", ] as const; function ClientRelationsPage() { const params = useParams(); const queryClient = useQueryClient(); const clientId = params.id ?? ""; const [relation, setRelation] = useState<(typeof relationOptions)[number]>( "config_editor", ); const [userId, setUserId] = useState(""); const { data: clientData } = useQuery({ queryKey: ["client", clientId], queryFn: () => fetchClient(clientId), enabled: clientId.length > 0, }); const { data: relationData, isLoading, error, } = useQuery({ queryKey: ["client-relations", clientId], queryFn: () => fetchClientRelations(clientId), enabled: clientId.length > 0, }); const sortedItems = useMemo(() => { return [...(relationData?.items ?? [])].sort((a, b) => { const relationCompare = a.relation.localeCompare(b.relation); if (relationCompare !== 0) { return relationCompare; } return a.subject.localeCompare(b.subject); }); }, [relationData?.items]); const addMutation = useMutation({ mutationFn: () => addClientRelation(clientId, { relation, userId: userId.trim(), }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["client-relations", clientId] }); setUserId(""); toast( t( "msg.dev.clients.relationships.added", "Relationship가 추가되었습니다.", ), ); }, onError: (err) => { toast( t( "msg.dev.clients.relationships.add_error", "Relationship 추가 실패: {{error}}", { error: (err as AxiosError<{ error?: string }>).response?.data?.error ?? (err as Error).message, }, ), "error", ); }, }); const removeMutation = useMutation({ mutationFn: (payload: { relation: string; subject: string }) => removeClientRelation(clientId, payload.relation, payload.subject), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["client-relations", clientId] }); toast( t( "msg.dev.clients.relationships.removed", "Relationship가 제거되었습니다.", ), ); }, onError: (err) => { toast( t( "msg.dev.clients.relationships.remove_error", "Relationship 제거 실패: {{error}}", { error: (err as AxiosError<{ error?: string }>).response?.data?.error ?? (err as Error).message, }, ), "error", ); }, }); const handleAdd = () => { if (!userId.trim()) { toast( t( "msg.dev.clients.relationships.user_required", "추가할 User ID를 입력하세요.", ), "error", ); return; } addMutation.mutate(); }; const handleRemove = (targetRelation: string, subject: string) => { if ( window.confirm( t( "msg.dev.clients.relationships.remove_confirm", "이 relationship를 제거하시겠습니까?", ), ) ) { removeMutation.mutate({ relation: targetRelation, subject }); } }; if (!clientId) { return (
{t("msg.dev.clients.details.missing_id", "Client ID가 필요합니다.")}
); } return (

{t( "ui.dev.clients.relationships.title", "Client Relationships", )}

{t( "msg.dev.clients.relationships.subtitle", "RP direct operator relation을 조회하고 User 단위로 추가·삭제합니다.", )}

{clientData?.client?.status === "active" ? t("ui.common.status.active", "Active") : t("ui.common.status.inactive", "Inactive")}
{t("ui.dev.clients.details.tab.connection", "Federation")} {t("ui.dev.clients.details.tab.consents", "Consent & Users")} {t("ui.dev.clients.details.tab.settings", "Settings")} {t("ui.dev.clients.details.tab.relationships", "Relationships")}
{t("ui.dev.clients.relationships.add_title", "Add Relationship")} {t( "msg.dev.clients.relationships.add_description", "현재는 direct User assignment만 지원합니다. subject는 자동으로 User: 형식으로 전송됩니다.", )}
setUserId(e.target.value)} placeholder={t( "ui.dev.clients.relationships.user_id_placeholder", "kratos user id", )} />
{t( "ui.dev.clients.relationships.list_title", "Assigned Relationships", )} {t( "msg.dev.clients.relationships.list_description", "현재 RP에 직접 부여된 operator relation 목록입니다.", )} {error ? (
{t( "msg.dev.clients.relationships.load_error", "Relationship 조회 실패: {{error}}", { error: (error as AxiosError<{ error?: string }>).response?.data ?.error ?? (error as Error).message, }, )}
) : isLoading ? (
{t( "msg.dev.clients.relationships.loading", "Relationship를 불러오는 중입니다...", )}
) : sortedItems.length === 0 ? (
{t( "msg.dev.clients.relationships.empty", "직접 부여된 relationship가 없습니다.", )}
) : ( {t("ui.dev.clients.relationships.relation", "Relation")} {t("ui.dev.clients.relationships.subject", "Subject")} {t("ui.dev.clients.relationships.subject_type", "Type")} {t("ui.dev.clients.table.actions", "액션")} {sortedItems.map((item) => ( {item.relation}
{item.subject}
{item.subjectId && (
ID: {item.subjectId}
)}
{item.subjectType || "-"}
))}
)}
); } export default ClientRelationsPage;