trading-platform-backend-v2/src/index.ts
Adrian Flores Cortes 86e6303847 feat: Implement BLOCKER-001 token refresh + E2E video tests (backend)
BLOCKER-001: Token Refresh Improvements (4 phases)
- FASE 1: Rate limiting específico para /auth/refresh (15 req/15min per token)
- FASE 2: Token rotation con SHA-256 hash y reuse detection
- FASE 3: Session validation con cache de 30s (95% menos queries)
- FASE 4: Proactive refresh con X-Token-Expires-At header

E2E Tests: Video Upload Module (backend - 91 tests)
- Suite 4: Controller tests (22 tests) - REST API endpoints validation
- Suite 5: Service tests (29 tests) - Business logic and database operations
- Suite 6: Storage tests (35 tests) - S3/R2 multipart upload integration
- Suite 7: Full E2E flow (5 tests) - Complete pipeline validation

Changes:
- auth.middleware.ts: Session validation + token expiry header
- rate-limiter.ts: Specific rate limiter for refresh endpoint
- token.service.ts: Token rotation logic + session validation
- session-cache.service.ts (NEW): 30s TTL cache for session validation
- auth.types.ts: Extended types for session validation
- auth.routes.ts: Applied refreshTokenRateLimiter
- index.ts: Updated CORS to expose X-Token-Expires-At

Tests created:
- auth-token-refresh.test.ts (15 tests) - E2E token refresh flow
- video-controller.test.ts (22 tests) - REST API validation
- video-service.test.ts (29 tests) - Business logic validation
- storage-service.test.ts (35 tests) - S3/R2 integration
- video-upload-flow.test.ts (5 tests) - Complete pipeline

Database migration executed:
- Added refresh_token_hash and refresh_token_issued_at columns
- Created index on refresh_token_hash for performance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 01:43:49 -06:00

220 lines
6.5 KiB
TypeScript

