Files
kota-api-server/src/routes/sensorbox.ts
2026-06-05 15:39:58 +02:00

191 lines
5.5 KiB
TypeScript

import express from "express";
import { config } from "../config";
import {
createDownloadLinkDb,
deviceGroups,
deviceTypes,
findLatestReleaseDb,
getDeviceByIdDb,
getUpdateByIdDb,
registerUpdateLogDb,
saveDevice,
} from "../data";
import { generateDeviceToken, sensorboxAuth, verifyDeviceToken } from "../auth";
const router = express.Router();
router.post("/", (req, res) => {
const authorization = req.headers.authorization;
const deviceId = String(req.body.deviceId || "").trim();
const type = String(req.body.type || "").trim();
const hardwareVersion = String(req.body.hardwareVersion || "");
const softwareVersion = String(req.body.softwareVersion || "");
const group = String("release") as (typeof deviceGroups)[number];
const token = authorization?.startsWith("Bearer ")
? authorization.slice(7)
: String(req.body.token || "");
if (!deviceId || !type) {
console.log("Missing deviceId or type in registration request");
res.status(400).send("deviceId and type are required");
return;
}
const deviceType = deviceTypes.find((entry) => entry.id === type);
if (!deviceType) {
console.log(`Unknown device type: ${type}`);
res.status(400).send("Unknown device type");
return;
}
if (!deviceType.supportedGroups.includes(group)) {
console.log(`Unsupported group: ${group}`);
res.status(400).send("Unsupported group for device type");
return;
}
if (token !== config.sensorboxBootstrapToken) {
const existingDevice = verifyDeviceToken(token);
if (!existingDevice || existingDevice.id !== deviceId) {
console.log("Invalid bootstrap token or device token");
res.status(401).send("Invalid bootstrap token or existing device token");
return;
}
console.log(`Device with ID ${deviceId} already registered, updating info`);
// Regenerate token for existing device
const jwtToken = generateDeviceToken(existingDevice.id);
existingDevice.token = jwtToken;
existingDevice.lastSeen = new Date().toISOString();
saveDevice(existingDevice);
res.send(jwtToken);
return;
}
const existingDevice = getDeviceByIdDb(deviceId);
if (existingDevice) {
console.log(`Device with ID ${deviceId} already exists`);
res.status(409).send("Device already registered");
return;
}
const deviceRecord = {
id: deviceId,
type,
group,
token: "",
registeredAt: new Date().toISOString(),
currentVersion: softwareVersion || undefined,
hardwareVersion: hardwareVersion || undefined,
status: "registered",
lastSeen: new Date().toISOString(),
};
const jwtToken = generateDeviceToken(deviceRecord.id);
deviceRecord.token = jwtToken;
deviceRecord.type = type;
deviceRecord.group = group;
deviceRecord.currentVersion = softwareVersion || deviceRecord.currentVersion;
deviceRecord.hardwareVersion =
hardwareVersion || deviceRecord.hardwareVersion;
deviceRecord.lastSeen = new Date().toISOString();
saveDevice(deviceRecord);
// Add Content-Length header to prevent chunked encoding which can cause issues for some clients
res.contentType("text/plain").send(jwtToken);
});
router.get("/ota/check", sensorboxAuth, (req, res) => {
const device = req.device!;
const hardwareVersion = String(
req.query.hardwareVersion || device.hardwareVersion || "",
);
const softwareVersion = String(
req.query.softwareVersion || device.currentVersion || "0.0.0",
);
const group = String(
req.query.group || device.group,
) as (typeof deviceGroups)[number];
if (!hardwareVersion) {
res.status(400).send("hardwareVersion is required");
return;
}
device.lastSeen = new Date().toISOString();
device.currentVersion = softwareVersion;
device.hardwareVersion = hardwareVersion;
// Update the device
saveDevice(device);
const update = findLatestReleaseDb(device.type, group, hardwareVersion);
if (!update || update.version === softwareVersion) {
res.sendStatus(204);
return;
}
const link = createDownloadLinkDb(device.id, update.id);
const downloadUrl = `${config.baseUrl}/download/${link.id}`;
// Get size of the update file to include in the response header
const fs = require("fs");
let fileSize = 0;
try {
const stats = fs.statSync(update.filePath);
fileSize = stats.size;
} catch (err) {
console.error("Error getting update file size:", err);
}
res
.contentType("text/plain")
.send(
"" +
update.id +
"\n" +
update.version +
"\n" +
downloadUrl +
"\n" +
new Date(link.expiresAt).toISOString() +
"\n" +
fileSize,
);
});
router.post("/ota/report", sensorboxAuth, (req, res) => {
const device = req.device!;
const updateId = String(req.body.updateId || "");
const success = req.body.success === true;
const message = String(req.body.message || "");
if (!updateId) {
res.status(400).send("updateId is required");
return;
}
const update = getUpdateByIdDb(updateId);
if (!update) {
res.status(404).send("Update not found");
return;
}
registerUpdateLogDb(device.id, updateId, success, message);
if (success) {
device.currentVersion = update.version;
device.hardwareVersion = update.hardwareVersion;
device.status = "updated";
} else {
device.status = "update_failed";
}
device.lastSeen = new Date().toISOString();
res.send(true);
});
router.get("/device", sensorboxAuth, (req, res) => {
res.json({ device: req.device });
});
export { router as sensorboxRouter };