Public go-live hardening: Redis-backed rate limiting on the previously invite-gated public surface (explore browsing, post/comment creation, invite-cookie routes) plus CSP, HSTS, and standard security headers on every response, now that the invite gate no longer filters traffic.
72 lines
2.3 KiB
TypeScript
72 lines
2.3 KiB
TypeScript
import type { NextConfig } from "next";
|
|
import createNextIntlPlugin from "next-intl/plugin";
|
|
|
|
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
|
|
|
|
function getSupabaseHostname(): string {
|
|
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
if (url) {
|
|
try { return new URL(url).hostname; } catch { /* fall through */ }
|
|
}
|
|
return "*.supabase.co";
|
|
}
|
|
|
|
function buildCsp(): string {
|
|
const supabase = `https://${getSupabaseHostname()}`;
|
|
const directives: Record<string, string[]> = {
|
|
"default-src": ["'self'"],
|
|
"script-src": ["'self'", "https://clerk.pawfeed.org"],
|
|
// Radix/shadcn primitives set inline style="" attributes for positioning —
|
|
// no nonce mechanism covers style attributes, so 'unsafe-inline' is required here.
|
|
"style-src": ["'self'", "'unsafe-inline'"],
|
|
"img-src": ["'self'", "data:", supabase, "https://image.mux.com", "https://img.clerk.com"],
|
|
"font-src": ["'self'", "data:"],
|
|
"media-src": ["'self'", "blob:", "https://stream.mux.com"],
|
|
"worker-src": ["'self'", "blob:"],
|
|
"connect-src": [
|
|
"'self'",
|
|
"https://clerk.pawfeed.org",
|
|
"https://accounts.pawfeed.org",
|
|
supabase,
|
|
"https://stream.mux.com",
|
|
"https://image.mux.com",
|
|
"https://litix.io",
|
|
"https://storage.googleapis.com",
|
|
],
|
|
"frame-src": ["'self'", "https://clerk.pawfeed.org"],
|
|
"frame-ancestors": ["'none'"],
|
|
"object-src": ["'none'"],
|
|
"base-uri": ["'self'"],
|
|
};
|
|
return Object.entries(directives)
|
|
.map(([key, values]) => `${key} ${values.join(" ")}`)
|
|
.join("; ");
|
|
}
|
|
|
|
const nextConfig: NextConfig = {
|
|
output: "standalone",
|
|
allowedDevOrigins: ["192.168.1.92"],
|
|
images: {
|
|
remotePatterns: [
|
|
{ protocol: "https", hostname: getSupabaseHostname() },
|
|
],
|
|
},
|
|
async headers() {
|
|
return [
|
|
{
|
|
source: "/(.*)",
|
|
headers: [
|
|
{ key: "Content-Security-Policy", value: buildCsp() },
|
|
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
{ key: "X-Frame-Options", value: "DENY" },
|
|
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
|
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
|
|
{ key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" },
|
|
],
|
|
},
|
|
];
|
|
},
|
|
};
|
|
|
|
export default withNextIntl(nextConfig);
|