1
0
forked from baron/baron-sso

ory용 MCP 제작, devfront/adminfront 백엔드 연결

This commit is contained in:
Lectom C Han
2026-01-28 10:57:22 +09:00
parent 1aaa772907
commit 93cab064fc
75 changed files with 7327 additions and 454 deletions

View File

@@ -0,0 +1,366 @@
import { useQuery } from "@tanstack/react-query";
import {
Activity,
BookOpenText,
Copy,
Laptop,
Plus,
Search,
ServerCog,
ShieldHalf,
} from "lucide-react";
import { Link } from "react-router-dom";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "../../components/ui/avatar";
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 { Separator } from "../../components/ui/separator";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../components/ui/table";
import { fetchClients } from "../../lib/devApi";
import { cn } from "../../lib/utils";
function ClientsPage() {
const { data, isLoading, error } = useQuery({
queryKey: ["clients"],
queryFn: fetchClients,
});
const clients = data?.items || [];
const totalClients = clients.length;
// TODO: Add real stats for active sessions and auth failures
const stats = [
{
label: "총 클라이언트",
value: totalClients.toString(),
delta: "Realtime",
tone: "up" as const,
},
{
label: "활성 세션",
value: "-",
delta: "Not impl",
tone: "stable" as const,
},
{
label: "인증 실패 (24h)",
value: "0",
delta: "Stable",
tone: "stable" as const,
},
];
if (isLoading) {
return <div className="p-8 text-center">Loading clients...</div>;
}
if (error) {
const errMsg =
(error as any).response?.data?.error || (error as Error).message;
return (
<div className="p-8 text-center text-red-500">
Error loading clients: {errMsg}
</div>
);
}
return (
<div className="space-y-8">
<Card className="glass-panel">
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
RP registry
</p>
<CardTitle className="text-3xl font-black tracking-tight">
Relying Parties
</CardTitle>
<CardDescription>
OIDC , , URI,
.
</CardDescription>
</div>
<div className="hidden items-center gap-2 md:flex">
<Button variant="outline" size="sm">
</Button>
<Button size="sm" className="shadow-lg shadow-primary/30">
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
<div className="mt-4 grid gap-3 md:grid-cols-[1.5fr,1fr]">
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
className="pl-10"
placeholder="클라이언트 이름/ID로 검색..."
/>
</div>
<div className="flex items-center justify-end gap-2 md:justify-start">
<Badge variant="muted">테넌트: 선택됨</Badge>
<Badge variant="success"> </Badge>
</div>
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="grid gap-4 md:grid-cols-3">
{stats.map((item) => (
<Card key={item.label} className="border border-border/60">
<CardHeader className="pb-2">
<CardDescription>{item.label}</CardDescription>
<div className="mt-1 flex items-baseline gap-2">
<span className="text-3xl font-bold">{item.value}</span>
<Badge
variant={
item.tone === "up"
? "success"
: item.tone === "down"
? "warning"
: "muted"
}
className={cn(
"px-2",
item.tone === "down" &&
"bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-200",
item.tone === "stable" && "bg-muted/40 text-foreground",
)}
>
{item.delta}
</Badge>
</div>
</CardHeader>
</Card>
))}
</div>
</CardContent>
</Card>
<Card className="glass-panel">
<CardHeader className="pb-0">
<div className="flex items-center justify-between">
<CardTitle className="text-xl font-semibold">
</CardTitle>
<div className="flex items-center gap-2 md:hidden">
<Button variant="outline" size="sm">
</Button>
<Button size="sm">
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead>Client ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{clients.map((client) => (
<TableRow key={client.id} className="bg-card/40">
<TableCell>
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
{client.type === "confidential" ? (
<ServerCog className="h-4 w-4" />
) : (
<ShieldHalf className="h-4 w-4" />
)}
</div>
<div>
<p className="font-semibold">
{client.name || "Untitled"}
</p>
<p className="text-xs text-muted-foreground">
Tenant-scoped
</p>
</div>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<code className="rounded-md bg-secondary/60 px-2 py-1 font-mono text-xs text-muted-foreground">
{client.id}
</code>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-primary"
aria-label="Copy client id"
onClick={() =>
navigator.clipboard.writeText(client.id)
}
>
<Copy className="h-4 w-4" />
</Button>
</div>
</TableCell>
<TableCell>
<Badge
variant={
client.type === "confidential" ? "success" : "muted"
}
>
{client.type === "confidential"
? "기밀(Confidential)"
: "Public"}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div
className={cn(
"flex h-5 w-10 items-center rounded-full p-1",
client.status === "active"
? "bg-primary/40"
: "bg-muted/50",
)}
>
<div
className={cn(
"h-3 w-3 rounded-full bg-background transition",
client.status === "active"
? "translate-x-5"
: "translate-x-0",
)}
/>
</div>
<span
className={cn(
"text-sm font-medium",
client.status === "active"
? "text-emerald-400"
: "text-muted-foreground",
)}
>
{client.status === "active" ? "활성" : "비활성"}
</span>
</div>
</TableCell>
<TableCell className="text-muted-foreground">
{client.createdAt
? new Date(client.createdAt).toLocaleDateString()
: "-"}
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Button variant="ghost" size="sm" asChild>
<Link to={`/clients/${client.id}`}></Link>
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-destructive"
aria-label="Delete client"
>
<Activity className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="mt-4 flex items-center justify-between rounded-xl border border-border/60 bg-secondary/60 px-4 py-3 text-sm text-muted-foreground">
<span>
Showing {clients.length} of {totalClients} clients
</span>
<div className="flex gap-2">
<Button variant="outline" size="sm" disabled>
Previous
</Button>
<Button variant="outline" size="sm" disabled>
Next
</Button>
</div>
</div>
</CardContent>
</Card>
<div className="grid gap-6 lg:grid-cols-[2fr,1fr]">
<Card className="glass-panel">
<CardHeader className="pb-2">
<CardTitle className="text-lg font-bold">
Need help with OIDC configuration?
</CardTitle>
<CardDescription>
Developer guides for Confidential/Public clients, redirect URIs,
and auth methods.
</CardDescription>
</CardHeader>
<CardContent className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary/15 text-primary">
<BookOpenText className="h-6 w-6" />
</div>
<div>
<p className="font-semibold">Docs &amp; Examples</p>
<p className="text-sm text-muted-foreground">
Includes PKCE, client_secret_basic, redirect URI validation
tips.
</p>
</div>
</div>
<Button variant="secondary">View guides</Button>
</CardContent>
</Card>
<Card className="glass-panel">
<CardHeader className="pb-2">
<CardTitle className="text-lg font-semibold">Owner</CardTitle>
<CardDescription>Tenant admin on-call</CardDescription>
</CardHeader>
<CardContent className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Avatar>
<AvatarImage
src="https://gitea.hmac.kr/avatars/11ed71f61227be4a9ab6c61885371d92304a4c36a5f71036890625c55daa8c41?size=512"
alt="ops user"
/>
<AvatarFallback>AR</AvatarFallback>
</Avatar>
<div>
<p className="font-semibold">AI Admin Bot</p>
<p className="text-xs text-muted-foreground">admin@brsw.kr</p>
</div>
</div>
<Separator className="mx-4 hidden h-10 w-px md:block" />
<div className="hidden flex-col items-end text-sm text-muted-foreground md:flex">
<span>Role: Tenant Admin</span>
<span>Scope: TENANT-12</span>
</div>
</CardContent>
</Card>
</div>
</div>
);
}
export default ClientsPage;