Basis: Certvia dev@a48c5fb als Fundament für Craftvia
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s

Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation
und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 11:05:39 +02:00
co-authored by Claude Opus 5
commit c8e6f30a27
720 changed files with 140143 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
import { NextResponse, type NextRequest } from "next/server";
/**
* Route gate (Next.js 16 proxy, formerly middleware): redirects anonymous
* visitors to /login. This is a UX-level gate only — authoritative checks
* happen server-side via requireSession()/requirePermission().
*/
// SEC2: die Wiederherstellungs-Abläufe müssen ohne Session erreichbar sein —
// der Nutzer ist gerade ausgesperrt. Ihre Absicherung sind Rate-Limit,
// Enumeration-Neutralität und single-use-Tokens, nicht dieses Gate.
const PUBLIC_PATHS = ["/login", "/api/auth", "/forgot-password", "/reset", "/invite", "/verify-email", "/platform/login", "/api/platform-auth"];
// Plattform-Bereich (getrennte Session/Login): diese Routen werden über das
// Plattform-Cookie gegatet und leiten anonyme Besucher auf /platform/login —
// NICHT auf die Mandanten-Login-Maske. `/admin` deckt auch `/admin/<id>` ab.
const PLATFORM_PATHS = ["/admin", "/admins", "/profile", "/platform", "/templates"];
const matchesAny = (pathname: string, paths: string[]) =>
paths.some((p) => pathname === p || pathname.startsWith(p + "/"));
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
if (matchesAny(pathname, PUBLIC_PATHS)) {
return NextResponse.next();
}
// Plattform-Routen: eigenes Cookie, eigener Login. Ohne Tenant-Cookie-Bezug, sonst
// landet ein angemeldeter Plattform-Admin fälschlich auf der Mandanten-Login-Maske.
if (matchesAny(pathname, PLATFORM_PATHS)) {
const hasPlatformCookie =
request.cookies.has("platform-authjs.session-token") ||
request.cookies.has("__Secure-platform-authjs.session-token");
if (!hasPlatformCookie) {
return NextResponse.redirect(new URL("/platform/login", request.url));
}
return NextResponse.next();
}
const hasSessionCookie =
request.cookies.has("authjs.session-token") ||
request.cookies.has("__Secure-authjs.session-token");
if (!hasSessionCookie) {
const loginUrl = new URL("/login", request.url);
if (pathname !== "/") loginUrl.searchParams.set("callbackUrl", pathname);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
export const config = {
// Everything except static assets.
//
// Ergänzt um `webmanifest`: das PWA-Manifest muss auch ohne Session ausgeliefert
// werden, sonst bekommt der Browser für /site.webmanifest die Login-Seite statt
// JSON. Die Logo-/Favicon-Dateien sind über die Endungen bereits abgedeckt.
// Bewusst KEINE Verzeichnis-Ausnahme für `assets/` — das ist zugleich die
// App-Route des Asset-Inventars und muss hinter dem Gate bleiben.
matcher: [
"/((?!_next/static|_next/image|favicon.ico|site.webmanifest|.*\\.(?:svg|png|jpg|ico|webmanifest)).*)",
],
};