From 39c3b397112a666488271dadcd6edaead2138f78 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 9 Apr 2026 23:24:22 +0200 Subject: [PATCH] feat: role-based sidebar visibility and landing redirect - Vendor (platform:admin): sees only TENANTS in sidebar - Tenant admin (tenant:manage): sees Dashboard, License, OIDC, Team, Settings - Regular user (operator/viewer): redirected to server dashboard directly - LandingRedirect checks scopes in priority order: vendor > admin > server Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/components/Layout.tsx | 99 +++++++++++++++++++----------------- ui/src/router.tsx | 14 ++++- 2 files changed, 65 insertions(+), 48 deletions(-) diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 5d271b7..25fd0e3 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -34,6 +34,7 @@ export function Layout() { const { username, organizations, currentOrgId } = useOrgStore(); const isVendor = scopes.has('platform:admin'); + const isTenantAdmin = scopes.has('tenant:manage'); // Determine current org slug for server dashboard link const currentOrg = organizations.find((o) => o.id === currentOrgId); @@ -41,7 +42,7 @@ export function Layout() { // Build breadcrumbs from path const segments = location.pathname.replace(/^\//, '').split('/').filter(Boolean); - const breadcrumb = segments.map((seg, i) => { + const breadcrumb = segments.map((seg) => { const label = seg.charAt(0).toUpperCase() + seg.slice(1).replace(/-/g, ' '); return { label }; }); @@ -67,56 +68,60 @@ export function Layout() { )} - {/* Tenant portal */} - } - label="Dashboard" - open={false} - active={location.pathname === '/tenant'} - onToggle={() => navigate('/tenant')} - > - {null} - + {/* Tenant portal — only visible to tenant admins (tenant:manage scope) */} + {isTenantAdmin && ( + <> + } + label="Dashboard" + open={false} + active={location.pathname === '/tenant'} + onToggle={() => navigate('/tenant')} + > + {null} + - } - label="License" - open={false} - active={isActive(location, '/tenant/license')} - onToggle={() => navigate('/tenant/license')} - > - {null} - + } + label="License" + open={false} + active={isActive(location, '/tenant/license')} + onToggle={() => navigate('/tenant/license')} + > + {null} + - } - label="OIDC" - open={false} - active={isActive(location, '/tenant/oidc')} - onToggle={() => navigate('/tenant/oidc')} - > - {null} - + } + label="OIDC" + open={false} + active={isActive(location, '/tenant/oidc')} + onToggle={() => navigate('/tenant/oidc')} + > + {null} + - } - label="Team" - open={false} - active={isActive(location, '/tenant/team')} - onToggle={() => navigate('/tenant/team')} - > - {null} - + } + label="Team" + open={false} + active={isActive(location, '/tenant/team')} + onToggle={() => navigate('/tenant/team')} + > + {null} + - } - label="Settings" - open={false} - active={isActive(location, '/tenant/settings')} - onToggle={() => navigate('/tenant/settings')} - > - {null} - + } + label="Settings" + open={false} + active={isActive(location, '/tenant/settings')} + onToggle={() => navigate('/tenant/settings')} + > + {null} + + + )} o.id === currentOrgId); + + // Vendor → vendor console if (scopes.has('platform:admin')) { return ; } - return ; + // Tenant admin → tenant portal + if (scopes.has('tenant:manage')) { + return ; + } + // Regular user (operator/viewer) → server dashboard directly + const serverUrl = currentOrg?.slug ? `/t/${currentOrg.slug}/` : '/server/'; + window.location.href = serverUrl; + return null; } export function AppRouter() {