diff --git a/adminfront/src/app/routes.tsx b/adminfront/src/app/routes.tsx index 054331cd..a390a46e 100644 --- a/adminfront/src/app/routes.tsx +++ b/adminfront/src/app/routes.tsx @@ -3,6 +3,10 @@ import AppLayout from "../components/layout/AppLayout"; import AuditLogsPage from "../features/audit/AuditLogsPage"; import AuthPage from "../features/auth/AuthPage"; import DashboardPage from "../features/dashboard/DashboardPage"; +import GlobalOverviewPage from "../features/overview/GlobalOverviewPage"; +import TenantCreatePage from "../features/tenants/TenantCreatePage"; +import TenantDetailPage from "../features/tenants/TenantDetailPage"; +import TenantListPage from "../features/tenants/TenantListPage"; export const router = createBrowserRouter( [ @@ -10,9 +14,13 @@ export const router = createBrowserRouter( path: "/", element: , children: [ - { index: true, element: }, + { index: true, element: }, + { path: "dashboard", element: }, { path: "audit-logs", element: }, { path: "auth", element: }, + { path: "tenants", element: }, + { path: "tenants/new", element: }, + { path: "tenants/:id", element: }, ], }, ], diff --git a/adminfront/src/components/layout/AppLayout.tsx b/adminfront/src/components/layout/AppLayout.tsx index 3df91ace..a412692a 100644 --- a/adminfront/src/components/layout/AppLayout.tsx +++ b/adminfront/src/components/layout/AppLayout.tsx @@ -1,5 +1,6 @@ import { BadgeCheck, + Building2, KeyRound, LayoutDashboard, Moon, @@ -12,6 +13,8 @@ import { NavLink, Outlet } from "react-router-dom"; const navItems = [ { label: "Overview", to: "/", icon: LayoutDashboard }, + { label: "Tenant Dashboard", to: "/dashboard", icon: ShieldHalf }, + { label: "Tenants", to: "/tenants", icon: Building2 }, { label: "Audit Logs", to: "/audit-logs", icon: NotebookTabs }, { label: "Auth Guard", to: "/auth", icon: KeyRound }, ]; diff --git a/adminfront/src/features/audit/AuditLogsPage.tsx b/adminfront/src/features/audit/AuditLogsPage.tsx index 2faf1973..b0299695 100644 --- a/adminfront/src/features/audit/AuditLogsPage.tsx +++ b/adminfront/src/features/audit/AuditLogsPage.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; import { Filter, ListChecks, Search, Terminal } from "lucide-react"; import { fetchAuditLogs } from "../../lib/adminApi"; @@ -22,7 +23,8 @@ function AuditLogsPage() { if (error) { const errMsg = - (error as any).response?.data?.error || (error as Error).message; + (error as AxiosError<{ error?: string }>).response?.data?.error ?? + (error as Error).message; return (
Error loading logs: {errMsg} @@ -88,10 +90,9 @@ function AuditLogsPage() { No audit logs found.
) : ( - logs.map((row, idx) => ( + logs.map((row) => (
{row.event_type}
diff --git a/adminfront/src/features/overview/GlobalOverviewPage.tsx b/adminfront/src/features/overview/GlobalOverviewPage.tsx new file mode 100644 index 00000000..b8a05e6e --- /dev/null +++ b/adminfront/src/features/overview/GlobalOverviewPage.tsx @@ -0,0 +1,170 @@ +import { + Activity, + ArrowUpRight, + Box, + Database, + ShieldCheck, + Users, +} 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"; + +const summaryCards = [ + { + label: "Total Tenants", + value: "-", + hint: "Tenant-aware core", + icon: Users, + }, + { + label: "OIDC Clients", + value: "-", + hint: "Hydra registry", + icon: ShieldCheck, + }, + { + label: "Audit Events (24h)", + value: "-", + hint: "ClickHouse stream", + icon: Activity, + }, + { + label: "Policy Gate", + value: "Planned", + hint: "Keto + Admin checks", + icon: Database, + }, +]; + +function GlobalOverviewPage() { + return ( +
+
+
+

+ Global Overview +

+

+ Tenant-independent control plane +

+

+ 모든 테넌트 공통 지표와 정책 상태를 한 곳에서 확인합니다. +

+
+
+ IDP: Ory primary + Fallback: Descope +
+
+ +
+ {summaryCards.map(({ label, value, hint, icon: Icon }) => ( + + + {label} +
+ +
+
+ +
{value}
+

{hint}

+
+
+ ))} +
+ +
+ + + Admin playbook + + 운영 정책, 레이트리밋, 감사 로그의 기본 룰을 요약합니다. + + + +
+
+ +
+
+

+ Backend-only IDP access +

+

+ 모든 IDP 호출은 backend를 통해서만 수행하며, Hydra/Kratos + admin 포트는 외부에 노출하지 않습니다. +

+
+
+
+
+ +
+
+

+ Tenant isolation +

+

+ Tenant 헤더와 감사 로그 규칙을 기본 적용하며, 향후 Keto + 정책으로 확장 예정입니다. +

+
+
+
+
+ + + + 빠른 이동 + + 주요 운영 화면으로 바로 이동합니다. + + + + + + + + +
+
+ ); +} + +export default GlobalOverviewPage; diff --git a/adminfront/src/features/tenants/TenantCreatePage.tsx b/adminfront/src/features/tenants/TenantCreatePage.tsx new file mode 100644 index 00000000..f48b67e4 --- /dev/null +++ b/adminfront/src/features/tenants/TenantCreatePage.tsx @@ -0,0 +1,153 @@ +import { useMutation } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { Building2, Sparkles } from "lucide-react"; +import { useState } from "react"; +import { useNavigate } 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 { Textarea } from "../../components/ui/textarea"; +import { createTenant } from "../../lib/adminApi"; + +function TenantCreatePage() { + const navigate = useNavigate(); + const [name, setName] = useState(""); + const [slug, setSlug] = useState(""); + const [description, setDescription] = useState(""); + const [status, setStatus] = useState("active"); + + const mutation = useMutation({ + mutationFn: () => + createTenant({ + name, + slug: slug || undefined, + description: description || undefined, + status, + }), + onSuccess: () => { + navigate("/tenants"); + }, + }); + + const errorMsg = (mutation.error as AxiosError<{ error?: string }>)?.response + ?.data?.error; + + return ( +
+
+
+ Tenants + / + Create +
+
+
+

테넌트 추가

+

+ 글로벌 운영 기준의 신규 테넌트를 등록합니다. +

+
+ Admin only +
+
+ + + + + + Tenant Profile + + + 필수 정보만 입력해도 생성 가능합니다. Slug는 없으면 자동 생성됩니다. + + + +
+ + setName(e.target.value)} /> +
+
+ + setSlug(e.target.value)} + placeholder="tenant-slug" + /> +
+
+ +