require('dotenv').config(); const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const cookieParser = require('cookie-parser'); const path = require('path'); const http = require('http'); const loggingMiddleware = require('./src/middleware/logging'); const app = express(); const server = http.createServer(app); // ============================================================ // SECURITY — Helmet with CSP // ============================================================ app.use(helmet({ crossOriginEmbedderPolicy: false, contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], // unsafe-inline needed for onclick handlers in HTML styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com', 'https://cdnjs.cloudflare.com'], fontSrc: ["'self'", 'https://fonts.gstatic.com', 'https://cdnjs.cloudflare.com'], imgSrc: ["'self'", 'data:', 'blob:'], mediaSrc: ["'self'", 'blob:'], connectSrc: ["'self'"], workerSrc: ["'self'"], frameSrc: ["'none'"], objectSrc: ["'none'"], } } })); // ============================================================ // CORS — restrict to APP_URL origin in production // ============================================================ var allowedOrigin = process.env.APP_URL ? process.env.APP_URL.replace(/\/$/, '') : null; app.use(cors({ origin: function(origin, callback) { // Allow requests with no origin (mobile apps, curl, server-to-server) if (!origin) return callback(null, true); // In development (no APP_URL set) allow all if (!allowedOrigin) return callback(null, true); if (origin === allowedOrigin) return callback(null, true); callback(new Error('CORS: origin not allowed')); }, credentials: true })); app.use(cookieParser()); // ============================================================ // BODY LIMITS — 1mb for JSON, transcribe uses multipart (no limit here) // ============================================================ app.use(express.json({ limit: '1mb' })); // ============================================================ // RATE LIMITING // ============================================================ // General API: 60 req/min app.use('/api/', rateLimit({ windowMs: 60000, max: 60, message: { error: 'Too many requests' }, standardHeaders: true, legacyHeaders: false })); // Auth routes: 10 attempts/15min per IP (brute-force protection) app.use('/api/auth/login', rateLimit({ windowMs: 15 * 60 * 1000, max: 10, message: { error: 'Too many login attempts. Try again in 15 minutes.' }, standardHeaders: true, legacyHeaders: false })); app.use('/api/auth/register', rateLimit({ windowMs: 60 * 60 * 1000, max: 5, message: { error: 'Too many registration attempts.' }, standardHeaders: true, legacyHeaders: false })); app.use('/api/auth/forgot-password', rateLimit({ windowMs: 60 * 60 * 1000, max: 5, message: { error: 'Too many password reset attempts.' }, standardHeaders: true, legacyHeaders: false })); app.use(loggingMiddleware); app.use(express.static(path.join(__dirname, 'public'))); // Routes app.use('/api/auth', require('./src/routes/auth')); app.use('/api/admin', require('./src/routes/admin')); app.use('/api', require('./src/routes/transcribe')); app.use('/api', require('./src/routes/hpi')); app.use('/api', require('./src/routes/hospitalCourse')); app.use('/api', require('./src/routes/chartReview')); app.use('/api', require('./src/routes/milestones')); app.use('/api', require('./src/routes/soap')); app.use('/api', require('./src/routes/tts')); app.use('/api', require('./src/routes/nextcloud')); app.use('/api', require('./src/routes/refine')); app.use('/api', require('./src/routes/logs')); const { getAvailableModels, activeProvider: modelsProvider } = require('./src/utils/models'); app.get('/api/models', (req, res) => res.json({ models: getAvailableModels(), provider: modelsProvider })); const { activeProvider } = require('./src/utils/ai'); app.get('/api/health', (req, res) => { res.json({ status: 'running', version: '3.0.0', provider: activeProvider, timestamp: new Date().toISOString(), openrouter: process.env.OPENROUTER_API_KEY ? 'configured' : 'missing', bedrock: process.env.AWS_BEDROCK_REGION ? 'configured' : 'not configured', azure: process.env.AZURE_OPENAI_ENDPOINT ? 'configured' : 'not configured', whisper: process.env.OPENAI_API_KEY ? 'configured' : 'missing' }); }); app.get('*', (req, res) => res.sendFile(path.join(__dirname, 'public', 'index.html'))); const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(''); console.log('=========================================='); console.log('🏥 PEDIATRIC AI SCRIBE v3.0'); console.log('=========================================='); console.log('🌐 http://localhost:' + PORT); console.log('🤖 Provider: ' + activeProvider); console.log('=========================================='); });