Initial commit
This commit is contained in:
126
dist/routes/device.js
vendored
Normal file
126
dist/routes/device.js
vendored
Normal file
@ -0,0 +1,126 @@
|
||||
import { Router } from "express";
|
||||
import { ApiError } from "../shared/errors";
|
||||
import { requireDeviceToken } from "../shared/middleware/auth";
|
||||
import { successResponse } from "../shared/middleware/errorMiddleware";
|
||||
import { getDeviceById, patchDevice } from "../shared/store/deviceStore";
|
||||
import { createDeviceHeartbeat } from "../shared/store/heartbeatStore";
|
||||
import { acknowledgeDeviceCommand } from "../shared/store/deviceCommandStore";
|
||||
const router = Router();
|
||||
function normalizeNumberOrNull(value) {
|
||||
if (typeof value === "string") {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isNaN(parsed) && Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function normalizeSignalStrength(value) {
|
||||
const normalized = normalizeNumberOrNull(value);
|
||||
if (normalized === null) {
|
||||
return null;
|
||||
}
|
||||
if (normalized < 0 || normalized > 100) {
|
||||
throw new Error("NETWORK_STRENGTH_OUT_OF_RANGE");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
function normalizeBatteryLevel(value) {
|
||||
const normalized = normalizeNumberOrNull(value);
|
||||
if (normalized === null) {
|
||||
return null;
|
||||
}
|
||||
if (normalized < 0 || normalized > 100) {
|
||||
throw new Error("BATTERY_LEVEL_OUT_OF_RANGE");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
router.post("/heartbeat", requireDeviceToken, async (req, res, next) => {
|
||||
const payload = req.body;
|
||||
if (!payload || !payload.device_id) {
|
||||
return next(new ApiError("BAD_REQUEST", "device_id is required", 400));
|
||||
}
|
||||
const device = await getDeviceById(payload.device_id);
|
||||
if (!device) {
|
||||
return next(new ApiError("NOT_FOUND", "device not found", 404));
|
||||
}
|
||||
const eventTs = payload.timestamp ? new Date(payload.timestamp) : new Date();
|
||||
if (Number.isNaN(eventTs.getTime())) {
|
||||
return next(new ApiError("BAD_REQUEST", "timestamp must be valid ISO datetime", 400));
|
||||
}
|
||||
let payloadNetworkStrength;
|
||||
let payloadBattery;
|
||||
try {
|
||||
payloadNetworkStrength = normalizeSignalStrength(payload.network_strength);
|
||||
payloadBattery = normalizeBatteryLevel(payload.battery_level);
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error && error.message === "NETWORK_STRENGTH_OUT_OF_RANGE") {
|
||||
return next(new ApiError("BAD_REQUEST", "network_strength must be between 0 and 100", 400));
|
||||
}
|
||||
if (error instanceof Error && error.message === "BATTERY_LEVEL_OUT_OF_RANGE") {
|
||||
return next(new ApiError("BAD_REQUEST", "battery_level must be between 0 and 100", 400));
|
||||
}
|
||||
return next(error);
|
||||
}
|
||||
const heartbeat = await createDeviceHeartbeat({
|
||||
device_id: payload.device_id,
|
||||
timestamp: eventTs.toISOString(),
|
||||
firmware_version: payload.firmware_version,
|
||||
network_strength: payloadNetworkStrength,
|
||||
battery_level: payloadBattery,
|
||||
state: payload.state,
|
||||
payload_json: {
|
||||
network_strength_raw: payload.network_strength,
|
||||
battery_level_raw: payload.battery_level,
|
||||
state: payload.state,
|
||||
firmware_version: payload.firmware_version,
|
||||
timestamp: payload.timestamp,
|
||||
request_id: req.requestId
|
||||
}
|
||||
});
|
||||
await patchDevice(payload.device_id, {
|
||||
last_seen_at: heartbeat.timestamp,
|
||||
firmware_version: payload.firmware_version || device.firmware_version
|
||||
});
|
||||
res.json(successResponse(req, {
|
||||
heartbeat_id: heartbeat.id,
|
||||
device_id: heartbeat.device_id,
|
||||
request_id: req.requestId,
|
||||
server_time: heartbeat.received_at
|
||||
}));
|
||||
});
|
||||
router.post("/commands/ack", requireDeviceToken, async (req, res, next) => {
|
||||
const payload = req.body;
|
||||
if (!payload || !payload.command_id || !payload.device_id || !payload.status) {
|
||||
return next(new ApiError("BAD_REQUEST", "command_id, device_id, status are required", 400));
|
||||
}
|
||||
if (!["delivered", "failed", "timeout"].includes(payload.status)) {
|
||||
return next(new ApiError("BAD_REQUEST", "status must be delivered, failed, or timeout", 400));
|
||||
}
|
||||
const device = await getDeviceById(payload.device_id);
|
||||
if (!device) {
|
||||
return next(new ApiError("NOT_FOUND", "device not found", 404));
|
||||
}
|
||||
const updated = await acknowledgeDeviceCommand({
|
||||
device_id: device.id,
|
||||
command_id: payload.command_id,
|
||||
status: payload.status,
|
||||
reason: payload.reason,
|
||||
result_payload: payload.result_payload
|
||||
});
|
||||
if (!updated) {
|
||||
return next(new ApiError("NOT_FOUND", "command not found", 404));
|
||||
}
|
||||
res.json(successResponse(req, {
|
||||
command_id: updated.id,
|
||||
device_id: updated.device_id,
|
||||
status: updated.status,
|
||||
acknowledged_at: updated.acknowledged_at
|
||||
}));
|
||||
});
|
||||
export default router;
|
||||
Reference in New Issue
Block a user