diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index a7c40e7..269a859 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -3,6 +3,8 @@ import { NavLink, Outlet } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { usePluginStore } from '@/store/pluginStore'; import { useUIStore } from '@/store/uiStore'; +import { usePermission } from '@/hooks/usePermission'; +import { useAuthStore } from '@/store/authStore'; import { Settings, Mail, Bell, Sparkles, Bot, Shield, Users, UsersRound, Package, ArrowLeft } from 'lucide-react'; const ICON_MAP: Record> = { @@ -22,12 +24,17 @@ export function SettingsPage() { const { t } = useTranslation(); const manifests = usePluginStore(s => s.manifests); const sidebarOpen = useUIStore((state) => state.sidebarOpen); + const { hasPermission } = usePermission(); + const user = useAuthStore((state) => state.user); const pluginSettingsPages = useMemo( () => (manifests || []) .flatMap((m) => (Array.isArray(m.settings_pages) ? m.settings_pages : [])) .filter((p): p is NonNullable => !!p && !!p.path) + // ARCH-006 parity: hide settings pages the user has no permission for + // (fail-closed while permissions are loading) + .filter(p => !p.permission || (user?.is_system_admin || hasPermission(p.permission))) .sort((a, b) => a.order - b.order), - [manifests] + [manifests, user] ); const hardcodedNavItems = [ @@ -44,11 +51,10 @@ export function SettingsPage() { ]; const existingPaths = new Set(hardcodedNavItems.map(item => item.to)); - // Filter out plugin nav items that are duplicates of hardcoded ones - const duplicateLabels = new Set(['Roles', 'Users', 'Groups', 'AI Settings', 'Automation', 'Proactive AI']); + // Path-based dedup only — the old label-based hack hid legitimate plugin + // pages whenever their English label collided with a hardcoded one. const pluginNavItems = pluginSettingsPages .filter(p => !existingPaths.has(`/settings/${p.path}`)) - .filter(p => !duplicateLabels.has(p.label)) .map(p => ({ to: `/settings/${p.path}`, label: t(p.label_key, p.label),