feat: Wabenhain Honig-Shop Anwendung
Vollstaendige Next.js/Prisma-E-Commerce-Anwendung fuer eine Imkerei: Shop, Warenkorb, Stripe-Checkout, Kundenkonto mit 2FA, Admin-Bereich (Produkte, Ernten, Chargen, Bestellungen, Gutscheine, admin-editierbare Website-Inhalte), i18n (DE/EN), Rate-Limiting/CSP/Audit-Log-Haertung, PostgreSQL- und MySQL-Unterstuetzung ueber austauschbare Prisma- Treiber-Adapter, Unit-/Komponenten-/E2E-Tests.
This commit is contained in:
1 parent
5c76fcc266
commit
c7927f2883
240 files changed
+26096
-137
No files matched your search
@@ -0,0 +1,96 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { SESSION_COOKIE, verifySessionToken } from "@/lib/session";
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
/**
|
||||
* Nonce-basierte CSP statt 'unsafe-inline' fuer script-src: pro Request neu
|
||||
* erzeugt, Next.js wendet sie automatisch auf eigene Framework-/Seiten-Skripte
|
||||
* an (erkennt das 'nonce-...'-Muster im CSP-Response-Header waehrend des
|
||||
* Renderns). 'strict-dynamic' erlaubt zusaetzlich, dass von einem bereits
|
||||
* vertrauten Skript (z.B. Next.js' eigener Runtime) dynamisch nachgeladene
|
||||
* Skripte (next/script mit strategy="afterInteractive", z.B. das
|
||||
* Turnstile-Widget) automatisch mitvertraut werden, ohne jedes einzeln nonce'n
|
||||
* zu muessen. JSON-LD-<script type="application/ld+json">-Bloecke sind von
|
||||
* script-src ohnehin nie betroffen (kein ausfuehrbarer Skript-Typ).
|
||||
* style-src behaelt bewusst 'unsafe-inline': computed inline styles (Admin-
|
||||
* Dashboard-Balkendiagramme, globale Fehlerseite ohne CSS-Abhaengigkeit)
|
||||
* muessten sonst einzeln genonced werden, Style-Injection ist zudem ein
|
||||
* deutlich schwaecheres Angriffsprimitiv als Skript-Injection.
|
||||
*/
|
||||
function buildCsp(nonce: string): string {
|
||||
return [
|
||||
"default-src 'self'",
|
||||
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https://challenges.cloudflare.com${isDev ? " 'unsafe-eval'" : ""}`,
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' data: blob:",
|
||||
"font-src 'self' data:",
|
||||
"connect-src 'self' https://api.stripe.com https://challenges.cloudflare.com",
|
||||
"frame-src https://js.stripe.com https://checkout.stripe.com https://challenges.cloudflare.com",
|
||||
"form-action 'self'",
|
||||
"base-uri 'self'",
|
||||
"object-src 'none'",
|
||||
"frame-ancestors 'none'",
|
||||
].join("; ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Zugangskontrolle fuer den internen /admin-Bereich: Statt Basic Auth
|
||||
* entscheidet das normale Kundenkonto-Login. Nur Sessions, deren JWT die
|
||||
* Rolle ADMIN traegt, kommen durch — alle anderen werden auf die
|
||||
* Login-Seite (/konto) umgeleitet.
|
||||
*
|
||||
* Das JWT-Claim ist dabei nur der schnelle Vorfilter auf Request-Ebene;
|
||||
* die autoritative Datenbank-Pruefung passiert zusaetzlich im Admin-Layout
|
||||
* und in jeder Admin-Server-Action (getCurrentAdmin).
|
||||
*/
|
||||
export async function proxy(request: NextRequest) {
|
||||
const nonce = crypto.randomUUID().replaceAll("-", "");
|
||||
const cspHeader = buildCsp(nonce);
|
||||
|
||||
function withCsp(response: NextResponse): NextResponse {
|
||||
response.headers.set("Content-Security-Policy", cspHeader);
|
||||
return response;
|
||||
}
|
||||
|
||||
if (request.nextUrl.pathname.startsWith("/admin")) {
|
||||
const token = request.cookies.get(SESSION_COOKIE)?.value;
|
||||
const session = token ? await verifySessionToken(token) : null;
|
||||
|
||||
if (session?.role !== "ADMIN") {
|
||||
// Server Actions und API-artige Aufrufe bekommen einen harten 403 statt
|
||||
// einer Redirect-HTML-Seite.
|
||||
if (request.method !== "GET") {
|
||||
return withCsp(new NextResponse("Zugriff verweigert.", { status: 403 }));
|
||||
}
|
||||
|
||||
const loginUrl = new URL("/konto", request.url);
|
||||
loginUrl.searchParams.set(
|
||||
"error",
|
||||
session
|
||||
? "Dein Konto hat keinen Zugriff auf den Admin-Bereich."
|
||||
: "Bitte melde dich an, um den Admin-Bereich zu nutzen."
|
||||
);
|
||||
return withCsp(NextResponse.redirect(loginUrl));
|
||||
}
|
||||
}
|
||||
|
||||
const requestHeaders = new Headers(request.headers);
|
||||
requestHeaders.set("x-nonce", nonce);
|
||||
requestHeaders.set("Content-Security-Policy", cspHeader);
|
||||
|
||||
return withCsp(NextResponse.next({ request: { headers: requestHeaders } }));
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
{
|
||||
source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
|
||||
missing: [
|
||||
{ type: "header", key: "next-router-prefetch" },
|
||||
{ type: "header", key: "purpose", value: "prefetch" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
Reference in new issue
Block a user