- Docker: Multi-stage Dockerfile with security hardening, docker-compose for production and development environments - Environment: Comprehensive .env.example with all config options, lib/config/env.ts for typed environment validation - Logging: Structured JSON logging with request/response middleware - Monitoring: Prometheus metrics endpoint, Grafana dashboard, health checks (liveness/readiness probes) - Security: Security headers, rate limiting, CORS middleware - CI/CD: GitHub Actions workflows for CI, production deploy, and preview deployments - Error tracking: Sentry integration foundation Files created: - Docker: Dockerfile, docker-compose.yml, docker-compose.dev.yml, .dockerignore - Config: lib/config/env.ts, lib/config/index.ts - Logging: lib/logging/logger.ts, lib/logging/middleware.ts - Monitoring: lib/monitoring/sentry.ts, lib/monitoring/metrics.ts, lib/monitoring/health.ts - Security: lib/security/headers.ts, lib/security/rateLimit.ts, lib/security/cors.ts - API: pages/api/health/*, pages/api/metrics.ts - Infra: infra/prometheus/prometheus.yml, infra/grafana/*
131 lines
3.1 KiB
TypeScript
131 lines
3.1 KiB
TypeScript
/**
|
|
* CORS Middleware
|
|
* Agent 4: Production Deployment
|
|
*
|
|
* Configures Cross-Origin Resource Sharing for API routes.
|
|
*/
|
|
|
|
import type { NextApiRequest, NextApiResponse, NextApiHandler } from 'next';
|
|
import { env } from '../config';
|
|
|
|
interface CorsConfig {
|
|
origins: string[];
|
|
methods: string[];
|
|
allowedHeaders: string[];
|
|
exposedHeaders: string[];
|
|
credentials: boolean;
|
|
maxAge: number;
|
|
}
|
|
|
|
const DEFAULT_CONFIG: CorsConfig = {
|
|
origins: env.corsOrigins,
|
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
allowedHeaders: [
|
|
'Content-Type',
|
|
'Authorization',
|
|
'X-Requested-With',
|
|
'X-Request-Id',
|
|
'X-API-Key',
|
|
],
|
|
exposedHeaders: [
|
|
'X-Request-Id',
|
|
'X-RateLimit-Limit',
|
|
'X-RateLimit-Remaining',
|
|
'X-RateLimit-Reset',
|
|
],
|
|
credentials: true,
|
|
maxAge: 86400, // 24 hours
|
|
};
|
|
|
|
/**
|
|
* Check if origin is allowed
|
|
*/
|
|
function isOriginAllowed(origin: string | undefined, allowedOrigins: string[]): boolean {
|
|
if (!origin) return false;
|
|
|
|
return allowedOrigins.some((allowed) => {
|
|
// Exact match
|
|
if (allowed === origin) return true;
|
|
|
|
// Wildcard subdomain match (e.g., *.example.com)
|
|
if (allowed.startsWith('*.')) {
|
|
const domain = allowed.slice(2);
|
|
return origin.endsWith(domain) || origin === `https://${domain}` || origin === `http://${domain}`;
|
|
}
|
|
|
|
// All origins allowed
|
|
if (allowed === '*') return true;
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Apply CORS headers to response
|
|
*/
|
|
export function applyCorsHeaders(
|
|
req: NextApiRequest,
|
|
res: NextApiResponse,
|
|
config: CorsConfig = DEFAULT_CONFIG
|
|
): void {
|
|
const origin = req.headers.origin;
|
|
|
|
// Set allowed origin
|
|
if (isOriginAllowed(origin, config.origins)) {
|
|
res.setHeader('Access-Control-Allow-Origin', origin!);
|
|
} else if (config.origins.includes('*')) {
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
}
|
|
|
|
// Set other CORS headers
|
|
if (config.credentials) {
|
|
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
}
|
|
|
|
res.setHeader('Access-Control-Allow-Methods', config.methods.join(', '));
|
|
res.setHeader('Access-Control-Allow-Headers', config.allowedHeaders.join(', '));
|
|
res.setHeader('Access-Control-Expose-Headers', config.exposedHeaders.join(', '));
|
|
res.setHeader('Access-Control-Max-Age', config.maxAge.toString());
|
|
}
|
|
|
|
/**
|
|
* CORS middleware for API routes
|
|
*/
|
|
export function withCors(
|
|
handler: NextApiHandler,
|
|
config?: Partial<CorsConfig>
|
|
): NextApiHandler {
|
|
const mergedConfig: CorsConfig = { ...DEFAULT_CONFIG, ...config };
|
|
|
|
return async (req: NextApiRequest, res: NextApiResponse) => {
|
|
applyCorsHeaders(req, res, mergedConfig);
|
|
|
|
// Handle preflight requests
|
|
if (req.method === 'OPTIONS') {
|
|
return res.status(204).end();
|
|
}
|
|
|
|
return handler(req, res);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Strict CORS for internal APIs only
|
|
*/
|
|
export const strictCors = (handler: NextApiHandler) =>
|
|
withCors(handler, {
|
|
origins: ['http://localhost:3001'],
|
|
credentials: true,
|
|
});
|
|
|
|
/**
|
|
* Open CORS for public APIs
|
|
*/
|
|
export const openCors = (handler: NextApiHandler) =>
|
|
withCors(handler, {
|
|
origins: ['*'],
|
|
credentials: false,
|
|
});
|
|
|
|
// Export types
|
|
export type { CorsConfig };
|