217 lines
6.0 KiB
Markdown
217 lines
6.0 KiB
Markdown
# Phase 7: API Server Refactor + Composition Root
|
|
|
|
## Middleware stack (ORDEN IMPORTA)
|
|
```typescript
|
|
// server.ts
|
|
export function createServer(deps: ServerDependencies): Express {
|
|
const app = express();
|
|
|
|
// 1. Request ID (PRIMERO — todo log necesita esto)
|
|
app.use(requestIdMiddleware);
|
|
|
|
// 2. Security headers
|
|
app.use(helmet({
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
connectSrc: ["'self'", "ws:", "wss:"],
|
|
scriptSrc: ["'self'", "'unsafe-inline'"], // para Scalar docs
|
|
},
|
|
},
|
|
}));
|
|
|
|
// 3. CORS
|
|
app.use(cors({
|
|
origin: deps.config.cors.origin,
|
|
credentials: true,
|
|
}));
|
|
|
|
// 4. Rate limiting global
|
|
app.use(rateLimit({
|
|
windowMs: deps.config.api.rateLimitWindowMs,
|
|
max: deps.config.api.rateLimitMax,
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
}));
|
|
|
|
// 5. Body parsing
|
|
app.use(express.json({ limit: '10mb' }));
|
|
|
|
// 6. Health endpoints (SIN auth)
|
|
app.get('/health/live', (_, res) => res.json({ status: 'ok' }));
|
|
app.get('/health/ready', async (_, res) => { /* check DB */ });
|
|
|
|
// 7. Auth routes (SIN auth middleware general)
|
|
app.use('/api/auth', deps.authController.router);
|
|
|
|
// 8. Auth middleware (TODOS los /api/ a partir de aquí)
|
|
app.use('/api', deps.authMiddleware);
|
|
|
|
// 9. Module routes
|
|
app.use('/api', deps.crawlingController.router);
|
|
app.use('/api', deps.findingsController.router);
|
|
app.use('/api', deps.fuzzingController.router);
|
|
// ... más módulos
|
|
|
|
// 10. 404 handler
|
|
app.use(notFoundMiddleware);
|
|
|
|
// 11. Error handler (SIEMPRE último)
|
|
app.use(globalErrorHandler);
|
|
|
|
return app;
|
|
}
|
|
```
|
|
|
|
## Error hierarchy
|
|
```typescript
|
|
export class AppError extends Error {
|
|
constructor(
|
|
message: string,
|
|
public readonly statusCode: number,
|
|
public readonly code: string,
|
|
public readonly isOperational = true,
|
|
) { super(message); }
|
|
}
|
|
|
|
export class ValidationError extends AppError {
|
|
constructor(message: string, public readonly details?: unknown) {
|
|
super(message, 400, 'VALIDATION_ERROR');
|
|
}
|
|
}
|
|
export class AuthenticationError extends AppError {
|
|
constructor(message = 'Unauthorized') {
|
|
super(message, 401, 'AUTHENTICATION_ERROR');
|
|
}
|
|
}
|
|
export class ForbiddenError extends AppError {
|
|
constructor(message = 'Forbidden') {
|
|
super(message, 403, 'FORBIDDEN');
|
|
}
|
|
}
|
|
export class NotFoundError extends AppError {
|
|
constructor(resource: string) {
|
|
super(`${resource} not found`, 404, 'NOT_FOUND');
|
|
}
|
|
}
|
|
export class ConflictError extends AppError {
|
|
constructor(message: string) {
|
|
super(message, 409, 'CONFLICT');
|
|
}
|
|
}
|
|
export class RateLimitError extends AppError {
|
|
constructor() {
|
|
super('Too many requests', 429, 'RATE_LIMIT');
|
|
}
|
|
}
|
|
```
|
|
|
|
## Global error handler
|
|
```typescript
|
|
export function globalErrorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
|
|
const logger = req.log || console; // pino child logger
|
|
|
|
if (err instanceof AppError && err.isOperational) {
|
|
logger.warn({ err, statusCode: err.statusCode }, err.message);
|
|
return res.status(err.statusCode).json({
|
|
error: err.message,
|
|
code: err.code,
|
|
...(err instanceof ValidationError && err.details ? { details: err.details } : {}),
|
|
});
|
|
}
|
|
|
|
// Programmer error — log full stack, return generic message
|
|
logger.error({ err }, 'Unhandled error');
|
|
return res.status(500).json({
|
|
error: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message,
|
|
code: 'INTERNAL_ERROR',
|
|
});
|
|
}
|
|
```
|
|
|
|
## Composition root (main.ts)
|
|
```typescript
|
|
async function bootstrap() {
|
|
// 1. Config
|
|
const config = loadConfig();
|
|
|
|
// 2. Logger
|
|
const logger = createLogger(config);
|
|
logger.info({ port: config.port }, 'Starting ABE...');
|
|
|
|
// 3. Database + migrations
|
|
const db = createDatabase(config.db);
|
|
await runMigrations(db, logger);
|
|
|
|
// 4. Event bus
|
|
const eventBus = new InProcessEventBus(logger);
|
|
|
|
// 5. Storage
|
|
const storage = new LocalStorageProvider(config.storage.path);
|
|
|
|
// 6. Repositories
|
|
const sessionRepo = new KyselyCrawlSessionRepository(db);
|
|
const findingRepo = new KyselyFindingRepository(db);
|
|
// ... etc
|
|
|
|
// 7. Use cases
|
|
const startCrawl = new StartCrawlCommand(sessionRepo, eventBus);
|
|
const listFindings = new ListFindingsQuery(findingRepo);
|
|
// ... etc
|
|
|
|
// 8. Event handlers — subscribe to event bus
|
|
const onAnomalyDetected = new OnAnomalyDetected(findingRepo, eventBus);
|
|
eventBus.subscribe('crawling.anomaly_detected', onAnomalyDetected);
|
|
// ... etc
|
|
|
|
// 9. Controllers
|
|
const crawlingController = new CrawlingController(startCrawl, ...);
|
|
const findingsController = new FindingsController(listFindings, ...);
|
|
// ... etc
|
|
|
|
// 10. HTTP server
|
|
const app = createServer({ config, authMiddleware, crawlingController, findingsController, ... });
|
|
const httpServer = createServer(app);
|
|
|
|
// 11. Socket.io
|
|
const io = new Server(httpServer, { cors: { origin: config.cors.origin } });
|
|
const gateway = new SocketGateway(io, eventBus);
|
|
|
|
// 12. Job queue
|
|
const jobQueue = new SQLiteJobQueue(db, logger);
|
|
jobQueue.start();
|
|
|
|
// 13. Listen
|
|
httpServer.listen(config.port, config.host, () => {
|
|
logger.info({ port: config.port }, 'ABE server ready');
|
|
});
|
|
|
|
// 14. Graceful shutdown
|
|
async function shutdown(signal: string) {
|
|
logger.info({ signal }, 'Shutting down...');
|
|
httpServer.close();
|
|
io.close();
|
|
jobQueue.pause();
|
|
await jobQueue.waitForActive(30000);
|
|
await db.destroy();
|
|
logger.info('Shutdown complete');
|
|
process.exit(0);
|
|
}
|
|
|
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
}
|
|
|
|
bootstrap().catch((err) => {
|
|
console.error('Fatal: failed to start ABE', err);
|
|
process.exit(1);
|
|
});
|
|
```
|
|
|
|
## IMPORTANTE
|
|
- El código existente en src/server/ debe DEJAR DE USARSE gradualmente
|
|
- Mantener los endpoints viejos funcionando durante la migración
|
|
- Cada controller es una clase con un `.router` getter que retorna Express.Router
|
|
- NUNCA meter lógica de negocio en controllers — solo parse request → call use case → format response
|
|
|