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', '/api/auth/login', '/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/auth/login', '/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) { console.log('[auth] No session token') return false } const adminPassword = process.env.ADMIN_PASSWORD if (!adminPassword) { console.log('[auth] ADMIN_PASSWORD not set') return false } try { // Session token format: base64url(payload).hmac const [payloadB64, signature] = token.split('.') if (!payloadB64 || !signature) { console.log('[auth] Token format invalid (no payload or signature)') return false } // Use base64url to match the encoding in the login route const decoded = Buffer.from(payloadB64, 'base64url').toString() const payload = JSON.parse(decoded) if (payload.exp && Date.now() > payload.exp) { console.log('[auth] Token expired') 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') const match = signature === expectedSig if (!match) { console.log('[auth] HMAC mismatch') console.log('[auth] expected:', expectedSig?.substring(0, 20)) console.log('[auth] got:', signature?.substring(0, 20)) console.log('[auth] AUTH_SECRET set:', !!process.env.AUTH_SECRET) console.log('[auth] ADMIN_PASSWORD set:', !!adminPassword) } return match } catch (e) { console.log('[auth] Verification error:', e instanceof Error ? e.message : 'unknown') 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 adminPassword = process.env.ADMIN_PASSWORD // Debug logging (remove in production) console.log('[middleware]', pathname, '| hasCookie:', !!sessionToken, '| hasAdminPw:', !!adminPassword, '| AUTH_SECRET:', !!process.env.AUTH_SECRET) if (!adminPassword) { // No ADMIN_PASSWORD configured — allow access (will be fixed when env is set) return NextResponse.next() } 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).*)', ], }