-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathproxy.ts
More file actions
150 lines (128 loc) · 4.83 KB
/
Copy pathproxy.ts
File metadata and controls
150 lines (128 loc) · 4.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import { getSessionCookie } from "better-auth/cookies";
import { type NextRequest, NextResponse } from "next/server";
import createMiddleware from "next-intl/middleware";
import { routing } from "@/i18n/routing";
import { AUTH_ROUTE_KEYS, PUBLIC_ROUTE_KEYS } from "@/lib/auth/route-access";
import {
extractLocale,
getAllLocalizedPaths,
stripLocalePrefix,
} from "@/lib/i18n-utils";
import { buildCspHeader } from "@/lib/security/csp";
const intlMiddleware = createMiddleware(routing);
/** All localized variants, pre-computed at module load. */
const authRoutes = AUTH_ROUTE_KEYS.flatMap(getAllLocalizedPaths);
const publicRoutes = PUBLIC_ROUTE_KEYS.flatMap(getAllLocalizedPaths);
const securityHeaders: Record<string, string> = {
"Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy":
"camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=(), browsing-topics=()",
};
function applyHeaders(
response: NextResponse,
cspHeader?: string,
): NextResponse {
for (const [key, value] of Object.entries(securityHeaders)) {
response.headers.set(key, value);
}
if (cspHeader) {
response.headers.set("Content-Security-Policy", cspHeader);
}
return response;
}
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
const isDev = process.env.NODE_ENV === "development";
// API routes: security headers only (no CSP, no i18n)
if (pathname.startsWith("/api")) {
return applyHeaders(NextResponse.next());
}
// Generate nonce + CSP (production only)
const nonce = isDev
? ""
: Buffer.from(crypto.randomUUID()).toString("base64");
const cspHeader = isDev ? "" : buildCspHeader(nonce);
// Auth check: redirect unauthenticated users on protected routes,
// and redirect authenticated users away from sign-in/sign-up pages.
const pathnameWithoutLocale = stripLocalePrefix(pathname);
const isAuthRoute = authRoutes.some((route) =>
pathnameWithoutLocale.startsWith(route),
);
const isPublicRouteKey = publicRoutes.some((route) =>
pathnameWithoutLocale.startsWith(route),
);
const isPublicRoute =
pathnameWithoutLocale === "/" || isAuthRoute || isPublicRouteKey;
const hasSession = !!getSessionCookie(request, { cookiePrefix: "avisio" });
if (isAuthRoute && hasSession) {
const locale = extractLocale(pathname);
return applyHeaders(
NextResponse.redirect(new URL(`/${locale}`, request.url)),
cspHeader,
);
}
if (!isPublicRoute && !hasSession) {
const locale = extractLocale(pathname);
return applyHeaders(
NextResponse.redirect(new URL(`/${locale}/anmelden`, request.url)),
cspHeader,
);
}
// i18n routing
const intlResponse = intlMiddleware(request);
// Redirects: add headers and return directly
if (intlResponse.headers.get("location")) {
return applyHeaders(intlResponse, cspHeader);
}
// Page responses: merge intl middleware's forwarded request headers with our
// nonce headers. Next.js uses x-middleware-override-headers (comma-separated
// list of header names) + x-middleware-request-<name> (values) to forward
// request headers from middleware to server components.
// We must merge both sets so i18n AND nonce forwarding work together.
const intlOverrides =
intlResponse.headers.get("x-middleware-override-headers") ?? "";
const headerNames = intlOverrides
? intlOverrides.split(",").map((h) => h.trim())
: [];
// Add our nonce headers to the forwarded set
if (!isDev) {
headerNames.push("x-nonce", "content-security-policy");
}
const requestHeaders = new Headers(request.headers);
// Copy intl's forwarded request header values onto our request headers
for (const name of headerNames) {
const intlValue = intlResponse.headers.get(`x-middleware-request-${name}`);
if (intlValue != null) {
requestHeaders.set(name, intlValue);
}
}
// Set our nonce values
if (!isDev) {
requestHeaders.set("x-nonce", nonce);
requestHeaders.set("content-security-policy", cspHeader);
}
const response = NextResponse.next({
request: { headers: requestHeaders },
});
// Copy intl middleware's response headers (skip x-middleware-override/request-*
// since NextResponse.next() already encodes our merged request headers)
for (const [key, value] of intlResponse.headers) {
if (
key === "x-middleware-override-headers" ||
key.startsWith("x-middleware-request-")
) {
continue;
}
response.headers.set(key, value);
}
for (const cookie of intlResponse.cookies.getAll()) {
response.cookies.set(cookie);
}
return applyHeaders(response, cspHeader);
}
export const config = {
matcher: ["/((?!ingest|monitoring|_next|_vercel|.*\\..*).*)"],
};