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 (
+
+ );
+ }
+
+ 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 && (