mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
lockdown fixes
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user