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;
|