import { useQuery } from "@tanstack/react-query"; import { BadgeCheck, Building2, ChevronDown, Key, KeyRound, LayoutDashboard, LogOut, Moon, NotebookTabs, ShieldHalf, Sun, User as UserIcon, Users, } from "lucide-react"; import * as React from "react"; import { useEffect, useState } from "react"; import { useAuth } from "react-oidc-context"; import { NavLink, Outlet, useNavigate } from "react-router-dom"; import { fetchMe } from "../../lib/adminApi"; import { t } from "../../lib/i18n"; import LanguageSelector from "../common/LanguageSelector"; import RoleSwitcher from "./RoleSwitcher"; const staticNavItems = [ { label: "ui.admin.nav.overview", to: "/", icon: LayoutDashboard }, { label: "ui.admin.nav.users", to: "/users", icon: Users }, { label: "ui.admin.nav.api_keys", to: "/api-keys", icon: Key }, { label: "ui.admin.nav.audit_logs", to: "/audit-logs", icon: NotebookTabs }, { label: "ui.admin.nav.auth_guard", to: "/auth", icon: KeyRound }, ]; function AppLayout() { const auth = useAuth(); const navigate = useNavigate(); const [theme, setTheme] = useState<"light" | "dark">(() => { const stored = window.localStorage.getItem("admin_theme"); return stored === "dark" ? "dark" : "light"; }); const [isProfileOpen, setIsProfileOpen] = useState(false); const { data: profile } = useQuery({ queryKey: ["me"], queryFn: fetchMe, enabled: (auth.isAuthenticated && !auth.isLoading) || import.meta.env.MODE === "development" || (window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }) ._IS_TEST_MODE === true, }); const navItems = React.useMemo(() => { const items = [...staticNavItems]; const isTest = (window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }) ._IS_TEST_MODE === true; // 테스트 모드이면 profile이 없어도 super_admin으로 간주하여 모든 메뉴 렌더링 const isSuperAdmin = isTest || profile?.role === "super_admin"; const isTenantAdmin = profile?.role === "tenant_admin"; const manageableCount = profile?.manageableTenants?.length ?? 0; const filteredItems = items.filter((item) => { if (isTest) return true; if (item.to === "/api-keys") return isSuperAdmin; return true; }); if (isSuperAdmin) { filteredItems.splice(1, 0, { label: "ui.admin.nav.tenants", to: "/tenants", icon: Building2, }); } else if (isTenantAdmin || manageableCount > 0) { if (manageableCount <= 1 && profile?.tenantId) { // Direct link if only one (or zero in array but has tenantId) tenant filteredItems.splice(1, 0, { label: "ui.admin.nav.my_tenant", to: `/tenants/${profile.tenantId}`, icon: Building2, }); } else if (manageableCount > 1) { // Show list menu if multiple tenants filteredItems.splice(1, 0, { label: "ui.admin.nav.tenants", to: "/tenants", icon: Building2, }); } } return filteredItems; }, [profile]); const handleLogout = () => { if ( window.confirm(t("msg.admin.logout_confirm", "로그아웃 하시겠습니까?")) ) { window.localStorage.removeItem("admin_session"); auth.removeUser(); navigate("/login"); } }; useEffect(() => { if (!auth.isLoading && !auth.isAuthenticated) { navigate("/login"); } }, [auth.isLoading, auth.isAuthenticated, navigate]); useEffect(() => { if (auth.user?.access_token) { window.localStorage.setItem("admin_session", auth.user.access_token); } }, [auth.user]); useEffect(() => { const root = document.documentElement; root.classList.remove("light", "dark"); if (theme === "light") { root.classList.add("light"); } else { root.classList.add("dark"); } window.localStorage.setItem("admin_theme", theme); }, [theme]); const toggleTheme = () => { setTheme((prev) => (prev === "light" ? "dark" : "light")); }; if (auth.isLoading) { return (
); } return (

{t("ui.admin.header.plane", "Admin Plane")}

{t( "msg.admin.header.subtitle", "Tenant isolation & least privilege by default", )}
{isProfileOpen && ( <>
setIsProfileOpen(false)} onKeyDown={(e) => { if (e.key === "Escape") setIsProfileOpen(false); }} role="button" tabIndex={-1} aria-label="Close profile menu" />

{profile?.name || auth.user?.profile.name}

{profile?.email || auth.user?.profile.email}

{t( `ui.admin.role.${profile?.role || "user"}`, profile?.role || "USER", )}
{/* Manageable Tenants Section */} {profile?.manageableTenants && profile.manageableTenants.length > 0 && (

{t( "ui.admin.profile.manageable_tenants", "Manageable Tenants", )}

{profile.manageableTenants.map((tenant) => ( ))}
)}
)}
{t("msg.admin.session_ttl", "Session TTL: 15m admin")}
); } export default AppLayout;