import { Context, Next } from "@oak/oak"; import { config } from "../config/env.ts"; // Rate limiting store (in-memory, use Redis in production) const rateLimitStore = new Map(); // Rate limiting middleware export async function rateLimit(ctx: Context, next: Next): Promise { const ip = ctx.request.ip || "unknown"; const now = Date.now(); const windowMs = config.RATE_LIMIT_WINDOW_MS; const maxRequests = config.RATE_LIMIT_MAX_REQUESTS; const record = rateLimitStore.get(ip); if (!record || now > record.resetTime) { rateLimitStore.set(ip, { count: 1, resetTime: now + windowMs }); } else { record.count++; if (record.count > maxRequests) { ctx.response.status = 429; ctx.response.body = { error: "Too many requests", retryAfter: Math.ceil((record.resetTime - now) / 1000), }; return; } } await next(); } // Security headers middleware export async function securityHeaders(ctx: Context, next: Next): Promise { await next(); // Prevent clickjacking ctx.response.headers.set("X-Frame-Options", "DENY"); // Prevent MIME type sniffing ctx.response.headers.set("X-Content-Type-Options", "nosniff"); // XSS protection ctx.response.headers.set("X-XSS-Protection", "1; mode=block"); // Referrer policy ctx.response.headers.set( "Referrer-Policy", "strict-origin-when-cross-origin", ); // Content Security Policy ctx.response.headers.set( "Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';", ); // Strict Transport Security (only in production with HTTPS) if (config.isProduction()) { ctx.response.headers.set( "Strict-Transport-Security", "max-age=31536000; includeSubDomains", ); } } // CORS middleware export async function cors(ctx: Context, next: Next): Promise { const origin = ctx.request.headers.get("Origin"); const allowedOrigins = config.CORS_ORIGIN.split(",").map((o) => o.trim()); // Check if origin is allowed if ( origin && (allowedOrigins.includes(origin) || allowedOrigins.includes("*")) ) { ctx.response.headers.set("Access-Control-Allow-Origin", origin); } else if (config.isDevelopment()) { // Allow all origins in development ctx.response.headers.set("Access-Control-Allow-Origin", origin || "*"); } ctx.response.headers.set( "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS", ); ctx.response.headers.set( "Access-Control-Allow-Headers", "Content-Type, Authorization", ); ctx.response.headers.set("Access-Control-Allow-Credentials", "true"); ctx.response.headers.set("Access-Control-Max-Age", "86400"); // Handle preflight requests if (ctx.request.method === "OPTIONS") { ctx.response.status = 204; return; } await next(); } // Request logging middleware export async function requestLogger(ctx: Context, next: Next): Promise { const start = Date.now(); const { method, url } = ctx.request; await next(); const ms = Date.now() - start; const status = ctx.response.status; // Color code based on status let statusColor = "\x1b[32m"; // Green for 2xx if (status >= 400 && status < 500) statusColor = "\x1b[33m"; // Yellow for 4xx if (status >= 500) statusColor = "\x1b[31m"; // Red for 5xx console.log( `${ new Date().toISOString() } - ${method} ${url.pathname} ${statusColor}${status}\x1b[0m ${ms}ms`, ); } // Input sanitization helper export function sanitizeInput(input: string): string { return input .replace(/[<>]/g, "") // Remove angle brackets .replace(/javascript:/gi, "") // Remove javascript: protocol .replace(/on\w+=/gi, "") // Remove event handlers .trim(); } // Validate email format export function isValidEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } // Validate password strength export function isStrongPassword( password: string, ): { valid: boolean; message?: string } { if (password.length < 8) { return { valid: false, message: "Password must be at least 8 characters long", }; } if (!/[A-Z]/.test(password)) { return { valid: false, message: "Password must contain at least one uppercase letter", }; } if (!/[a-z]/.test(password)) { return { valid: false, message: "Password must contain at least one lowercase letter", }; } if (!/[0-9]/.test(password)) { return { valid: false, message: "Password must contain at least one number", }; } return { valid: true }; } // Clean up old rate limit entries periodically setInterval(() => { const now = Date.now(); for (const [ip, record] of rateLimitStore.entries()) { if (now > record.resetTime) { rateLimitStore.delete(ip); } } }, 60000); // Clean up every minute