Add ApiContextDebug component for API server debugging and testing functionality; removed PrintServerContextDebug component.

This commit is contained in:
Tom Butcher 2025-09-05 23:17:08 +01:00
parent fbdb451659
commit f0cb4b3b83
4 changed files with 1032 additions and 127 deletions

View File

@ -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: <ReloadIcon />
}
],
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 (
<Flex vertical gap='large' style={{ height: '100%', minHeight: 0 }}>
{contextHolder}
{/* Header with Actions */}
<Flex justify={'space-between'} align={'center'}>
<Space>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
</Space>
</Flex>
<Descriptions bordered>
<Descriptions.Item label='Status'>
<Space>
<Tag color={getConnectionStatusColor()} icon={<CloudIcon />}>
{connectionStatus.charAt(0).toUpperCase() +
connectionStatus.slice(1)}
</Tag>
</Space>
</Descriptions.Item>
<Descriptions.Item label='Connecting'>
<BoolDisplay value={connecting} />
</Descriptions.Item>
<Descriptions.Item label='Connected'>
<BoolDisplay value={connected} />
</Descriptions.Item>
<Descriptions.Item label='Socket ID'>
<Text code>{socketId || 'None'}</Text>
</Descriptions.Item>
<Descriptions.Item label='Fetch Loading'>
<BoolDisplay value={fetchLoading} />
</Descriptions.Item>
<Descriptions.Item label='Error'>
<Text type={error ? 'danger' : 'secondary'}>{error || 'None'}</Text>
</Descriptions.Item>
</Descriptions>
<InfoCollapse
title='Params'
icon={<span>🔎</span>}
active={collapseStates.params}
onToggle={() => toggleCollapse('params')}
collapseKey='params'
>
<Descriptions bordered column={2}>
<Descriptions.Item label='Object ID'>
<Input
value={testInputs.objectId}
onChange={(e) => updateTestInput('objectId', e.target.value)}
placeholder='Enter object ID'
size='small'
/>
</Descriptions.Item>
<Descriptions.Item label='Object Type'>
<Select
value={testInputs.objectType}
onChange={(value) => updateTestInput('objectType', value)}
style={{ width: '100%' }}
options={[
{ value: 'user', label: 'User' },
{ value: 'printer', label: 'Printer' },
{ value: 'job', label: 'Job' },
{ value: 'filament', label: 'Filament' },
{ value: 'gcodefile', label: 'GCode File' }
]}
/>
</Descriptions.Item>
<Descriptions.Item label='Host ID'>
<Input
value={testInputs.hostId}
onChange={(e) => updateTestInput('hostId', e.target.value)}
placeholder='Enter host ID'
/>
</Descriptions.Item>
<Descriptions.Item label='Parent ID'>
<Input
value={testInputs.parentId}
onChange={(e) => updateTestInput('parentId', e.target.value)}
placeholder='Enter parent ID'
/>
</Descriptions.Item>
<Descriptions.Item label='File Name'>
<Input
value={testInputs.fileName}
onChange={(e) => updateTestInput('fileName', e.target.value)}
placeholder='Enter file name'
/>
</Descriptions.Item>
<Descriptions.Item label='Query'>
<Input
value={testInputs.query}
onChange={(e) => updateTestInput('query', e.target.value)}
placeholder='Enter search query'
/>
</Descriptions.Item>
<Descriptions.Item label='Action'>
<Select
value={testInputs.action}
onChange={(value) => updateTestInput('action', value)}
style={{ width: '100%' }}
options={[
{ value: 'start', label: 'Start' },
{ value: 'stop', label: 'Stop' },
{ value: 'pause', label: 'Pause' },
{ value: 'resume', label: 'Resume' }
]}
/>
</Descriptions.Item>
<Descriptions.Item label='Event Type'>
<Select
value={testInputs.eventType}
onChange={(value) => updateTestInput('eventType', value)}
style={{ width: '100%' }}
options={[
{ value: 'status', label: 'Status' },
{ value: 'progress', label: 'Progress' },
{ value: 'error', label: 'Error' },
{ value: 'complete', label: 'Complete' }
]}
/>
</Descriptions.Item>
<Descriptions.Item label='Scale'>
<InputNumber
value={testInputs.scale}
onChange={(value) => updateTestInput('scale', value)}
min={0.1}
max={10}
step={0.1}
style={{ width: '100%' }}
/>
</Descriptions.Item>
<Descriptions.Item label='Template Content' span={1}>
<Input.TextArea
value={testInputs.templateContent}
onChange={(e) =>
updateTestInput('templateContent', e.target.value)
}
placeholder='Enter template content'
rows={2}
/>
</Descriptions.Item>
</Descriptions>
</InfoCollapse>
{/* Test Sections */}
<div style={{ height: '100%', overflow: 'auto' }}>
<Flex vertical gap={'large'}>
{/* Fetch Tests */}
<InfoCollapse
title='Fetch Tests'
icon={<span>📥</span>}
active={collapseStates.fetchTests}
onToggle={() => toggleCollapse('fetchTests')}
collapseKey='fetchTests'
>
<Space direction='vertical' style={{ width: '100%' }}>
<Button onClick={testFetchObject} block>
Test fetchObject
</Button>
<Button onClick={testFetchObjects} block>
Test fetchObjects
</Button>
<Button onClick={testFetchObjectLock} block>
Test fetchObjectLock
</Button>
<Button onClick={testFetchObjectsByProperty} block>
Test fetchObjectsByProperty
</Button>
<Button onClick={testFetchSpotlightData} block>
Test fetchSpotlightData
</Button>
<Button onClick={testFetchObjectContent} block>
Test fetchObjectContent
</Button>
<Button onClick={testFetchNotes} block>
Test fetchNotes
</Button>
</Space>
</InfoCollapse>
{/* CRUD Tests */}
<InfoCollapse
title='CRUD Tests'
icon={<span></span>}
active={collapseStates.crudTests}
onToggle={() => toggleCollapse('crudTests')}
collapseKey='crudTests'
>
<Space direction='vertical' style={{ width: '100%' }}>
<Button onClick={testLockObject} block>
Test Lock Object
</Button>
<Button onClick={testUnlockObject} block>
Test Unlock Object
</Button>
<Button onClick={testUpdateObject} block>
Test updateObject
</Button>
<Button onClick={testCreateObject} block>
Test createObject
</Button>
<Button onClick={testDeleteObject} block>
Test deleteObject
</Button>
</Space>
</InfoCollapse>
{/* Special Tests */}
<InfoCollapse
title='Special Tests'
icon={<span>🔧</span>}
active={collapseStates.specialTests}
onToggle={() => toggleCollapse('specialTests')}
collapseKey='specialTests'
>
<Space direction='vertical' style={{ width: '100%' }}>
<Button onClick={testFetchTemplatePreview} block>
Test fetchTemplatePreview
</Button>
<Button onClick={testFetchHostOTP} block>
Test fetchHostOTP
</Button>
<Button onClick={testSendObjectAction} block>
Test sendObjectAction
</Button>
</Space>
</InfoCollapse>
{/* Subscription Tests */}
<InfoCollapse
title='Subscription Tests'
icon={<span>📡</span>}
active={collapseStates.subscriptionTests}
onToggle={() => toggleCollapse('subscriptionTests')}
collapseKey='subscriptionTests'
>
<Space direction='vertical' style={{ width: '100%' }}>
<Button onClick={testSubscribeToObjectUpdates} block>
Test Subscribe Object Updates
</Button>
<Button onClick={testSubscribeToObjectEvent} block>
Test Subscribe Object Events
</Button>
<Button onClick={testSubscribeToObjectTypeUpdates} block>
Test Subscribe Object Type Updates
</Button>
<Button onClick={testSubscribeToObjectLock} block>
Test Subscribe Object Lock
</Button>
</Space>
</InfoCollapse>
{/* API Server Instance Info */}
<InfoCollapse
title='API Server Instance'
icon={<span>🖥</span>}
active={false}
onToggle={() => {}}
collapseKey='apiServer'
>
<pre style={{ margin: 0, fontSize: 12 }}>
{apiServer ? (
<Paragraph>
<pre>
{JSON.stringify(
{
connected: apiServer.connected,
id: apiServer.id,
transport: apiServer.io.engine.transport.name,
readyState: apiServer.io.engine.readyState
},
null,
2
)}
</pre>
</Paragraph>
) : (
<Text type='secondary'>No API server instance</Text>
)}
</pre>
</InfoCollapse>
</Flex>
</div>
</Flex>
)
}
export default ApiContextDebug

