diff --git a/frontends/nextjs/src/app/[tenant]/[package]/layout.tsx b/frontends/nextjs/src/app/[tenant]/[package]/layout.tsx new file mode 100644 index 000000000..fbe5ac194 --- /dev/null +++ b/frontends/nextjs/src/app/[tenant]/[package]/layout.tsx @@ -0,0 +1,48 @@ +/** + * Tenant-Scoped Layout + * + * This layout wraps all pages under /{tenant}/{package}/... + * It provides tenant context to all child components. + */ + +import { headers } from 'next/headers' +import { notFound } from 'next/navigation' +import { TenantProvider } from './tenant-context' + +interface TenantLayoutProps { + children: React.ReactNode + params: Promise<{ + tenant: string + package: string + }> +} + +export default async function TenantLayout({ + children, + params, +}: TenantLayoutProps) { + const resolvedParams = await params + const { tenant, package: pkg } = resolvedParams + + // Validate tenant exists + // TODO: Check against database + // const tenantData = await getTenant(tenant) + // if (!tenantData) { + // notFound() + // } + + // Validate package exists and is installed for tenant + // TODO: Check against database + // const packageInstalled = await isPackageInstalled(tenant, pkg) + // if (!packageInstalled) { + // notFound() + // } + + return ( + +
+ {children} +
+
+ ) +} diff --git a/frontends/nextjs/src/middleware.ts b/frontends/nextjs/src/middleware.ts new file mode 100644 index 000000000..f00cb66af --- /dev/null +++ b/frontends/nextjs/src/middleware.ts @@ -0,0 +1,51 @@ +/** + * Next.js Middleware + * + * Handles multi-tenant routing at the edge. + * Routes: /{tenant}/{package}/... are treated as tenant-scoped requests. + */ + +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' +import { isReservedPath, RESERVED_PATHS } from '@/lib/routing/route-parser' + +export function middleware(request: NextRequest) { + const { pathname } = request.nextUrl + + // Skip reserved paths + const firstSegment = pathname.split('/')[1] + if (!firstSegment || isReservedPath(firstSegment)) { + return NextResponse.next() + } + + // Check if this looks like a tenant route: /{tenant}/{package}/... + const segments = pathname.split('/').filter(Boolean) + + if (segments.length >= 2) { + // Looks like a tenant route + const tenant = segments[0] + const pkg = segments[1] + + // Add tenant info to headers for downstream use + const response = NextResponse.next() + response.headers.set('x-tenant-id', tenant) + response.headers.set('x-package-id', pkg) + + return response + } + + return NextResponse.next() +} + +export const config = { + matcher: [ + /* + * Match all request paths except: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * - public folder + */ + '/((?!_next/static|_next/image|favicon.ico|public/).*)', + ], +}