mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
add identity to socket auth hopefully will fix a lot of probem
This commit is contained in:
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -12,7 +12,7 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<!-- site-metadata:inject -->
|
||||
<!-- analytics:inject -->
|
||||
<script type="module" crossorigin src="/assets/index-KnOn2V1J.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-gn8wwSnH.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-7PpZTwSc.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -21,7 +21,6 @@ function registerVerificationHooks(deps) {
|
||||
socket.on('session:identify', (payload = {}, cb = () => {}) => {
|
||||
try {
|
||||
const result = identifySocket(socket, payload || {});
|
||||
socket.data.lastClientIdentifyAt = Date.now();
|
||||
cb({ success: true, ...result });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
|
||||
@@ -38,8 +38,6 @@ const {
|
||||
const { shouldEnforceSingleDriverTab } = require('../../helpers/bandwidthSavings');
|
||||
|
||||
const verificationEvents = new EventEmitter();
|
||||
const IDENTITY_TIMEOUT_MS = 2 * 60 * 1000;
|
||||
const IDENTITY_SWEEP_INTERVAL_MS = 15 * 1000;
|
||||
const DUPLICATE_IDENTITY_DISCONNECT_DELAY_MS = 250;
|
||||
const DETERRED_DISCONNECT_DELAY_MS = 250;
|
||||
|
||||
@@ -157,9 +155,9 @@ function identifySocket(socket, payload = {}) {
|
||||
}
|
||||
|
||||
/*
|
||||
Spectator-style pages send identity heartbeats too, but only the driver
|
||||
surface participates in duplicate-driver enforcement. This flag remains on
|
||||
the socket because it is connection-specific, not person-specific.
|
||||
Spectator-style pages carry identity too, but only the driver surface
|
||||
participates in duplicate-driver enforcement. This flag remains on the
|
||||
socket because it is connection-specific, not person-specific.
|
||||
*/
|
||||
socket.data.identitySurface = payload.identitySurface === 'driver' ? 'driver' : 'passive';
|
||||
|
||||
@@ -644,16 +642,41 @@ function getVerificationStatus(socket) {
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
Namespace middleware completes before Socket.IO emits `connection` to any
|
||||
service. Attaching canonical identity here guarantees that authorization,
|
||||
session construction, queues, chat, and media handlers never observe the old
|
||||
intermediate state where a transport existed but session:identify had not
|
||||
arrived yet. Missing keys remain valid: the canonical service creates the
|
||||
first portable key and exposes it through the initial session sync.
|
||||
*/
|
||||
io.use((socket, next) => {
|
||||
try {
|
||||
socket.data = socket.data || {};
|
||||
socket.data.connectedAt = Date.now();
|
||||
identifySocket(socket, socket.handshake?.auth || {});
|
||||
next();
|
||||
} catch (err) {
|
||||
logger.warn('Rejected socket with invalid handshake identity', {
|
||||
socketId: socket.id,
|
||||
error: err.message,
|
||||
});
|
||||
next(new Error(err.message));
|
||||
}
|
||||
});
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
socket.data = socket.data || {};
|
||||
socket.data.connectedAt = Date.now();
|
||||
installDeterredSocketGuard(socket);
|
||||
identifySocket(socket, {});
|
||||
/*
|
||||
Duplicate-driver enforcement emits a normal socket event before disconnecting
|
||||
the losing tab, so it runs after middleware admits the namespace connection.
|
||||
Identity itself is already present before this point.
|
||||
*/
|
||||
enforceSingleDriverSocketPerIdentity(socket);
|
||||
|
||||
socket.on('session:identify', (payload = {}, cb = () => {}) => {
|
||||
try {
|
||||
const result = identifySocket(socket, payload || {});
|
||||
socket.data.lastClientIdentifyAt = Date.now();
|
||||
cb({ success: true, ...result });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
@@ -712,27 +735,6 @@ identityEvents.on('change', ({ userId, reason } = {}) => {
|
||||
emitChange('identity_change', { userId, reason });
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
io.sockets.sockets.forEach((socket) => {
|
||||
if (!socket?.id) return;
|
||||
const role = getRole(socket);
|
||||
if (role === 'admin' || role === 'lockdown' || role === 'spectator') return;
|
||||
const connectedAt = Number(socket?.data?.connectedAt || 0);
|
||||
const lastClientIdentifyAt = Number(socket?.data?.lastClientIdentifyAt || 0);
|
||||
const referenceTs = lastClientIdentifyAt || connectedAt;
|
||||
if (!referenceTs) return;
|
||||
if (now - referenceTs < IDENTITY_TIMEOUT_MS) return;
|
||||
logger.info('Disconnecting socket due to stale identity heartbeat', {
|
||||
socketId: socket.id,
|
||||
role,
|
||||
ageMs: now - referenceTs,
|
||||
hadClientIdentify: Boolean(lastClientIdentifyAt),
|
||||
});
|
||||
socket.disconnect(true);
|
||||
});
|
||||
}, IDENTITY_SWEEP_INTERVAL_MS);
|
||||
|
||||
module.exports = {
|
||||
identifySocket,
|
||||
getVerificationStatus,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Hook: useUserIdentitySync
|
||||
// Purpose: Keeps local identity state synchronized with server session/auth updates. Scope: Handles identity hydration, change propagation, and persistence touch points.
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useSessionActions, useSessionSelector } from '../context/SessionContext.jsx';
|
||||
import { useSocket } from '../context/SocketContext.jsx';
|
||||
import { getBrowserFingerprintId } from '../lib/browserFingerprint.js';
|
||||
@@ -9,6 +9,7 @@ import { useSettingsNamespace } from '../settings/index.js';
|
||||
export default function useUserIdentitySync({ identitySurface = 'passive' } = {}) {
|
||||
const socket = useSocket();
|
||||
const connected = useSessionSelector((state) => state.connected);
|
||||
const serverCookieUserId = useSessionSelector((state) => state.session?.identity?.cookieUserId || '');
|
||||
const { identifySession } = useSessionActions();
|
||||
const { value: identity, status: identityStatus, save: saveIdentity } = useSettingsNamespace('identity', {
|
||||
cookieUserId: '',
|
||||
@@ -19,9 +20,6 @@ export default function useUserIdentitySync({ identitySurface = 'passive' } = {}
|
||||
{ enabled: false },
|
||||
);
|
||||
|
||||
const lastAckSocketRef = useRef(null);
|
||||
const fingerprintRef = useRef('');
|
||||
|
||||
const ready =
|
||||
identityStatus === 'ready' && profileStatus === 'ready' && overseerPreferenceStatus === 'ready';
|
||||
const cookieUserId = (identity?.cookieUserId || '').trim();
|
||||
@@ -32,23 +30,15 @@ export default function useUserIdentitySync({ identitySurface = 'passive' } = {}
|
||||
const sendIdentify = useCallback(async () => {
|
||||
if (!ready || !connected || !socket?.id) return;
|
||||
try {
|
||||
if (!fingerprintRef.current) {
|
||||
/*
|
||||
The portable cookie key is still the cross-device identity signal.
|
||||
Thumbmark adds a same-device signal that survives cookie clearing, so
|
||||
both are sent together whenever the heartbeat identifies this socket.
|
||||
*/
|
||||
fingerprintRef.current = await getBrowserFingerprintId();
|
||||
}
|
||||
/*
|
||||
Every route shares the same persisted identity key, but only the main
|
||||
driver page should trigger duplicate-tab prevention. Sending the surface
|
||||
with the heartbeat lets the server make that decision before spectator
|
||||
pages finish their role switch.
|
||||
Handshake auth establishes identity before connection handlers run.
|
||||
This event remains the live-update path for settings that change while
|
||||
the current transport stays connected, so existing callers and server
|
||||
behavior do not need a second update contract.
|
||||
*/
|
||||
const resp = await identifySession({
|
||||
cookieUserId,
|
||||
fingerprintId: fingerprintRef.current,
|
||||
fingerprintId: await getBrowserFingerprintId(),
|
||||
nickname,
|
||||
overseerEnabled,
|
||||
identitySurface: normalizedIdentitySurface,
|
||||
@@ -57,12 +47,11 @@ export default function useUserIdentitySync({ identitySurface = 'passive' } = {}
|
||||
if (nextKey && nextKey !== cookieUserId) {
|
||||
saveIdentity((current) => ({ ...(current || {}), cookieUserId: nextKey }));
|
||||
}
|
||||
lastAckSocketRef.current = socket.id;
|
||||
} catch {
|
||||
/*
|
||||
The permanent heartbeat below is the retry mechanism. A failed or
|
||||
half-open request must not create separate timer state that can stop
|
||||
future identity sends or disappear during a socket transition.
|
||||
A failed live update must not replace Socket.IO's connection lifecycle
|
||||
with custom retry state. The next reconnect reads the latest persisted
|
||||
settings through handshake auth and re-establishes the complete identity.
|
||||
*/
|
||||
}
|
||||
}, [
|
||||
@@ -78,46 +67,25 @@ export default function useUserIdentitySync({ identitySurface = 'passive' } = {}
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ready || !connected || !socket?.id) return;
|
||||
if (lastAckSocketRef.current === socket.id) return;
|
||||
sendIdentify();
|
||||
}, [connected, ready, sendIdentify, socket?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
This runs once when the route's settings become ready and again only when
|
||||
an identity value changes. Reconnects receive the same data from the auth
|
||||
callback, so there is intentionally no timer, visibility retry, or online
|
||||
retry here.
|
||||
*/
|
||||
if (!ready || !connected || !socket?.id) return;
|
||||
sendIdentify();
|
||||
}, [ready, connected, socket?.id, cookieUserId, nickname, overseerEnabled, normalizedIdentitySurface, sendIdentify]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleOnline = () => {
|
||||
if (!socket?.connected) return;
|
||||
sendIdentify();
|
||||
};
|
||||
const handleVisibility = () => {
|
||||
if (typeof document === 'undefined' || document.visibilityState !== 'visible') return;
|
||||
if (!socket?.connected) return;
|
||||
sendIdentify();
|
||||
};
|
||||
window.addEventListener('online', handleOnline);
|
||||
document.addEventListener('visibilitychange', handleVisibility);
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
document.removeEventListener('visibilitychange', handleVisibility);
|
||||
};
|
||||
}, [sendIdentify, socket?.connected]);
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
Keep this interval installed whenever persisted identity settings are
|
||||
ready, including while Socket.IO is reconnecting. Each tick checks the
|
||||
current connection before sending, so reconnects resume heartbeats without
|
||||
depending on an acknowledgement or another effect recreating the timer.
|
||||
A first-time browser has no portable key to send in its handshake, so the
|
||||
canonical identity service creates one. Session sync is authoritative for
|
||||
that generated value; persisting it here makes every later handshake carry
|
||||
the same user key without depending on a session:identify acknowledgement.
|
||||
*/
|
||||
if (!ready) return undefined;
|
||||
const timer = setInterval(() => {
|
||||
if (!socket?.connected) return;
|
||||
sendIdentify();
|
||||
}, 2000);
|
||||
return () => clearInterval(timer);
|
||||
}, [ready, sendIdentify, socket]);
|
||||
const nextKey = String(serverCookieUserId || '').trim();
|
||||
if (!nextKey || nextKey === cookieUserId) return;
|
||||
saveIdentity((current) => ({ ...(current || {}), cookieUserId: nextKey }));
|
||||
}, [cookieUserId, saveIdentity, serverCookieUserId]);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Socket Library Helper
|
||||
// Purpose: Builds and exports the browser Socket.IO client with shared defaults. Scope: Centralizes connection URL/options so all modules use consistent socket behavior.
|
||||
import { io } from 'socket.io-client';
|
||||
import { getBrowserFingerprintId } from './browserFingerprint.js';
|
||||
import { loadSettings } from '../settings/persistence.js';
|
||||
|
||||
const configured = import.meta.env.VITE_ROVERD_URL?.trim();
|
||||
@@ -9,8 +10,48 @@ console.info('[socket] connecting to', resolvedUrl);
|
||||
const settings = loadSettings();
|
||||
const transportPref = settings?.page?.connectionTransport || 'websocket';
|
||||
const transports = transportPref === 'polling' ? ['polling'] : ['websocket', 'polling'];
|
||||
|
||||
function getIdentitySurface() {
|
||||
/*
|
||||
Duplicate-driver protection is connection-specific, so the route must be
|
||||
known during the handshake rather than waiting for a React route component
|
||||
to mount. The main driver and dedicated PTZ controller are the two active
|
||||
control surfaces; every other route is passive identity-wise.
|
||||
*/
|
||||
return window.location.pathname === '/' || window.location.pathname === '/ptz'
|
||||
? 'driver'
|
||||
: 'passive';
|
||||
}
|
||||
|
||||
async function buildSocketIdentity() {
|
||||
/*
|
||||
Socket.IO invokes this auth callback again for every reconnection. Reading
|
||||
persistence here, instead of reusing the module-level settings snapshot,
|
||||
ensures that nickname, preferences, imported settings, and a server-issued
|
||||
identity key are current whenever a new socket is accepted.
|
||||
*/
|
||||
const currentSettings = loadSettings();
|
||||
const fingerprintId = await getBrowserFingerprintId();
|
||||
return {
|
||||
cookieUserId: String(currentSettings?.identity?.cookieUserId || '').trim(),
|
||||
fingerprintId,
|
||||
nickname: String(currentSettings?.profile?.nickname || '').trim(),
|
||||
overseerEnabled: Boolean(currentSettings?.overseerPreference?.enabled),
|
||||
identitySurface: getIdentitySurface(),
|
||||
};
|
||||
}
|
||||
|
||||
export const socket = io(resolvedUrl, {
|
||||
transports,
|
||||
timeout: 15000,
|
||||
/*
|
||||
The callback form lets the existing asynchronous fingerprint finish before
|
||||
Socket.IO sends its namespace CONNECT packet. Server connection middleware
|
||||
can therefore attach the canonical identity before any feature service sees
|
||||
the socket, eliminating the previous connected-but-unidentified window.
|
||||
*/
|
||||
auth: (callback) => {
|
||||
buildSocketIdentity().then(callback);
|
||||
},
|
||||
});
|
||||
socket.on('connect_error', (err) => console.error('connect_error', err.code, err.message, err.data));
|
||||
|
||||
Reference in New Issue
Block a user