feat: refactor server setup to use createPlatformServer and streamline middleware
This commit is contained in:
@@ -1,85 +1,48 @@
|
||||
import express from 'express';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import { createServer } from 'http';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createRequire } from 'module';
|
||||
import {
|
||||
loadFullState, saveFullState, recordItemHistory,
|
||||
saveItems, saveFurnaces, saveAlerts, saveState, loadState,
|
||||
getHistory, getHistorySummary, closeDb, flushPendingSave,
|
||||
} from './db.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const {
|
||||
createPlatformServer,
|
||||
setupGracefulShutdown,
|
||||
createRateLimiter,
|
||||
createProxyEndpoint,
|
||||
} = require('@cc-platform/server');
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
|
||||
// CORS intentionally omitted — nginx reverse-proxy makes all requests same-origin.
|
||||
// If you need direct server access during dev, add: app.use(require('cors')())
|
||||
app.disable('x-powered-by');
|
||||
app.use(express.json({ limit: '5mb' }));
|
||||
|
||||
// ========== API Key Authentication ==========
|
||||
|
||||
const API_KEY = process.env.API_KEY || '';
|
||||
|
||||
// Validate bearer token from Authorization header or ?key= query param
|
||||
function extractApiKey(req) {
|
||||
const auth = req.headers.authorization || '';
|
||||
if (auth.startsWith('Bearer ')) return auth.slice(7);
|
||||
return req.query.key || '';
|
||||
}
|
||||
|
||||
// Middleware: require API key on mutating endpoints
|
||||
function requireAuth(req, res, next) {
|
||||
if (!API_KEY) return next(); // Auth disabled when no key configured
|
||||
const token = extractApiKey(req);
|
||||
if (token === API_KEY) return next();
|
||||
return res.status(401).json({ error: 'Unauthorized — invalid or missing API key' });
|
||||
}
|
||||
|
||||
// Apply auth to all POST/PUT/DELETE routes
|
||||
app.use((req, res, next) => {
|
||||
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
|
||||
return next(); // Read-only endpoints stay open
|
||||
}
|
||||
return requireAuth(req, res, next);
|
||||
// ========== Platform Server Setup ==========
|
||||
// Provides Express app, HTTP server, auth middleware, and health endpoint.
|
||||
// CORS intentionally disabled — nginx reverse-proxy makes all requests same-origin.
|
||||
const { app, server, auth, start, port } = createPlatformServer({
|
||||
serviceName: 'inventory-manager',
|
||||
cors: false,
|
||||
rateLimit: false, // custom rate limiting below (excludes bridge endpoints)
|
||||
healthExtras: () => ({
|
||||
lastUpdate,
|
||||
bridgeConnected: bridgeClients.size > 0,
|
||||
webClients: webClients.size,
|
||||
}),
|
||||
});
|
||||
|
||||
// ========== Rate Limiting (in-memory, no external dependencies) ==========
|
||||
const { requireAuth } = auth;
|
||||
|
||||
function createRateLimiter(windowMs, max) {
|
||||
const hits = new Map();
|
||||
setInterval(() => {
|
||||
const cutoff = Date.now() - windowMs;
|
||||
for (const [key, entry] of hits) {
|
||||
if (entry.start < cutoff) hits.delete(key);
|
||||
}
|
||||
}, windowMs);
|
||||
return (req, res, next) => {
|
||||
const key = req.ip || req.socket.remoteAddress || 'unknown';
|
||||
const now = Date.now();
|
||||
const entry = hits.get(key);
|
||||
if (!entry || now - entry.start > windowMs) {
|
||||
hits.set(key, { start: now, count: 1 });
|
||||
return next();
|
||||
}
|
||||
entry.count++;
|
||||
if (entry.count > max) {
|
||||
res.set('Retry-After', String(Math.ceil((entry.start + windowMs - now) / 1000)));
|
||||
return res.status(429).json({ error: 'Too many requests — try again later' });
|
||||
}
|
||||
return next();
|
||||
};
|
||||
}
|
||||
// API key reference needed for WebSocket upgrade authentication
|
||||
const API_KEY = process.env.API_KEY || '';
|
||||
|
||||
// 30 mutating requests per minute per IP (excludes bridge state updates)
|
||||
const commandLimiter = createRateLimiter(60_000, 30);
|
||||
// Custom rate limiting: 30 mutating requests/min per IP, excluding bridge endpoints
|
||||
const commandLimiter = createRateLimiter({ windowMs: 60000, maxRequests: 30 });
|
||||
app.use((req, res, next) => {
|
||||
if (req.method !== 'POST' || req.path.startsWith('/api/bridge/')) return next();
|
||||
if (req.path.startsWith('/api/bridge/')) return next();
|
||||
return commandLimiter(req, res, next);
|
||||
});
|
||||
|
||||
@@ -171,9 +134,6 @@ function pushCommandToBridge(command) {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== HTTP Server ==========
|
||||
const server = createServer(app);
|
||||
|
||||
// ========== Texture Proxy / Cache ==========
|
||||
const TEXTURE_CACHE_DIR = process.env.TEXTURE_CACHE_DIR || path.join('/data', 'texture-cache');
|
||||
const NEGATIVE_CACHE_TTL = 7 * 24 * 60 * 60 * 1000; // 7 days for 404s
|
||||
@@ -524,16 +484,7 @@ app.delete('/api/texture-cache/negative', (req, res) => {
|
||||
res.json({ cleared });
|
||||
});
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
lastUpdate,
|
||||
uptime: process.uptime(),
|
||||
bridgeConnected: bridgeClients.size > 0,
|
||||
webClients: webClients.size,
|
||||
});
|
||||
});
|
||||
// Health endpoint provided by platform (createPlatformServer) with custom extras
|
||||
|
||||
// Get current inventory state
|
||||
app.get('/api/inventory', (req, res) => {
|
||||
@@ -1383,53 +1334,24 @@ app.get('/api/integration/low-stock', (req, res) => {
|
||||
});
|
||||
|
||||
// Proxy to turtle server for combined dashboard info
|
||||
app.get('/api/integration/turtle-status', async (req, res) => {
|
||||
if (!TURTLE_SERVER_URL) {
|
||||
return res.json({ configured: false, message: 'TURTLE_SERVER_URL not configured' });
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(`${TURTLE_SERVER_URL}/api/turtles`);
|
||||
const data = await resp.json();
|
||||
res.json({ configured: true, ...data });
|
||||
} catch (err) {
|
||||
res.status(502).json({ configured: true, error: `Cannot reach turtle server: ${err.message}` });
|
||||
}
|
||||
});
|
||||
createProxyEndpoint(app, '/api/integration/turtle-status', 'TURTLE_SERVER_URL', '/api/turtles');
|
||||
|
||||
// ========== Start Server ==========
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
console.log(`✅ Inventory Manager Web Server ready on ${HOST}:${PORT}`);
|
||||
console.log(`\nBridge HTTP endpoint: http://localhost:${PORT}/api/bridge/state`);
|
||||
console.log(`Bridge WebSocket: ws://localhost:${PORT}/ws/bridge`);
|
||||
console.log(`Web client WebSocket: ws://localhost:${PORT}/ws`);
|
||||
setupGracefulShutdown({
|
||||
serviceName: 'inventory-manager',
|
||||
cleanup: [
|
||||
() => wss.close(),
|
||||
() => server.close(),
|
||||
() => { closeDb(); console.log('💾 Database closed'); },
|
||||
],
|
||||
});
|
||||
|
||||
start(() => {
|
||||
console.log(`\nBridge HTTP endpoint: http://localhost:${port}/api/bridge/state`);
|
||||
console.log(`Bridge WebSocket: ws://localhost:${port}/ws/bridge`);
|
||||
console.log(`Web client WebSocket: ws://localhost:${port}/ws`);
|
||||
if (TURTLE_SERVER_URL) {
|
||||
console.log(`🐢 Turtle server integration: ${TURTLE_SERVER_URL}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
function shutdown() {
|
||||
console.log('\n🛑 Shutting down server...');
|
||||
try {
|
||||
wss.close();
|
||||
server.close();
|
||||
closeDb();
|
||||
console.log('💾 Database closed');
|
||||
} catch (err) {
|
||||
console.error('❌ Error during shutdown:', err.message);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
|
||||
// Catch unhandled errors to prevent silent crashes
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
console.error('❌ Unhandled rejection:', reason);
|
||||
});
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('❌ Uncaught exception:', err);
|
||||
shutdown();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user