import { useMutation, useQuery } from "@tanstack/react-query"; import { ArrowLeft, ChevronLeft, ChevronRight, Download, Filter, Search, } from "lucide-react"; import { 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, CardHeader, CardTitle, } from "../../components/ui/card"; import { Input } from "../../components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../../components/ui/table"; import { fetchClient, fetchConsents, revokeConsent } from "../../lib/devApi"; import { t } from "../../lib/i18n"; import { cn } from "../../lib/utils"; function ClientConsentsPage() { const params = useParams(); const clientId = params.id ?? ""; const [subjectInput, setSubjectInput] = useState(""); const [subject, setSubject] = useState(""); const [statusFilter, setStatusFilter] = useState([]); const [scopeFilter, setScopeFilter] = useState([]); const [isAdvancedFilterOpen, setIsAdvancedFilterOpen] = useState(false); const { data: clientData } = useQuery({ queryKey: ["client", clientId], queryFn: () => fetchClient(clientId), enabled: clientId.length > 0, }); const { data: consentsData, isLoading, error, refetch, } = useQuery({ queryKey: ["consents", clientId, subject], queryFn: () => fetchConsents(subject, clientId, "all"), enabled: clientId.length > 0, }); const revokeMutation = useMutation({ mutationFn: (payload: { subject: string }) => revokeConsent(payload.subject, clientId), onSuccess: () => { refetch(); }, }); const handleRevoke = (sub: string) => { if ( window.confirm( t( "msg.dev.clients.consents.revoke_confirm", "정말로 이 사용자의 권한을 철회하시겠습니까? 철회 시 사용자는 다음 접속 시 다시 동의해야 합니다.", ), ) ) { revokeMutation.mutate({ subject: sub }); } }; const rows = consentsData?.items ?? []; const allScopes = Array.from(new Set(rows.flatMap((r) => r.grantedScopes))); const filteredRows = rows.filter((row) => { const matchStatus = statusFilter.length === 0 || statusFilter.includes(row.status); const matchScope = scopeFilter.length === 0 || scopeFilter.some((s) => row.grantedScopes.includes(s)); return matchStatus && matchScope; }); const handleExportCSV = () => { if (filteredRows.length === 0) return; const headers = [ t("ui.dev.clients.consents.table.user", "User"), t("ui.dev.clients.consents.table.tenant", "Tenant"), t("ui.dev.clients.table.status", "Status"), t("ui.dev.clients.consents.table.scopes", "Granted Scopes"), t("ui.dev.clients.consents.table.first_granted", "First Granted"), t( "ui.dev.clients.consents.table.last_auth", "Last Authenticated / Revoked", ), ]; const csvContent = [ headers.join(","), ...filteredRows.map((row) => { const lastAuthRevoked = row.status === "revoked" && row.deletedAt ? `${t("ui.dev.clients.consents.status_revoked", "Revoked")}: ${new Date(row.deletedAt).toLocaleString()}` : row.authenticatedAt ? new Date(row.authenticatedAt).toLocaleString() : "-"; return [ `"${row.subject} (${row.userName || ""})"`, `"${row.tenantName || row.tenantId || ""}"`, `"${row.status}"`, `"${row.grantedScopes.join(", ")}"`, `"${new Date(row.createdAt).toLocaleString()}"`, `"${lastAuthRevoked}"`, ].join(","); }), ].join("\n"); const blob = new Blob([`\uFEFF${csvContent}`], { type: "text/csv;charset=utf-8;", }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); const date = new Date().toISOString().split("T")[0]; link.setAttribute("href", url); link.setAttribute("download", `consents_${clientId}_${date}.csv`); link.style.visibility = "hidden"; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; const handleStatusFilterChange = (status: string, checked: boolean) => { if (checked) { setStatusFilter((prev) => [...prev, status]); } else { setStatusFilter((prev) => prev.filter((s) => s !== status)); } }; const handleScopeFilterChange = (scope: string, checked: boolean) => { if (checked) { setScopeFilter((prev) => [...prev, scope]); } else { setScopeFilter((prev) => prev.filter((s) => s !== scope)); } }; const handleAllScopesChange = (checked: boolean) => { if (checked) { setScopeFilter(allScopes); } else { setScopeFilter([]); } }; return (

{t("ui.dev.clients.consents.title", "User Consent Grants")}

{t( "msg.dev.clients.consents.subtitle", "OIDC Relying Party 사용자 권한을 검토·관리합니다.", )}

{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")}
setSubjectInput(e.target.value)} />
{isAdvancedFilterOpen && (
{t("ui.dev.clients.consents.status_label", "Status:")}
{t("ui.dev.clients.consents.scope_label", "Scope:")}
{allScopes.length > 0 && ( )} {allScopes.map((scope) => ( ))}
)}
{error && ( {t( "msg.dev.clients.consents.load_error", "Error loading consents: {{error}}", { error: (error as Error).message, }, )} )} {isLoading && ( {t("msg.dev.clients.consents.loading", "Loading consents...")} )} {t("ui.dev.clients.consents.table.user", "User")} {t("ui.dev.clients.consents.table.tenant", "Tenant")} {t("ui.dev.clients.consents.table.status", "Status")} {t("ui.dev.clients.consents.table.scopes", "Granted Scopes")} {t( "ui.dev.clients.consents.table.first_granted", "First Granted", )} {t( "ui.dev.clients.consents.table.last_auth", "Last Authenticated / Revoked", )} {t("ui.dev.clients.consents.table.action", "Action")} {filteredRows.length === 0 && !isLoading ? ( {t("msg.dev.clients.consents.empty", "No consents found.")} ) : ( filteredRows.map((row) => (
{(row.userName || row.subject) .slice(0, 2) .toUpperCase()}
{row.userName || t("ui.dev.clients.consents.subject", "Subject")} {row.subject}
{row.tenantName || t("ui.common.na", "N/A")} {row.tenantId}
{row.status === "active" ? ( {t("ui.common.status.active", "Active")} ) : ( {t("ui.dev.clients.consents.status_revoked", "Revoked")} )}
{row.grantedScopes.map((scope) => ( {scope} ))}
{new Date(row.createdAt).toLocaleString()} {row.status === "revoked" && row.deletedAt ? ( {t("ui.dev.clients.consents.revoked_at", "Revoked: ")} {new Date(row.deletedAt).toLocaleString()} ) : row.authenticatedAt ? ( new Date(row.authenticatedAt).toLocaleString() ) : ( "-" )} {row.status === "active" && ( )}
)) )}

{t( "msg.dev.clients.consents.showing", "Showing {{from}} to {{to}} of {{total}} users", { from: filteredRows.length > 0 ? 1 : 0, to: filteredRows.length, total: rows.length, }, )}

{t( "ui.dev.clients.consents.stats.active_grants", "Active Grants", )}

{rows.filter((r) => r.status === "active").length}

{t( "ui.dev.clients.consents.stats.total_scopes", "Total Scopes Issued", )}

{rows.reduce((acc, row) => acc + row.grantedScopes.length, 0)}

{t( "ui.dev.clients.consents.stats.avg_scopes", "Avg. Scopes per User", )}

{rows.length > 0 ? ( rows.reduce( (acc, row) => acc + row.grantedScopes.length, 0, ) / rows.length ).toFixed(1) : "0.0"}
); } export default ClientConsentsPage;