From f0cb4b3b8389cdcff5c81933ab9bd3cb64c9decc Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Fri, 5 Sep 2025 23:17:08 +0100 Subject: [PATCH] Add ApiContextDebug component for API server debugging and testing functionality; removed PrintServerContextDebug component. --- .../Dashboard/Developer/ApiContextDebug.jsx | 842 ++++++++++++++++++ .../Developer/PrintServerContextDebug.jsx | 77 -- .../Dashboard/context/AuthContext.jsx | 232 ++++- src/routes/DeveloperRoutes.jsx | 8 +- 4 files changed, 1032 insertions(+), 127 deletions(-) create mode 100644 src/components/Dashboard/Developer/ApiContextDebug.jsx delete mode 100644 src/components/Dashboard/Developer/PrintServerContextDebug.jsx diff --git a/src/components/Dashboard/Developer/ApiContextDebug.jsx b/src/components/Dashboard/Developer/ApiContextDebug.jsx new file mode 100644 index 0000000..48a75c2 --- /dev/null +++ b/src/components/Dashboard/Developer/ApiContextDebug.jsx @@ -0,0 +1,842 @@ +import { useContext, useState, useEffect } from 'react' +import { + Descriptions, + Button, + Typography, + Flex, + Space, + Dropdown, + message, + Tag, + Input, + InputNumber, + Select +} from 'antd' +import ReloadIcon from '../../Icons/ReloadIcon.jsx' +import CloudIcon from '../../Icons/CloudIcon.jsx' +import { ApiServerContext } from '../context/ApiServerContext.jsx' +import BoolDisplay from '../common/BoolDisplay.jsx' +import InfoCollapse from '../common/InfoCollapse.jsx' + +const { Text, Paragraph } = Typography + +const ApiContextDebug = () => { + const { + apiServer, + error, + connecting, + connected, + fetchLoading, + lockObject, + unlockObject, + fetchObjectLock, + updateObject, + createObject, + deleteObject, + subscribeToObjectUpdates, + subscribeToObjectEvent, + subscribeToObjectTypeUpdates, + subscribeToObjectLock, + fetchObject, + fetchObjects, + fetchObjectsByProperty, + fetchSpotlightData, + fetchObjectContent, + fetchTemplatePreview, + fetchNotes, + fetchHostOTP, + sendObjectAction + } = useContext(ApiServerContext) + + const [msgApi, contextHolder] = message.useMessage() + const [connectionStatus, setConnectionStatus] = useState('disconnected') + const [socketId, setSocketId] = useState(null) + + // Test input states + const [testInputs, setTestInputs] = useState({ + objectId: 'test-id', + objectType: 'user', + hostId: 'test-host-id', + parentId: 'test-parent-id', + fileName: 'test.gcode', + query: 'test query', + action: 'start', + eventType: 'status', + properties: ['name', 'email'], + filter: { active: true }, + scale: 1.0, + templateContent: 'Test template content', + testObject: { name: 'Test Object' } + }) + + // Collapse states + const [collapseStates, setCollapseStates] = useState({ + fetchTests: false, + crudTests: false, + subscriptionTests: false, + specialTests: false + }) + + useEffect(() => { + if (apiServer) { + setConnectionStatus(apiServer.connected ? 'connected' : 'disconnected') + setSocketId(apiServer.id) + + // Listen for connection status changes + const handleConnect = () => { + setConnectionStatus('connected') + setSocketId(apiServer.id) + } + + const handleDisconnect = () => { + setConnectionStatus('disconnected') + setSocketId(null) + } + + apiServer.on('connect', handleConnect) + apiServer.on('disconnect', handleDisconnect) + + return () => { + apiServer.off('connect', handleConnect) + apiServer.off('disconnect', handleDisconnect) + } + } else { + setConnectionStatus('disconnected') + setSocketId(null) + } + }, [apiServer]) + + // Helper functions + const updateTestInput = (key, value) => { + setTestInputs((prev) => ({ ...prev, [key]: value })) + } + + const toggleCollapse = (key) => { + setCollapseStates((prev) => ({ ...prev, [key]: !prev[key] })) + } + + const handleReconnect = () => { + if (apiServer) { + apiServer.connect() + msgApi.info('Attempting to reconnect...') + } else { + msgApi.warning('No API server instance available') + } + } + + const handleDisconnect = () => { + if (apiServer) { + apiServer.disconnect() + msgApi.info('Disconnected from API server') + } + } + + const testFetchObject = async () => { + try { + msgApi.loading('Testing fetchObject...', 0) + const result = await fetchObject( + testInputs.objectId, + testInputs.objectType + ) + msgApi.destroy() + msgApi.success('fetchObject test completed') + console.log('fetchObject result:', result) + } catch (err) { + msgApi.destroy() + msgApi.error('fetchObject test failed') + console.error('fetchObject error:', err) + } + } + + const testFetchObjects = async () => { + try { + msgApi.loading('Testing fetchObjects...', 0) + const result = await fetchObjects(testInputs.objectType, { + page: 1, + limit: 5 + }) + msgApi.destroy() + msgApi.success('fetchObjects test completed') + console.log('fetchObjects result:', result) + } catch (err) { + msgApi.destroy() + msgApi.error('fetchObjects test failed') + console.error('fetchObjects error:', err) + } + } + + const testLockObject = () => { + try { + lockObject(testInputs.objectId, testInputs.objectType) + msgApi.success('Lock command sent') + } catch (err) { + msgApi.error('Lock command failed') + console.error('Lock error:', err) + } + } + + const testUnlockObject = () => { + try { + unlockObject(testInputs.objectId, testInputs.objectType) + msgApi.success('Unlock command sent') + } catch (err) { + msgApi.error('Unlock command failed') + console.error('Unlock error:', err) + } + } + + const testFetchObjectLock = async () => { + try { + msgApi.loading('Testing fetchObjectLock...', 0) + const result = await fetchObjectLock( + testInputs.objectId, + testInputs.objectType + ) + msgApi.destroy() + msgApi.success('fetchObjectLock test completed') + console.log('fetchObjectLock result:', result) + } catch (err) { + msgApi.destroy() + msgApi.error('fetchObjectLock test failed') + console.error('fetchObjectLock error:', err) + } + } + + const testUpdateObject = async () => { + try { + msgApi.loading('Testing updateObject...', 0) + const testData = { + name: 'Test Update', + updated: new Date().toISOString() + } + const result = await updateObject( + testInputs.objectId, + testInputs.objectType, + testData + ) + msgApi.destroy() + msgApi.success('updateObject test completed') + console.log('updateObject result:', result) + } catch (err) { + msgApi.destroy() + msgApi.error('updateObject test failed') + console.error('updateObject error:', err) + } + } + + const testCreateObject = async () => { + try { + msgApi.loading('Testing createObject...', 0) + const testData = { + name: 'Test Create', + created: new Date().toISOString() + } + const result = await createObject(testInputs.objectType, testData) + msgApi.destroy() + msgApi.success('createObject test completed') + console.log('createObject result:', result) + } catch (err) { + msgApi.destroy() + msgApi.error('createObject test failed') + console.error('createObject error:', err) + } + } + + const testDeleteObject = async () => { + try { + msgApi.loading('Testing deleteObject...', 0) + const result = await deleteObject( + testInputs.objectId, + testInputs.objectType + ) + msgApi.destroy() + msgApi.success('deleteObject test completed') + console.log('deleteObject result:', result) + } catch (err) { + msgApi.destroy() + msgApi.error('deleteObject test failed') + console.error('deleteObject error:', err) + } + } + + const testFetchObjectsByProperty = async () => { + try { + msgApi.loading('Testing fetchObjectsByProperty...', 0) + const params = { + properties: testInputs.properties, + filter: testInputs.filter, + masterFilter: {} + } + const result = await fetchObjectsByProperty(testInputs.objectType, params) + msgApi.destroy() + msgApi.success('fetchObjectsByProperty test completed') + console.log('fetchObjectsByProperty result:', result) + } catch (err) { + msgApi.destroy() + msgApi.error('fetchObjectsByProperty test failed') + console.error('fetchObjectsByProperty error:', err) + } + } + + const testFetchSpotlightData = async () => { + try { + msgApi.loading('Testing fetchSpotlightData...', 0) + const result = await fetchSpotlightData(testInputs.query) + msgApi.destroy() + msgApi.success('fetchSpotlightData test completed') + console.log('fetchSpotlightData result:', result) + } catch (err) { + msgApi.destroy() + msgApi.error('fetchSpotlightData test failed') + console.error('fetchSpotlightData error:', err) + } + } + + const testFetchObjectContent = async () => { + try { + msgApi.loading('Testing fetchObjectContent...', 0) + await fetchObjectContent( + testInputs.objectId, + 'gcodefile', + testInputs.fileName + ) + msgApi.destroy() + msgApi.success('fetchObjectContent test completed') + } catch (err) { + msgApi.destroy() + msgApi.error('fetchObjectContent test failed') + console.error('fetchObjectContent error:', err) + } + } + + const testFetchTemplatePreview = async () => { + try { + msgApi.loading('Testing fetchTemplatePreview...', 0) + + fetchTemplatePreview( + testInputs.objectId, + testInputs.templateContent, + testInputs.testObject, + testInputs.scale, + (result) => { + msgApi.destroy() + if (result.success) { + msgApi.success('fetchTemplatePreview test completed') + } else { + msgApi.error('fetchTemplatePreview test failed') + } + console.log('fetchTemplatePreview result:', result) + } + ) + } catch (err) { + msgApi.destroy() + msgApi.error('fetchTemplatePreview test failed') + console.error('fetchTemplatePreview error:', err) + } + } + + const testFetchNotes = async () => { + try { + msgApi.loading('Testing fetchNotes...', 0) + const result = await fetchNotes(testInputs.parentId) + msgApi.destroy() + msgApi.success('fetchNotes test completed') + console.log('fetchNotes result:', result) + } catch (err) { + msgApi.destroy() + msgApi.error('fetchNotes test failed') + console.error('fetchNotes error:', err) + } + } + + const testFetchHostOTP = async () => { + try { + msgApi.loading('Testing fetchHostOTP...', 0) + + fetchHostOTP(testInputs.hostId, (result) => { + msgApi.destroy() + if (result.otp) { + msgApi.success('fetchHostOTP test completed: ' + result.otp) + } else { + msgApi.error('fetchHostOTP test failed') + } + console.log('fetchHostOTP result:', result) + }) + } catch (err) { + msgApi.destroy() + msgApi.error('fetchHostOTP test failed') + console.error('fetchHostOTP error:', err) + } + } + + const testSendObjectAction = async () => { + try { + msgApi.loading('Testing sendObjectAction...', 0) + + sendObjectAction( + testInputs.objectId, + 'printer', + testInputs.action, + (result) => { + msgApi.destroy() + if (result.success) { + msgApi.success('sendObjectAction test completed') + } else { + msgApi.error('sendObjectAction test failed') + } + console.log('sendObjectAction result:', result) + } + ) + } catch (err) { + msgApi.destroy() + msgApi.error('sendObjectAction test failed') + console.error('sendObjectAction error:', err) + } + } + + const testSubscribeToObjectUpdates = () => { + try { + const callback = (data) => { + console.log('Object update received:', data) + } + const unsubscribe = subscribeToObjectUpdates( + testInputs.objectId, + testInputs.objectType, + callback + ) + msgApi.success('Subscribed to object updates') + console.log('Subscribed to object updates for test-id') + + // Store unsubscribe function for cleanup + setTimeout(() => { + if (unsubscribe) { + unsubscribe() + console.log('Unsubscribed from object updates') + } + }, 10000) // Auto-unsubscribe after 10 seconds + } catch (err) { + msgApi.error('Subscribe to object updates failed') + console.error('Subscribe error:', err) + } + } + + const testSubscribeToObjectEvent = () => { + try { + const callback = (event) => { + console.log('Object event received:', event) + } + const unsubscribe = subscribeToObjectEvent( + testInputs.objectId, + 'printer', + testInputs.eventType, + callback + ) + msgApi.success('Subscribed to object events') + console.log('Subscribed to object events for test-id') + + // Store unsubscribe function for cleanup + setTimeout(() => { + if (unsubscribe) { + unsubscribe() + console.log('Unsubscribed from object events') + } + }, 10000) // Auto-unsubscribe after 10 seconds + } catch (err) { + msgApi.error('Subscribe to object events failed') + console.error('Subscribe error:', err) + } + } + + const testSubscribeToObjectTypeUpdates = () => { + try { + const callback = (data) => { + console.log('Object type update received:', data) + } + const unsubscribe = subscribeToObjectTypeUpdates( + testInputs.objectType, + callback + ) + msgApi.success('Subscribed to object type updates') + console.log('Subscribed to object type updates for user') + + // Store unsubscribe function for cleanup + setTimeout(() => { + if (unsubscribe) { + unsubscribe() + console.log('Unsubscribed from object type updates') + } + }, 10000) // Auto-unsubscribe after 10 seconds + } catch (err) { + msgApi.error('Subscribe to object type updates failed') + console.error('Subscribe error:', err) + } + } + + const testSubscribeToObjectLock = () => { + try { + const callback = (lockData) => { + console.log('Object lock update received:', lockData) + } + const unsubscribe = subscribeToObjectLock( + testInputs.objectId, + testInputs.objectType, + callback + ) + msgApi.success('Subscribed to object lock updates') + console.log('Subscribed to object lock updates for test-id') + + // Store unsubscribe function for cleanup + setTimeout(() => { + if (unsubscribe) { + unsubscribe() + console.log('Unsubscribed from object lock updates') + } + }, 10000) // Auto-unsubscribe after 10 seconds + } catch (err) { + msgApi.error('Subscribe to object lock updates failed') + console.error('Subscribe error:', err) + } + } + + const actionItems = { + items: [ + { + label: 'Reconnect', + key: 'reconnect', + disabled: connected + }, + { + label: 'Disconnect', + key: 'disconnect', + disabled: !connected + }, + { + type: 'divider' + }, + { + label: 'Reload', + key: 'reload', + icon: + } + ], + onClick: ({ key }) => { + switch (key) { + case 'reconnect': + handleReconnect() + break + case 'disconnect': + handleDisconnect() + break + case 'reload': + msgApi.info('Reloading API State...') + window.location.reload() + break + default: + break + } + } + } + + const getConnectionStatusColor = () => { + switch (connectionStatus) { + case 'connected': + return 'success' + case 'connecting': + return 'processing' + case 'disconnected': + return 'error' + default: + return 'default' + } + } + + return ( + + {contextHolder} + + {/* Header with Actions */} + + + + + + + + + + + + }> + {connectionStatus.charAt(0).toUpperCase() + + connectionStatus.slice(1)} + + + + + + + + + + + {socketId || 'None'} + + + + + + {error || 'None'} + + + + 🔎} + active={collapseStates.params} + onToggle={() => toggleCollapse('params')} + collapseKey='params' + > + + + updateTestInput('objectId', e.target.value)} + placeholder='Enter object ID' + size='small' + /> + + + updateTestInput('hostId', e.target.value)} + placeholder='Enter host ID' + /> + + + updateTestInput('parentId', e.target.value)} + placeholder='Enter parent ID' + /> + + + updateTestInput('fileName', e.target.value)} + placeholder='Enter file name' + /> + + + updateTestInput('query', e.target.value)} + placeholder='Enter search query' + /> + + + updateTestInput('eventType', value)} + style={{ width: '100%' }} + options={[ + { value: 'status', label: 'Status' }, + { value: 'progress', label: 'Progress' }, + { value: 'error', label: 'Error' }, + { value: 'complete', label: 'Complete' } + ]} + /> + + + updateTestInput('scale', value)} + min={0.1} + max={10} + step={0.1} + style={{ width: '100%' }} + /> + + + + updateTestInput('templateContent', e.target.value) + } + placeholder='Enter template content' + rows={2} + /> + + + + + {/* Test Sections */} +
+ + {/* Fetch Tests */} + 📥} + active={collapseStates.fetchTests} + onToggle={() => toggleCollapse('fetchTests')} + collapseKey='fetchTests' + > + + + + + + + + + + + + {/* CRUD Tests */} + ✏️} + active={collapseStates.crudTests} + onToggle={() => toggleCollapse('crudTests')} + collapseKey='crudTests' + > + + + + + + + + + + {/* Special Tests */} + 🔧} + active={collapseStates.specialTests} + onToggle={() => toggleCollapse('specialTests')} + collapseKey='specialTests' + > + + + + + + + + {/* Subscription Tests */} + 📡} + active={collapseStates.subscriptionTests} + onToggle={() => toggleCollapse('subscriptionTests')} + collapseKey='subscriptionTests' + > + + + + + + + + + {/* API Server Instance Info */} + 🖥️} + active={false} + onToggle={() => {}} + collapseKey='apiServer' + > +
+              {apiServer ? (
+                
+                  
+                    {JSON.stringify(
+                      {
+                        connected: apiServer.connected,
+                        id: apiServer.id,
+                        transport: apiServer.io.engine.transport.name,
+                        readyState: apiServer.io.engine.readyState
+                      },
+                      null,
+                      2
+                    )}
+                  
+
+ ) : ( + No API server instance + )} +
+
+
+
+
+ ) +} + +export default ApiContextDebug diff --git a/src/components/Dashboard/Developer/PrintServerContextDebug.jsx b/src/components/Dashboard/Developer/PrintServerContextDebug.jsx deleted file mode 100644 index c49b6a8..0000000 --- a/src/components/Dashboard/Developer/PrintServerContextDebug.jsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useContext } from 'react' -import { - Descriptions, - Button, - Typography, - Flex, - Space, - Dropdown, - message -} from 'antd' -import ReloadIcon from '../../Icons/ReloadIcon.jsx' -import { PrintServerContext } from '../context/PrintServerContext.jsx' -import BoolDisplay from '../common/BoolDisplay.jsx' - -const { Text, Paragraph } = Typography - -const PrintServerContextDebug = () => { - const { printServer, error, connecting } = useContext(PrintServerContext) - const [msgApi, contextHolder] = message.useMessage() - - const actionItems = { - items: [ - { - label: 'Reload', - key: 'reload', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reload') { - msgApi.info('Reloading Page...') - window.location.reload() - } - } - } - - // Helper to display socket info safely - const getSocketInfo = () => { - if (!printServer) return 'n/a' - // Only show safe properties - const { id, connected, disconnected, nsp } = printServer - return JSON.stringify({ id, connected, disconnected, nsp }, null, 2) - } - - return ( - - {contextHolder} - - - - - - - -
- - - - - - - - - {error ? {error} : n/a} - - - -
{getSocketInfo()}
-
-
-
-
-
- ) -} - -export default PrintServerContextDebug diff --git a/src/components/Dashboard/context/AuthContext.jsx b/src/components/Dashboard/context/AuthContext.jsx index 7e497f5..f027af7 100644 --- a/src/components/Dashboard/context/AuthContext.jsx +++ b/src/components/Dashboard/context/AuthContext.jsx @@ -24,6 +24,16 @@ import config from '../../../config' import loglevel from 'loglevel' import { ElectronContext } from './ElectronContext' import { useLocation, useNavigate } from 'react-router-dom' +import { + getAuthCookies, + setAuthCookies, + clearAuthCookies, + areCookiesEnabled, + validateAuthCookies, + setupCookieSync, + checkAuthCookiesExpiry +} from '../../../utils/cookies' + const logger = loglevel.getLogger('ApiServerContext') logger.setLevel(config.logLevel) @@ -37,7 +47,7 @@ const AuthProvider = ({ children }) => { notification.useNotification() const [authenticated, setAuthenticated] = useState(false) const [initialized, setInitialized] = useState(false) - const [retreivedTokenFromSession, setRetreivedTokenFromSession] = + const [retreivedTokenFromCookies, setRetreivedTokenFromCookies] = useState(false) const [loading, setLoading] = useState(false) const [token, setToken] = useState(null) @@ -60,33 +70,98 @@ const AuthProvider = ({ children }) => { if (isElectron == true && import.meta.env.MODE != 'development') { redirectType = 'app-scheme' } - // Read token from session storage if present + + // Check if cookies are enabled and show warning if not useEffect(() => { - const storedToken = sessionStorage.getItem('authToken') - const storedUser = JSON.parse(sessionStorage.getItem('user')) - const storedExpiresAt = sessionStorage.getItem('authExpiresAt') - console.log('stored user', storedUser, storedToken) - if (storedToken && storedExpiresAt && storedUser) { - setToken(storedToken) - setUserProfile(storedUser) - setExpiresAt(storedExpiresAt) - setAuthenticated(true) - } else { + if (!areCookiesEnabled()) { + messageApi.warning( + 'Cookies are disabled. Login state may not persist between tabs.' + ) + } + }, [messageApi]) + + // Read token from cookies if present + useEffect(() => { + try { + // First validate existing cookies to clean up expired ones + if (validateAuthCookies()) { + const { + token: storedToken, + expiresAt: storedExpiresAt, + user: storedUser + } = getAuthCookies() + console.log('Retrieved from cookies:', { + storedUser, + storedToken, + storedExpiresAt + }) + + setToken(storedToken) + setUserProfile(storedUser) + setExpiresAt(storedExpiresAt) + setAuthenticated(true) + } else { + setAuthenticated(false) + setUserProfile(null) + setShowUnauthorizedModal(true) + } + } catch (error) { + console.error('Error reading auth cookies:', error) + clearAuthCookies() setAuthenticated(false) setUserProfile(null) setShowUnauthorizedModal(true) } - setRetreivedTokenFromSession(true) + setRetreivedTokenFromCookies(true) }, []) + // Set up cookie synchronization between tabs + useEffect(() => { + const cleanupCookieSync = setupCookieSync(() => { + // When cookies change in another tab, re-validate and update state + try { + if (validateAuthCookies()) { + const { + token: newToken, + expiresAt: newExpiresAt, + user: newUser + } = getAuthCookies() + if ( + newToken !== token || + newExpiresAt !== expiresAt || + JSON.stringify(newUser) !== JSON.stringify(userProfile) + ) { + setToken(newToken) + setExpiresAt(newExpiresAt) + setUserProfile(newUser) + setAuthenticated(true) + console.log('Auth state synchronized from another tab') + } + } else { + // Cookies are invalid, clear state + setToken(null) + setExpiresAt(null) + setUserProfile(null) + setAuthenticated(false) + setShowUnauthorizedModal(true) + console.log( + 'Auth state cleared due to invalid cookies from another tab' + ) + } + } catch (error) { + console.error('Error syncing auth state:', error) + } + }) + + return cleanupCookieSync + }, [token, expiresAt, userProfile]) + const logout = useCallback((redirectUri = '/login') => { setAuthenticated(false) setToken(null) setExpiresAt(null) setUserProfile(null) - sessionStorage.removeItem('authToken') - sessionStorage.removeItem('authExpiresAt') - sessionStorage.removeItem('user') + clearAuthCookies() window.location.href = `${config.backendUrl}/auth/logout?redirect_uri=${encodeURIComponent(redirectUri)}` }, []) @@ -128,18 +203,20 @@ const AuthProvider = ({ children }) => { if (response.status === 200 && response.data) { logger.debug('Got auth token!') - setToken(response.data.access_token) - setExpiresAt(response.data.expires_at) - setUserProfile(response.data) - sessionStorage.setItem('authToken', response.data.access_token) - sessionStorage.setItem('authExpiresAt', response.data.expires_at) - const userObject = { - ...response.data, - access_token: undefined, - refresh_token: undefined, - id_token: undefined + const authData = response.data + + setToken(authData.access_token) + setExpiresAt(authData.expires_at) + setUserProfile(authData) + + // Store in cookies for persistence between tabs + const cookieSuccess = setAuthCookies(authData) + if (!cookieSuccess) { + messageApi.warning( + 'Authentication successful but failed to save login state. You may need to log in again if you close this tab.' + ) } - sessionStorage.setItem('user', JSON.stringify(userObject)) + const searchParams = new URLSearchParams(location.search) searchParams.delete('authCode') const newSearch = searchParams.toString() @@ -167,8 +244,9 @@ const AuthProvider = ({ children }) => { setLoading(false) } }, - [isElectron] + [isElectron, navigate, location.search, location.pathname, messageApi] ) + // Function to check if the user is logged in const checkAuthStatus = useCallback(async () => { setLoading(true) @@ -183,12 +261,19 @@ const AuthProvider = ({ children }) => { if (response.status === 200 && response.data) { logger.debug('Got auth token!') - setToken(response.data.access_token) - setExpiresAt(response.data.expires_at) - setUserProfile(response.data) - sessionStorage.setItem('authToken', response.data.access_token) - sessionStorage.setItem('authExpiresAt', response.data.expires_at) - sessionStorage.setItem('user', response.data) + const authData = response.data + + setToken(authData.access_token) + setExpiresAt(authData.expires_at) + setUserProfile(authData) + + // Update cookies with fresh data + const cookieSuccess = setAuthCookies(authData) + if (!cookieSuccess) { + messageApi.warning( + 'Failed to update login state. You may need to log in again if you close this tab.' + ) + } } else { setAuthenticated(false) setAuthError('Failed to authenticate user.') @@ -206,15 +291,13 @@ const AuthProvider = ({ children }) => { } finally { setLoading(false) } - }, [token]) + }, [token, messageApi]) const setUnauthenticated = () => { setToken(null) setExpiresAt(null) setUserProfile(null) - sessionStorage.removeItem('authToken') - sessionStorage.removeItem('authExpiresAt') - sessionStorage.removeItem('user') + clearAuthCookies() setShowUnauthorizedModal(true) } @@ -227,15 +310,23 @@ const AuthProvider = ({ children }) => { } }) if (response.status === 200 && response.data) { - setToken(response.data.access_token) - setExpiresAt(response.data.expires_at) - sessionStorage.setItem('authToken', response.data.access_token) - sessionStorage.setItem('authExpiresAt', response.data.expires_at) + const authData = response.data + + setToken(authData.access_token) + setExpiresAt(authData.expires_at) + + // Update cookies with fresh token data + const cookieSuccess = setAuthCookies(authData) + if (!cookieSuccess) { + messageApi.warning( + 'Failed to update login state. You may need to log in again if you close this tab.' + ) + } } } catch (error) { console.error('Token refresh failed', error) } - }, [token]) + }, [token, messageApi]) const handleSessionExpiredModalOk = () => { setShowSessionExpiredModal(false) @@ -315,6 +406,55 @@ const AuthProvider = ({ children }) => { notificationApi.destroy('token-expiration') } } + } else { + // Check cookies directly if expiresAt is not set in state + const expiryInfo = checkAuthCookiesExpiry(5) // Check if expiring within 5 minutes + if (expiryInfo.isExpiringSoon && expiryInfo.minutesRemaining <= 1) { + // Show notification for cookies expiring soon + const seconds = Math.floor((expiryInfo.timeRemaining % 60000) / 1000) + const totalSeconds = 60 + const remainingSeconds = totalSeconds - seconds + const progress = (remainingSeconds / totalSeconds) * 100 + + notificationApi.info({ + message: 'Session Expiring Soon', + description: ( +
+
+ Your session will expire in {seconds} seconds +
+ +
+ ), + duration: 0, + key: 'token-expiration', + icon: null, + placement: 'bottomRight', + style: { + width: 360 + }, + className: 'token-expiration-notification', + closeIcon: null, + onClose: () => {}, + btn: ( + + ) + }) + } } } @@ -335,12 +475,12 @@ const AuthProvider = ({ children }) => { getLoginToken(authCode) } else if ( token == null && - retreivedTokenFromSession == true && + retreivedTokenFromCookies == true && initialized == false && authCode == null ) { setInitialized(true) - console.log('Showing unauth') + console.log('Showing unauth', token, authCode) setShowUnauthorizedModal(true) setAuthenticated(false) } @@ -352,7 +492,7 @@ const AuthProvider = ({ children }) => { location.pathname, navigate, token, - retreivedTokenFromSession + retreivedTokenFromCookies ]) return ( diff --git a/src/routes/DeveloperRoutes.jsx b/src/routes/DeveloperRoutes.jsx index a1007a0..8797ac9 100644 --- a/src/routes/DeveloperRoutes.jsx +++ b/src/routes/DeveloperRoutes.jsx @@ -1,7 +1,7 @@ import { Route } from 'react-router-dom' import SessionStorage from '../components/Dashboard/Developer/SessionStorage.jsx' import AuthContextDebug from '../components/Dashboard/Developer/AuthContextDebug.jsx' -import PrintServerContextDebug from '../components/Dashboard/Developer/PrintServerContextDebug.jsx' +import ApiContextDebug from '../components/Dashboard/Developer/ApiContextDebug.jsx' const DeveloperRoutes = [ } />, } + key='apicontextdebug' + path='developer/apicontextdebug' + element={} /> ]