Files
homelab-topology/server/index.ts
Christopher Mayor 0910c966a5 feat: migrate from static config to database and add authentication
- Replaced static hosts.json and staticConfig.ts with SQLite database (Prisma)

- Implemented JWT authentication and Login UI

- Added dynamic API routes for hosts, topology, and settings

- Updated UI components to fetch and manage state dynamically

- Added Settings interface for managing hosts and topology nodes
2026-02-25 14:07:11 -08:00

120 lines
3.5 KiB
TypeScript

import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import { createServer } from 'http';
import { Server, Socket } from 'socket.io';
import discoverRouter from './routes/discover';
import configRouter from './routes/config';
import statsRouter from './routes/stats';
import filesRouter from './routes/files';
import terminalRouter from './routes/terminal';
import authRouter from './routes/auth';
import topologyRouter from './routes/topology';
import hostsRouter from './routes/hosts';
import { requireAuth } from './middleware/auth';
import { getHostConfigs } from './config';
import { requestLogger, logger } from './middleware/requestLogger';
import { errorHandler } from './middleware/errorHandler';
const app = express();
const httpServer = createServer(app);
const PORT = 3001;
const allowedOrigins = ['http://localhost:3000', 'http://localhost:4173'];
const corsOrigin = process.env.CORS_ORIGIN ? [process.env.CORS_ORIGIN, ...allowedOrigins] : allowedOrigins;
// --- Socket.IO setup (websocket-engineer skill) ---
const io = new Server(httpServer, {
cors: {
origin: corsOrigin,
credentials: true,
},
pingInterval: 25000,
pingTimeout: 10000,
});
io.on('connection', (socket: Socket) => {
logger.info({ socketId: socket.id }, 'Client connected via WebSocket');
socket.on('disconnect', (reason: string) => {
logger.info({ socketId: socket.id, reason }, 'Client disconnected');
});
});
// Export io so routes can emit events
export { io };
// --- Security middleware (api-security-hardening skill) ---
app.use(helmet({
contentSecurityPolicy: false, // Disable CSP for dev — configure per-environment in production
}));
// CORS — restrict to configured origins
app.use(cors({
origin: corsOrigin,
credentials: true,
}));
// Rate limiting — general API
app.use('/api/', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 200,
message: { status: 'error', message: 'Too many requests, please try again later' },
standardHeaders: true,
legacyHeaders: false,
}));
// Stricter rate limiting for discovery (expensive operation)
app.use('/api/discover', rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 10,
message: { status: 'error', message: 'Discovery rate limited — max 10 per minute' },
standardHeaders: true,
legacyHeaders: false,
}));
// --- Body parsing ---
app.use(express.json({ limit: '1mb' }));
// --- Observability (observability-monitoring skill) ---
app.use(requestLogger);
// --- Health check ---
app.get('/api/health', (_req, res) => {
res.json({
status: 'ok',
uptime: process.uptime(),
timestamp: new Date().toISOString(),
connections: io.engine.clientsCount,
});
});
// --- Debug endpoint (dev only) ---
if (process.env.NODE_ENV !== 'production') {
app.get('/api/debug-config', async (_req, res) => {
const hosts = await getHostConfigs();
res.json({ hosts });
});
}
// --- Public Routes ---
app.use('/api', authRouter);
// --- Protected Routes ---
app.use('/api', requireAuth, discoverRouter);
app.use('/api', requireAuth, configRouter);
app.use('/api', requireAuth, statsRouter);
app.use('/api', requireAuth, filesRouter);
app.use('/api', requireAuth, terminalRouter);
app.use('/api', requireAuth, topologyRouter);
app.use('/api', requireAuth, hostsRouter);
// --- Global error handler (must be last) ---
app.use(errorHandler);
// --- Start server ---
httpServer.listen(PORT, () => {
logger.info(`Server running on http://localhost:${PORT}`);
});