Files
DashDescomplicar/api/server.ts

204 lines
6.9 KiB
TypeScript
Executable File

/**
* 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)
}
})