mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 22:16:38 +00:00
Add session-based username/password auth for CCS dashboard: - Backend: Express session middleware with bcrypt password verification - Frontend: React AuthContext, login page, protected routes - Config: dashboard_auth section in config.yaml + env var overrides - Security: Rate limiting (5 attempts/15min), persistent session secret - 16 unit tests for auth middleware Auth disabled by default for backward compatibility.
107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
/**
|
|
* CCS Config Dashboard - Web Server
|
|
*
|
|
* Express server with WebSocket support for real-time config management.
|
|
* Single HTTP server handles REST API, static files, and WebSocket connections.
|
|
* In dev mode, integrates Vite for HMR.
|
|
*/
|
|
|
|
import express from 'express';
|
|
import http from 'http';
|
|
import path from 'path';
|
|
import { WebSocketServer } from 'ws';
|
|
import { setupWebSocket } from './websocket';
|
|
import { createSessionMiddleware, authMiddleware } from './middleware/auth-middleware';
|
|
|
|
export interface ServerOptions {
|
|
port: number;
|
|
staticDir?: string;
|
|
dev?: boolean;
|
|
}
|
|
|
|
export interface ServerInstance {
|
|
server: http.Server;
|
|
wss: WebSocketServer;
|
|
cleanup: () => void;
|
|
}
|
|
|
|
/**
|
|
* Start Express server with WebSocket support
|
|
*/
|
|
export async function startServer(options: ServerOptions): Promise<ServerInstance> {
|
|
const app = express();
|
|
const server = http.createServer(app);
|
|
const wss = new WebSocketServer({ server });
|
|
|
|
// JSON body parsing with error handler for malformed JSON
|
|
app.use(express.json());
|
|
app.use(
|
|
(
|
|
err: Error & { status?: number; body?: string },
|
|
_req: express.Request,
|
|
res: express.Response,
|
|
next: express.NextFunction
|
|
) => {
|
|
if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
|
|
res.status(400).json({ error: 'Invalid JSON in request body' });
|
|
return;
|
|
}
|
|
next(err);
|
|
}
|
|
);
|
|
|
|
// Session middleware (for dashboard auth)
|
|
app.use(createSessionMiddleware());
|
|
|
|
// Auth middleware (protects API routes when enabled)
|
|
app.use(authMiddleware);
|
|
|
|
// REST API routes (modularized)
|
|
const { apiRoutes } = await import('./routes/index');
|
|
app.use('/api', apiRoutes);
|
|
|
|
// Shared data routes (Phase 07)
|
|
const { sharedRoutes } = await import('./shared-routes');
|
|
app.use('/api/shared', sharedRoutes);
|
|
|
|
// Overview routes (Phase 07)
|
|
const { overviewRoutes } = await import('./overview-routes');
|
|
app.use('/api/overview', overviewRoutes);
|
|
|
|
// Usage analytics routes
|
|
const { usageRoutes } = await import('./usage-routes');
|
|
app.use('/api/usage', usageRoutes);
|
|
|
|
// Dev mode: use Vite middleware for HMR
|
|
if (options.dev) {
|
|
const { createServer: createViteServer } = await import('vite');
|
|
const vite = await createViteServer({
|
|
root: path.join(__dirname, '../../ui'),
|
|
server: { middlewareMode: true },
|
|
appType: 'spa',
|
|
});
|
|
app.use(vite.middlewares);
|
|
} else {
|
|
// Production: serve static files from dist/ui/
|
|
const staticDir = options.staticDir || path.join(__dirname, '../ui');
|
|
app.use(express.static(staticDir));
|
|
|
|
// SPA fallback - return index.html for all non-API routes
|
|
app.get('*', (_req, res) => {
|
|
res.sendFile(path.join(staticDir, 'index.html'));
|
|
});
|
|
}
|
|
|
|
// WebSocket connection handler + file watcher
|
|
const { cleanup } = setupWebSocket(wss);
|
|
|
|
// Start listening
|
|
return new Promise<ServerInstance>((resolve) => {
|
|
server.listen(options.port, () => {
|
|
// Usage cache loads on-demand when Analytics page is visited
|
|
// This keeps server startup instant for users who don't need analytics
|
|
resolve({ server, wss, cleanup });
|
|
});
|
|
});
|
|
}
|