import { createHash, timingSafeEqual } from "node:crypto"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { isAdminNetworkAllowed } from "./app/_lib/auth"; function tokenMatches(actual: string, expected: string) { const actualValue = createHash("sha256").update(actual).digest(); const expectedValue = createHash("sha256").update(expected).digest(); return timingSafeEqual(actualValue, expectedValue); } export function proxy(request: NextRequest) { const { pathname } = request.nextUrl; if (pathname === "/api/health") return NextResponse.next(); // Admin has its own password + session and is reachable only from a direct LAN connection. if (pathname === "/admin" || pathname.startsWith("/admin/") || pathname.startsWith("/api/admin/") || pathname === "/photo-admin") { if (!isAdminNetworkAllowed(request.headers)) { return new NextResponse("Not found", { status: 404, headers: { "Cache-Control": "no-store" } }); } if (pathname === "/photo-admin") { const target = new URL("/admin", request.url); target.host = request.headers.get("host") || target.host; target.hash = "photos"; return NextResponse.redirect(target, 307); } return NextResponse.next(); } const configuredToken = process.env.ORGANIZER_ACCESS_TOKEN?.trim(); if (!configuredToken) return NextResponse.next(); const authorization = request.headers.get("authorization") || ""; const suppliedToken = authorization.startsWith("Bearer ") ? authorization.slice(7) : ""; if (tokenMatches(suppliedToken, configuredToken)) return NextResponse.next(); return NextResponse.json( { error: "Unauthorized" }, { status: 401, headers: { "Cache-Control": "no-store", "WWW-Authenticate": "Bearer realm=\"Hobbs Family Organizer\"", }, }, ); } export const config = { matcher: ["/((?!api/health).*)"], };