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,143 @@
import { Filter, ListChecks, Search, Terminal } from "lucide-react";
const auditFilters = [
"Actor role = admin",
"Action = client.rotate_secret",
"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() {
return (
<div className="space-y-8">
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-xs uppercase tracking-[0.2em] text-[var(--color-muted)]">
Audit stream
</p>
<h2 className="text-2xl font-semibold">
Observe admin actions per tenant
</h2>
<p className="text-sm text-[var(--color-muted)]">
ClickHouse-backed feed. Filter by tenant, actor, action, and
rate-limit status. Enforce admin-only access under /admin.
</p>
</div>
<div className="flex items-center gap-2">
<button
type="button"
className="inline-flex items-center gap-2 rounded-full border border-[var(--color-border)] px-3 py-2 text-sm text-[var(--color-muted)]"
>
<Filter size={14} />
Saved filters
</button>
<button
type="button"
className="inline-flex items-center gap-2 rounded-full bg-[var(--color-accent)] px-4 py-2 text-sm font-semibold text-black"
>
<ListChecks size={14} />
Export CSV
</button>
</div>
</div>
<div className="grid gap-4 md:grid-cols-[1.1fr,0.9fr]">
<div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-panel)] p-5">
<div className="flex items-center gap-2 rounded-xl border border-[var(--color-border)] bg-[rgba(255,255,255,0.02)] px-3 py-2 text-[var(--color-muted)]">
<Search size={14} />
<span className="text-sm">
Try: tenant:TENANT-12 action:client.*
</span>
</div>
<div className="mt-4 space-y-3">
{auditFilters.map((filter) => (
<span
key={filter}
className="inline-flex items-center gap-2 rounded-full border border-[var(--color-border)] px-3 py-1 text-xs text-[var(--color-muted)]"
>
<Terminal size={12} />
{filter}
</span>
))}
</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>
</div>
))}
</div>
</div>
<div className="space-y-4">
<div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-panel)] p-5">
<p className="text-xs uppercase tracking-[0.18em] text-[var(--color-muted)]">
Guard rails
</p>
<h3 className="mt-1 text-lg font-semibold">Tenant admin only</h3>
<p className="text-sm text-[var(--color-muted)]">
Enforce Tenant Admin middleware and admin session TTL before
surfacing any audit feed. Super Admin role can bypass tenant
filter when needed.
</p>
</div>
<div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-panel)] p-5">
<p className="text-xs uppercase tracking-[0.18em] text-[var(--color-muted)]">
Export rules
</p>
<h3 className="mt-1 text-lg font-semibold">
Rate-limit sensitive exports
</h3>
<p className="text-sm text-[var(--color-muted)]">
Keep export endpoints behind admin-only routes with ClickHouse
query limits. Log download attempts with IP, role, and tenant
scope.
</p>
</div>
</div>
</div>
</div>
);
}
export default AuditLogsPage;

View File

@@ -0,0 +1,111 @@
import { ArrowRight, Fingerprint, Smartphone, Sparkles } from "lucide-react";
const flows = [
{
title: "Admin login",
description:
"Enforce short TTL and step-up MFA. Keep admin session separate from app session.",
pill: "15m TTL",
},
{
title: "Tenant pick",
description:
"Admin chooses target tenant before hitting APIs. Propagate X-Tenant-ID on every call.",
pill: "Header-ready",
},
{
title: "Device approval",
description:
"If app session exists and user opts in, use push/deeplink approval as MFA replacement.",
pill: "App session",
},
];
function AuthPage() {
return (
<div className="space-y-8">
<section className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-panel)] p-6 shadow-[var(--shadow-card)]">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-xs uppercase tracking-[0.2em] text-[var(--color-muted)]">
Admin auth
</p>
<h2 className="text-2xl font-semibold">Admin auth guardrails</h2>
<p className="text-sm text-[var(--color-muted)]">
Build the admin-only login flow first, keeping app login separate.
Respect the fallback only when user chooses rule for SMS/email
vs app approval.
</p>
</div>
<div className="flex items-center gap-2">
<span className="rounded-full border border-[var(--color-border)] px-3 py-2 text-sm text-[var(--color-muted)]">
IDP session placeholder
</span>
<button
type="button"
className="inline-flex items-center gap-2 rounded-full bg-[var(--color-accent)] px-4 py-2 text-sm font-semibold text-black"
>
<Sparkles size={14} />
Connect auth layer
</button>
</div>
</div>
</section>
<section className="grid gap-4 md:grid-cols-3">
{flows.map((flow) => (
<div
key={flow.title}
className="rounded-xl border border-[var(--color-border)] bg-[var(--color-panel)] p-5"
>
<div className="flex items-center justify-between text-xs uppercase tracking-[0.16em] text-[var(--color-muted)]">
<span>{flow.pill}</span>
<Fingerprint size={14} />
</div>
<h3 className="mt-3 text-lg font-semibold">{flow.title}</h3>
<p className="text-sm text-[var(--color-muted)]">
{flow.description}
</p>
</div>
))}
</section>
<section className="grid gap-6 md:grid-cols-[1fr,0.9fr]">
<div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-panel)] p-6">
<div className="flex items-center gap-2 text-[var(--color-muted)]">
<Smartphone size={16} />
<span className="text-xs uppercase tracking-[0.18em]">
App-based approvals
</span>
</div>
<h3 className="mt-2 text-xl font-semibold">
App session as MFA replacement
</h3>
<p className="text-sm text-[var(--color-muted)]">
If the admin keeps the mobile app signed in and opts in, use
push/deeplink approval instead of OTP. Otherwise fall back to
SMS/email based on user choice.
</p>
</div>
<div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-panel)] p-6">
<div className="flex items-center gap-2 text-[var(--color-muted)]">
<ArrowRight size={16} />
<span className="text-xs uppercase tracking-[0.18em]">
TTL discipline
</span>
</div>
<h3 className="mt-2 text-xl font-semibold">
Keep admin sessions short
</h3>
<p className="text-sm text-[var(--color-muted)]">
Default admin TTL is 15 minutes. Show countdown and nudge re-auth
with step-up MFA when critical actions (rotate secret, export logs)
happen.
</p>
</div>
</section>
</div>
);
}
export default AuthPage;

