fix: Login komplett kaputt seit dem Security-Header-Commit - CSP blockierte jede API-Anfrage
Backend Image / build-and-push (push) Successful in 2m16s
Frontend Image / build-and-push (push) Successful in 2m1s

Root Cause, zweifach:

1. proxy.ts lag im Projekt-Root statt unter src/ (wo app/ tatsaechlich
   liegt). Next.js hat die Datei dadurch nie erkannt - ueberprueft via
   Build-Output ("Proxy (Middleware)" fehlte komplett) und via
   routes-manifest.json. Das betraf nicht nur die neuen
   Security-Header, sondern auch die schon laenger bestehende
   Login-Redirect-Logik, die dadurch vermutlich nie serverseitig
   gegriffen hat - vorher unbemerkt, weil Client-seitige Auth-Checks
   das kaschiert haben.

2. Die CSP (mit dynamischem connect-src aus API_URL) lag in
   next.config.ts's headers() - die wird beim Docker-Build EINMAL
   aufgeloest und ins Standalone-Output eingefroren, nicht bei jedem
   Container-Start neu gelesen, wie im Code-Kommentar faelschlich
   behauptet. Jedes Deployment bekam dadurch permanent
   "connect-src http://localhost:3001" (den Fallback, da API_URL beim
   Build nicht gesetzt ist) - unabhaengig vom tatsaechlich im Pod
   gesetzten API_URL. Jede Anfrage vom Frontend ans Backend (Login,
   SSO-Status, Herkunftsseite) wurde dadurch von jedem Browser
   CSP-blockiert.

Fix: CSP + Security-Header ziehen nach proxy.ts (jetzt korrekt unter
src/) um - Proxy laeuft mit Node.js-Runtime frisch bei jeder Anfrage,
liest process.env.API_URL also korrekt zur Laufzeit. next.config.ts
behaelt nur noch die statische sw.js-Cache-Control-Regel.

Lokal end-to-end verifiziert: Build zeigt jetzt "ƒ Proxy (Middleware)"
(vorher komplett abwesend), CSP-Header zeigt nach Server-Start einen
distinct getesteten API_URL-Wert korrekt an (nicht mehr den
Localhost-Fallback), und ein echter Login-Klick im Browser schickt die
Anfrage jetzt an die richtige, zur Laufzeit gesetzte Backend-Origin.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Sonnet 5 committed 2026-09-02 21:44:23 +02:00
1 parent cd24523ea5
commit c95e1c4b18
3 files changed
+105 -81

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const SESSION_COOKIE = "refreshToken";
// Paths that redirect to /login when there's no session - kept identical to
// the set this file already covered before security headers moved in here.
// Broadening this would silently change auth-redirect behaviour for pages
// that currently rely on client-side auth checks instead (dashboard,
// bottling, etc.) and must not now start doing it as a side effect of the
// matcher below being widened for headers.
const AUTH_REDIRECT_PATTERNS = [
/^\/settings(\/.*)?$/,
/^\/apiaries(\/.*)?$/,
/^\/colonies(\/.*)?$/,
/^\/queens(\/.*)?$/,
];
function needsAuthRedirectCheck(pathname: string): boolean {
return AUTH_REDIRECT_PATTERNS.some((pattern) => pattern.test(pathname));
}
// Deliberately computed here, not in next.config.ts: that file's headers()
// is resolved once at build time and frozen into the standalone output, so
// process.env.API_URL there is whatever was (or wasn't) set during the
// Docker build - never the container's actual runtime value. Proxy runs the
// Node.js runtime fresh on every request, so this reads the real deployed
// API_URL correctly instead of silently falling back to localhost forever.
function apiOrigin(): string {
try {
return new URL(process.env.API_URL ?? "http://localhost:3001").origin;
} catch {
return "";
}
}
function cspHeader(): string {
return `
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data:;
font-src 'self';
connect-src 'self' ${apiOrigin()};
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'self';
upgrade-insecure-requests;
`
.replace(/\s{2,}/g, " ")
.trim();
}
function withSecurityHeaders(response: NextResponse): NextResponse {
response.headers.set("Content-Security-Policy", cspHeader());
response.headers.set("X-Frame-Options", "SAMEORIGIN");
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("Referrer-Policy", "no-referrer");
response.headers.set(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains",
);
return response;
}
export function proxy(request: NextRequest) {
const hasSession = request.cookies.has(SESSION_COOKIE);
const { pathname } = request.nextUrl;
if (pathname.startsWith("/login")) {
if (hasSession) {
return withSecurityHeaders(
NextResponse.redirect(new URL("/settings/users", request.url)),
);
}
return withSecurityHeaders(NextResponse.next());
}
if (needsAuthRedirectCheck(pathname) && !hasSession) {
return withSecurityHeaders(
NextResponse.redirect(new URL("/login", request.url)),
);
}
return withSecurityHeaders(NextResponse.next());
}
export const config = {
// Broadened from the original 5-path list so every page gets the security
// headers (not just the ones that also happen to need the auth-redirect
// check, which is now handled separately above, unchanged in scope).
matcher: [
"/((?!_next/static|_next/image|favicon.ico|sw.js|manifest.webmanifest|icon.svg).*)",
],
};