const express = require("express"); const config = require("./config"); const logger = require("./logger"); function createCorsMiddleware() { return (req, res, next) => { if (!config.http.cors || !config.http.cors.enabled) { return next(); } const origin = req.headers.origin || "*"; const allowed = config.http.cors.origins.includes("*") || config.http.cors.origins.includes(origin); if (allowed) { res.header("Access-Control-Allow-Origin", origin); res.header("Access-Control-Allow-Methods", "GET,POST,OPTIONS"); res.header("Access-Control-Allow-Headers", "Content-Type"); } if (req.method === "OPTIONS") { return res.sendStatus(204); } next(); }; } function createHttpServer({ dataCache, rateLimiter, propertyMapper, deviceManager }) { if (!config.http || !config.http.enabled) { logger.info("HTTP server is disabled by config"); return null; } const app = express(); app.use(express.json({ limit: "1mb" })); app.use(createCorsMiddleware()); app.get("/api/device/data", (req, res) => { const deviceNumber = req.query.deviceNumber; const mapped = String(req.query.mapped || "false").toLowerCase() === "true"; if (!deviceNumber) { return res.status(400).json({ error: "deviceNumber is required" }); } const limit = rateLimiter.checkLimit( `device:${deviceNumber}`, config.http.rateLimit.singleDeviceMs ); if (!limit.allowed) { return res.status(429).json({ error: "too many requests", waitMs: limit.waitMs }); } const entry = dataCache.getDeviceData(deviceNumber); if (!entry) { return res.status(404).json({ error: "device not found" }); } const base = mapped ? propertyMapper.transformForHTTP(entry.data, deviceNumber) : { deviceNumber, data: entry.data }; return res.json({ ...base, updatedAt: entry.updatedAt }); }); app.get("/api/device/all", (req, res) => { const mapped = String(req.query.mapped || "false").toLowerCase() === "true"; const limit = rateLimiter.checkLimit( "__all_devices__", config.http.rateLimit.allDevicesMs ); if (!limit.allowed) { return res.status(429).json({ error: "too many requests", waitMs: limit.waitMs }); } const all = dataCache.getAllDeviceData(); const result = {}; for (const [deviceNumber, entry] of Object.entries(all)) { if (mapped) { result[deviceNumber] = { ...propertyMapper.transformForHTTP(entry.data, deviceNumber), updatedAt: entry.updatedAt }; } else { result[deviceNumber] = { deviceNumber, data: entry.data, updatedAt: entry.updatedAt }; } } return res.json(result); }); app.get("/api/device/list", (req, res) => { return res.json(dataCache.getDeviceList()); }); app.get("/api/cache/stats", (req, res) => { return res.json(dataCache.getStats()); }); app.post("/api/cache/clear", (req, res) => { dataCache.clear(); return res.json({ ok: true }); }); app.get("/api/device/idle", (req, res) => { const timeout = parseInt(req.query.timeout, 10); const timeoutMs = Number.isFinite(timeout) && timeout > 0 ? timeout : 5 * 60 * 1000; return res.json(dataCache.getIdleDevices(timeoutMs)); }); app.get("/api/ratelimit/stats", (req, res) => { return res.json(rateLimiter.getStats()); }); app.post("/api/ratelimit/clear", (req, res) => { rateLimiter.clear(); return res.json({ ok: true }); }); app.get("/api/health", (req, res) => { res.json({ status: "ok", tcp: "listening", devices: deviceManager ? deviceManager.getStats() : [] }); }); const server = app.listen(config.http.port, config.http.host, () => { logger.info("HTTP server listening", { host: config.http.host, port: config.http.port }); }); return server; } module.exports = createHttpServer;