Compare commits

...

2 Commits

2 changed files with 66 additions and 8 deletions

View File

@@ -2835,6 +2835,12 @@ local function main()
while true do
local event, side, channel, replyChannel, message, distance = os.pullEvent("modem_message")
if channel == ORDER_CHANNEL and type(message) == "table" then
-- Idempotency: skip duplicate commands
if isCommandDuplicate(message.commandId) then
log.debug("NET", "Duplicate command skipped: %s", tostring(message.commandId))
else
recordCommandId(message.commandId)
cleanupCommandIds()
local handlerOk, handlerErr = pcall(function()
if message.type == "order" and message.itemName and message.amount then
log.info("NET", "Order: %s x%d", message.itemName, message.amount)
@@ -2848,10 +2854,11 @@ local function main()
end
pcall(function()
networkModem.transmit(replyChannel, ORDER_CHANNEL, {
type = "order_result",
success = success,
message = statusMessage,
color = statusColor,
type = "order_result",
commandId = message.commandId,
success = success,
message = statusMessage,
color = statusColor,
})
end)
pcall(broadcastState)
@@ -2941,9 +2948,10 @@ local function main()
end
pcall(function()
networkModem.transmit(replyChannel, ORDER_CHANNEL, {
type = "craft_result",
success = ok,
error = err,
type = "craft_result",
commandId = message.commandId,
success = ok,
error = err,
})
end)
smelterNeedsRedraw = true
@@ -2954,6 +2962,7 @@ local function main()
if not handlerOk then
log.error("NET", "Handler error: %s", tostring(handlerErr))
end
end -- end idempotency else
end
end
end

View File

@@ -21,6 +21,33 @@ const HOST = process.env.HOST || '0.0.0.0';
app.use(cors());
app.use(express.json({ limit: '5mb' }));
// ========== API Key Authentication ==========
const API_KEY = process.env.API_KEY || '';
// Validate bearer token from Authorization header or ?key= query param
function extractApiKey(req) {
const auth = req.headers.authorization || '';
if (auth.startsWith('Bearer ')) return auth.slice(7);
return req.query.key || '';
}
// Middleware: require API key on mutating endpoints
function requireAuth(req, res, next) {
if (!API_KEY) return next(); // Auth disabled when no key configured
const token = extractApiKey(req);
if (token === API_KEY) return next();
return res.status(401).json({ error: 'Unauthorized — invalid or missing API key' });
}
// Apply auth to all POST/PUT/DELETE routes
app.use((req, res, next) => {
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
return next(); // Read-only endpoints stay open
}
return requireAuth(req, res, next);
});
// ========== State ==========
const webClients = new Set();
const bridgeClients = new Set();
@@ -659,11 +686,33 @@ function updateStateFromBridge(data) {
// ========== WebSocket Server ==========
const wss = new WebSocketServer({ server });
const wss = new WebSocketServer({ noServer: true });
console.log(`🚀 Inventory Manager Web Server starting...`);
console.log(`📡 HTTP Server: http://localhost:${PORT}`);
console.log(`🔌 WebSocket Server: ws://localhost:${PORT}/ws`);
if (API_KEY) {
console.log('🔒 API key authentication enabled');
} else {
console.log('⚠️ No API_KEY set \u2014 authentication disabled (open access)');
}
// Authenticate WebSocket upgrades
server.on('upgrade', (req, socket, head) => {
if (API_KEY) {
// Extract key from query string: /ws?key=... or /ws/bridge?key=...
const urlObj = new URL(req.url, `http://${req.headers.host}`);
const token = urlObj.searchParams.get('key') || '';
if (token !== API_KEY) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}
}
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req);
});
});
wss.on('connection', (ws, req) => {
const url = req.url || '';