Edge-runtime check via auth-cookie.verifySession. /api/* paths get a 401 (no body) when unauthenticated; pages get a 307 to /login with the original path encoded into ?next=. Allowlist explicitly excludes /api/events and /api/qr — both were unauthenticated in v1.1.0 and let an unauthenticated client snoop the entire SSE event stream and enumerate paired account QR codes.
42 lines
1.2 KiB
TypeScript
42 lines
1.2 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { COOKIE_NAME, verifySession } from "./lib/auth-cookie";
|
|
|
|
const PUBLIC_PATHS = new Set<string>([
|
|
"/login",
|
|
"/logout",
|
|
"/api/health",
|
|
"/manifest.webmanifest",
|
|
"/favicon.ico",
|
|
"/robots.txt",
|
|
]);
|
|
|
|
function isPublic(path: string): boolean {
|
|
if (PUBLIC_PATHS.has(path)) return true;
|
|
if (path.startsWith("/icon-")) return true;
|
|
if (path.startsWith("/_next/")) return true;
|
|
return false;
|
|
}
|
|
|
|
export async function middleware(req: NextRequest): Promise<NextResponse> {
|
|
const path = req.nextUrl.pathname;
|
|
if (isPublic(path)) return NextResponse.next();
|
|
|
|
const cookie = req.cookies.get(COOKIE_NAME)?.value;
|
|
const secret = process.env.AUTH_SECRET;
|
|
const ok =
|
|
!!cookie && !!secret && (await verifySession(cookie, secret)) !== null;
|
|
if (ok) return NextResponse.next();
|
|
|
|
if (path.startsWith("/api/")) {
|
|
return new NextResponse("Unauthorized", { status: 401 });
|
|
}
|
|
const url = req.nextUrl.clone();
|
|
url.pathname = "/login";
|
|
url.searchParams.set("next", path + (req.nextUrl.search || ""));
|
|
return NextResponse.redirect(url);
|
|
}
|
|
|
|
export const config = {
|
|
matcher: ["/((?!_next/static|_next/image).*)"],
|
|
};
|