forked from baron/baron-sso
ory용 MCP 제작, devfront/adminfront 백엔드 연결
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Filter, ListChecks, Search, Terminal } from "lucide-react";
|
||||
import { fetchAuditLogs } from "../../lib/adminApi";
|
||||
|
||||
const auditFilters = [
|
||||
"Actor role = admin",
|
||||
@@ -6,31 +8,28 @@ const auditFilters = [
|
||||
"Tenant = selected header",
|
||||
];
|
||||
|
||||
const auditRows = [
|
||||
{
|
||||
action: "client.create",
|
||||
tenant: "TENANT-12",
|
||||
actor: "ops.jane@baron",
|
||||
result: "ok",
|
||||
ts: "2026-01-26 15:21 KST",
|
||||
},
|
||||
{
|
||||
action: "client.rotate_secret",
|
||||
tenant: "TENANT-12",
|
||||
actor: "ops.jane@baron",
|
||||
result: "ok",
|
||||
ts: "2026-01-26 15:22 KST",
|
||||
},
|
||||
{
|
||||
action: "audit.export",
|
||||
tenant: "TENANT-07",
|
||||
actor: "auditor.lee@baron",
|
||||
result: "rate_limited",
|
||||
ts: "2026-01-26 15:30 KST",
|
||||
},
|
||||
];
|
||||
|
||||
function AuditLogsPage() {
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["audit-logs"],
|
||||
queryFn: () => fetchAuditLogs(),
|
||||
});
|
||||
|
||||
const logs = data?.items || [];
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="p-8 text-center">Loading audit logs...</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 logs: {errMsg}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
@@ -84,28 +83,42 @@ function AuditLogsPage() {
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-5 divide-y divide-[var(--color-border)]">
|
||||
{auditRows.map((row) => (
|
||||
<div
|
||||
key={`${row.action}-${row.ts}`}
|
||||
className="grid grid-cols-[1.2fr,1fr,1fr,1fr] items-center gap-2 py-3 text-sm"
|
||||
>
|
||||
<div className="font-semibold">{row.action}</div>
|
||||
<div className="text-[var(--color-muted)]">{row.tenant}</div>
|
||||
<div className="text-[var(--color-muted)]">{row.actor}</div>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<span
|
||||
className={`rounded-full px-2 py-1 text-xs ${
|
||||
row.result === "ok"
|
||||
? "bg-[rgba(54,211,153,0.16)] text-[var(--color-accent)]"
|
||||
: "bg-[rgba(249,168,38,0.16)] text-[var(--color-accent-strong)]"
|
||||
}`}
|
||||
>
|
||||
{row.result}
|
||||
</span>
|
||||
<span className="text-[var(--color-muted)]">{row.ts}</span>
|
||||
</div>
|
||||
{logs.length === 0 ? (
|
||||
<div className="py-8 text-center text-sm text-[var(--color-muted)]">
|
||||
No audit logs found.
|
||||
</div>
|
||||
))}
|
||||
) : (
|
||||
logs.map((row, idx) => (
|
||||
<div
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: simple list
|
||||
key={`${row.event_type}-${idx}`}
|
||||
className="grid grid-cols-[1.2fr,1fr,1fr,1fr] items-center gap-2 py-3 text-sm"
|
||||
>
|
||||
<div className="font-semibold">{row.event_type}</div>
|
||||
<div className="text-[var(--color-muted)]">
|
||||
{/* Tenant info not yet in basic schema, show generic or details snippet */}
|
||||
Tenant-?
|
||||
</div>
|
||||
<div className="text-[var(--color-muted)] overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{row.user_id}
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<span
|
||||
className={`rounded-full px-2 py-1 text-xs ${
|
||||
row.status === "success" || row.status === "ok"
|
||||
? "bg-[rgba(54,211,153,0.16)] text-[var(--color-accent)]"
|
||||
: "bg-[rgba(249,168,38,0.16)] text-[var(--color-accent-strong)]"
|
||||
}`}
|
||||
>
|
||||
{row.status}
|
||||
</span>
|
||||
<span className="text-[var(--color-muted)] text-xs">
|
||||
{new Date(row.timestamp).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
import {
|
||||
ArrowLeft,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Filter,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
import { Link } 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 {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../components/ui/table";
|
||||
|
||||
const rows = [
|
||||
{
|
||||
initials: "JD",
|
||||
name: "John Doe",
|
||||
email: "john.doe@example.com",
|
||||
scopes: ["openid", "profile", "email", "offline_access"],
|
||||
lastAuth: "Oct 24, 2023 14:22",
|
||||
},
|
||||
{
|
||||
initials: "AS",
|
||||
name: "Alice Smith",
|
||||
email: "alice.smith@devmail.com",
|
||||
scopes: ["openid", "profile"],
|
||||
lastAuth: "Oct 23, 2023 09:15",
|
||||
},
|
||||
{
|
||||
initials: "RJ",
|
||||
name: "Robert Johnson",
|
||||
email: "r.johnson@corporate.org",
|
||||
scopes: ["openid", "profile", "groups"],
|
||||
lastAuth: "Oct 21, 2023 18:45",
|
||||
},
|
||||
{
|
||||
initials: "ML",
|
||||
name: "Maria Lopez",
|
||||
email: "maria.l@provider.net",
|
||||
scopes: ["openid", "email"],
|
||||
lastAuth: "Oct 20, 2023 11:30",
|
||||
},
|
||||
];
|
||||
|
||||
function ClientConsentsPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<header className="space-y-4">
|
||||
<div className="flex flex-wrap justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<nav className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
||||
<Link to="/" className="hover:text-primary">
|
||||
Home
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<Link to="/clients" className="hover:text-primary">
|
||||
Clients
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span>OIDC Relying Party</span>
|
||||
<span>/</span>
|
||||
<span className="text-foreground font-semibold">
|
||||
User Consent Grants
|
||||
</span>
|
||||
</nav>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
<Link to="/clients/cli_481...8k2">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<div>
|
||||
<p className="text-3xl font-black leading-tight">
|
||||
User Consent Grants
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
OIDC Relying Party 사용자 권한을 검토·관리합니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="success">Active</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-6 overflow-x-auto border-b border-border pb-3 text-sm font-bold">
|
||||
<Link
|
||||
to="/clients/cli_481...8k2"
|
||||
className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Overview
|
||||
</Link>
|
||||
<span className="whitespace-nowrap border-b-2 border-primary pb-1 text-primary">
|
||||
Consent & Users
|
||||
</span>
|
||||
<Link
|
||||
to="/clients/cli_481...8k2/settings"
|
||||
className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<Card className="glass-panel">
|
||||
<CardContent className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex flex-wrap items-center gap-4 flex-1">
|
||||
<div className="relative w-full max-w-md">
|
||||
<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 gap-2">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
||||
Status:
|
||||
</span>
|
||||
<select className="h-10 rounded-lg border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/30">
|
||||
<option>All Statuses</option>
|
||||
<option selected>Active</option>
|
||||
<option>Revoked</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="ghost" className="gap-1 text-muted-foreground">
|
||||
<Filter className="h-4 w-4" />
|
||||
Advanced Filters
|
||||
</Button>
|
||||
<Button className="shadow-sm shadow-primary/30">Export CSV</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-panel">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Granted Scopes</TableHead>
|
||||
<TableHead>Last Authenticated</TableHead>
|
||||
<TableHead className="text-right">Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((row) => (
|
||||
<TableRow key={row.email}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-xs font-bold text-primary">
|
||||
{row.initials}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-semibold">{row.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{row.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="success">Active</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{row.scopes.map((scope) => (
|
||||
<Badge
|
||||
key={scope}
|
||||
variant="muted"
|
||||
className="border bg-muted/40 text-foreground"
|
||||
>
|
||||
{scope}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{row.lastAuth}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="ghost" className="text-destructive">
|
||||
Revoke
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<CardContent className="flex items-center justify-between border-t border-border bg-muted/10 px-6 py-4 text-sm text-muted-foreground">
|
||||
<p>
|
||||
Showing <span className="font-semibold text-foreground">1</span> to{" "}
|
||||
<span className="font-semibold text-foreground">4</span> of{" "}
|
||||
<span className="font-semibold text-foreground">1,250</span> users
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="icon" disabled>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm">1</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
2
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
3
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<Card className="glass-panel">
|
||||
<CardHeader className="pb-2">
|
||||
<p className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
||||
Active Grants
|
||||
</p>
|
||||
<CardTitle className="text-2xl font-black">1,250</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card className="glass-panel">
|
||||
<CardHeader className="pb-2">
|
||||
<p className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
||||
Total Scopes Issued
|
||||
</p>
|
||||
<CardTitle className="text-2xl font-black">4,812</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card className="glass-panel">
|
||||
<CardHeader className="pb-2">
|
||||
<p className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
||||
Avg. Scopes per User
|
||||
</p>
|
||||
<CardTitle className="text-2xl font-black">3.8</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClientConsentsPage;
|
||||
@@ -1,194 +0,0 @@
|
||||
import { AlertCircle, Copy, Eye, Link2, Shield, Workflow } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Card, CardContent } from "../../components/ui/card";
|
||||
import { Separator } from "../../components/ui/separator";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from "../../components/ui/table";
|
||||
|
||||
const endpoints = [
|
||||
{
|
||||
label: "Discovery Endpoint",
|
||||
value: "https://auth.acme-idp.com/.well-known/openid-configuration",
|
||||
},
|
||||
{ label: "Issuer URL", value: "https://auth.acme-idp.com/" },
|
||||
{
|
||||
label: "Authorization Endpoint",
|
||||
value: "https://auth.acme-idp.com/oauth2/authorize",
|
||||
},
|
||||
{ label: "Token Endpoint", value: "https://auth.acme-idp.com/oauth2/token" },
|
||||
{
|
||||
label: "UserInfo Endpoint",
|
||||
value: "https://auth.acme-idp.com/oauth2/userinfo",
|
||||
},
|
||||
];
|
||||
|
||||
function ClientDetailsPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
||||
<Link to="/clients" className="text-primary hover:underline">
|
||||
Relying Parties
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-foreground">클라이언트 상세</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h1 className="text-4xl font-black leading-tight tracking-tight">
|
||||
Developer Portal App
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
OIDC 자격 증명과 엔드포인트를 관리합니다.
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="success" className="px-3 py-1 text-xs uppercase">
|
||||
Active
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex gap-6 border-b border-border">
|
||||
<Link
|
||||
to="/clients/cli_481...8k2"
|
||||
className="border-b-2 border-primary pb-3 text-sm font-bold text-primary"
|
||||
>
|
||||
Overview
|
||||
</Link>
|
||||
<Link
|
||||
to="/clients/cli_481...8k2/consents"
|
||||
className="pb-3 text-sm font-bold text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Consent & Users
|
||||
</Link>
|
||||
<Link
|
||||
to="/clients/cli_481...8k2/settings"
|
||||
className="pb-3 text-sm font-bold text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-bold">클라이언트 자격 증명</h2>
|
||||
<Card className="glass-panel">
|
||||
<CardContent className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-muted-foreground">
|
||||
Client ID
|
||||
</p>
|
||||
<p className="font-mono text-lg">721948305612-oidc-client-prod</p>
|
||||
</div>
|
||||
<Button variant="secondary" className="gap-2">
|
||||
<Copy className="h-4 w-4" />
|
||||
ID 복사
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-panel">
|
||||
<CardContent className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-muted-foreground">
|
||||
Client Secret
|
||||
</p>
|
||||
<p className="font-mono text-lg tracking-widest">
|
||||
••••••••••••••••
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="secondary" className="gap-2">
|
||||
<Eye className="h-4 w-4" />
|
||||
보기
|
||||
</Button>
|
||||
<Button variant="secondary" className="gap-2">
|
||||
<Copy className="h-4 w-4" />
|
||||
복사
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 border-amber-500/50 text-amber-500"
|
||||
>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
비밀키 재발행
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-xl font-bold">OIDC 엔드포인트</h2>
|
||||
<Badge variant="muted" className="gap-1">
|
||||
<Link2 className="h-3 w-3" />
|
||||
읽기 전용
|
||||
</Badge>
|
||||
</div>
|
||||
<Card className="glass-panel">
|
||||
<Table>
|
||||
<TableBody>
|
||||
{endpoints.map((endpoint) => (
|
||||
<TableRow key={endpoint.label} className="border-border/70">
|
||||
<TableCell className="w-1/3">
|
||||
<p className="text-xs font-bold uppercase tracking-[0.12em] text-muted-foreground">
|
||||
{endpoint.label}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className="flex items-center justify-between gap-3">
|
||||
<span className="break-all font-mono text-sm">
|
||||
{endpoint.value}
|
||||
</span>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
aria-label={`${endpoint.label} 복사`}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="glass-panel p-6 opacity-80">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary/15 text-primary">
|
||||
<Shield className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-semibold">보안 메모</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
엔드포인트는 읽기 전용으로 유지하고, 비밀키 재발행/복사는 감사
|
||||
로그와 연계하세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 md:flex">
|
||||
<Badge variant="outline" className="gap-1">
|
||||
<Workflow className="h-4 w-4" />
|
||||
감사 이벤트 필요
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
비밀키 재발행 작업에는 관리자 세션 TTL 확인과 레이트리밋, 알림 연동을
|
||||
권장합니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClientDetailsPage;
|
||||
@@ -1,206 +0,0 @@
|
||||
import { Info, Search, Shield, Sparkles, Upload } from "lucide-react";
|
||||
import { Link } 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 { Separator } from "../../components/ui/separator";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const meta = {
|
||||
clientId: "client_82910_ax99",
|
||||
created: "2023-10-12 10:45",
|
||||
updated: "2 hours ago",
|
||||
};
|
||||
|
||||
function ClientGeneralPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<header className="space-y-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
||||
<Link to="/clients" className="text-primary hover:underline">
|
||||
Applications
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-foreground">Customer Support Portal</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-3xl font-black leading-tight">
|
||||
Client Details
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
RP 설정과 메타데이터를 관리합니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="success" className="px-3 py-1 text-xs uppercase">
|
||||
Active
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-6 overflow-x-auto border-b border-border pb-3 text-sm font-bold">
|
||||
<Link
|
||||
to="/clients/cli_481...8k2"
|
||||
className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Overview
|
||||
</Link>
|
||||
<Link
|
||||
to="/clients/cli_481...8k2/consents"
|
||||
className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Consent & Users
|
||||
</Link>
|
||||
<span className="whitespace-nowrap border-b-2 border-primary pb-1 text-primary">
|
||||
Settings
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="glass-panel p-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-border pb-4">
|
||||
<div>
|
||||
<CardTitle className="text-xl font-bold">
|
||||
Application Identity
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
앱 이름과 설명, 로고를 설정합니다. 필수 필드는 * 로 표시됩니다.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-10 items-center rounded-lg border border-input bg-secondary/50 px-3 text-sm text-muted-foreground">
|
||||
<Search className="mr-2 h-4 w-4" />
|
||||
Search
|
||||
</div>
|
||||
<div className="h-10 w-10 overflow-hidden rounded-full border border-border bg-muted/40">
|
||||
<img
|
||||
className="h-full w-full object-cover"
|
||||
alt="앱 로고"
|
||||
src="https://lh3.googleusercontent.com/aida-public/AB6AXuBFGWfyQ8ZzHXZmha91pG-09N58hcUap10-bU30aIf_CpfOqm8fPIv6j2v_BVGaJMF2gABxv_hnEXUCBvmjZeFpr-c76uC1QQkgMwsdkc2Im0gqS5X1c8sCWLZudDydZo5m7XW-QW1nRSZHYE5XzTqrW2ITgruSa7eC2Oe9RtxeVFCrqcHw3RO3h0WLtyJ8yhkkeZrAyBc4UQtpcL5bhBDSdlUNgw0odf12Mk6oNojf7Rcg4HPnywh6C-mUtJd-UfX7Y3Yv_W704T1a"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 pt-6 md:grid-cols-2">
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-1 text-sm font-semibold">
|
||||
앱 이름 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input defaultValue="Customer Support Portal" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-semibold">Description</Label>
|
||||
<Textarea
|
||||
rows={3}
|
||||
defaultValue="Internal tool for managing customer support tickets and user data."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-semibold">App Logo URL</Label>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1 space-y-2">
|
||||
<Input defaultValue="https://brand.example.com/assets/logo-support.png" />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
PNG/SVG URL을 입력하세요.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-20 w-20 items-center justify-center overflow-hidden rounded-lg border-2 border-dashed border-border bg-muted/40">
|
||||
<Upload className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="glass-panel">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-xl font-bold">보안 설정</CardTitle>
|
||||
<CardDescription>
|
||||
클라이언트 유형을 선택하세요. 비밀키를 안전하게 보관할 수 없는 경우
|
||||
Public을 선택합니다.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Label className="flex items-center gap-2 text-base font-semibold">
|
||||
Client Type
|
||||
<Info className="h-4 w-4 text-muted-foreground" />
|
||||
</Label>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<label className="relative flex cursor-pointer flex-col gap-1 rounded-xl border-2 border-primary bg-primary/5 p-4 transition">
|
||||
<input
|
||||
className="sr-only"
|
||||
type="radio"
|
||||
name="client-type"
|
||||
defaultChecked
|
||||
/>
|
||||
<span className="flex items-center gap-2 text-sm font-bold uppercase text-foreground">
|
||||
<Shield className="h-4 w-4 text-primary" />
|
||||
Confidential
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
서버 사이드 앱(예: Node.js, Java)처럼 비밀키를 안전하게 보관
|
||||
가능.
|
||||
</span>
|
||||
<span className="absolute right-4 top-4 text-primary">✓</span>
|
||||
</label>
|
||||
|
||||
<label className="relative flex cursor-pointer flex-col gap-1 rounded-xl border-2 border-border bg-card p-4 transition hover:border-muted-foreground/40">
|
||||
<input className="sr-only" type="radio" name="client-type" />
|
||||
<span className="flex items-center gap-2 text-sm font-bold uppercase text-foreground">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Public
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
SPA/모바일 앱처럼 비밀키 보관이 어려운 경우. PKCE를 기본
|
||||
적용하세요.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 border-t border-border pt-4">
|
||||
<Button variant="outline">취소</Button>
|
||||
<Button>저장</Button>
|
||||
</div>
|
||||
|
||||
<div className="glass-panel flex flex-wrap gap-x-12 gap-y-4 p-4">
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
Client ID
|
||||
</span>
|
||||
<span className="font-mono text-sm">{meta.clientId}</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
Created On
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">{meta.created}</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
Last Updated
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">{meta.updated}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClientGeneralPage;
|
||||
@@ -1,388 +0,0 @@
|
||||
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 { cn } from "../../lib/utils";
|
||||
|
||||
const clients = [
|
||||
{
|
||||
name: "Customer Portal",
|
||||
type: "Confidential",
|
||||
clientId: "cli_481...8k2",
|
||||
status: "Active",
|
||||
created: "2023-10-12",
|
||||
icon: <Laptop className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
name: "Mobile App (iOS)",
|
||||
type: "Public",
|
||||
clientId: "cli_922...4m1",
|
||||
status: "Inactive",
|
||||
created: "2023-11-04",
|
||||
icon: <ShieldHalf className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
name: "Internal Analytics",
|
||||
type: "Confidential",
|
||||
clientId: "cli_773...5z9",
|
||||
status: "Active",
|
||||
created: "2024-01-12",
|
||||
icon: <ServerCog className="h-4 w-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
const stats = [
|
||||
{ label: "총 클라이언트", value: "24", delta: "+2%", tone: "up" as const },
|
||||
{
|
||||
label: "활성 세션",
|
||||
value: "1,204",
|
||||
delta: "-5%",
|
||||
tone: "down" as const,
|
||||
},
|
||||
{
|
||||
label: "인증 실패 (24h)",
|
||||
value: "12",
|
||||
delta: "Stable",
|
||||
tone: "stable" as const,
|
||||
},
|
||||
];
|
||||
|
||||
function ClientsPage() {
|
||||
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.clientId}
|
||||
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.icon}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold">
|
||||
{client.name}
|
||||
</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.clientId}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-muted-foreground hover:text-primary"
|
||||
aria-label="Copy 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.created}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
to={`/clients/${client.clientId}`}
|
||||
>
|
||||
관리
|
||||
</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 1 to 3 of 24 clients</span>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
Previous
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
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 & 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;
|
||||
Reference in New Issue
Block a user