import { NextRequest, NextResponse } from 'next/server' // Middleware: protects all routes except public ones. // Uses a simple session cookie (HMAC-signed) for auth. // Password is set via ADMIN_PASSWORD env var. const PUBLIC_PATHS = [ '/api/health', '/api/aos/seed-prod', '/api/aos/licenses/check', '/api/aos/licenses/validate', '/api/aos/stripe/webhook', '/login', '/_next', '/favicon.ico', '/manifest.json', '/icons/', '/apple-touch-icon', ] const PUBLIC_API_PREFIXES = [ '/api/health', '/api/aos/seed-prod', '/api/aos/licenses/check', '/api/aos/licenses/validate', '/api/aos/stripe/webhook', '/api/v1/', // public REST API (token-auth'd separately) '/api/mcp', // MCP (token-auth'd) ] function isPublic(pathname: string): boolean { // Exact matches if (PUBLIC_PATHS.includes(pathname)) return true // Prefix matches for (const prefix of PUBLIC_API_PREFIXES) { if (pathname.startsWith(prefix)) return true } // Static assets if (pathname.startsWith('/_next/') || pathname.startsWith('/icons/')) return true return false } async function verifySession(token: string | undefined): Promise { if (!token) return false const adminPassword = process.env.ADMIN_PASSWORD if (!adminPassword) return false try { // Session token format: base64(payload).hmac const [payloadB64, signature] = token.split('.') if (!payloadB64 || !signature) return false const payload = JSON.parse(Buffer.from(payloadB64, 'base64').toString()) if (payload.exp && Date.now() > payload.exp) return false // Verify HMAC const crypto = await import('crypto') const key = process.env.AUTH_SECRET || adminPassword const expectedSig = crypto .createHmac('sha256', key) .update(payloadB64) .digest('base64url') return signature === expectedSig } catch { return false } } export async function middleware(req: NextRequest) { const { pathname } = req.nextUrl // Allow public paths if (isPublic(pathname)) { return NextResponse.next() } // Check session cookie const sessionToken = req.cookies.get('aos-session')?.value const isAuthenticated = await verifySession(sessionToken) if (!isAuthenticated) { // For API routes, return 401 if (pathname.startsWith('/api/')) { return NextResponse.json( { error: 'unauthorized', message: 'Authentication required' }, { status: 401 } ) } // For pages, redirect to login const loginUrl = new URL('/login', req.url) loginUrl.searchParams.set('redirect', pathname) return NextResponse.redirect(loginUrl) } return NextResponse.next() } export const config = { matcher: [ /* * Match all request paths except: * - _next/static, _next/image (static files) * - favicon.ico, icons, manifest (public assets) */ '/((?!_next/static|_next/image|favicon.ico|icons|manifest.json|apple-touch-icon).*)', ], }