diff --git a/portal/src/app/it/page.tsx b/portal/src/app/it/page.tsx index e47915f..bc45365 100644 --- a/portal/src/app/it/page.tsx +++ b/portal/src/app/it/page.tsx @@ -3,8 +3,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { RoleGate } from '@/components/auth/RoleGate'; -import { IT_OPS_ALLOWED_ROLES } from '@/lib/it-ops-roles'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'; +import { IT_OPS_ALLOWED_ROLES } from '@/lib/it-ops-roles'; type DriftShape = { collected_at?: string; diff --git a/portal/src/components/auth/RoleGate.tsx b/portal/src/components/auth/RoleGate.tsx new file mode 100644 index 0000000..89fa788 --- /dev/null +++ b/portal/src/components/auth/RoleGate.tsx @@ -0,0 +1,89 @@ +'use client'; + +import Link from 'next/link'; +import { useSession } from 'next-auth/react'; +import { type ReactNode } from 'react'; + +import { PortalSignInCard } from '@/components/auth/PortalSignInCard'; + +interface RoleGateProps { + allowedRoles: readonly string[]; + callbackUrl: string; + badge: string; + title: string; + subtitle: string; + children: ReactNode; +} + +function hasAllowedRole(sessionRoles: string[] | undefined, allowedRoles: readonly string[]) { + const normalizedAllowed = new Set(allowedRoles.map((role) => role.toLowerCase())); + return (sessionRoles || []).some((role) => normalizedAllowed.has(role.toLowerCase())); +} + +export function RoleGate({ + allowedRoles, + callbackUrl, + badge, + title, + subtitle, + children, +}: RoleGateProps) { + const { data: session, status } = useSession(); + + if (status === 'loading') { + return ( +
+
+
+

Loading...

+
+
+ ); + } + + if (status === 'unauthenticated') { + return ( +
+ +
+ ); + } + + if (!hasAllowedRole(session?.roles, allowedRoles)) { + return ( +
+
+

{badge}

+

Access Restricted

+

+ Your account does not currently include one of the roles required for this workspace. +

+

+ Required roles: {allowedRoles.join(', ')} +

+
+ + Return Home + + + Contact Support + +
+
+
+ ); + } + + return <>{children}; +} diff --git a/portal/src/components/layout/MobileNavigation.tsx b/portal/src/components/layout/MobileNavigation.tsx index e0b4156..407929d 100644 --- a/portal/src/components/layout/MobileNavigation.tsx +++ b/portal/src/components/layout/MobileNavigation.tsx @@ -1,32 +1,11 @@ 'use client'; -import { - LayoutDashboard, - Server, - Network, - Settings, - Activity, - Users, - CreditCard, - Shield, - Menu, - X, -} from 'lucide-react'; +import { Menu, X } from 'lucide-react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { useState } from 'react'; -const navigation = [ - { name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard }, - { name: 'Resources', href: '/resources', icon: Server }, - { name: 'Virtual Machines', href: '/vms', icon: Server }, - { name: 'Networking', href: '/network', icon: Network }, - { name: 'Monitoring', href: '/dashboards', icon: Activity }, - { name: 'Users & Access', href: '/users', icon: Users }, - { name: 'Billing', href: '/billing', icon: CreditCard }, - { name: 'Security', href: '/security', icon: Shield }, - { name: 'Settings', href: '/settings', icon: Settings }, -]; +import { primaryNavigation } from '@/lib/portal-navigation'; export function MobileNavigation() { const [isOpen, setIsOpen] = useState(false); @@ -47,7 +26,7 @@ export function MobileNavigation() { {isOpen && (