View File

@ -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: <ReloadIcon />
}
],
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 (
<Flex vertical gap='large' style={{ height: '100%', minHeight: 0 }}>
{contextHolder}
<Flex justify={'space-between'} align={'center'}>
<Space>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
</Space>
</Flex>
<div style={{ height: '100%', overflow: 'auto' }}>
<Descriptions bordered column={1}>
<Descriptions.Item label='Connected'>
<BoolDisplay value={printServer?.connected || false} />
</Descriptions.Item>
<Descriptions.Item label='Connecting'>
<BoolDisplay value={connecting} />
</Descriptions.Item>
<Descriptions.Item label='Error'>
{error ? <Text type='danger'>{error}</Text> : <Text>n/a</Text>}
</Descriptions.Item>
<Descriptions.Item label='Socket'>
<Paragraph>
<pre>{getSocketInfo()}</pre>
</Paragraph>
</Descriptions.Item>
</Descriptions>
</div>
</Flex>
)
}
export default PrintServerContextDebug

View File

@ -24,6 +24,16 @@ import config from '../../../config'
import loglevel from 'loglevel' import loglevel from 'loglevel'
import { ElectronContext } from './ElectronContext' import { ElectronContext } from './ElectronContext'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import {
getAuthCookies,
setAuthCookies,
clearAuthCookies,
areCookiesEnabled,
validateAuthCookies,
setupCookieSync,
checkAuthCookiesExpiry
} from '../../../utils/cookies'
const logger = loglevel.getLogger('ApiServerContext') const logger = loglevel.getLogger('ApiServerContext')
logger.setLevel(config.logLevel) logger.setLevel(config.logLevel)
@ -37,7 +47,7 @@ const AuthProvider = ({ children }) => {
notification.useNotification() notification.useNotification()
const [authenticated, setAuthenticated] = useState(false) const [authenticated, setAuthenticated] = useState(false)
const [initialized, setInitialized] = useState(false) const [initialized, setInitialized] = useState(false)
const [retreivedTokenFromSession, setRetreivedTokenFromSession] = const [retreivedTokenFromCookies, setRetreivedTokenFromCookies] =
useState(false) useState(false)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [token, setToken] = useState(null) const [token, setToken] = useState(null)
@ -60,13 +70,32 @@ const AuthProvider = ({ children }) => {
if (isElectron == true && import.meta.env.MODE != 'development') { if (isElectron == true && import.meta.env.MODE != 'development') {
redirectType = 'app-scheme' redirectType = 'app-scheme'
} }
// Read token from session storage if present
// Check if cookies are enabled and show warning if not
useEffect(() => { useEffect(() => {
const storedToken = sessionStorage.getItem('authToken') if (!areCookiesEnabled()) {
const storedUser = JSON.parse(sessionStorage.getItem('user')) messageApi.warning(
const storedExpiresAt = sessionStorage.getItem('authExpiresAt') 'Cookies are disabled. Login state may not persist between tabs.'
console.log('stored user', storedUser, storedToken) )
if (storedToken && storedExpiresAt && storedUser) { }
}, [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) setToken(storedToken)
setUserProfile(storedUser) setUserProfile(storedUser)
setExpiresAt(storedExpiresAt) setExpiresAt(storedExpiresAt)
@ -76,17 +105,63 @@ const AuthProvider = ({ children }) => {
setUserProfile(null) setUserProfile(null)
setShowUnauthorizedModal(true) setShowUnauthorizedModal(true)
} }
setRetreivedTokenFromSession(true) } catch (error) {
console.error('Error reading auth cookies:', error)
clearAuthCookies()
setAuthenticated(false)
setUserProfile(null)
setShowUnauthorizedModal(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') => { const logout = useCallback((redirectUri = '/login') => {
setAuthenticated(false) setAuthenticated(false)
setToken(null) setToken(null)
setExpiresAt(null) setExpiresAt(null)
setUserProfile(null) setUserProfile(null)
sessionStorage.removeItem('authToken') clearAuthCookies()
sessionStorage.removeItem('authExpiresAt')
sessionStorage.removeItem('user')
window.location.href = `${config.backendUrl}/auth/logout?redirect_uri=${encodeURIComponent(redirectUri)}` window.location.href = `${config.backendUrl}/auth/logout?redirect_uri=${encodeURIComponent(redirectUri)}`
}, []) }, [])
@ -128,18 +203,20 @@ const AuthProvider = ({ children }) => {
if (response.status === 200 && response.data) { if (response.status === 200 && response.data) {
logger.debug('Got auth token!') logger.debug('Got auth token!')
setToken(response.data.access_token) const authData = response.data
setExpiresAt(response.data.expires_at)
setUserProfile(response.data) setToken(authData.access_token)
sessionStorage.setItem('authToken', response.data.access_token) setExpiresAt(authData.expires_at)
sessionStorage.setItem('authExpiresAt', response.data.expires_at) setUserProfile(authData)
const userObject = {
...response.data, // Store in cookies for persistence between tabs
access_token: undefined, const cookieSuccess = setAuthCookies(authData)
refresh_token: undefined, if (!cookieSuccess) {
id_token: undefined 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) const searchParams = new URLSearchParams(location.search)
searchParams.delete('authCode') searchParams.delete('authCode')
const newSearch = searchParams.toString() const newSearch = searchParams.toString()
@ -167,8 +244,9 @@ const AuthProvider = ({ children }) => {
setLoading(false) setLoading(false)
} }
}, },
[isElectron] [isElectron, navigate, location.search, location.pathname, messageApi]
) )
// Function to check if the user is logged in // Function to check if the user is logged in
const checkAuthStatus = useCallback(async () => { const checkAuthStatus = useCallback(async () => {
setLoading(true) setLoading(true)
@ -183,12 +261,19 @@ const AuthProvider = ({ children }) => {
if (response.status === 200 && response.data) { if (response.status === 200 && response.data) {
logger.debug('Got auth token!') logger.debug('Got auth token!')
setToken(response.data.access_token) const authData = response.data
setExpiresAt(response.data.expires_at)
setUserProfile(response.data) setToken(authData.access_token)
sessionStorage.setItem('authToken', response.data.access_token) setExpiresAt(authData.expires_at)
sessionStorage.setItem('authExpiresAt', response.data.expires_at) setUserProfile(authData)
sessionStorage.setItem('user', response.data)
// 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 { } else {
setAuthenticated(false) setAuthenticated(false)
setAuthError('Failed to authenticate user.') setAuthError('Failed to authenticate user.')
@ -206,15 +291,13 @@ const AuthProvider = ({ children }) => {
} finally { } finally {
setLoading(false) setLoading(false)
} }
}, [token]) }, [token, messageApi])
const setUnauthenticated = () => { const setUnauthenticated = () => {
setToken(null) setToken(null)
setExpiresAt(null) setExpiresAt(null)
setUserProfile(null) setUserProfile(null)
sessionStorage.removeItem('authToken') clearAuthCookies()
sessionStorage.removeItem('authExpiresAt')
sessionStorage.removeItem('user')
setShowUnauthorizedModal(true) setShowUnauthorizedModal(true)
} }
@ -227,15 +310,23 @@ const AuthProvider = ({ children }) => {
} }
}) })
if (response.status === 200 && response.data) { if (response.status === 200 && response.data) {
setToken(response.data.access_token) const authData = response.data
setExpiresAt(response.data.expires_at)
sessionStorage.setItem('authToken', response.data.access_token) setToken(authData.access_token)
sessionStorage.setItem('authExpiresAt', response.data.expires_at) 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) { } catch (error) {
console.error('Token refresh failed', error) console.error('Token refresh failed', error)
} }
}, [token]) }, [token, messageApi])
const handleSessionExpiredModalOk = () => { const handleSessionExpiredModalOk = () => {
setShowSessionExpiredModal(false) setShowSessionExpiredModal(false)
@ -315,6 +406,55 @@ const AuthProvider = ({ children }) => {
notificationApi.destroy('token-expiration') 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: (
<div>
<div style={{ marginBottom: 8 }}>
Your session will expire in {seconds} seconds
</div>
<Progress
percent={progress}
size='small'
status='active'
showInfo={false}
/>
</div>
),
duration: 0,
key: 'token-expiration',
icon: null,
placement: 'bottomRight',
style: {
width: 360
},
className: 'token-expiration-notification',
closeIcon: null,
onClose: () => {},
btn: (
<Button
type='primary'
size='small'
onClick={() => {
notificationApi.destroy('token-expiration')
refreshToken()
}}
>
Reload Session
</Button>
)
})
}
} }
} }
@ -335,12 +475,12 @@ const AuthProvider = ({ children }) => {
getLoginToken(authCode) getLoginToken(authCode)
} else if ( } else if (
token == null && token == null &&
retreivedTokenFromSession == true && retreivedTokenFromCookies == true &&
initialized == false && initialized == false &&
authCode == null authCode == null
) { ) {
setInitialized(true) setInitialized(true)
console.log('Showing unauth') console.log('Showing unauth', token, authCode)
setShowUnauthorizedModal(true) setShowUnauthorizedModal(true)
setAuthenticated(false) setAuthenticated(false)
} }
@ -352,7 +492,7 @@ const AuthProvider = ({ children }) => {
location.pathname, location.pathname,
navigate, navigate,
token, token,
retreivedTokenFromSession retreivedTokenFromCookies
]) ])
return ( return (

View File

@ -1,7 +1,7 @@
import { Route } from 'react-router-dom' import { Route } from 'react-router-dom'
import SessionStorage from '../components/Dashboard/Developer/SessionStorage.jsx' import SessionStorage from '../components/Dashboard/Developer/SessionStorage.jsx'
import AuthContextDebug from '../components/Dashboard/Developer/AuthContextDebug.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 = [ const DeveloperRoutes = [
<Route <Route
@ -15,9 +15,9 @@ const DeveloperRoutes = [
element={<AuthContextDebug />} element={<AuthContextDebug />}
/>, />,
<Route <Route
key='printservercontextdebug' key='apicontextdebug'
path='developer/printservercontextdebug' path='developer/apicontextdebug'
element={<PrintServerContextDebug />} element={<ApiContextDebug />}
/> />
] ]