This commit is contained in:
legop3
2026-07-16 16:45:51 -04:00
parent 3a8a2ebb13
commit 385e7c25fa
5 changed files with 44 additions and 35 deletions
+14 -11
View File
@@ -7,7 +7,7 @@ const logger = require('../../globals/logger').child('liftService');
const { loadConfig } = require('../../helpers/configLoader');
const { isFeatureEnabled } = require('../../helpers/features');
const { getMode, MODES } = require('../modeManager');
const { isLockdownAdmin } = require('../roleService');
const { isAdmin, isLockdownAdmin } = require('../roleService');
const {
homeAssistantEvents,
getRawEntitySnapshot,
@@ -186,13 +186,20 @@ if (featureEnabled) {
homeAssistantEvents.on('status', emitUpdate);
io.on('connection', (socket) => {
function assertFeatureAccess() {
const mode = getMode();
// Lift is a public activity feature in open and turns modes. Restricted
// access modes mirror the rest of the server: admin mode admits normal
// admins, while lockdown admits only the explicitly stronger lockdown
// role. Enforcing this in the owning service keeps UI buttons and text
// command behavior aligned instead of trusting individual callers.
if (mode === MODES.ADMIN && !isAdmin(socket)) throw new Error('Admin mode: admins only');
if (mode === MODES.LOCKDOWN && !isLockdownAdmin(socket)) throw new Error('Server in lockdown');
}
socket.on('lift:up', async (_, cb = () => {}) => {
try {
if (getMode() === MODES.LOCKDOWN && !isLockdownAdmin(socket)) {
throw new Error('Server in lockdown');
}
// Lift movement is now a public activity feature. Lockdown still wins
// above because that mode is the global safety/admin gate for the room.
assertFeatureAccess();
const resp = await moveUp(socket.id || 'socket');
cb({ success: true, ...resp });
} catch (err) {
@@ -202,11 +209,7 @@ if (featureEnabled) {
socket.on('lift:down', async (_, cb = () => {}) => {
try {
if (getMode() === MODES.LOCKDOWN && !isLockdownAdmin(socket)) {
throw new Error('Server in lockdown');
}
// Public access intentionally mirrors lift:up so both directions share
// the same policy and cannot drift into different permission behavior.
assertFeatureAccess();
const resp = await moveDown(socket.id || 'socket');
cb({ success: true, ...resp });
} catch (err) {
+14 -18
View File
@@ -8,7 +8,7 @@ const { loadConfig } = require('../../helpers/configLoader');
const { isFeatureEnabled } = require('../../helpers/features');
const { isVerified } = require('../verificationService');
const { getMode, MODES } = require('../modeManager');
const { isLockdownAdmin } = require('../roleService');
const { isAdmin, isLockdownAdmin } = require('../roleService');
const {
homeAssistantEvents,
getRawEntitySnapshot,
@@ -269,17 +269,19 @@ function hasVerifiedSockets() {
if (featureEnabled) {
io.on('connection', (socket) => {
function assertLockdownAccess() {
if (getMode() === MODES.LOCKDOWN && !isLockdownAdmin(socket)) {
throw new Error('Server in lockdown');
}
function assertFeatureAccess() {
const mode = getMode();
// Neato shares the same public-activity policy as lift: everyone may use
// it in open/turns modes, admin mode requires an admin, and lockdown
// requires a lockdown admin. This service-level gate protects every socket
// action even if a future client bypasses the current UI presentation.
if (mode === MODES.ADMIN && !isAdmin(socket)) throw new Error('Admin mode: admins only');
if (mode === MODES.LOCKDOWN && !isLockdownAdmin(socket)) throw new Error('Server in lockdown');
}
socket.on('neato:start', async (_, cb = () => {}) => {
try {
assertLockdownAccess();
// Neato commands are public activity features. The lockdown check above
// remains the room-wide safety/admin gate when the server is restricted.
assertFeatureAccess();
await startCleaning();
cb({ success: true });
} catch (err) {
@@ -289,8 +291,7 @@ if (featureEnabled) {
socket.on('neato:sendHome', async (_, cb = () => {}) => {
try {
assertLockdownAccess();
// Keep send-home public for consistency with the rest of the Neato card.
assertFeatureAccess();
await sendHome();
cb({ success: true });
} catch (err) {
@@ -300,8 +301,7 @@ if (featureEnabled) {
socket.on('neato:locate', async (_, cb = () => {}) => {
try {
assertLockdownAccess();
// Locate is a public activity action; lockdown still blocks it above.
assertFeatureAccess();
await locateRobot();
cb({ success: true });
} catch (err) {
@@ -311,9 +311,7 @@ if (featureEnabled) {
socket.on('neato:clearErrors', async (_, cb = () => {}) => {
try {
assertLockdownAccess();
// Error clearing is grouped with the public Neato controls so the UI does
// not show a button that only some public users can actually run.
assertFeatureAccess();
await clearErrors();
cb({ success: true });
} catch (err) {
@@ -323,9 +321,7 @@ if (featureEnabled) {
socket.on('neato:powerCycle', async (_, cb = () => {}) => {
try {
assertLockdownAccess();
// Power cycle follows the same public policy as the rest of the card;
// operational safety remains controlled by lockdown mode.
assertFeatureAccess();
await powerCycle();
cb({ success: true });
} catch (err) {
@@ -97,8 +97,19 @@ function createCommandHandlers(deps) {
// light locking belongs here because it can force the physical room lights
// on and disables ordinary Home Assistant room controls for everyone else.
const moderationActions = new Set(['lock', 'unlock', 'mode', 'goal', 'reason', 'verify', 'deter', 'lights', 'kick', 'lift', 'neato']);
const isAccessModeCommand = commandDefinition?.permission === 'access-mode';
if (!isAdmin && action !== '' && action !== 'status' && action !== 'help' && action !== 'replay' && action !== 'bridge' && action !== 'goal' && action !== 'reason' && action !== 'verify' && action !== 'deter') {
// Feature commands are public activities while access is open or managed
// by turns. In admin mode they follow the same admin-only boundary as rover
// access, and lockdown continues to require the stricter lockdown role.
// Keeping this policy in the shared dispatcher makes web chat and Discord
// behave identically instead of each transport interpreting modes itself.
if (isAccessModeCommand && mode === MODES.ADMIN && !isAdmin) {
await request.reply({ content: 'Admin mode: only admins can run feature commands.', allowedMentions: { parse: [], repliedUser: false } });
return;
}
if (!isAccessModeCommand && !isAdmin && action !== '' && action !== 'status' && action !== 'help' && action !== 'replay' && action !== 'bridge' && action !== 'goal' && action !== 'reason' && action !== 'verify' && action !== 'deter') {
await request.reply({ content: 'Only admins can run that command.', allowedMentions: { parse: [], repliedUser: false } });
return;
}
@@ -23,8 +23,8 @@ function buildCommandRegistry(prefix, timeCommand) {
kick: { category: 'admin', summary: 'Remove a user from their current rover.', usage: [`${prefix} kick <user> [reason]`], access: 'Admin', permission: 'admin' },
verify: { category: 'admin', summary: 'List or remove verified identities.', usage: [`${prefix} verify list`, `${prefix} verify remove <identity>`], access: 'Lockdown admin', permission: 'lockdown-admin' },
deter: { category: 'admin', summary: 'List, add, or remove identity deterrence.', usage: [`${prefix} deter list`, `${prefix} deter ban <identity>`, `${prefix} deter unban <identity>`], access: 'Lockdown admin', permission: 'lockdown-admin' },
lift: { category: 'features', summary: 'Show or move the lift.', usage: [`${prefix} lift <status|up|down>`], access: 'Admin', permission: 'admin', requiredFeature: 'lift', unavailableLabel: 'Lift' },
neato: { category: 'features', summary: 'Show or control Neato.', usage: [`${prefix} neato <status|start|home|locate|clear-errors>`], access: 'Admin', permission: 'admin', requiredFeature: 'neato', unavailableLabel: 'Neato' },
lift: { category: 'features', summary: 'Show or move the lift.', usage: [`${prefix} lift <status|up|down>`], access: 'Public unless server access is restricted', permission: 'access-mode', requiredFeature: 'lift', unavailableLabel: 'Lift' },
neato: { category: 'features', summary: 'Show or control Neato.', usage: [`${prefix} neato <status|start|home|locate|clear-errors>`], access: 'Public unless server access is restricted', permission: 'access-mode', requiredFeature: 'neato', unavailableLabel: 'Neato' },
bridge: { category: 'discord', summary: 'Configure this Discord server chat bridge.', usage: [`${prefix} bridge`, `${prefix} bridge here <global|private>`, `${prefix} bridge mode <global|private>`, `${prefix} bridge off`], access: 'Discord server manager' },
};
}
+2 -3
View File
@@ -1,9 +1,8 @@
1. assign rovers based on battery percentage, give people highest one
2. add admin ui for VIP and private requests instead of only through discord
3. make overcurrent limiter speed sensitive, slower fill at lower speeds
4. add feature chat commands, like /neato start, /lift up, etc
5. add more background gap themes
6. fix this:
4. add more background gap themes
5. fix this:
`Jun 18 15:14:18 roombaserver.local node[216731]: /home/daniel/MultiRoombaRover/server/src/services/roverManager/socketHandlers.js:92
Jun 18 15:14:18 roombaserver.local node[216731]: cb({ error: err.message });
Jun 18 15:14:18 roombaserver.local node[216731]: ^