bf8577dbaf
- Split auth config into auth.config.ts (edge-safe, no argon2) and auth-edge.ts for middleware use; auth.ts now spreads the shared config - Middleware wraps with auth() to redirect unauthenticated requests to /auth/signin before any page render; passes through /auth/, /api/, /invite/ paths - SessionGuard client component watches useSession() and redirects on status=unauthenticated, closing the SPA navigation gap - QueryCache + MutationCache in TRPCProvider redirect on UNAUTHORIZED tRPC errors without retrying; SessionProvider polls session state every 5 minutes - Middleware tests updated for async auth wrapper and auth-edge mock Co-Authored-By: claude-flow <ruv@ruv.net>
71 lines
2.3 KiB
TypeScript
71 lines
2.3 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { auth } from "./server/auth-edge.js";
|
|
|
|
// Paths that are accessible without a session.
|
|
// Everything else requires a valid JWT session.
|
|
const PUBLIC_PREFIXES = [
|
|
"/auth/", // signin, forgot-password, reset-password
|
|
"/api/", // tRPC, health, auth endpoints — these manage their own auth
|
|
"/invite/", // public invite acceptance flow
|
|
];
|
|
|
|
function isPublicPath(pathname: string): boolean {
|
|
return PUBLIC_PREFIXES.some((prefix) => pathname.startsWith(prefix));
|
|
}
|
|
|
|
function buildCsp(nonce: string, isProd: boolean): string {
|
|
const scriptSrc = isProd
|
|
? `'self' 'nonce-${nonce}'`
|
|
: `'self' 'unsafe-eval' 'unsafe-inline'`;
|
|
|
|
const imgSrc = isProd ? "'self' data: blob:" : "'self' data: blob: https:";
|
|
|
|
return [
|
|
"default-src 'self'",
|
|
`script-src ${scriptSrc}`,
|
|
"style-src 'self' 'unsafe-inline'",
|
|
`img-src ${imgSrc}`,
|
|
"font-src 'self' data:",
|
|
"connect-src 'self' https://generativelanguage.googleapis.com https://*.openai.com https://*.azure.com",
|
|
"frame-ancestors 'none'",
|
|
"base-uri 'self'",
|
|
"form-action 'self'",
|
|
].join("; ");
|
|
}
|
|
|
|
export default auth(function middleware(request) {
|
|
const { pathname } = request.nextUrl;
|
|
|
|
// Redirect unauthenticated requests for protected routes to signin
|
|
if (!isPublicPath(pathname) && !request.auth) {
|
|
const signInUrl = new URL("/auth/signin", request.url);
|
|
signInUrl.searchParams.set("callbackUrl", request.url);
|
|
return NextResponse.redirect(signInUrl);
|
|
}
|
|
|
|
// Generate a cryptographically random nonce for this request
|
|
const nonceBytes = new Uint8Array(16);
|
|
crypto.getRandomValues(nonceBytes);
|
|
const nonce = btoa(String.fromCharCode(...nonceBytes));
|
|
|
|
const isProd = process.env.NODE_ENV === "production";
|
|
const csp = buildCsp(nonce, isProd);
|
|
|
|
// Forward nonce to server components via request header
|
|
const requestHeaders = new Headers(request.headers);
|
|
requestHeaders.set("x-nonce", nonce);
|
|
requestHeaders.set("Content-Security-Policy", csp);
|
|
|
|
const response = NextResponse.next({ request: { headers: requestHeaders } });
|
|
response.headers.set("Content-Security-Policy", csp);
|
|
|
|
return response;
|
|
});
|
|
|
|
export const config = {
|
|
matcher: [
|
|
// Apply to all routes except Next.js internals and static assets
|
|
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
|
|
],
|
|
};
|