fix(app): stabilize auth bootstrap and direct backend integration
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
// @ts-nocheck
|
||||
import { io } from 'socket.io-client';
|
||||
|
||||
import { api } from './api';
|
||||
import { api, getBackendUrl } from './api';
|
||||
import { createMotionDetector } from './motion-detector';
|
||||
import { getAppState, patchAppState, resetAppState, setAppState } from './store';
|
||||
|
||||
@@ -14,6 +14,13 @@ const PAGE_PATHS = {
|
||||
activity: '/activity',
|
||||
settings: '/settings'
|
||||
};
|
||||
const DEVICE_STORAGE_KEY = 'mobileSimDevice';
|
||||
const INVALID_DEVICE_TOKEN_ERRORS = new Set([
|
||||
'Missing device token',
|
||||
'Invalid device token',
|
||||
'Device not found',
|
||||
'Token role does not match device role'
|
||||
]);
|
||||
|
||||
const MOTION_DETECTION_SETTINGS_STORAGE_KEY = 'securecam-motion-detection-settings';
|
||||
const MOTION_DETECTION_PROFILES = {
|
||||
@@ -253,6 +260,151 @@ const navigateToScreen = (screen, options = {}) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const readSavedDeviceRecord = () => {
|
||||
if (typeof localStorage === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const saved = localStorage.getItem(DEVICE_STORAGE_KEY);
|
||||
if (!saved) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(saved);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse saved device', error);
|
||||
localStorage.removeItem(DEVICE_STORAGE_KEY);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const persistSavedDeviceRecord = ({ device, deviceToken, userId }) => {
|
||||
if (typeof localStorage === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(
|
||||
DEVICE_STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
device,
|
||||
deviceToken,
|
||||
userId
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const clearSavedDeviceRecord = () => {
|
||||
if (typeof localStorage === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.removeItem(DEVICE_STORAGE_KEY);
|
||||
};
|
||||
|
||||
const applySavedDeviceState = (device, deviceToken) => {
|
||||
setAppState({
|
||||
device,
|
||||
deviceToken,
|
||||
onboardingForm: {
|
||||
...getAppState().onboardingForm,
|
||||
name: device?.name ?? '',
|
||||
role: device?.role ?? 'client',
|
||||
pushToken: ''
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const clearDeviceState = () => {
|
||||
setAppState({
|
||||
device: null,
|
||||
deviceToken: null,
|
||||
socketConnected: false,
|
||||
isMotionActive: false,
|
||||
activeMotionSource: null,
|
||||
cameraStatus: 'idle',
|
||||
cameraPreviewReady: false,
|
||||
linkedCameras: [],
|
||||
recordings: [],
|
||||
activeCameraDeviceId: null,
|
||||
activeStreamSessionId: null,
|
||||
openLinkedCameraMenuId: null,
|
||||
cameraSessions: {},
|
||||
connectedStreamSessionIds: [],
|
||||
clientStreamMode: 'none',
|
||||
clientPlaceholderText: 'Select a camera to view',
|
||||
onboardingForm: {
|
||||
...getAppState().onboardingForm,
|
||||
name: '',
|
||||
role: 'client',
|
||||
pushToken: ''
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const restoreSavedDeviceForSession = async (session, options = {}) => {
|
||||
const { showMissingToast = false, showInvalidToast = false } = options;
|
||||
const saved = readSavedDeviceRecord();
|
||||
|
||||
if (!saved) {
|
||||
if (showMissingToast) {
|
||||
pushToast('No saved device found', 'info');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const sessionUserId = session?.user?.id;
|
||||
const savedUserId = typeof saved.userId === 'string' ? saved.userId : null;
|
||||
const savedDeviceId = saved?.device?.id;
|
||||
const savedDeviceToken = typeof saved?.deviceToken === 'string' ? saved.deviceToken : '';
|
||||
|
||||
if (!sessionUserId || !savedDeviceId || !savedDeviceToken) {
|
||||
clearSavedDeviceRecord();
|
||||
clearDeviceState();
|
||||
if (showInvalidToast) {
|
||||
pushToast('Saved device is incomplete. Please register again.', 'error');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (savedUserId && savedUserId !== sessionUserId) {
|
||||
clearSavedDeviceRecord();
|
||||
clearDeviceState();
|
||||
if (showInvalidToast) {
|
||||
pushToast('Saved device belongs to a different account.', 'info');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await api.devices.list();
|
||||
const matchingDevice = result.devices?.find((device) => device.id === savedDeviceId);
|
||||
|
||||
if (!matchingDevice) {
|
||||
clearSavedDeviceRecord();
|
||||
clearDeviceState();
|
||||
if (showInvalidToast) {
|
||||
pushToast('Saved device was not found for this account.', 'info');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
applySavedDeviceState(matchingDevice, savedDeviceToken);
|
||||
persistSavedDeviceRecord({
|
||||
device: matchingDevice,
|
||||
deviceToken: savedDeviceToken,
|
||||
userId: sessionUserId
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to restore saved device', error);
|
||||
if (showInvalidToast) {
|
||||
pushToast('Unable to restore saved device right now.', 'error');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const setConnectedStreamSessionIds = () => {
|
||||
setAppState({ connectedStreamSessionIds: Array.from(connectedPeers) });
|
||||
};
|
||||
@@ -1267,7 +1419,10 @@ const connectSocket = () => {
|
||||
if (!deviceToken) return;
|
||||
|
||||
if (socket) socket.disconnect();
|
||||
socket = io({ auth: { token: deviceToken } });
|
||||
socket = io(getBackendUrl(), {
|
||||
auth: { token: deviceToken },
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
setAppState({ socketConnected: true });
|
||||
@@ -1286,6 +1441,20 @@ const connectSocket = () => {
|
||||
applyMotionDetectionReadiness();
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
const message = error?.message || 'Realtime connection failed';
|
||||
setAppState({ socketConnected: false });
|
||||
addActivity('System', `Realtime connection failed: ${message}`);
|
||||
|
||||
if (INVALID_DEVICE_TOKEN_ERRORS.has(message)) {
|
||||
void invalidateSavedDevice('Saved device is invalid for this account. Please register this browser again.');
|
||||
return;
|
||||
}
|
||||
|
||||
pushToast(message, 'error');
|
||||
applyMotionDetectionReadiness();
|
||||
});
|
||||
|
||||
socket.on('command:received', async (payload) => {
|
||||
addActivity('Command', `Received ${payload.commandType}`);
|
||||
|
||||
@@ -1519,6 +1688,17 @@ const cleanupConnectionState = async () => {
|
||||
requestedStreams.clear();
|
||||
};
|
||||
|
||||
const invalidateSavedDevice = async (message, options = {}) => {
|
||||
const { showToast = true } = options;
|
||||
clearSavedDeviceRecord();
|
||||
await cleanupConnectionState();
|
||||
clearDeviceState();
|
||||
if (showToast) {
|
||||
pushToast(message || 'Saved device is no longer valid. Please register this browser again.', 'error');
|
||||
}
|
||||
navigateToScreen('onboarding', { replace: true });
|
||||
};
|
||||
|
||||
const enforceRouteForSession = () => {
|
||||
const state = getAppState();
|
||||
const page = pageFromPath(window.location.pathname);
|
||||
@@ -1554,7 +1734,7 @@ const init = async () => {
|
||||
if (initPromise) return initPromise;
|
||||
|
||||
initPromise = (async () => {
|
||||
setAppState({ page: pageFromPath(window.location.pathname) });
|
||||
setAppState({ page: pageFromPath(window.location.pathname), loading: true });
|
||||
if (navigator.mediaDevices?.addEventListener) {
|
||||
navigator.mediaDevices.addEventListener('devicechange', onMediaDeviceChange);
|
||||
}
|
||||
@@ -1562,39 +1742,24 @@ const init = async () => {
|
||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
}
|
||||
|
||||
const saved = localStorage.getItem('mobileSimDevice');
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
setAppState({
|
||||
device: parsed.device,
|
||||
deviceToken: parsed.deviceToken,
|
||||
onboardingForm: {
|
||||
...getAppState().onboardingForm,
|
||||
name: parsed.device?.name ?? '',
|
||||
role: parsed.device?.role ?? 'client'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load saved device', error);
|
||||
}
|
||||
}
|
||||
|
||||
setAppState({ motionDetection: loadMotionDetectionSettings() });
|
||||
|
||||
try {
|
||||
const session = await api.auth.getSession();
|
||||
if (session?.session) {
|
||||
setAppState({ session });
|
||||
if (getAppState().deviceToken) {
|
||||
const restoredSavedDevice = await restoreSavedDeviceForSession(session);
|
||||
if (restoredSavedDevice) {
|
||||
connectSocket();
|
||||
startPolling();
|
||||
}
|
||||
} else {
|
||||
setAppState({ session: null });
|
||||
clearDeviceState();
|
||||
}
|
||||
} catch {
|
||||
setAppState({ session: null });
|
||||
clearDeviceState();
|
||||
}
|
||||
|
||||
enforceRouteForSession();
|
||||
@@ -1606,10 +1771,10 @@ const init = async () => {
|
||||
});
|
||||
|
||||
initialized = true;
|
||||
})()
|
||||
.finally(() => {
|
||||
initPromise = null;
|
||||
});
|
||||
})().finally(() => {
|
||||
setAppState({ loading: false });
|
||||
initPromise = null;
|
||||
});
|
||||
|
||||
return initPromise;
|
||||
};
|
||||
@@ -1667,7 +1832,8 @@ const actions = {
|
||||
setAppState({ session, authForm: { ...state.authForm, password: '' } });
|
||||
pushToast(`Welcome, ${session.user.name}`, 'success');
|
||||
|
||||
if (getAppState().deviceToken) {
|
||||
const restoredSavedDevice = await restoreSavedDeviceForSession(session);
|
||||
if (restoredSavedDevice) {
|
||||
connectSocket();
|
||||
startPolling();
|
||||
navigateToScreen('home', { role: getAppState().device?.role });
|
||||
@@ -1704,8 +1870,12 @@ const actions = {
|
||||
}
|
||||
|
||||
const result = await api.devices.register(payload);
|
||||
setAppState({ device: result.device, deviceToken: result.deviceToken });
|
||||
localStorage.setItem('mobileSimDevice', JSON.stringify({ device: result.device, deviceToken: result.deviceToken }));
|
||||
applySavedDeviceState(result.device, result.deviceToken);
|
||||
persistSavedDeviceRecord({
|
||||
device: result.device,
|
||||
deviceToken: result.deviceToken,
|
||||
userId: getAppState().session?.user?.id ?? null
|
||||
});
|
||||
|
||||
pushToast('Device Registered', 'success');
|
||||
connectSocket();
|
||||
@@ -1717,27 +1887,17 @@ const actions = {
|
||||
},
|
||||
|
||||
loadSavedDevice() {
|
||||
const saved = localStorage.getItem('mobileSimDevice');
|
||||
if (!saved) {
|
||||
pushToast('No saved device found', 'info');
|
||||
const session = getAppState().session;
|
||||
if (!session) {
|
||||
pushToast('Please sign in before loading a saved device', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
setAppState({
|
||||
device: parsed.device,
|
||||
deviceToken: parsed.deviceToken,
|
||||
onboardingForm: {
|
||||
...getAppState().onboardingForm,
|
||||
name: parsed.device?.name ?? '',
|
||||
role: parsed.device?.role ?? 'client'
|
||||
}
|
||||
});
|
||||
pushToast('Loaded saved device', 'success');
|
||||
} catch {
|
||||
pushToast('Saved device is invalid', 'error');
|
||||
}
|
||||
void restoreSavedDeviceForSession(session, { showMissingToast: true, showInvalidToast: true }).then((restored) => {
|
||||
if (restored) {
|
||||
pushToast('Loaded saved device', 'success');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async signOut() {
|
||||
@@ -1748,7 +1908,7 @@ const actions = {
|
||||
}
|
||||
|
||||
await cleanupConnectionState();
|
||||
localStorage.removeItem('mobileSimDevice');
|
||||
clearSavedDeviceRecord();
|
||||
const keep = { page: 'auth', toasts: [] };
|
||||
resetAppState(keep);
|
||||
pushToast('Signed Out', 'info');
|
||||
|
||||
Reference in New Issue
Block a user