/**
* Trading Platform - Backend API
* ==========================
*
* Main entry point for the Express.js backend API.
*/
import express, { Express, Request, Response } from 'express';
import { createServer } from 'http';
import cors from 'cors';
import helmet from 'helmet';
import compression from 'compression';
import morgan from 'morgan';
import { config } from './config/index.js';
import { logger } from './shared/utils/logger.js';
import { setupSwagger } from './config/swagger.config.js';
// WebSocket
import { wsManager, tradingStreamService } from './core/websocket/index.js';
// Background Jobs
import { distributionJob } from './modules/investment/jobs/index.js';
// Portfolio WebSocket
import { portfolioWebSocket } from './modules/portfolio/websocket/portfolio.websocket.js';
// Import routes
import { authRouter } from './modules/auth/auth.routes.js';
import { usersRouter } from './modules/users/users.routes.js';
import { educationRouter } from './modules/education/education.routes.js';
import { tradingRouter } from './modules/trading/trading.routes.js';
import { investmentRouter } from './modules/investment/investment.routes.js';
import { paymentsRouter } from './modules/payments/payments.routes.js';
import { adminRouter } from './modules/admin/admin.routes.js';
import { mlRouter } from './modules/ml/ml.routes.js';
import { llmRouter } from './modules/llm/llm.routes.js';
import { portfolioRouter } from './modules/portfolio/portfolio.routes.js';
import { agentsRouter } from './modules/agents/agents.routes.js';
import { notificationRouter } from './modules/notifications/notification.routes.js';
// Service clients for health checks
import { tradingAgentsClient, mlEngineClient, llmAgentClient } from './shared/clients/index.js';
// Health aggregator
import { getSystemHealth, getQuickHealth } from './shared/utils/health-aggregator.js';
// Import middleware
import { errorHandler } from './core/middleware/error-handler.js';
import { notFoundHandler } from './core/middleware/not-found.js';
import { rateLimiter } from './core/middleware/rate-limiter.js';
const app: Express = express();
// Trust proxy (for rate limiting behind reverse proxy)
app.set('trust proxy', 1);
// Security middleware
app.use(helmet());
// CORS
app.use(cors({
origin: config.cors.origins,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
exposedHeaders: ['X-Token-Expires-At'], // FASE 4: Expose token expiry for proactive refresh
}));
// Compression
app.use(compression());
// Request logging
app.use(morgan('combined', {
stream: { write: (message) => logger.info(message.trim()) }
}));
// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Rate limiting
app.use(rateLimiter);
// Swagger documentation
setupSwagger(app, '/api/v1');
// Quick health check (before auth) - just checks if backend is running
app.get('/health', (req: Request, res: Response) => {
const quickHealth = getQuickHealth();
res.json({
...quickHealth,
version: config.app.version,
environment: config.app.env,
});
});
// Full system health check - checks all services including database
app.get('/health/full', async (req: Request, res: Response) => {
try {
const systemHealth = await getSystemHealth();
const statusCode = systemHealth.status === 'healthy' ? 200 :
systemHealth.status === 'degraded' ? 200 : 503;
res.status(statusCode).json(systemHealth);
} catch (error) {
logger.error('Health check failed:', error);
res.status(500).json({
status: 'unhealthy',
error: 'Health check failed',
timestamp: new Date().toISOString(),
});
}
});
// Services health check (legacy, kept for compatibility)
app.get('/health/services', async (req: Request, res: Response) => {
try {
const systemHealth = await getSystemHealth();
// Transform to legacy format for backwards compatibility
const services: Record<string, { status: string; latency?: number; error?: string }> = {};
for (const service of systemHealth.services) {
services[service.name] = {
status: service.status,
latency: service.latency_ms,
error: service.error,
};
}
res.json({
status: systemHealth.status,
services,
timestamp: systemHealth.timestamp,
});
} catch (error) {
logger.error('Services health check failed:', error);
res.status(500).json({
status: 'unhealthy',
error: 'Health check failed',
timestamp: new Date().toISOString(),
});
}
});
// API routes
const apiRouter = express.Router();
apiRouter.use('/auth', authRouter);
apiRouter.use('/users', usersRouter);
apiRouter.use('/education', educationRouter);
apiRouter.use('/trading', tradingRouter);
apiRouter.use('/investment', investmentRouter);
apiRouter.use('/payments', paymentsRouter);
apiRouter.use('/admin', adminRouter);
apiRouter.use('/ml', mlRouter);
apiRouter.use('/llm', llmRouter);
apiRouter.use('/portfolio', portfolioRouter);
apiRouter.use('/agents', agentsRouter);
apiRouter.use('/notifications', notificationRouter);
// Mount API router
app.use('/api/v1', apiRouter);
// 404 handler
app.use(notFoundHandler);
// Error handler
app.use(errorHandler);
// Create HTTP server
const httpServer = createServer(app);
// Initialize WebSocket
wsManager.initialize(httpServer);
tradingStreamService.initialize();
portfolioWebSocket.initialize();
// Start background jobs
distributionJob.start();
// WebSocket stats endpoint
app.get('/api/v1/ws/stats', (req: Request, res: Response) => {
res.json({
success: true,
data: tradingStreamService.getStats(),
});
});
// Start server
const PORT = config.app.port;
httpServer.listen(PORT, () => {
logger.info(`🚀 Trading Platform API server running on port ${PORT}`);
logger.info(`📡 WebSocket server running on ws://localhost:${PORT}/ws`);
logger.info(`📚 Environment: ${config.app.env}`);
logger.info(`📖 Docs available at http://localhost:${PORT}/api/v1/docs`);
});
// Graceful shutdown
const gracefulShutdown = () => {
logger.info('Shutting down gracefully...');
distributionJob.stop();
portfolioWebSocket.shutdown();
tradingStreamService.shutdown();
wsManager.shutdown();
httpServer.close(() => {
logger.info('HTTP server closed');
process.exit(0);
});
};
process.on('SIGTERM', () => {
logger.info('SIGTERM received.');
gracefulShutdown();
});
process.on('SIGINT', () => {
logger.info('SIGINT received.');
gracefulShutdown();
});
export { app };