mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
appoint spectator access when you login from external, actually...
This commit is contained in:
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -78,7 +78,7 @@
|
|||||||
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
||||||
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
||||||
<title>Roomba Rover</title>
|
<title>Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-D5SKLi1e.js"></script>
|
<script type="module" crossorigin src="/assets/index-CmNL6XEv.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-sm_EKOxJ.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-sm_EKOxJ.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const {
|
|||||||
const {
|
const {
|
||||||
getFeatureState,
|
getFeatureState,
|
||||||
getUserIdForSocket,
|
getUserIdForSocket,
|
||||||
|
updateFeatureState,
|
||||||
} = require('../identityService');
|
} = require('../identityService');
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
@@ -76,6 +77,49 @@ function externalSpectatorAccessError() {
|
|||||||
return 'External spectator access is disabled.';
|
return 'External spectator access is disabled.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function grantExternalSpectatorAccessAfterAdminLogin(socket) {
|
||||||
|
const policy = getBandwidthSavingsPolicy();
|
||||||
|
if (policy.externalSpectatorAccess !== 'admin') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const ip = getSocketIp(socket);
|
||||||
|
if (isLocalNetwork(ip)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const userId = getUserIdForSocket(socket);
|
||||||
|
if (!userId) {
|
||||||
|
/*
|
||||||
|
Sockets are normally identified on connection before login, but keeping a
|
||||||
|
guard here makes the admin grant fail closed instead of writing an orphan
|
||||||
|
feature-state row if identity setup changes later.
|
||||||
|
*/
|
||||||
|
logger.warn('External spectator grant skipped because socket has no identity', { socketId: socket?.id });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
updateFeatureState(
|
||||||
|
userId,
|
||||||
|
SPECTATOR_ACCESS_NAMESPACE,
|
||||||
|
(current) => ({
|
||||||
|
/*
|
||||||
|
Preserve any future spectatorAccess settings beside `external`. The
|
||||||
|
login flow is only approving this identity for external spectating, not
|
||||||
|
resetting the whole namespace back to a one-field object.
|
||||||
|
*/
|
||||||
|
...(current || {}),
|
||||||
|
external: true,
|
||||||
|
grantedByAdminLoginAt: Date.now(),
|
||||||
|
grantedByAdminUsername: socket?.data?.user?.username || null,
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
logger.info('External spectator access granted after admin login', {
|
||||||
|
socketId: socket.id,
|
||||||
|
userId,
|
||||||
|
username: socket?.data?.user?.username || null,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
io.on('connection', (socket) => {
|
io.on('connection', (socket) => {
|
||||||
const requestedRole = socket.handshake?.query?.role;
|
const requestedRole = socket.handshake?.query?.role;
|
||||||
/*
|
/*
|
||||||
@@ -96,6 +140,13 @@ io.on('connection', (socket) => {
|
|||||||
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);
|
||||||
|
/*
|
||||||
|
In admin-gated external spectator mode, logging in from /spectate is the
|
||||||
|
approval action for this browser identity. Persist the grant before the
|
||||||
|
client retries switching back to spectator, otherwise the user would
|
||||||
|
lose the admin bypass and immediately fall back into the gate.
|
||||||
|
*/
|
||||||
|
grantExternalSpectatorAccessAfterAdminLogin(socket);
|
||||||
socket.emit('auth:role', { role });
|
socket.emit('auth:role', { role });
|
||||||
clearLockdownTimer(socket);
|
clearLockdownTimer(socket);
|
||||||
logger.info('Login success', socket.id, role);
|
logger.info('Login success', socket.id, role);
|
||||||
|
|||||||
@@ -14,64 +14,35 @@ const PRIVILEGED_ROLES = new Set(['admin', 'lockdown']);
|
|||||||
const LOCKDOWN_ROLES = new Set(['lockdown']);
|
const LOCKDOWN_ROLES = new Set(['lockdown']);
|
||||||
const RESTRICTED_MODES = new Set(['admin', 'lockdown']);
|
const RESTRICTED_MODES = new Set(['admin', 'lockdown']);
|
||||||
|
|
||||||
function getModeDetails({ mode = 'admin', spectatorAccessBlocked = false, spectatorAccessMode = 'on' } = {}) {
|
function getModeDetails(mode = 'admin') {
|
||||||
if (spectatorAccessBlocked) {
|
|
||||||
/*
|
|
||||||
External spectator access is not a server mode like admin/lockdown; it is
|
|
||||||
a route-specific bandwidth/access gate. It still needs the same overlay
|
|
||||||
because the AuthPanel is the only browser-side way for an admin to prove
|
|
||||||
they should bypass that gate from /spectate.
|
|
||||||
*/
|
|
||||||
return {
|
|
||||||
title: 'Spectate access required',
|
|
||||||
description: spectatorAccessMode === 'admin'
|
|
||||||
? 'External spectators need admin approval or an admin login before viewing this page.'
|
|
||||||
: 'External spectator access is disabled. Admins can log in to continue.',
|
|
||||||
reasonLabel: 'Access state:',
|
|
||||||
reason: spectatorAccessMode === 'admin' ? 'Waiting for approved spectator identity or admin login.' : 'External spectating is off.',
|
|
||||||
chatIntro: 'You can still use the chat while access is blocked:',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (mode === 'lockdown') {
|
if (mode === 'lockdown') {
|
||||||
return {
|
return {
|
||||||
title: 'Lockdown mode active',
|
title: 'Lockdown mode active',
|
||||||
description:
|
description:
|
||||||
'Only the server owners can access the interface at this time.',
|
'Only the server owners can access the interface at this time.',
|
||||||
reasonLabel: 'Reason for locking:',
|
|
||||||
chatIntro: 'You can still use the chat while the server is locked:',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
title: 'Admin mode active',
|
title: 'Admin mode active',
|
||||||
// description:
|
// description:
|
||||||
// 'The server is currently in admin mode. Only admins can access the interface.',
|
// 'The server is currently in admin mode. Only admins can access the interface.',
|
||||||
reasonLabel: 'Reason for locking:',
|
|
||||||
chatIntro: 'You can still use the chat while the server is locked:',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ModeGateOverlay({ includeSpectatorAccessGate = false }) {
|
export default function ModeGateOverlay() {
|
||||||
const mode = useSessionSelector((state) => state.session?.mode || null);
|
const mode = useSessionSelector((state) => state.session?.mode || null);
|
||||||
const role = useSessionSelector((state) => state.session?.role || null);
|
const role = useSessionSelector((state) => state.session?.role || null);
|
||||||
const reason = useSessionSelector((state) => state.session?.adminReason?.text || '');
|
const reason = useSessionSelector((state) => state.session?.adminReason?.text || '');
|
||||||
const timezone = useSessionSelector((state) => state.session?.timezone || 'UTC');
|
const timezone = useSessionSelector((state) => state.session?.timezone || 'UTC');
|
||||||
const interInstanceEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'interInstance'));
|
const interInstanceEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'interInstance'));
|
||||||
const spectatorAccessAllowed = useSessionSelector(
|
|
||||||
(state) => state.session?.bandwidthSavings?.canUseExternalSpectatorAccess,
|
|
||||||
);
|
|
||||||
const spectatorAccessMode = useSessionSelector(
|
|
||||||
(state) => state.session?.bandwidthSavings?.externalSpectatorAccess || 'on',
|
|
||||||
);
|
|
||||||
const restricted = RESTRICTED_MODES.has(mode);
|
const restricted = RESTRICTED_MODES.has(mode);
|
||||||
const privileged = mode === 'lockdown' ? LOCKDOWN_ROLES.has(role) : PRIVILEGED_ROLES.has(role);
|
const privileged = mode === 'lockdown' ? LOCKDOWN_ROLES.has(role) : PRIVILEGED_ROLES.has(role);
|
||||||
const spectatorAccessBlocked = Boolean(includeSpectatorAccessGate && spectatorAccessAllowed === false);
|
|
||||||
const blocked = (restricted && !privileged) || spectatorAccessBlocked;
|
|
||||||
/*
|
/*
|
||||||
The overlay is mounted for the whole app, but the server-time display is
|
The overlay is mounted for the whole app, but the server-time display is
|
||||||
only visible while access is actually blocked. Gating the shared clock here
|
only visible while access is actually blocked. Gating the shared clock here
|
||||||
prevents the hidden overlay from registering a permanent interval.
|
prevents the hidden overlay from registering a permanent interval.
|
||||||
*/
|
*/
|
||||||
const nowMs = useSharedClock(1000, blocked);
|
const nowMs = useSharedClock(1000, restricted && !privileged);
|
||||||
|
|
||||||
const serverTime = useMemo(() => {
|
const serverTime = useMemo(() => {
|
||||||
const now = new Date(nowMs);
|
const now = new Date(nowMs);
|
||||||
@@ -87,12 +58,11 @@ export default function ModeGateOverlay({ includeSpectatorAccessGate = false })
|
|||||||
}
|
}
|
||||||
}, [nowMs, timezone]);
|
}, [nowMs, timezone]);
|
||||||
|
|
||||||
if (!blocked) {
|
if (!restricted || privileged) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const details = getModeDetails({ mode, spectatorAccessBlocked, spectatorAccessMode });
|
const details = getModeDetails(mode);
|
||||||
const displayedReason = spectatorAccessBlocked ? details.reason : reason || 'No reason set.';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-auto fixed inset-0 z-50 overflow-y-auto bg-black px-0.5 py-0.5">
|
<div className="pointer-events-auto fixed inset-0 z-50 overflow-y-auto bg-black px-0.5 py-0.5">
|
||||||
@@ -103,9 +73,9 @@ export default function ModeGateOverlay({ includeSpectatorAccessGate = false })
|
|||||||
<p className="text-sm text-slate-300">{details.description}</p>
|
<p className="text-sm text-slate-300">{details.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="surface-muted space-y-0.5">
|
<div className="surface-muted space-y-0.5">
|
||||||
<p className="text-[0.7rem] tracking-wide text-slate-400">{details.reasonLabel}</p>
|
<p className="text-[0.7rem] tracking-wide text-slate-400">Reason for locking:</p>
|
||||||
<p className="text-lg font-semibold text-slate-100">
|
<p className="text-lg font-semibold text-slate-100">
|
||||||
{displayedReason}
|
{reason ? reason : 'No reason set.'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-center text-sm text-slate-300">Server time: {serverTime}</p>
|
<p className="text-center text-sm text-slate-300">Server time: {serverTime}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,7 +83,7 @@ export default function ModeGateOverlay({ includeSpectatorAccessGate = false })
|
|||||||
<AuthPanel />
|
<AuthPanel />
|
||||||
</div>
|
</div>
|
||||||
<SocialButton id="discord" label="Join our Discord server for updates!" />
|
<SocialButton id="discord" label="Join our Discord server for updates!" />
|
||||||
{details.chatIntro}
|
You can still use the chat while the server is locked:
|
||||||
{/* set max height of this box */}
|
{/* set max height of this box */}
|
||||||
<div className='max-h-80 overflow-y-auto'>
|
<div className='max-h-80 overflow-y-auto'>
|
||||||
<ChatPanel nicknameLayout="stacked" />
|
<ChatPanel nicknameLayout="stacked" />
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
// Spectator App Root
|
// Spectator App Root
|
||||||
// Purpose: Defines the Spectator App Root module and the local helpers/components used in this file.
|
// Purpose: Defines the Spectator App Root module and the local helpers/components used in this file.
|
||||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||||
import ModeGateOverlay from '../../components/ModeGateOverlay/index.jsx';
|
|
||||||
import SpectatorContent from './SpectatorContent.jsx';
|
import SpectatorContent from './SpectatorContent.jsx';
|
||||||
|
|
||||||
export default function SpectatorAppRoot() {
|
export default function SpectatorAppRoot() {
|
||||||
return (
|
return <SpectatorContent />;
|
||||||
<>
|
|
||||||
<SpectatorContent />
|
|
||||||
{/*
|
|
||||||
The spectator route does not render App.jsx, so it must mount the gate
|
|
||||||
overlay itself. This keeps admin/lockdown login behavior available on
|
|
||||||
/spectate and, more importantly, gives external spectators a real
|
|
||||||
AuthPanel when the bandwidth policy requires admin approval or admin
|
|
||||||
login before spectating.
|
|
||||||
*/}
|
|
||||||
<ModeGateOverlay includeSpectatorAccessGate />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user