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) <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ export function Layout() {
|
|||||||
const { username, organizations, currentOrgId } = useOrgStore();
|
const { username, organizations, currentOrgId } = useOrgStore();
|
||||||
|
|
||||||
const isVendor = scopes.has('platform:admin');
|
const isVendor = scopes.has('platform:admin');
|
||||||
|
const isTenantAdmin = scopes.has('tenant:manage');
|
||||||
|
|
||||||
// Determine current org slug for server dashboard link
|
// Determine current org slug for server dashboard link
|
||||||
const currentOrg = organizations.find((o) => o.id === currentOrgId);
|
const currentOrg = organizations.find((o) => o.id === currentOrgId);
|
||||||
@@ -41,7 +42,7 @@ export function Layout() {
|
|||||||
|
|
||||||
// Build breadcrumbs from path
|
// Build breadcrumbs from path
|
||||||
const segments = location.pathname.replace(/^\//, '').split('/').filter(Boolean);
|
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, ' ');
|
const label = seg.charAt(0).toUpperCase() + seg.slice(1).replace(/-/g, ' ');
|
||||||
return { label };
|
return { label };
|
||||||
});
|
});
|
||||||
@@ -67,56 +68,60 @@ export function Layout() {
|
|||||||
</Sidebar.Section>
|
</Sidebar.Section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tenant portal */}
|
{/* Tenant portal — only visible to tenant admins (tenant:manage scope) */}
|
||||||
<Sidebar.Section
|
{isTenantAdmin && (
|
||||||
icon={<LayoutDashboard size={16} />}
|
<>
|
||||||
label="Dashboard"
|
<Sidebar.Section
|
||||||
open={false}
|
icon={<LayoutDashboard size={16} />}
|
||||||
active={location.pathname === '/tenant'}
|
label="Dashboard"
|
||||||
onToggle={() => navigate('/tenant')}
|
open={false}
|
||||||
>
|
active={location.pathname === '/tenant'}
|
||||||
{null}
|
onToggle={() => navigate('/tenant')}
|
||||||
</Sidebar.Section>
|
>
|
||||||
|
{null}
|
||||||
|
</Sidebar.Section>
|
||||||
|
|
||||||
<Sidebar.Section
|
<Sidebar.Section
|
||||||
icon={<ShieldCheck size={16} />}
|
icon={<ShieldCheck size={16} />}
|
||||||
label="License"
|
label="License"
|
||||||
open={false}
|
open={false}
|
||||||
active={isActive(location, '/tenant/license')}
|
active={isActive(location, '/tenant/license')}
|
||||||
onToggle={() => navigate('/tenant/license')}
|
onToggle={() => navigate('/tenant/license')}
|
||||||
>
|
>
|
||||||
{null}
|
{null}
|
||||||
</Sidebar.Section>
|
</Sidebar.Section>
|
||||||
|
|
||||||
<Sidebar.Section
|
<Sidebar.Section
|
||||||
icon={<KeyRound size={16} />}
|
icon={<KeyRound size={16} />}
|
||||||
label="OIDC"
|
label="OIDC"
|
||||||
open={false}
|
open={false}
|
||||||
active={isActive(location, '/tenant/oidc')}
|
active={isActive(location, '/tenant/oidc')}
|
||||||
onToggle={() => navigate('/tenant/oidc')}
|
onToggle={() => navigate('/tenant/oidc')}
|
||||||
>
|
>
|
||||||
{null}
|
{null}
|
||||||
</Sidebar.Section>
|
</Sidebar.Section>
|
||||||
|
|
||||||
<Sidebar.Section
|
<Sidebar.Section
|
||||||
icon={<Users size={16} />}
|
icon={<Users size={16} />}
|
||||||
label="Team"
|
label="Team"
|
||||||
open={false}
|
open={false}
|
||||||
active={isActive(location, '/tenant/team')}
|
active={isActive(location, '/tenant/team')}
|
||||||
onToggle={() => navigate('/tenant/team')}
|
onToggle={() => navigate('/tenant/team')}
|
||||||
>
|
>
|
||||||
{null}
|
{null}
|
||||||
</Sidebar.Section>
|
</Sidebar.Section>
|
||||||
|
|
||||||
<Sidebar.Section
|
<Sidebar.Section
|
||||||
icon={<Settings size={16} />}
|
icon={<Settings size={16} />}
|
||||||
label="Settings"
|
label="Settings"
|
||||||
open={false}
|
open={false}
|
||||||
active={isActive(location, '/tenant/settings')}
|
active={isActive(location, '/tenant/settings')}
|
||||||
onToggle={() => navigate('/tenant/settings')}
|
onToggle={() => navigate('/tenant/settings')}
|
||||||
>
|
>
|
||||||
{null}
|
{null}
|
||||||
</Sidebar.Section>
|
</Sidebar.Section>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Sidebar.Footer>
|
<Sidebar.Footer>
|
||||||
<Sidebar.FooterLink
|
<Sidebar.FooterLink
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { OrgResolver } from './auth/OrgResolver';
|
|||||||
import { Layout } from './components/Layout';
|
import { Layout } from './components/Layout';
|
||||||
import { RequireScope } from './components/RequireScope';
|
import { RequireScope } from './components/RequireScope';
|
||||||
import { useScopes } from './auth/useScopes';
|
import { useScopes } from './auth/useScopes';
|
||||||
|
import { useOrgStore } from './auth/useOrganization';
|
||||||
|
|
||||||
import { VendorTenantsPage } from './pages/vendor/VendorTenantsPage';
|
import { VendorTenantsPage } from './pages/vendor/VendorTenantsPage';
|
||||||
import { CreateTenantPage } from './pages/vendor/CreateTenantPage';
|
import { CreateTenantPage } from './pages/vendor/CreateTenantPage';
|
||||||
@@ -18,10 +19,21 @@ import { SettingsPage } from './pages/tenant/SettingsPage';
|
|||||||
|
|
||||||
function LandingRedirect() {
|
function LandingRedirect() {
|
||||||
const scopes = useScopes();
|
const scopes = useScopes();
|
||||||
|
const { organizations, currentOrgId } = useOrgStore();
|
||||||
|
const currentOrg = organizations.find((o) => o.id === currentOrgId);
|
||||||
|
|
||||||
|
// Vendor → vendor console
|
||||||
if (scopes.has('platform:admin')) {
|
if (scopes.has('platform:admin')) {
|
||||||
return <Navigate to="/vendor/tenants" replace />;
|
return <Navigate to="/vendor/tenants" replace />;
|
||||||
}
|
}
|
||||||
return <Navigate to="/tenant" replace />;
|
// Tenant admin → tenant portal
|
||||||
|
if (scopes.has('tenant:manage')) {
|
||||||
|
return <Navigate to="/tenant" replace />;
|
||||||
|
}
|
||||||
|
// Regular user (operator/viewer) → server dashboard directly
|
||||||
|
const serverUrl = currentOrg?.slug ? `/t/${currentOrg.slug}/` : '/server/';
|
||||||
|
window.location.href = serverUrl;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppRouter() {
|
export function AppRouter() {
|
||||||
|
|||||||
Reference in New Issue
Block a user