diff --git a/dashboard/app/panel/account/page.tsx b/dashboard/app/panel/account/page.tsx index 34117b0..2eb0a58 100644 --- a/dashboard/app/panel/account/page.tsx +++ b/dashboard/app/panel/account/page.tsx @@ -1,15 +1,17 @@ export const dynamic = 'error' -import UserOverview from '../components/UserOverview' -import MfaSetupPanel from './MfaSetupPanel' -import ThemePreferenceCard from './ThemePreferenceCard' +import { redirect } from 'next/navigation' -export default function AccountPage() { - return ( -
- - - -
- ) +import { resolveExtensionRouteComponent } from '@extensions/loader' + +export default async function AccountPage() { + try { + const Component = await resolveExtensionRouteComponent('/panel/account') + return + } catch (error) { + if (error instanceof Error && error.message.includes('disabled')) { + redirect('/panel') + } + throw error + } } diff --git a/dashboard/app/panel/agent/page.tsx b/dashboard/app/panel/agent/page.tsx index 53cc040..57defc1 100644 --- a/dashboard/app/panel/agent/page.tsx +++ b/dashboard/app/panel/agent/page.tsx @@ -1,12 +1,17 @@ export const dynamic = 'error' -import Card from '../components/Card' +import { redirect } from 'next/navigation' -export default function AgentPage() { - return ( - -

Agent Management

-

Manage node agents and rollout updates from a unified workspace.

-
- ) +import { resolveExtensionRouteComponent } from '@extensions/loader' + +export default async function AgentPage() { + try { + const Component = await resolveExtensionRouteComponent('/panel/agent') + return + } catch (error) { + if (error instanceof Error && error.message.includes('disabled')) { + redirect('/panel') + } + throw error + } } diff --git a/dashboard/app/panel/api/page.tsx b/dashboard/app/panel/api/page.tsx index 168c806..a8ee929 100644 --- a/dashboard/app/panel/api/page.tsx +++ b/dashboard/app/panel/api/page.tsx @@ -1,12 +1,17 @@ export const dynamic = 'error' -import Card from '../components/Card' +import { redirect } from 'next/navigation' -export default function APIPage() { - return ( - -

API Status

-

View backend API health and toggle feature matrices.

-
- ) +import { resolveExtensionRouteComponent } from '@extensions/loader' + +export default async function ApiPage() { + try { + const Component = await resolveExtensionRouteComponent('/panel/api') + return + } catch (error) { + if (error instanceof Error && error.message.includes('disabled')) { + redirect('/panel') + } + throw error + } } diff --git a/dashboard/app/panel/components/Sidebar.tsx b/dashboard/app/panel/components/Sidebar.tsx index 8503ddc..ac03799 100644 --- a/dashboard/app/panel/components/Sidebar.tsx +++ b/dashboard/app/panel/components/Sidebar.tsx @@ -2,13 +2,17 @@ import Link from 'next/link' import { usePathname } from 'next/navigation' -import { Home, Server, Code, CreditCard, User, Shield, Settings, type LucideIcon } from 'lucide-react' -import { useMemo } from 'react' +import { useMemo, type ComponentType } from 'react' +import { getExtensionRegistry } from '@extensions/loader' import { useLanguage } from '@i18n/LanguageProvider' import { translations } from '@i18n/translations' +import { resolveAccess } from '@lib/accessControl' import { useUser } from '@lib/userStore' +const registry = getExtensionRegistry() +const PlaceholderIcon: ComponentType<{ className?: string }> = () => null + export interface SidebarProps { className?: string onNavigate?: () => void @@ -18,8 +22,8 @@ interface NavItem { href: string label: string description: string - icon: LucideIcon - disabled?: boolean + Icon: ComponentType<{ className?: string }> + disabled: boolean } interface NavSection { @@ -27,65 +31,6 @@ interface NavSection { items: NavItem[] } -const baseNavSections: NavSection[] = [ - { - title: '用户中心', - items: [ - { - href: '/panel', - label: 'Dashboard', - description: '专属于你的信息总览', - icon: Home, - }, - ], - }, - { - title: '功能特性', - items: [ - { - href: '/panel/agent', - label: 'Agents', - description: '管理运行时节点', - icon: Server, - disabled: true, - }, - { - href: '/panel/api', - label: 'APIs', - description: '洞察后端服务', - icon: Code, - disabled: true, - }, - { - href: '/panel/subscription', - label: 'Subscription', - description: '订阅方案与计费规则', - icon: CreditCard, - disabled: true, - }, - ], - }, - { - title: '权限设置', - items: [ - { - href: '/panel/account', - label: 'Accounts', - description: '目录与多因素设置', - icon: User, - disabled: true, - }, - { - href: '/panel/ldp', - label: 'LDP', - description: '低时延身份平面', - icon: Shield, - disabled: true, - }, - ], - }, -] - function isActive(pathname: string, href: string) { if (href === '/panel') { return pathname === '/panel' @@ -100,28 +45,47 @@ export default function Sidebar({ className = '', onNavigate }: SidebarProps) { const { user } = useUser() const requiresSetup = Boolean(user && (!user.mfaEnabled || user.mfaPending)) - const navSections = useMemo(() => { - const sections = baseNavSections.map((section) => ({ - ...section, - items: [...section.items], - })) + const navSections = useMemo(() => { + return registry.sidebar + .map((section) => { + const items = section.items + .map((item) => { + const { route } = item + const guardResult = route.guard ? resolveAccess(user, route.guard) : { allowed: true } + const requiresRole = Boolean(route.guard?.roles?.length) + if (requiresRole && !guardResult.allowed) { + return null + } - if (user?.isAdmin || user?.isOperator) { - sections.push({ - title: '管理页面', - items: [ - { - href: '/panel/management', - label: 'Management', - description: '集中化的权限矩阵与用户编排', - icon: Settings, - }, - ], + const disabledByGuard = !requiresRole && !guardResult.allowed + const disabled = + item.disabled || + disabledByGuard || + (requiresSetup && route.path !== '/panel/account') + + const Icon = route.icon ?? PlaceholderIcon + + return { + href: route.path, + label: route.label, + description: route.description ?? '', + Icon, + disabled, + } + }) + .filter((value): value is NavItem => Boolean(value)) + + if (items.length === 0) { + return null + } + + return { + title: section.title, + items, + } }) - } - - return sections - }, [user?.isAdmin, user?.isOperator]) + .filter((value): value is NavSection => Boolean(value)) + }, [requiresSetup, user]) return (