Added big fixes allowing dynamic obtaining of the token.

This commit is contained in:
Tom Butcher 2025-07-20 22:50:40 +01:00
parent a20235a953
commit 08311a4a94
7 changed files with 80 additions and 91 deletions

View File

@ -66,7 +66,7 @@
"eject": "react-scripts eject", "eject": "react-scripts eject",
"minify-svgs": "node scripts/minify-svgs.js", "minify-svgs": "node scripts/minify-svgs.js",
"dev:electron": "concurrently \"react-scripts start\" \"ELECTRON_START_URL=http://192.168.68.53:3000 electron src/electron/main.js\"", "dev:electron": "concurrently \"react-scripts start\" \"ELECTRON_START_URL=http://192.168.68.53:3000 electron public/electron.js\"",
"build:electron": "npm run build && electron-builder" "build:electron": "npm run build && electron-builder"
}, },
"eslintConfig": { "eslintConfig": {

View File

@ -68,6 +68,10 @@
line-height: 32.5px; line-height: 32.5px;
} }
.loading-modal .ant-modal-footer {
display: none;
}
:root { :root {
--unit-100vh: 100vh; --unit-100vh: 100vh;

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState, useCallback } from 'react' import React, { useEffect, useState, useCallback, useContext } from 'react'
import { import {
Descriptions, Descriptions,
Space, Space,
@ -21,10 +21,12 @@ import ReloadIcon from '../../Icons/ReloadIcon'
import useCollapseState from '../hooks/useCollapseState' import useCollapseState from '../hooks/useCollapseState'
import config from '../../../config' import config from '../../../config'
import { AuthContext } from '../context/AuthContext'
const { Title, Text } = Typography const { Title, Text } = Typography
const ProductionOverview = () => { const ProductionOverview = () => {
const { token } = useContext(AuthContext)
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
const [error, setError] = useState(null) const [error, setError] = useState(null)
const [fetchPrinterStatsLoading, setFetchPrinterStatsLoading] = useState(true) const [fetchPrinterStatsLoading, setFetchPrinterStatsLoading] = useState(true)
@ -68,9 +70,9 @@ const ProductionOverview = () => {
setFetchPrinterStatsLoading(true) setFetchPrinterStatsLoading(true)
const response = await axios.get(`${config.backendUrl}/printers/stats`, { const response = await axios.get(`${config.backendUrl}/printers/stats`, {
headers: { headers: {
Accept: 'application/json' Accept: 'application/json',
}, Authorization: `Bearer ${token}`
withCredentials: true }
}) })
const printStats = response.data const printStats = response.data
setStats((prev) => ({ ...prev, printers: printStats })) setStats((prev) => ({ ...prev, printers: printStats }))
@ -88,9 +90,9 @@ const ProductionOverview = () => {
setFetchPrinterStatsLoading(true) setFetchPrinterStatsLoading(true)
const response = await axios.get(`${config.backendUrl}/jobs/stats`, { const response = await axios.get(`${config.backendUrl}/jobs/stats`, {
headers: { headers: {
Accept: 'application/json' Accept: 'application/json',
}, Authorization: `Bearer ${token}`
withCredentials: true }
}) })
const jobstats = response.data const jobstats = response.data
setStats((prev) => ({ ...prev, jobs: jobstats })) setStats((prev) => ({ ...prev, jobs: jobstats }))
@ -107,9 +109,9 @@ const ProductionOverview = () => {
try { try {
const response = await axios.get(`${config.backendUrl}/stats/history`, { const response = await axios.get(`${config.backendUrl}/stats/history`, {
headers: { headers: {
Accept: 'application/json' Accept: 'application/json',
}, Authorization: `Bearer ${token}`
withCredentials: true }
}) })
setChartData(response.data) setChartData(response.data)
} catch (err) { } catch (err) {
@ -118,8 +120,10 @@ const ProductionOverview = () => {
} }
useEffect(() => { useEffect(() => {
if (token != null) {
fetchAllStats() fetchAllStats()
}, [fetchAllStats]) }
}, [fetchAllStats, token])
if (fetchPrinterStatsLoading || fetchPrinterStatsLoading) { if (fetchPrinterStatsLoading || fetchPrinterStatsLoading) {
return ( return (

View File

@ -402,9 +402,6 @@ const ApiServerProvider = ({ children }) => {
sorter = {}, sorter = {},
onDataChange onDataChange
} = params } = params
if (token == null) {
return []
}
logger.debug('Fetching table data from:', type, { logger.debug('Fetching table data from:', type, {
page, page,
limit, limit,
@ -456,9 +453,6 @@ const ApiServerProvider = ({ children }) => {
// Fetch table data with pagination, filtering, and sorting // Fetch table data with pagination, filtering, and sorting
const fetchObjectsByProperty = async (type, params = {}) => { const fetchObjectsByProperty = async (type, params = {}) => {
if (token == null) {
return []
}
const { filter = {}, properties = [] } = params const { filter = {}, properties = [] } = params
logger.debug('Fetching property object data from:', type, { logger.debug('Fetching property object data from:', type, {
@ -571,9 +565,6 @@ const ApiServerProvider = ({ children }) => {
// Download GCode file content // Download GCode file content
const fetchObjectContent = async (id, type, fileName) => { const fetchObjectContent = async (id, type, fileName) => {
if (!token) {
return
}
try { try {
const response = await axios.get( const response = await axios.get(
`${config.backendUrl}/${type.toLowerCase()}s/${id}/content`, `${config.backendUrl}/${type.toLowerCase()}s/${id}/content`,

View File

@ -7,7 +7,16 @@ import React, {
useContext useContext
} from 'react' } from 'react'
import axios from 'axios' import axios from 'axios'
import { message, Modal, notification, Progress, Button, Space } from 'antd' import {
message,
Modal,
notification,
Progress,
Button,
Space,
Typography
} from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import ExclamationOctogonIcon from '../../Icons/ExclamationOctagonIcon' import ExclamationOctogonIcon from '../../Icons/ExclamationOctagonIcon'
import InfoCircleIcon from '../../Icons/InfoCircleIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon'
@ -21,12 +30,16 @@ logger.setLevel(config.logLevel)
const AuthContext = createContext() const AuthContext = createContext()
const Title = Typography
const AuthProvider = ({ children }) => { const AuthProvider = ({ children }) => {
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
const [notificationApi, notificationContextHolder] = const [notificationApi, notificationContextHolder] =
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] =
useState(false)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [token, setToken] = useState(null) const [token, setToken] = useState(null)
const [expiresAt, setExpiresAt] = useState(null) const [expiresAt, setExpiresAt] = useState(null)
@ -46,7 +59,11 @@ const AuthProvider = ({ children }) => {
setToken(storedToken) setToken(storedToken)
setExpiresAt(storedExpiresAt) setExpiresAt(storedExpiresAt)
setAuthenticated(true) setAuthenticated(true)
} else {
setAuthenticated(false)
setShowUnauthorizedModal(true)
} }
setRetreivedTokenFromSession(true)
}, []) }, [])
const logout = useCallback((redirectUri = '/login') => { const logout = useCallback((redirectUri = '/login') => {
@ -61,18 +78,19 @@ const AuthProvider = ({ children }) => {
// Login using query parameters // Login using query parameters
const loginWithSSO = useCallback( const loginWithSSO = useCallback(
(redirectUri = window.location.pathname + window.location.search) => { (redirectUri = location.pathname + location.search) => {
messageApi.info('Logging in with tombutcher.work') messageApi.info('Logging in with tombutcher.work')
const loginUrl = `${config.backendUrl}/auth/${isElectron ? 'app/' : ''}login?redirect_uri=${encodeURIComponent(redirectUri)}` const loginUrl = `${config.backendUrl}/auth/${isElectron ? 'app/' : ''}login?redirect_uri=${encodeURIComponent(redirectUri)}`
if (isElectron) { if (isElectron) {
console.log('Opening external url...') console.log('Opening external url...')
openExternalUrl(loginUrl) openExternalUrl(loginUrl)
setLoading(true)
} else { } else {
console.log('Redirecting...') console.log('Redirecting...')
window.location.href = loginUrl window.location.href = loginUrl
} }
}, },
[messageApi, openExternalUrl, isElectron] [messageApi, openExternalUrl, isElectron, location.search]
) )
const getLoginToken = useCallback( const getLoginToken = useCallback(
@ -94,6 +112,11 @@ const AuthProvider = ({ children }) => {
setUserProfile(response.data) setUserProfile(response.data)
sessionStorage.setItem('authToken', response.data.access_token) sessionStorage.setItem('authToken', response.data.access_token)
sessionStorage.setItem('authExpiresAt', response.data.expires_at) sessionStorage.setItem('authExpiresAt', response.data.expires_at)
const searchParams = new URLSearchParams(location.search)
searchParams.delete('authCode')
const newSearch = searchParams.toString()
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
navigate(newPath, { replace: true })
} else { } else {
setAuthenticated(false) setAuthenticated(false)
setAuthError('Failed to authenticate user.') setAuthError('Failed to authenticate user.')
@ -261,24 +284,21 @@ const AuthProvider = ({ children }) => {
}, [expiresAt, authenticated, notificationApi, refreshToken]) }, [expiresAt, authenticated, notificationApi, refreshToken])
useEffect(() => { useEffect(() => {
if (initialized == false) {
const authCode = const authCode =
new URLSearchParams(location.search).get('authCode') || null new URLSearchParams(location.search).get('authCode') || null
if (authCode != null) { if (authCode != null) {
getLoginToken(authCode) getLoginToken(authCode)
const searchParams = new URLSearchParams(location.search) } else if (
if (searchParams.has('authCode')) { token == null &&
searchParams.delete('authCode') retreivedTokenFromSession == true &&
const newSearch = searchParams.toString() initialized == false &&
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '') authCode == null
navigate(newPath, { replace: true }) ) {
} setInitialized(true)
} else if (token == null) { console.log('Showing unauth')
setShowUnauthorizedModal(true) setShowUnauthorizedModal(true)
setAuthenticated(false) setAuthenticated(false)
} }
setInitialized(true)
}
}, [ }, [
checkAuthStatus, checkAuthStatus,
location.search, location.search,
@ -286,7 +306,8 @@ const AuthProvider = ({ children }) => {
initialized, initialized,
location.pathname, location.pathname,
navigate, navigate,
token token,
retreivedTokenFromSession
]) ])
if (authError) { if (authError) {
@ -370,18 +391,22 @@ const AuthProvider = ({ children }) => {
tombutcher.work to continue. tombutcher.work to continue.
</Modal> </Modal>
<Modal <Modal
title={
<Space size={'middle'}>
<ExclamationOctogonIcon />
Loading...
</Space>
}
open={loading} open={loading}
style={{ maxWidth: 200, top: '50%', transform: 'translateY(-50%)' }} className={'loading-modal'}
title={false}
height={20}
style={{ maxWidth: 220, top: '50%', transform: 'translateY(-50%)' }}
closable={false} closable={false}
maskClosable={false} maskClosable={false}
footer={false} footer={false}
/> >
<Space size={'middle'}>
<LoadingOutlined />
<Title level={5} style={{ margin: 0 }}>
Loading, please wait...
</Title>
</Space>
</Modal>
</> </>
) )
} }

View File

@ -1,45 +1,16 @@
// PrivateRoute.js // PrivateRoute.js
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React, { useContext, useState, useEffect } from 'react' import React, { useContext } from 'react'
import { AuthContext } from './Dashboard/context/AuthContext' import { AuthContext } from './Dashboard/context/AuthContext'
import AuthLoading from './App/AppLoading'
import { useThemeContext } from './Dashboard/context/ThemeContext'
const PrivateRoute = ({ component: Component }) => { const PrivateRoute = ({ component: Component }) => {
const { isDarkMode } = useThemeContext() const { authenticated, showSessionExpiredModal } = useContext(AuthContext)
const { authenticated, loading, showSessionExpiredModal } =
useContext(AuthContext)
const [fadeIn, setFadeIn] = useState(false)
useEffect(() => {
if (!loading) {
// Small delay to ensure smooth transition
const timer = setTimeout(() => setFadeIn(true), 50)
return () => clearTimeout(timer)
}
}, [loading])
// Show loading state while auth state is being determined
if (loading) {
return <AuthLoading />
}
// Redirect to login if not authenticated // Redirect to login if not authenticated
return ( return (
<div style={{ background: isDarkMode ? '#000000' : '#ffffff' }}> <>
<div {authenticated || showSessionExpiredModal ? <Component /> : <Component />}
style={{ </>
opacity: fadeIn ? 1 : 0,
transition: 'opacity 0.3s ease-in-out'
}}
>
{authenticated || showSessionExpiredModal ? (
<Component />
) : (
<Component />
)}
</div>
</div>
) )
} }

View File

@ -3,15 +3,9 @@ import PropTypes from 'prop-types'
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { Navigate } from 'react-router-dom' import { Navigate } from 'react-router-dom'
import { AuthContext } from './Dashboard/context/AuthContext' import { AuthContext } from './Dashboard/context/AuthContext'
import AuthLoading from './App/AppLoading'
const PublicRoute = ({ component: Component }) => { const PublicRoute = ({ component: Component }) => {
const { authenticated, loading } = useContext(AuthContext) const { authenticated } = useContext(AuthContext)
// Show loading state while auth state is being determined
if (loading) {
return <AuthLoading />
}
// Redirect to login if not authenticated // Redirect to login if not authenticated
return !authenticated ? ( return !authenticated ? (