View File

@@ -0,0 +1,259 @@
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 &amp; 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;

View File

@@ -0,0 +1,194 @@
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 &amp; 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;

View File

@@ -0,0 +1,206 @@
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 &amp; 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;

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;

View File

@@ -0,0 +1,215 @@
import {
Activity,
ArrowRight,
BarChart3,
CheckCircle2,
Database,
KeyRound,
ShieldCheck,
Sparkles,
} from "lucide-react";
const guardHighlights = [
{
title: "RP 정책 통제",
body: "Relying Party 상태를 활성/비활성으로 관리하고 정책 변경을 기록합니다.",
metric: "Policy",
},
{
title: "Consent 흐름",
body: "사용자 Consent를 조회하고 필요 시 회수해 리스크를 제어합니다.",
metric: "Consent",
},
{
title: "Hydra Admin",
body: "Hydra Admin API를 통해 RP 등록 현황을 동기화합니다.",
metric: "Hydra",
},
];
const stackReadiness = [
"React 19 + Vite 7, strict TS, Router v6 data router.",
"TanStack Query 5로 RP/Consent 데이터를 캐시합니다.",
"Axios 클라이언트에서 Bearer + 테넌트 헤더를 주입합니다.",
"Tailwind + shadcn/ui로 devfront 톤을 맞춥니다.",
"Hydra Admin API 연동을 위한 프록시 엔드포인트 준비.",
];
const nextSteps = [
"RP 등록/수정/삭제 워크플로우 추가",
"Consent 검색 필터 고도화 및 CSV 내보내기",
"권한 가드 및 감사 로그 연동",
];
function DashboardPage() {
return (
<div className="space-y-10">
<section className="relative overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-panel)] p-7 shadow-[var(--shadow-card)]">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_24%_20%,rgba(54,211,153,0.14),transparent_32%)]" />
<div className="relative flex flex-col gap-6 md:flex-row md:items-center md:justify-between">
<div className="space-y-3 max-w-2xl">
<div className="inline-flex items-center gap-2 rounded-full border border-[var(--color-border)] px-3 py-1 text-xs uppercase tracking-[0.2em] text-[var(--color-muted)]">
<Sparkles size={14} />
devfront ready
</div>
<h2 className="text-3xl font-semibold leading-tight">
RP Consent
<span className="text-[var(--color-accent)]"> </span>
.
</h2>
<p className="text-[var(--color-muted)]">
Hydra Admin API와 RP , , Consent
devfront에서 .
</p>
<div className="flex flex-wrap gap-3 text-sm">
<span className="rounded-full bg-[rgba(54,211,153,0.16)] px-3 py-2 text-[var(--color-accent)]">
RP registry synced
</span>
<span className="rounded-full border border-[var(--color-border)] px-3 py-2 text-[var(--color-muted)]">
Consent guard ready
</span>
<span className="rounded-full bg-[rgba(249,168,38,0.16)] px-3 py-2 font-semibold text-[var(--color-accent-strong)]">
Policy toggle enabled
</span>
</div>
</div>
<div className="grid gap-3 text-sm">
<div className="flex items-center gap-2 rounded-xl border border-[var(--color-border)] bg-[rgba(255,255,255,0.02)] px-4 py-3 text-[var(--color-muted)]">
<ShieldCheck size={16} />
RP dev scope에서만
</div>
<div className="flex items-center gap-2 rounded-xl border border-[var(--color-border)] bg-[rgba(255,255,255,0.02)] px-4 py-3 text-[var(--color-muted)]">
<KeyRound size={16} />
Consent
</div>
<div className="flex items-center gap-2 rounded-xl border border-[var(--color-border)] bg-[rgba(255,255,255,0.02)] px-4 py-3 text-[var(--color-muted)]">
<Database size={16} />
Hydra Admin
</div>
</div>
</div>
</section>
<section className="grid gap-4 md:grid-cols-3">
{guardHighlights.map((item) => (
<div
key={item.title}
className="relative overflow-hidden rounded-xl border border-[var(--color-border)] bg-[var(--color-panel)] p-5 transition hover:-translate-y-1 hover:shadow-[0_16px_48px_rgba(7,15,26,0.4)]"
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_25%_25%,rgba(54,211,153,0.12),transparent_45%)]" />
<div className="relative flex items-center justify-between gap-2">
<div className="text-xs uppercase tracking-[0.2em] text-[var(--color-muted)]">
{item.metric}
</div>
<span className="rounded-full border border-[var(--color-border)] px-3 py-1 text-[11px] text-[var(--color-muted)]">
active
</span>
</div>
<div className="relative mt-3 space-y-2">
<h3 className="text-lg font-semibold">{item.title}</h3>
<p className="text-sm text-[var(--color-muted)]">{item.body}</p>
</div>
</div>
))}
</section>
<section className="grid gap-6 md:grid-cols-[1.2fr,0.8fr]">
<div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-panel)] p-6">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-xs uppercase tracking-[0.2em] text-[var(--color-muted)]">
Stack readiness
</p>
<h3 className="text-xl font-semibold">Devfront baseline</h3>
</div>
<button
type="button"
className="inline-flex items-center gap-2 rounded-full border border-[var(--color-border)] px-3 py-2 text-sm text-[var(--color-muted)] transition hover:border-[var(--color-accent)] hover:text-[var(--color-accent)]"
>
Setup notes
<ArrowRight size={14} />
</button>
</div>
<div className="mt-4 grid gap-3 md:grid-cols-2">
{stackReadiness.map((item) => (
<div
key={item}
className="flex items-center gap-3 rounded-xl border border-[var(--color-border)] bg-[rgba(255,255,255,0.02)] px-4 py-3"
>
<CheckCircle2
size={16}
className="text-[var(--color-accent)]"
/>
<p className="text-sm">{item}</p>
</div>
))}
</div>
</div>
<div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-panel)] p-6">
<p className="text-xs uppercase tracking-[0.2em] text-[var(--color-muted)]">
Next actions
</p>
<h3 className="mt-2 text-xl font-semibold">Ship the RP controls</h3>
<div className="mt-4 space-y-3">
{nextSteps.map((item, idx) => (
<div
key={item}
className="flex gap-3 rounded-xl border border-[var(--color-border)] bg-[rgba(255,255,255,0.02)] px-4 py-3"
>
<div className="grid h-8 w-8 place-items-center rounded-full bg-[rgba(249,168,38,0.12)] text-sm font-semibold text-[var(--color-accent-strong)]">
{idx + 1}
</div>
<p className="text-sm text-[var(--color-text)]">{item}</p>
</div>
))}
</div>
</div>
</section>
<section className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-panel)] p-6">
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-xs uppercase tracking-[0.2em] text-[var(--color-muted)]">
Ops board
</p>
<h3 className="text-xl font-semibold"> </h3>
</div>
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
<span className="rounded-full border border-[var(--color-border)] px-3 py-2">
Consent grants
</span>
<span className="rounded-full border border-[var(--color-border)] px-3 py-2">
RP status
</span>
</div>
</div>
<div className="mt-4 grid gap-4 md:grid-cols-3">
<div className="rounded-xl border border-[var(--color-border)] bg-[rgba(255,255,255,0.02)] p-4">
<div className="flex items-center gap-2 text-[var(--color-muted)]">
<BarChart3 size={16} />
RP
</div>
<p className="mt-3 text-2xl font-semibold"> </p>
</div>
<div className="rounded-xl border border-[var(--color-border)] bg-[rgba(255,255,255,0.02)] p-4">
<div className="flex items-center gap-2 text-[var(--color-muted)]">
<Activity size={16} />
Consent
</div>
<p className="mt-3 text-2xl font-semibold"> </p>
</div>
<div className="rounded-xl border border-[var(--color-border)] bg-[rgba(255,255,255,0.02)] p-4">
<div className="flex items-center gap-2 text-[var(--color-muted)]">
<Database size={16} />
Hydra
</div>
<p className="mt-3 text-2xl font-semibold"></p>
</div>
</div>
</section>
</div>
);
}
export default DashboardPage;