SPB Git

spb/chat-spboucher Public

Private universal chat interface over the OpenRouter ecosystem — 400+ models, branching, streaming, usage tracking. Next.js 16 + SQLite, PWA, deployed on m4m64a at chat.spboucher.ai

TypeScript 78.8% CSS 15.1% JavaScript 4.9% Shell 1.2%
2.0 KB · 67 lines typescript
Raw Blame History
1// Author: Simon-Pierre Boucher2// Contact: contact@spboucher.ai3// Project: chat.spboucher.ai45// Edge proxy (Next 16 middleware convention): cheap cookie-presence gate + security headers.6// Real session validation (DB-backed) happens in every route handler via requireSession().7import { NextResponse, type NextRequest } from "next/server";89const SESSION_COOKIE = "spb_session";1011const PUBLIC_PATHS = new Set(["/login", "/api/auth/login", "/manifest.webmanifest", "/sw.js"]);1213function isPublic(pathname: string): boolean {14  if (PUBLIC_PATHS.has(pathname)) return true;15  if (pathname.startsWith("/_next/")) return true;16  if (pathname.startsWith("/icons/")) return true;17  if (pathname === "/favicon.ico") return true;18  return false;19}2021export function proxy(req: NextRequest) {22  const { pathname } = req.nextUrl;23  const hasCookie = Boolean(req.cookies.get(SESSION_COOKIE)?.value);2425  let res: NextResponse;26  if (!isPublic(pathname) && !hasCookie) {27    if (pathname.startsWith("/api/")) {28      res = NextResponse.json({ error: "Unauthorized" }, { status: 401 });29    } else {30      const url = req.nextUrl.clone();31      url.pathname = "/login";32      url.search = "";33      res = NextResponse.redirect(url);34    }35  } else if (pathname === "/login" && hasCookie) {36    const url = req.nextUrl.clone();37    url.pathname = "/";38    url.search = "";39    res = NextResponse.redirect(url);40  } else {41    res = NextResponse.next();42  }4344  res.headers.set("X-Content-Type-Options", "nosniff");45  res.headers.set("Referrer-Policy", "no-referrer");46  res.headers.set("X-Frame-Options", "DENY");47  res.headers.set(48    "Content-Security-Policy",49    [50      "default-src 'self'",51      "script-src 'self' 'unsafe-inline'",52      "style-src 'self' 'unsafe-inline'",53      "img-src 'self' data: blob:",54      "font-src 'self' data:",55      "connect-src 'self'",56      "frame-ancestors 'none'",57      "base-uri 'self'",58      "form-action 'self'",59    ].join("; ")60  );61  return res;62}6364export const config = {65  matcher: ["/((?!_next/static|_next/image).*)"],66};67