Files
MultiRoombaRover/server/src/services/buttonBoxService/httpRoute.js
T
2026-09-14 17:56:53 -04:00

68 lines
2.0 KiB
JavaScript

// button Box Service HTTP route
// Purpose: Registers button-box press endpoint and request validation against local-network policy.
// Scope: Keeps runtime behavior unchanged while isolating transport/validation from domain logic.
const express = require('express');
function registerButtonBoxRoute(deps) {
const {
app,
logger,
buttonCount,
normalizeIp,
isLocalNetwork,
isEnabled,
applyPress,
} = deps;
function parseButtonId(body) {
if (typeof body !== 'string') return null;
const value = Number.parseInt(body.trim(), 10);
if (Number.isFinite(value)) return value;
return null;
}
function getSocketIp(req) {
return (
req?.socket?.remoteAddress ||
req?.connection?.remoteAddress ||
null
);
}
function denyIfNotLocal(req, res) {
const ip = normalizeIp(getSocketIp(req));
if (isLocalNetwork(ip)) {
return false;
}
logger.warn('Rejected non-local button press request', { ip: ip || null });
res.status(403).json({ error: 'Button presses must originate from local network' });
return true;
}
app.post('/buttonbox/press', express.text({ type: 'text/plain' }), async (req, res) => {
// The route stays registered for the life of Express, but the service gate
// is evaluated per request so the physical endpoint enables and disables
// immediately without accumulating duplicate routes.
if (!isEnabled()) {
res.status(503).json({ error: 'Button box is disabled' });
return;
}
if (denyIfNotLocal(req, res)) return;
const buttonId = parseButtonId(req.body);
if (!Number.isFinite(buttonId) || buttonId < 1 || buttonId > buttonCount) {
res.status(400).json({ error: 'button must be 1-4' });
return;
}
try {
const button = await applyPress(buttonId);
res.json({ success: true, button });
} catch (err) {
res.status(500).json({ error: err.message || 'Button processing failed' });
}
});
}
module.exports = {
registerButtonBoxRoute,
};