Repository navigation
Expand file tree
/
Copy pathmiddleware.ts
More file actions
74 lines (65 loc) · 2.84 KB
/
Copy pathmiddleware.ts
File metadata and controls
74 lines (65 loc) · 2.84 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
import { type NextRequest, NextResponse } from "next/server";
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY } from "@/lib/supabase/env";
type CookieToSet = { name: string; value: string; options: CookieOptions };
/**
* Middleware responsibilities (CLAUDE.md §6, §8):
* 1. Generate a per-request nonce and emit a strict, nonce-based CSP.
* 2. Refresh the Supabase auth session (cookie rotation) on every request.
*
* The static security headers (HSTS, X-Frame-Options, etc.) live in next.config.ts.
*/
export async function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const csp = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
// Inline styles are allowed (chart bar sizing, gradients). Scripts stay strict (nonce).
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data: blob: https://*.supabase.co`,
`font-src 'self'`,
`connect-src 'self' https://*.supabase.co wss://*.supabase.co`,
`frame-ancestors 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`object-src 'none'`,
`upgrade-insecure-requests`,
].join("; ");
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
// Next.js reads this request header and applies the nonce to its own framework scripts.
requestHeaders.set("content-security-policy", csp);
let response = NextResponse.next({ request: { headers: requestHeaders } });
// Refresh the Supabase session only when configured, so the app still renders for
// local UI work before Supabase env vars are wired up.
if (SUPABASE_URL && SUPABASE_PUBLISHABLE_KEY) {
const supabase = createServerClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet: CookieToSet[]) {
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
response = NextResponse.next({ request: { headers: requestHeaders } });
cookiesToSet.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options),
);
},
},
});
// IMPORTANT: do not run code between createServerClient and getUser() — it refreshes
// the token and is the only thing keeping the session alive on the server.
await supabase.auth.getUser();
}
response.headers.set("content-security-policy", csp);
return response;
}
export const config = {
matcher: [
/*
* Match all request paths except static assets and image files, which don't need a
* session refresh or an HTML CSP.
*/
"/((?!_next/static|_next/image|favicon.ico|manifest.webmanifest|sw.js|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)$).*)",
],
};