mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 16:52:08 +01:00
feat: implement cache layer, CSP improvements, and database performance optimizations
- Add Redis cache implementation with LRU eviction - Enhance Content Security Policy with nonce generation - Optimize database queries with connection pooling - Add cache invalidation API endpoints - Improve security monitoring performance
This commit is contained in:
165
lib/csp-server.ts
Normal file
165
lib/csp-server.ts
Normal file
@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Server-only CSP utilities
|
||||
* This file should never be imported by client-side code
|
||||
*/
|
||||
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import type { CSPConfig } from "./csp";
|
||||
|
||||
/**
|
||||
* Generate a cryptographically secure nonce for CSP
|
||||
*/
|
||||
export function generateNonce(): string {
|
||||
// Use Web Crypto API for Edge Runtime and browser compatibility
|
||||
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
||||
const bytes = new Uint8Array(16);
|
||||
crypto.getRandomValues(bytes);
|
||||
return btoa(String.fromCharCode(...bytes));
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Web Crypto API not available - this should only be called in supported environments"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Content Security Policy header value based on configuration
|
||||
*/
|
||||
export function buildCSP(config: CSPConfig = {}): string {
|
||||
const {
|
||||
nonce,
|
||||
isDevelopment = false,
|
||||
reportUri,
|
||||
_enforceMode = true,
|
||||
strictMode = false,
|
||||
allowedExternalDomains = [],
|
||||
_reportingLevel = "violations",
|
||||
} = config;
|
||||
|
||||
// Base directives for all environments
|
||||
const baseDirectives = {
|
||||
"default-src": ["'self'"],
|
||||
"base-uri": ["'self'"],
|
||||
"form-action": ["'self'"],
|
||||
"frame-ancestors": ["'none'"],
|
||||
"object-src": ["'none'"],
|
||||
"upgrade-insecure-requests": true,
|
||||
};
|
||||
|
||||
// Script sources - more restrictive in production
|
||||
const scriptSrc = isDevelopment
|
||||
? ["'self'", "'unsafe-eval'", "'unsafe-inline'"]
|
||||
: nonce
|
||||
? ["'self'", `'nonce-${nonce}'`, "'strict-dynamic'"]
|
||||
: ["'self'"];
|
||||
|
||||
// Style sources - use nonce in production when available
|
||||
const styleSrc = nonce
|
||||
? ["'self'", `'nonce-${nonce}'`]
|
||||
: ["'self'", "'unsafe-inline'"]; // Fallback for TailwindCSS
|
||||
|
||||
// Image sources - allow self, data URIs, and specific trusted domains
|
||||
const imgSrc = [
|
||||
"'self'",
|
||||
"data:",
|
||||
"https://schema.org", // For structured data images
|
||||
"https://livedash.notso.ai", // Application domain
|
||||
"https://*.basemaps.cartocdn.com", // Leaflet map tiles
|
||||
"https://*.openstreetmap.org", // OpenStreetMap tiles
|
||||
...allowedExternalDomains
|
||||
.filter((domain) => domain.startsWith("https://"))
|
||||
.map((domain) => domain),
|
||||
].filter(Boolean);
|
||||
|
||||
// Font sources - restrict to self and data URIs
|
||||
const fontSrc = ["'self'", "data:"];
|
||||
|
||||
// Connect sources - API endpoints and trusted domains
|
||||
const connectSrc = isDevelopment
|
||||
? ["'self'", "https:", "wss:", "ws:"] // Allow broader sources in dev for HMR
|
||||
: strictMode
|
||||
? [
|
||||
"'self'",
|
||||
"https://api.openai.com", // OpenAI API
|
||||
"https://livedash.notso.ai", // Application API
|
||||
...allowedExternalDomains.filter(
|
||||
(domain) =>
|
||||
domain.startsWith("https://") || domain.startsWith("wss://")
|
||||
),
|
||||
].filter(Boolean)
|
||||
: [
|
||||
"'self'",
|
||||
"https://api.openai.com", // OpenAI API
|
||||
"https://livedash.notso.ai", // Application API
|
||||
"https:", // Allow all HTTPS in non-strict mode
|
||||
];
|
||||
|
||||
// Media sources - restrict to self
|
||||
const mediaSrc = ["'self'"];
|
||||
|
||||
// Worker sources - restrict to self
|
||||
const workerSrc = ["'self'"];
|
||||
|
||||
// Child sources - restrict to self
|
||||
const childSrc = ["'self'"];
|
||||
|
||||
// Manifest sources - restrict to self
|
||||
const manifestSrc = ["'self'"];
|
||||
|
||||
// Build the directive object
|
||||
const directives = {
|
||||
...baseDirectives,
|
||||
"script-src": scriptSrc,
|
||||
"style-src": styleSrc,
|
||||
"img-src": imgSrc,
|
||||
"font-src": fontSrc,
|
||||
"connect-src": connectSrc,
|
||||
"media-src": mediaSrc,
|
||||
"worker-src": workerSrc,
|
||||
"child-src": childSrc,
|
||||
"manifest-src": manifestSrc,
|
||||
};
|
||||
|
||||
// Add report URI if provided
|
||||
if (reportUri) {
|
||||
directives["report-uri"] = [reportUri];
|
||||
directives["report-to"] = ["csp-endpoint"];
|
||||
}
|
||||
|
||||
// Convert directives to CSP string
|
||||
const cspString = Object.entries(directives)
|
||||
.map(([directive, value]) => {
|
||||
if (value === true) return directive;
|
||||
if (Array.isArray(value)) return `${directive} ${value.join(" ")}`;
|
||||
return `${directive} ${value}`;
|
||||
})
|
||||
.join("; ");
|
||||
|
||||
return cspString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create CSP middleware for Next.js
|
||||
*/
|
||||
export function createCSPMiddleware(config: CSPConfig = {}) {
|
||||
return (_request: NextRequest) => {
|
||||
const nonce = generateNonce();
|
||||
const isDevelopment = process.env.NODE_ENV === "development";
|
||||
|
||||
const csp = buildCSP({
|
||||
...config,
|
||||
nonce,
|
||||
isDevelopment,
|
||||
});
|
||||
|
||||
const response = NextResponse.next();
|
||||
|
||||
// Set CSP header
|
||||
response.headers.set("Content-Security-Policy", csp);
|
||||
|
||||
// Store nonce for use in components
|
||||
response.headers.set("X-Nonce", nonce);
|
||||
|
||||
return response;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user