private rover request fixes finally

This commit is contained in:
legop3
2026-06-01 19:53:17 -04:00
parent d205d3a05d
commit af5ddfb11d
9 changed files with 81 additions and 27 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="Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Roomba Rover" />
<title>Roomba Rover</title> <title>Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-BwoVmbyy.js"></script> <script type="module" crossorigin src="/assets/index-OA6D7Zgr.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-0REyzzxs.css"> <link rel="stylesheet" crossorigin href="/assets/index-0REyzzxs.css">
</head> </head>
<body> <body>
@@ -64,6 +64,10 @@ function applySocketGrantCache(socket) {
socket.data.privateClosedAccessRovers = listGrantedRoversForRequester(requesterKey); socket.data.privateClosedAccessRovers = listGrantedRoversForRequester(requesterKey);
} }
function refreshAllSocketGrantCaches() {
io.sockets.sockets.forEach((socket) => applySocketGrantCache(socket));
}
function getGrantForRequester(requesterKey, roverId) { function getGrantForRequester(requesterKey, roverId) {
if (!requesterKey || !roverId) return null; if (!requesterKey || !roverId) return null;
return grants.get(buildGrantKey(requesterKey, roverId)) || null; return grants.get(buildGrantKey(requesterKey, roverId)) || null;
@@ -139,12 +143,19 @@ function tryAssignClosedPrivateRover(socket, roverId) {
if (!socket || !roverId) return false; if (!socket || !roverId) return false;
const record = roverManager.rovers.get(String(roverId)); const record = roverManager.rovers.get(String(roverId));
if (!record) return false; if (!record) return false;
const access = roverManager.canRequestControl(roverId, socket, { allowUser: true }); const access = roverManager.canRequestControl(roverId, socket, {
allowUser: true,
allowClosedPrivateGrantInLockdown: true,
});
if (!access.ok) { if (!access.ok) {
throw new Error(access.reason || 'Control denied'); throw new Error(access.reason || 'Control denied');
} }
const previousJoined = roverManager.getRoversForSocket(socket.id); const previousJoined = roverManager.getRoversForSocket(socket.id);
roverManager.requestControl(roverId, socket, { allowUser: true }); roverManager.requestControl(roverId, socket, {
allowUser: true,
allowClosedPrivateGrantInLockdown: true,
forceTurn: true,
});
previousJoined.forEach((rid) => { previousJoined.forEach((rid) => {
if (rid !== roverId) roverManager.releaseControl(rid, socket); if (rid !== roverId) roverManager.releaseControl(rid, socket);
}); });
@@ -156,6 +167,7 @@ function tryAssignClosedPrivateRover(socket, roverId) {
logger.warn('Failed to move assignment after private access grant', { socketId: socket.id, error: err.message }); logger.warn('Failed to move assignment after private access grant', { socketId: socket.id, error: err.message });
} }
socket.emit('controlGranted', { roverId: String(roverId) }); socket.emit('controlGranted', { roverId: String(roverId) });
roverManager.broadcastRoster();
return true; return true;
} }
@@ -304,5 +316,6 @@ module.exports = {
approveRequest, approveRequest,
denyRequest, denyRequest,
applySocketGrantCache, applySocketGrantCache,
refreshAllSocketGrantCaches,
clearPendingForRover, clearPendingForRover,
}; };
@@ -6,7 +6,7 @@ const roverManager = require('../roverManager');
const { requestEvents, grants } = require('./state'); const { requestEvents, grants } = require('./state');
function registerPrivateRoverAccessHooks(deps) { function registerPrivateRoverAccessHooks(deps) {
const { applySocketGrantCache, createRequest, clearPendingForRover } = deps; const { applySocketGrantCache, refreshAllSocketGrantCaches, createRequest, clearPendingForRover } = deps;
roverManager.managerEvents.on('private', ({ roverId, open } = {}) => { roverManager.managerEvents.on('private', ({ roverId, open } = {}) => {
if (!roverId) return; if (!roverId) return;
@@ -17,6 +17,8 @@ function registerPrivateRoverAccessHooks(deps) {
grants.delete(key); grants.delete(key);
} }
} }
refreshAllSocketGrantCaches();
roverManager.broadcastRoster();
} }
requestEvents.emit('change', { reason: 'private_state', roverId: String(roverId), open: Boolean(open) }); requestEvents.emit('change', { reason: 'private_state', roverId: String(roverId), open: Boolean(open) });
}); });
@@ -30,6 +32,8 @@ function registerPrivateRoverAccessHooks(deps) {
grants.delete(key); grants.delete(key);
} }
} }
refreshAllSocketGrantCaches();
roverManager.broadcastRoster();
} }
requestEvents.emit('change', { reason: 'rover', roverId: String(roverId), action: action || null }); requestEvents.emit('change', { reason: 'rover', roverId: String(roverId), action: action || null });
}); });
@@ -11,12 +11,14 @@ const {
approveRequest, approveRequest,
denyRequest, denyRequest,
applySocketGrantCache, applySocketGrantCache,
refreshAllSocketGrantCaches,
clearPendingForRover, clearPendingForRover,
} = require('./core'); } = require('./core');
const { registerPrivateRoverAccessHooks } = require('./hooks'); const { registerPrivateRoverAccessHooks } = require('./hooks');
registerPrivateRoverAccessHooks({ registerPrivateRoverAccessHooks({
applySocketGrantCache, applySocketGrantCache,
refreshAllSocketGrantCaches,
createRequest, createRequest,
clearPendingForRover, clearPendingForRover,
}); });
@@ -74,14 +74,25 @@ function createPrivateAccessPolicy(deps) {
} }
function getControlDenialReason(record, socket, options = {}) { function getControlDenialReason(record, socket, options = {}) {
const { allowUser = false } = options; const { allowUser = false, allowClosedPrivateGrantInLockdown = false } = options;
if (!record) return 'Unknown rover'; if (!record) return 'Unknown rover';
if (!allowUser && !isAdmin(socket)) return 'Only admins can request control'; if (!allowUser && !isAdmin(socket)) return 'Only admins can request control';
if (record.locked && !isAdmin(socket)) return 'Rover locked'; if (record.locked && !isAdmin(socket)) return 'Rover locked';
const mode = getMode(); const mode = getMode();
if (!allowUser && mode === MODES.ADMIN && !isAdmin(socket)) return 'Admins only'; if (!allowUser && mode === MODES.ADMIN && !isAdmin(socket)) return 'Admins only';
if (!allowUser && mode === MODES.LOCKDOWN && !isLockdownAdmin(socket)) return 'Server in lockdown'; if (!allowUser && mode === MODES.LOCKDOWN && !isLockdownAdmin(socket)) return 'Server in lockdown';
if (mode === MODES.LOCKDOWN && !isLockdownAdmin(socket)) return 'Server in lockdown'; if (
mode === MODES.LOCKDOWN &&
!isLockdownAdmin(socket) &&
!(
allowClosedPrivateGrantInLockdown &&
isPrivateRecord(record) &&
!isPrivateOpen(record) &&
socketHasClosedPrivateAccess(socket, record.id)
)
) {
return 'Server in lockdown';
}
const { isDeterred } = require('../verificationService'); const { isDeterred } = require('../verificationService');
if (!isAdmin(socket) && isDeterred(socket)) return 'Not authorized'; if (!isAdmin(socket) && isDeterred(socket)) return 'Not authorized';
if (!isPrivateRecord(record)) return null; if (!isPrivateRecord(record)) return null;
@@ -33,9 +33,17 @@ function createRoverLifecycle(deps) {
} }
function requestControl(roverId, socket, options = {}) { function requestControl(roverId, socket, options = {}) {
const { force = false, allowUser = false } = options; const {
force = false,
allowUser = false,
forceTurn = false,
allowClosedPrivateGrantInLockdown = false,
} = options;
const record = rovers.get(roverId); const record = rovers.get(roverId);
const denied = getControlDenialReason(record, socket, { allowUser }); const denied = getControlDenialReason(record, socket, {
allowUser,
allowClosedPrivateGrantInLockdown,
});
if (denied) throw new Error(denied); if (denied) throw new Error(denied);
record.drivers.add(socket.id); record.drivers.add(socket.id);
if (!socketToRovers.has(socket.id)) { if (!socketToRovers.has(socket.id)) {
@@ -43,7 +51,7 @@ function createRoverLifecycle(deps) {
} }
socketToRovers.get(socket.id).add(roverId); socketToRovers.get(socket.id).add(roverId);
socket.join(record.room); socket.join(record.room);
turnService.driverAdded(roverId, socket.id, force && isAdmin(socket)); turnService.driverAdded(roverId, socket.id, (force && isAdmin(socket)) || forceTurn);
socket.emit('controlGranted', { roverId }); socket.emit('controlGranted', { roverId });
managerEvents.emit('driver', { socketId: socket.id, roverId, action: 'add' }); managerEvents.emit('driver', { socketId: socket.id, roverId, action: 'add' });
sendAlert({ sendAlert({
@@ -77,7 +85,10 @@ function createRoverLifecycle(deps) {
function canDrive(roverId, socket) { function canDrive(roverId, socket) {
const record = rovers.get(roverId); const record = rovers.get(roverId);
if (!record) return false; if (!record) return false;
const denied = getControlDenialReason(record, socket, { allowUser: true }); const denied = getControlDenialReason(record, socket, {
allowUser: true,
allowClosedPrivateGrantInLockdown: true,
});
if (denied) return false; if (denied) return false;
const _mode = getMode(); const _mode = getMode();
if (isAdmin(socket)) return true; if (isAdmin(socket)) return true;
@@ -116,10 +127,13 @@ function createRoverLifecycle(deps) {
return false; return false;
} }
function canSwitchRover(socket, targetRoverId) { function canSwitchRover(socket, targetRoverId, options = {}) {
const target = rovers.get(targetRoverId); const target = rovers.get(targetRoverId);
if (!target) return { ok: false, message: 'Unknown rover' }; if (!target) return { ok: false, message: 'Unknown rover' };
const denied = getControlDenialReason(target, socket, { allowUser: true }); const denied = getControlDenialReason(target, socket, {
allowUser: true,
allowClosedPrivateGrantInLockdown: Boolean(options.allowClosedPrivateGrantInLockdown),
});
if (denied) return { ok: false, message: denied }; if (denied) return { ok: false, message: denied };
const currentId = getPrimaryRoverForSocket(socket.id); const currentId = getPrimaryRoverForSocket(socket.id);
if (!currentId || currentId === targetRoverId) return { ok: true, currentId }; if (!currentId || currentId === targetRoverId) return { ok: true, currentId };
@@ -57,15 +57,20 @@ function registerSocketHandlers(deps) {
throw new Error('No rovers available'); throw new Error('No rovers available');
} }
const previousJoined = getRoversForSocket(socket.id); const previousJoined = getRoversForSocket(socket.id);
const allowClosedPrivateGrantInLockdown = true;
if (!isAdmin(socket)) { if (!isAdmin(socket)) {
const { ok, message } = canSwitchRover(socket, targetId); const { ok, message } = canSwitchRover(socket, targetId, { allowClosedPrivateGrantInLockdown });
if (!ok) { if (!ok) {
throw new Error(message || 'Switch denied'); throw new Error(message || 'Switch denied');
} }
} }
const forceAllowed = Boolean(force) && isAdmin(socket); const forceAllowed = Boolean(force) && isAdmin(socket);
logger.info('Request control', socket.id, targetId, { force: forceAllowed }); logger.info('Request control', socket.id, targetId, { force: forceAllowed });
requestControl(targetId, socket, { force: forceAllowed, allowUser: true }); requestControl(targetId, socket, {
force: forceAllowed,
allowUser: true,
allowClosedPrivateGrantInLockdown,
});
previousJoined.forEach((rid) => { previousJoined.forEach((rid) => {
if (rid !== targetId) { if (rid !== targetId) {
releaseControl(rid, socket); releaseControl(rid, socket);
@@ -156,11 +156,16 @@ export default function RoverQueuesPanel({ title = 'Rovers' }) {
const isSelfCurrent = Boolean(selfId && currentId && currentId === selfId); const isSelfCurrent = Boolean(selfId && currentId && currentId === selfId);
const isSelfNext = Boolean(selfId && nextId && nextId === selfId); const isSelfNext = Boolean(selfId && nextId && nextId === selfId);
const showTimer = remainingSeconds != null && (isSelfCurrent || isSelfNext); const showTimer = remainingSeconds != null && (isSelfCurrent || isSelfNext);
const locked = Boolean(rover.locked);
const lockedBlocked = locked && !adminCapable;
const isPrivateOpen = Boolean(rover?.private?.enabled && rover?.private?.open); const isPrivateOpen = Boolean(rover?.private?.enabled && rover?.private?.open);
const isGrantedClosedPrivate = Boolean(rover?.private?.enabled && !rover?.private?.open);
const locked = Boolean(rover.locked);
const lockedBlocked = locked && !adminCapable && !isGrantedClosedPrivate;
const lockLabel = rover.lockReason ? `locked: ${rover.lockReason}` : 'locked'; const lockLabel = rover.lockReason ? `locked: ${rover.lockReason}` : 'locked';
const buttonLabel = locked ? lockLabel : pending[roverId] ? '...' : 'request'; const buttonLabel = pending[roverId]
? '...'
: locked && !isGrantedClosedPrivate
? lockLabel
: 'request';
const canClickRow = canRequest && !lockedBlocked && !pending[roverId]; const canClickRow = canRequest && !lockedBlocked && !pending[roverId];
return ( return (
<li <li