From 02c81bf4e451062982d1c883d5f917cb75c87576 Mon Sep 17 00:00:00 2001 From: lorentz Date: Tue, 17 Mar 2026 08:49:20 -0400 Subject: [PATCH] feat: enable Entra ID authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enable Better Auth middleware (was bypassed with return NextResponse.next()) - Re-enable AuthProvider in root layout - Add public routes for webhooks, sync, kiosk, QBO, legal, health endpoints - Set Microsoft Entra ID credentials and production Better Auth URLs - Hide Engagement and Admin nav sections from non-super-admins - Fix auth DB columns: snake_case → camelCase for Better Auth compatibility - Add twoFactorEnabled column to user table --- app/layout.tsx | 14 ++-- components/navigation/app-navigation.tsx | 14 +++- middleware.ts | 82 +++++++++++++++--------- 3 files changed, 70 insertions(+), 40 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index 3295be2..760172d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,8 +4,7 @@ import "./globals.css"; import { ThemeProvider } from "@/components/theme-provider"; import { AppNavigation } from "@/components/navigation/app-navigation"; import { Toaster } from "sonner"; -// TEMPORARY: AuthProvider disabled for private site access -// import { AuthProvider } from "@/components/auth/auth-provider"; +import { AuthProvider } from "@/components/auth/auth-provider"; const inter = Inter({ subsets: ["latin"] }); @@ -36,11 +35,12 @@ export default function RootLayout({ enableSystem disableTransitionOnChange > - {/* TEMPORARY: AuthProvider removed - TODO: Re-enable when site has public access */} -
- -
{children}
-
+ +
+ +
{children}
+
+
diff --git a/components/navigation/app-navigation.tsx b/components/navigation/app-navigation.tsx index 0f35382..18bd85b 100644 --- a/components/navigation/app-navigation.tsx +++ b/components/navigation/app-navigation.tsx @@ -38,6 +38,7 @@ import { } from '@/components/ui/navigation-menu'; import { Button } from '@/components/ui/button'; import { ThemeToggle } from '@/components/theme-toggle'; +import { useSession } from '@/lib/auth-client'; interface NavItem { title: string; @@ -190,12 +191,23 @@ const navigationItems: NavItem[] = [ export function AppNavigation() { const pathname = usePathname(); + const { data: session } = useSession(); + const userRole = (session?.user as any)?.role || 'user'; + const isSuperAdmin = userRole === 'super-admin'; const isActive = (href?: string) => { if (!href) return false; return pathname === href || pathname.startsWith(href + '/'); }; + // Hide Engagement and Admin from non-super-admins + const visibleItems = navigationItems.filter((item) => { + if (item.title === 'Engagement' || item.title === 'Admin') { + return isSuperAdmin; + } + return true; + }); + return (
@@ -215,7 +227,7 @@ export function AppNavigation() { {/* Main Navigation — centered */} - {navigationItems.map((item) => ( + {visibleItems.map((item) => ( {item.children ? ( <> diff --git a/middleware.ts b/middleware.ts index 8c1faa6..f1678f5 100644 --- a/middleware.ts +++ b/middleware.ts @@ -9,6 +9,28 @@ const publicRoutes = [ "/auth/2fa", "/auth/setup", "/api/auth", + // External service callbacks and webhooks + "/api/webhooks", + "/api/kiosk", + "/api/qbo/auth", + "/api/qbo/disconnect", + "/api/zabbix/webhook", + // Health and status checks + "/api/health", + "/api/integrations/status", + // Legal pages required by Intuit + "/legal", + // Sync endpoints called by scheduler + "/api/sync", + "/api/datto-rmm/sync", + "/api/itglue/sync", + "/api/veeam/sync", + "/api/sentinelone/sync", + "/api/engagement/sync", + "/api/zoom/sync", + "/api/qbo/sync", + "/api/reports/ticket-digest", + "/api/notifications/morning-summary/send", ]; // Routes that require admin or super-admin role @@ -17,44 +39,40 @@ const adminRoutes = ["/admin"]; export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; - // TEMPORARY: Authentication bypassed for private site access - // TODO: Re-enable authentication when site has public access - return NextResponse.next(); - // Allow public routes - // if (publicRoutes.some((route) => pathname.startsWith(route))) { - // return NextResponse.next(); - // } + if (publicRoutes.some((route) => pathname.startsWith(route))) { + return NextResponse.next(); + } - // // Allow static files and API routes (except admin API) - // if ( - // pathname.startsWith("/_next") || - // pathname.startsWith("/favicon") || - // pathname.includes(".") - // ) { - // return NextResponse.next(); - // } + // Allow static files + if ( + pathname.startsWith("/_next") || + pathname.startsWith("/favicon") || + pathname.includes(".") + ) { + return NextResponse.next(); + } - // // Check for session cookie - // const sessionCookie = getSessionCookie(request); + // Check for session cookie + const sessionCookie = getSessionCookie(request); - // if (!sessionCookie) { - // // Redirect to sign-in if no session - // const signInUrl = new URL("/auth/sign-in", request.url); - // signInUrl.searchParams.set("callbackUrl", pathname); - // return NextResponse.redirect(signInUrl); - // } + if (!sessionCookie) { + // Redirect to sign-in if no session + const signInUrl = new URL("/auth/sign-in", request.url); + signInUrl.searchParams.set("callbackUrl", pathname); + return NextResponse.redirect(signInUrl); + } - // // For admin routes, we need to verify the role - // // This is a basic check - the actual role verification happens in the API routes - // if (adminRoutes.some((route) => pathname.startsWith(route))) { - // // The session cookie exists, but we can't decode it here without the secret - // // Role-based access control is enforced at the API level - // // This middleware just ensures there's a session - // return NextResponse.next(); - // } + // For admin routes, we need to verify the role + // This is a basic check - the actual role verification happens in the API routes + if (adminRoutes.some((route) => pathname.startsWith(route))) { + // The session cookie exists, but we can't decode it here without the secret + // Role-based access control is enforced at the API level + // This middleware just ensures there's a session + return NextResponse.next(); + } - // return NextResponse.next(); + return NextResponse.next(); } export const config = {