100 lines
2.8 KiB
JavaScript
100 lines
2.8 KiB
JavaScript
import crypto from "crypto";
|
|
import express from "express";
|
|
import config from "../config.js";
|
|
import { Patch } from "../db/models/index.js";
|
|
import { getConfiguredDevice } from "../services/devices.js";
|
|
import { normalizeProduceIcon } from "../services/patches.js";
|
|
import {
|
|
NodeInputError,
|
|
listNodes,
|
|
setNodePatches,
|
|
upsertNodes,
|
|
} from "../services/nodes.js";
|
|
import { getLogger } from "../logger.js";
|
|
|
|
const logger = getLogger("HubApi");
|
|
|
|
function keysMatch(given, expected) {
|
|
const a = Buffer.from(given);
|
|
const b = Buffer.from(expected);
|
|
return a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
}
|
|
|
|
function requireHubKey(req, res, next) {
|
|
const expected = config.hub?.apiKey;
|
|
if (!expected) {
|
|
res.status(503).json({ status: "error", error: "Hub API not configured" });
|
|
return;
|
|
}
|
|
const match = /^Bearer\s+(.+)$/i.exec(req.get("authorization") || "");
|
|
if (!match || !keysMatch(match[1].trim(), expected)) {
|
|
res.status(401).json({ status: "error", error: "Unauthorized" });
|
|
return;
|
|
}
|
|
next();
|
|
}
|
|
|
|
function sendError(res, e, fallback) {
|
|
if (e instanceof NodeInputError) {
|
|
res.status(400).json({ status: "error", error: e.message });
|
|
return;
|
|
}
|
|
logger.error(fallback, e);
|
|
res.status(500).json({ status: "error", error: e.message || fallback });
|
|
}
|
|
|
|
/** HTTP API used by the hub firmware to relay BLE patch assignments (BLE_PROTOCOL.md Appendix A). */
|
|
export function createHubRouter() {
|
|
const router = express.Router();
|
|
router.use(requireHubKey);
|
|
router.use(express.json());
|
|
|
|
router.get("/patches", async (_req, res) => {
|
|
try {
|
|
const device = await getConfiguredDevice();
|
|
const docs = device
|
|
? await Patch.find({ deviceId: device._id })
|
|
.select("patchId name icon")
|
|
.sort({ patchId: 1 })
|
|
.lean()
|
|
: [];
|
|
const patches = docs.map((p) => ({
|
|
id: p.patchId,
|
|
name: p.name ?? "",
|
|
icon: normalizeProduceIcon(p.icon),
|
|
}));
|
|
res.json({ status: "ok", data: { patches } });
|
|
} catch (e) {
|
|
sendError(res, e, "list patches failed");
|
|
}
|
|
});
|
|
|
|
router.get("/nodes", async (_req, res) => {
|
|
try {
|
|
res.json({ status: "ok", data: { nodes: await listNodes() } });
|
|
} catch (e) {
|
|
sendError(res, e, "list nodes failed");
|
|
}
|
|
});
|
|
|
|
router.post("/nodes", async (req, res) => {
|
|
try {
|
|
const nodes = await upsertNodes(req.body?.serials);
|
|
res.json({ status: "ok", data: { nodes } });
|
|
} catch (e) {
|
|
sendError(res, e, "register nodes failed");
|
|
}
|
|
});
|
|
|
|
router.put("/nodes/:serial", async (req, res) => {
|
|
try {
|
|
const node = await setNodePatches(req.params.serial, req.body?.patches);
|
|
res.json({ status: "ok", data: { node } });
|
|
} catch (e) {
|
|
sendError(res, e, "set node patches failed");
|
|
}
|
|
});
|
|
|
|
return router;
|
|
}
|