import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' import { jwtVerify } from 'jose' // Dev fallback secret — must match lib/session.ts const DEV_FALLBACK_SECRET = 'dev-secret-NOT-for-production-change-me-32chars!!' function getSecretKey(): Uint8Array { const secret = process.env.AUTH_SECRET ?? DEV_FALLBACK_SECRET return new TextEncoder().encode(secret) } /** Paths (exact) that never require authentication. */ const PUBLIC_EXACT = new Set(['/', '/login']) /** Path prefixes that never require authentication. */ const PUBLIC_PREFIXES = [ '/files/', // share link pages — public by design '/api/files/', // individual file info + download — share links must work '/api/auth/', // auth endpoints themselves '/_next/', ] /** Path prefixes that require admin role in addition to being authenticated. */ const ADMIN_PREFIXES = ['/admin', '/api/admin'] function isPublic(pathname: string): boolean { if (PUBLIC_EXACT.has(pathname)) return true return PUBLIC_PREFIXES.some((p) => pathname.startsWith(p)) } function isAdminRoute(pathname: string): boolean { return ADMIN_PREFIXES.some((p) => pathname.startsWith(p)) } const SECURITY_HEADERS: Record = { 'X-Frame-Options': 'DENY', 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'strict-origin-when-cross-origin', 'Permissions-Policy': 'camera=(), microphone=(), geolocation=()', 'X-DNS-Prefetch-Control': 'off', } export async function proxy(request: NextRequest) { const { pathname } = request.nextUrl // Build the base response with security headers applied to every response function withHeaders(res: NextResponse): NextResponse { for (const [k, v] of Object.entries(SECURITY_HEADERS)) { res.headers.set(k, v) } return res } // Static assets — just add security headers if (pathname.startsWith('/_next/') || pathname === '/favicon.ico') { return withHeaders(NextResponse.next()) } // Auth disabled (no secret configured) — pass all traffic through if (!process.env.AUTH_SECRET && process.env.NODE_ENV !== 'production') { // Still add security headers in dev mode return withHeaders(NextResponse.next()) } // Public routes — no auth needed if (isPublic(pathname)) { return withHeaders(NextResponse.next()) } // Attempt to validate session const token = request.cookies.get('backerup_session')?.value let payload: { userId?: string; username?: string; role?: string } | null = null if (token) { try { const { payload: p } = await jwtVerify(token, getSecretKey(), { algorithms: ['HS256'] }) payload = p } catch { // expired or tampered — treat as unauthenticated } } // Not authenticated if (!payload) { if (pathname.startsWith('/api/')) { return withHeaders(NextResponse.json({ error: 'Unauthorized' }, { status: 401 })) } const loginUrl = request.nextUrl.clone() loginUrl.pathname = '/login' loginUrl.searchParams.set('from', pathname) return NextResponse.redirect(loginUrl) } // Admin-only routes if (isAdminRoute(pathname) && payload.role !== 'admin') { if (pathname.startsWith('/api/')) { return withHeaders(NextResponse.json({ error: 'Forbidden' }, { status: 403 })) } return withHeaders(NextResponse.redirect(new URL('/', request.url))) } // Authenticated — pass through const response = NextResponse.next() // Forward user info to server components/API routes via headers response.headers.set('x-user-id', payload.userId ?? '') response.headers.set('x-user-name', payload.username ?? '') response.headers.set('x-user-role', payload.role ?? '') return withHeaders(response) } export const config = { matcher: ['/((?!_next/static|_next/image|public/).*)'], }