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