/** * Express API Server * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ import 'dotenv/config' import express from 'express' import cors from 'cors' import rateLimit from 'express-rate-limit' import path from 'path' import { fileURLToPath } from 'url' import crypto from 'crypto' import dashboardRouter from './routes/dashboard.js' import monitorRouter from './routes/monitor.js' import diagnosticRouter from './routes/diagnostic.js' import hetznerRouter from './routes/hetzner.js' import wpMonitorRouter from './routes/wp-monitor.js' import serverMetricsRouter from './routes/server-metrics.js' import financialRouter from './routes/financial.js' import { collectAllServerMetrics } from './services/server-metrics.js' import { collectMonitoringData } from './services/monitoring-collector.js' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const app = express() const PORT = process.env.API_PORT || 3001 const isProduction = process.env.NODE_ENV === 'production' // ============================================================================ // SECURITY: Rate Limiting (Vulnerabilidade 2.1) // ============================================================================ const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutos max: isProduction ? 100 : 1000, // 100 requests em produção, 1000 em dev message: { error: 'Too many requests from this IP, please try again later.' }, standardHeaders: true, legacyHeaders: false, }) app.use('/api', limiter) // ============================================================================ // SECURITY: CORS Restrito (Vulnerabilidade 2.2) // ============================================================================ const allowedOrigins = [ 'https://dash.descomplicar.pt', 'https://dashboard.descomplicar.pt', 'https://desk.descomplicar.pt' ] // Em desenvolvimento, adicionar localhost if (!isProduction) { allowedOrigins.push('http://localhost:5173') allowedOrigins.push('http://localhost:3050') allowedOrigins.push(process.env.FRONTEND_URL || 'http://localhost:5173') } const corsMiddleware = cors({ origin: (origin, callback) => { // Permitir requests sem origin (curl, Postman, etc) em dev if (!origin && !isProduction) { return callback(null, true) } if (!origin || allowedOrigins.includes(origin)) { callback(null, true) } else { console.warn(`[SECURITY] Blocked CORS request from: ${origin}`) callback(new Error('Not allowed by CORS')) } }, credentials: true }) // CORS apenas nas rotas API (nao bloquear assets estaticos) app.use('/api', corsMiddleware) app.use(express.json()) // ============================================================================ // SECURITY: Autenticação Backend (Vulnerabilidade 2.5) - OPCIONAL // ============================================================================ // Se OIDC_ENABLED=true e OIDC_SECRET definido, ativa autenticação // Caso contrário, APIs ficam sem autenticação (para compatibilidade) const oidcEnabled = process.env.OIDC_ENABLED === 'true' && process.env.OIDC_SECRET if (oidcEnabled) { console.log('[SECURITY] OIDC authentication enabled for API routes') // Middleware simples de verificação de token // Para implementação completa, usar express-openid-connect app.use('/api', (req, res, next) => { const authHeader = req.headers.authorization if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Unauthorized', message: 'Missing or invalid token' }) } // Aqui deveria validar o JWT token com a OIDC authority // Por agora, aceitar qualquer token (placeholder para implementação futura) next() }) } else { console.warn('[SECURITY] API routes are NOT protected by authentication (set OIDC_ENABLED=true to enable)') } // Health check (sem rate limit) app.get('/health', (_req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }) }) // Health check com autenticação app.get('/api/health', (_req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }) }) // Routes app.use('/api/dashboard', dashboardRouter) app.use('/api/monitor', monitorRouter) app.use('/api/diagnostic', diagnosticRouter) app.use('/api/hetzner', hetznerRouter) app.use('/api/wp-monitor', wpMonitorRouter) app.use('/api/server-metrics', serverMetricsRouter) app.use('/api/financial', financialRouter) // Serve static files in production if (isProduction) { // __dirname is /app/api/dist, need to go up 2 levels to /app/dist const distPath = path.join(__dirname, '..', '..', 'dist') app.use(express.static(distPath)) // SPA fallback - serve index.html for all non-API routes app.get('*', (req, res, next) => { if (req.path.startsWith('/api')) { return next() } res.sendFile(path.join(distPath, 'index.html')) }) } // ============================================================================ // SECURITY: Error Handling Melhorado (Vulnerabilidade 2.5) // ============================================================================ app.use((err: any, req: express.Request, res: express.Response, _next: express.NextFunction) => { const errorId = crypto.randomUUID() // Log estruturado sem stack trace em produção if (isProduction) { console.error(JSON.stringify({ errorId, message: err.message, path: req.path, method: req.method, timestamp: new Date().toISOString() })) } else { console.error('Server error:', err) } // Resposta genérica ao cliente res.status(err.status || 500).json({ error: isProduction ? 'Internal server error' : err.message, errorId }) }) // Start server app.listen(PORT, () => { if (!isProduction) { console.log('='.repeat(50)) console.log(`API Server running on http://localhost:${PORT}`) console.log(`Dashboard: http://localhost:${PORT}/api/dashboard`) console.log(`Monitor: http://localhost:${PORT}/api/monitor`) console.log(`Hetzner: http://localhost:${PORT}/api/hetzner`) console.log('='.repeat(50)) } // Auto-collect metrics every 5 minutes if (isProduction) { const INTERVAL = 5 * 60 * 1000 console.log('[SCHEDULER] Server metrics + monitoring collection every 5min') // Initial collection after 30s (let server stabilize) setTimeout(() => { collectAllServerMetrics().catch(err => console.error('[SCHEDULER] Initial server metrics failed:', err.message) ) collectMonitoringData().catch(err => console.error('[SCHEDULER] Initial monitoring collection failed:', err.message) ) }, 30000) // Recurring collection setInterval(() => { collectAllServerMetrics().catch(err => console.error('[SCHEDULER] Server metrics failed:', err.message) ) collectMonitoringData().catch(err => console.error('[SCHEDULER] Monitoring collection failed:', err.message) ) }, INTERVAL) } })