lockdown fixes

This commit is contained in:
legop3
2026-01-24 23:40:22 -05:00
parent 2dda137855
commit 2494f6e276
10 changed files with 67 additions and 21 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,7 +11,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-Cge0aRRk.js"></script>
<script type="module" crossorigin src="/assets/index-BJNBFmS4.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-5vqbgc7L.css">
</head>
<body>
+9 -3
View File
@@ -2,7 +2,7 @@ const EventEmitter = require('events');
const io = require('../globals/io');
const logger = require('../globals/logger').child('assignment');
const { MODES, getMode, modeEvents } = require('./modeManager');
const { roleEvents, getRole, isAdmin } = require('./roleService');
const { roleEvents, getRole, isAdmin, isLockdownAdmin } = require('./roleService');
const roverManager = require('./roverManager');
const socketRefs = new Map(); // socketId -> socket
@@ -28,14 +28,20 @@ roleEvents.on('change', ({ socket, role }) => {
});
modeEvents.on('change', (mode) => {
if (mode === MODES.ADMIN || mode === MODES.LOCKDOWN) {
// release non-admin drivers
if (mode === MODES.ADMIN) {
for (const [socketId, roverId] of assignments.entries()) {
const socket = socketRefs.get(socketId);
if (socket && !isAdmin(socket)) {
releaseAssignment(socket, roverId);
}
}
} else if (mode === MODES.LOCKDOWN) {
for (const [socketId, roverId] of assignments.entries()) {
const socket = socketRefs.get(socketId);
if (socket && !isLockdownAdmin(socket)) {
releaseAssignment(socket, roverId);
}
}
}
reassignWaiting();
});
+4
View File
@@ -3,6 +3,7 @@ const io = require('../globals/io');
const logger = require('../globals/logger').child('authService');
const { loadConfig } = require('../helpers/configLoader');
const { clearLockdownTimer } = require('./lockdownGuard');
const { getMode, MODES } = require('./modeManager');
const { setRole } = require('./roleService');
const config = loadConfig();
@@ -41,6 +42,9 @@ io.on('connection', (socket) => {
socket.on('auth:login', async ({ username, password }, cb = () => {}) => {
try {
const admin = await authenticate(username, password);
if (getMode() === MODES.LOCKDOWN && !admin.lockdown) {
throw new Error('Lockdown admins only');
}
const role = admin.lockdown ? 'lockdown' : 'admin';
socket.data.user = { username: admin.username, discordId: admin.discord_id };
setRole(socket, role);
+23 -1
View File
@@ -36,6 +36,12 @@ const enabled = Boolean(discordConfig.token);
const adminIds = new Set(
(config.admins || []).map((a) => String(a.discord_id || '').trim()).filter(Boolean),
);
const lockdownAdminIds = new Set(
(config.admins || [])
.filter((admin) => admin.lockdown)
.map((admin) => String(admin.discord_id || '').trim())
.filter(Boolean),
);
if (!enabled) {
logger.info('Discord bot disabled; missing token in config.discord.token');
@@ -244,6 +250,10 @@ function isAdminUser(discordId) {
return adminIds.has(String(discordId || '').trim());
}
function isLockdownAdminUser(discordId) {
return lockdownAdminIds.has(String(discordId || '').trim());
}
function formatHelp() {
return [
'**Rover Bot Commands**',
@@ -447,7 +457,8 @@ async function handleModeCommand(message, mode) {
return;
}
try {
setMode(next, null, { force: true });
const role = isLockdownAdminUser(message.author?.id) ? 'lockdown' : 'admin';
setMode(next, { data: { role, user: { username: `discord:${message.author?.username || 'unknown'}` } } });
await message.reply({
content: sanitizeMentions(`Mode set to ${next}.`),
allowedMentions: { parse: [], repliedUser: false },
@@ -685,7 +696,10 @@ async function handleCommand(message) {
tokens.shift(); // remove prefix
const action = (tokens.shift() || '').toLowerCase();
const isAdmin = isAdminUser(message.author.id);
const isLockdownAdmin = isLockdownAdminUser(message.author.id);
const isBridgeAdmin = action === 'bridge' ? canManageBridge(message) : false;
const mode = getMode();
const moderationActions = new Set(['lock', 'unlock', 'mode', 'goal']);
if (
!isAdmin &&
@@ -700,6 +714,14 @@ async function handleCommand(message) {
return; // ignore non-admins for privileged commands
}
if (mode === MODES.LOCKDOWN && moderationActions.has(action) && !isLockdownAdmin) {
await message.reply({
content: 'Lockdown mode: only lockdown admins can run that command.',
allowedMentions: { parse: [], repliedUser: false },
});
return;
}
switch (action) {
case '':
await handleStatusCommand(message, tokens[0]);
+11 -3
View File
@@ -5,7 +5,7 @@ const io = require('../globals/io');
const logger = require('../globals/logger').child('homeAssistantService');
const { loadConfig } = require('../helpers/configLoader');
const { getMode } = require('./modeManager');
const { isAdmin } = require('./roleService');
const { isAdmin, isLockdownAdmin } = require('./roleService');
// home-assistant-js-websocket expects a global WebSocket in Node.
if (!global.WebSocket) {
@@ -231,7 +231,11 @@ connect();
io.on('connection', (socket) => {
socket.on('homeAssistant:toggle', async ({ entityId } = {}, cb = () => {}) => {
if ((getMode() === 'admin' || getMode() === 'lockdown') && isAdmin(socket) !== true) {
const mode = getMode();
if (
(mode === 'admin' && isAdmin(socket) !== true) ||
(mode === 'lockdown' && isLockdownAdmin(socket) !== true)
) {
return cb({ error: 'Insufficient permissions to control Home Assistant' });
}
@@ -245,7 +249,11 @@ io.on('connection', (socket) => {
});
socket.on('homeAssistant:setState', async ({ entityId, state } = {}, cb = () => {}) => {
if ((getMode() === 'admin' || getMode() === 'lockdown') && isAdmin(socket) !== true) {
const mode = getMode();
if (
(mode === 'admin' && isAdmin(socket) !== true) ||
(mode === 'lockdown' && isLockdownAdmin(socket) !== true)
) {
return cb({ error: 'Insufficient permissions to control Home Assistant' });
}
-6
View File
@@ -22,12 +22,6 @@ function enforceLockdown() {
}
}
io.on('connection', (socket) => {
if (getMode() === MODES.LOCKDOWN && !isLockdownAdmin(socket)) {
disconnectForLockdown(socket);
}
});
module.exports = {
enforceLockdown,
disconnectForLockdown,
+4 -1
View File
@@ -16,9 +16,12 @@ let currentMode = MODES.ADMIN;
const modeEvents = new EventEmitter();
function canChangeMode(socket, nextMode) {
if (nextMode === MODES.LOCKDOWN) {
if (currentMode === MODES.LOCKDOWN && nextMode !== MODES.LOCKDOWN) {
return isLockdownAdmin(socket);
}
if (nextMode === MODES.LOCKDOWN) {
return isAdmin(socket);
}
return isAdmin(socket);
}
+11 -3
View File
@@ -5,7 +5,7 @@ const { sendAlert } = require('./alertService');
const ALERT_COLOR = '#8bc34a';
const { parseSensorFrame } = require('../helpers/sensorDecoder');
const { MODES, getMode } = require('./modeManager');
const { isAdmin, roleEvents } = require('./roleService');
const { isAdmin, isLockdownAdmin, roleEvents } = require('./roleService');
const { publishEvent } = require('./eventBus');
const videoSessions = require('./videoSessions');
@@ -442,7 +442,7 @@ function requestControl(roverId, socket, options = {}) {
if (!allowUser && mode === MODES.ADMIN && !isAdmin(socket)) {
throw new Error('Admins only');
}
if (!allowUser && mode === MODES.LOCKDOWN && !isAdmin(socket)) {
if (!allowUser && mode === MODES.LOCKDOWN && !isLockdownAdmin(socket)) {
throw new Error('Server in lockdown');
}
record.drivers.add(socket.id);
@@ -485,6 +485,10 @@ function isDriver(roverId, socket) {
}
function canDrive(roverId, socket) {
const mode = getMode();
if (mode === MODES.LOCKDOWN) {
return isLockdownAdmin(socket);
}
if (isAdmin(socket)) {
return true;
}
@@ -586,7 +590,11 @@ io.on('connection', (socket) => {
if (socket.data?.role === 'spectator') {
throw new Error('Spectators cannot drive');
}
if ((getMode() === MODES.ADMIN || getMode() === MODES.LOCKDOWN) && !isAdmin(socket)) {
const mode = getMode();
if (
(mode === MODES.ADMIN && !isAdmin(socket)) ||
(mode === MODES.LOCKDOWN && !isLockdownAdmin(socket))
) {
throw new Error('Admins only');
}
const targetId = roverId || Array.from(rovers.keys())[0];
+2 -1
View File
@@ -5,6 +5,7 @@ import ChatPanel from './ChatPanel.jsx';
import NicknameForm from './NicknameForm.jsx';
const PRIVILEGED_ROLES = new Set(['admin', 'lockdown', 'lockdown-admin']);
const LOCKDOWN_ROLES = new Set(['lockdown', 'lockdown-admin']);
const RESTRICTED_MODES = new Set(['admin', 'lockdown']);
function getModeDetails(mode = 'admin') {
@@ -27,7 +28,7 @@ export default function ModeGateOverlay() {
const mode = session?.mode;
const role = session?.role;
const restricted = RESTRICTED_MODES.has(mode);
const privileged = PRIVILEGED_ROLES.has(role);
const privileged = mode === 'lockdown' ? LOCKDOWN_ROLES.has(role) : PRIVILEGED_ROLES.has(role);
if (!restricted || privileged) {
return null;