bytelyst-devops-tools/dashboard/backend/src/server.ts
2026-05-27 13:04:36 +00:00

295 lines
9.1 KiB
TypeScript

import Fastify from 'fastify';
import { config } from './lib/config.js';
import { initializeContainers } from './lib/cosmos-init.js';
import { extractAuth, AuthError, requireAdmin } from './lib/auth.js';
import { generateCsrfToken, validateCsrfToken, getSessionId } from './lib/csrf.js';
import { serviceRoutes } from './modules/services/routes.js';
import { deploymentRoutes } from './modules/deployments/routes.js';
import { healthRoutes } from './modules/health/routes.js';
import { auditRoutes } from './modules/audit/routes.js';
import { backupRoutes } from './modules/backup/routes.js';
import { systemRoutes } from './modules/system/routes.js';
import { envRoutes } from './modules/env/routes.js';
import { azureConfigRoutes } from './modules/azure-config/routes.js';
import { codeQualityRoutes } from './modules/code-quality/routes.js';
import { cosmosConfigRoutes } from './modules/cosmos-config/routes.js';
import { hermesOpsRoutes } from './modules/hermes-ops/routes.js';
// import sse from 'fastify-sse-v2';
import rateLimit from '@fastify/rate-limit';
import swagger from '@fastify/swagger';
import swaggerUi from '@fastify/swagger-ui';
const fastify = Fastify({
logger: true,
});
// Register SSE plugin
// TODO: fastify-sse-v2 has compatibility issues with Fastify 5
// await fastify.register(sse);
// Register rate limiting
await fastify.register(rateLimit, {
max: 100, // 100 requests per window
timeWindow: '1 minute',
errorResponseBuilder: (request, context) => ({
code: 429,
error: 'Too many requests',
retryAfter: context.ttl,
}),
skipOnError: false,
});
// Register Swagger
await fastify.register(swagger, {
openapi: {
openapi: '3.0.0',
info: {
title: 'ByteLyst DevOps API',
description: 'API for deployment orchestration and service monitoring',
version: '0.1.0',
},
servers: [
{
url: 'http://localhost:4004',
description: 'Development server',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
},
});
await fastify.register(swaggerUi, {
routePrefix: '/docs',
uiConfig: {
docExpansion: 'list',
deepLinking: false,
},
staticCSP: true,
transformStaticCSP: (header) => header,
transformSpecification: (swaggerObject, request, reply) => {
return swaggerObject;
},
transformSpecificationClone: true,
});
// Auth hook - extract user info from JWT
fastify.addHook('onRequest', async (request, reply) => {
const auth = await extractAuth(request);
if (auth) {
(request as any).authUserId = auth.userId;
(request as any).authRole = auth.role;
(request as any).authEmail = auth.email;
(request as any).authProductId = auth.productId;
}
});
// CSRF protection hook - validate CSRF tokens for state-changing requests
fastify.addHook('onRequest', async (request, reply) => {
const method = request.method;
const stateChangingMethods = ['POST', 'PUT', 'DELETE', 'PATCH'];
if (!stateChangingMethods.includes(method)) {
return;
}
const sessionId = getSessionId(request);
if (!sessionId) {
return reply.code(401).send({ error: 'Unauthorized' });
}
const csrfToken = request.headers['x-csrf-token'] as string;
if (!csrfToken) {
return reply.code(403).send({ error: 'CSRF token missing' });
}
if (!validateCsrfToken(csrfToken, sessionId)) {
return reply.code(403).send({ error: 'Invalid CSRF token' });
}
});
// Performance monitoring hook
fastify.addHook('onRequest', async (request, reply) => {
(request as any).startTime = Date.now();
});
fastify.addHook('onResponse', async (request, reply) => {
const startTime = (request as any).startTime;
if (startTime) {
const duration = Date.now() - startTime;
const route = request.routeOptions.url || request.url;
const method = request.method;
fastify.log.info({
method,
route,
statusCode: reply.statusCode,
duration,
performance: 'slow',
}, `${method} ${route} - ${reply.statusCode} (${duration}ms)`);
// Alert on slow responses (> 1 second)
if (duration > 1000) {
fastify.log.warn({
method,
route,
statusCode: reply.statusCode,
duration,
alert: 'slow-response',
}, `Slow response detected: ${method} ${route} took ${duration}ms`);
}
}
});
// Error handler for AuthError
fastify.setErrorHandler((error, request, reply) => {
if (error instanceof AuthError) {
return reply.code(error.statusCode).send({ error: error.message });
}
// Default error handling
reply.code(500).send({ error: 'Internal server error' });
});
// CORS - more secure configuration
fastify.addHook('onSend', async (request, reply) => {
const allowedOrigins = [
'http://localhost:3000',
'http://localhost:3001',
'https://devops.bytelyst.com',
];
const origin = request.headers.origin;
if (origin && allowedOrigins.includes(origin)) {
reply.header('Access-Control-Allow-Origin', origin);
}
reply.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
reply.header('Access-Control-Allow-Headers', 'Content-Type,Authorization');
reply.header('Access-Control-Allow-Credentials', 'true');
reply.header('Access-Control-Max-Age', '86400'); // 24 hours
// Security headers
reply.header('X-Content-Type-Options', 'nosniff');
reply.header('X-Frame-Options', 'DENY');
reply.header('X-XSS-Protection', '1; mode=block');
reply.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
reply.header('Referrer-Policy', 'strict-origin-when-cross-origin');
});
// Handle OPTIONS preflight requests
fastify.options('*', async (request, reply) => {
reply.code(204).send();
});
// Health check
fastify.get('/health', async () => ({ status: 'ok', service: 'devops-backend' }));
// Register standalone routes with /api prefix
await fastify.register(async function (fastify) {
// Performance metrics endpoint (admin only) - DEPRECATED: Use /api/system/metrics instead
fastify.get('/metrics', {
preHandler: async (req) => requireAdmin(req),
}, async (request, reply) => {
try {
const { getSystemMetrics } = await import('./modules/system/repository.js');
const metrics = await getSystemMetrics();
return reply.send(metrics);
} catch (error) {
fastify.log.error(error, 'Failed to get metrics');
return reply.code(500).send({ error: 'Failed to get metrics' });
}
});
// CSRF token endpoint
fastify.get('/csrf-token', async (request, reply) => {
const sessionId = getSessionId(request);
if (!sessionId) {
return reply.code(401).send({ error: 'Unauthorized' });
}
const token = generateCsrfToken(sessionId);
return { csrfToken: token };
});
// Seed default services
fastify.post('/seed', async (request, reply) => {
const { createService } = await import('./modules/services/repository.js');
const defaultServices = [
{
id: 'trading',
name: 'Investment Trading',
scriptPath: '../deploy-invttrdg.sh',
healthUrl: 'https://api.bytelyst.com/invttrdg/health',
repoPath: '../learning_ai_invt_trdg',
},
{
id: 'notes',
name: 'Agentic Notes',
scriptPath: '../deploy-notes.sh',
healthUrl: 'https://api.notelett.app/health',
repoPath: '../learning_ai_notes',
},
{
id: 'clock',
name: 'AI Clock',
scriptPath: '../deploy-clock.sh',
healthUrl: 'https://api.clock.bytelyst.com/health',
repoPath: '../learning_ai_clock',
},
];
for (const serviceData of defaultServices) {
try {
await createService(serviceData);
} catch (error) {
fastify.log.info({ serviceId: serviceData.id }, 'Service might already exist');
}
}
return reply.send({ message: 'Seeded default services' });
});
}, { prefix: '/api' });
// Register modular routes with /api prefix
await fastify.register(serviceRoutes, { prefix: '/api' });
await fastify.register(deploymentRoutes, { prefix: '/api' });
await fastify.register(healthRoutes, { prefix: '/api' });
await fastify.register(auditRoutes, { prefix: '/api' });
await fastify.register(backupRoutes, { prefix: '/api' });
await fastify.register(systemRoutes, { prefix: '/api' });
await fastify.register(envRoutes, { prefix: '/api' });
await fastify.register(azureConfigRoutes, { prefix: '/api' });
await fastify.register(codeQualityRoutes, { prefix: '/api' });
await fastify.register(cosmosConfigRoutes, { prefix: '/api' });
await fastify.register(hermesOpsRoutes, { prefix: '/api' });
// Start server
async function start() {
try {
// Try to initialize Cosmos containers, but allow server to start even if it fails
try {
await initializeContainers();
fastify.log.info('Cosmos containers initialized successfully');
} catch (err) {
fastify.log.warn(err, 'Failed to initialize Cosmos containers (server will start anyway)');
}
await fastify.listen({ port: parseInt(config.PORT), host: '0.0.0.0' });
fastify.log.info({ port: config.PORT }, 'DevOps backend listening');
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
}
start();