Enhance Authentication Handling for Slicer Integration
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Introduced separate session management for the slicer application, ensuring that slicer sessions do not interfere with standard authentication.
- Added utility functions for managing slicer authentication sessions, including retrieval, setting, and clearing of session data.
- Updated AuthProvider to handle slicer-specific authentication logic, improving user experience and session integrity.
- Enhanced ApiServerContext to manage socket connections based on the slicer authentication token, ensuring proper connection handling.
This commit is contained in:
Tom Butcher 2026-11-08 23:22:04 +00:00
parent 763bbf6f6e
commit 15f7a86c47
3 changed files with 124 additions and 4 deletions

View File

@ -717,12 +717,17 @@ const ApiServerProvider = ({ children }) => {
const connectToServer = useCallback(() => {
if (token && authenticated == true) {
if (socketRef.current) {
if (socketRef.current?.authToken === token) {
if (!socketRef.current.connected) {
socketRef.current.connect()
}
return
}
if (socketRef.current) {
const previousSocket = socketRef.current
socketRef.current = null
previousSocket.disconnect()
}
logger.debug('Token is available, connecting to api server...')
@ -738,6 +743,7 @@ const ApiServerProvider = ({ children }) => {
if (typeof newSocket.io?.reconnection === 'function') {
newSocket.io.reconnection(false)
}
newSocket.authToken = token
setConnecting(true)

View File

@ -30,6 +30,9 @@ import {
getAuthCookies,
setAuthCookies,
clearAuthCookies,
getSlicerAuthSession,
setSlicerAuthSession,
clearSlicerAuthSession,
areCookiesEnabled,
validateAuthCookies,
setupCookieSync,
@ -68,6 +71,30 @@ const AuthProvider = ({ children }) => {
const [notificationApi, notificationContextHolder] =
notification.useNotification()
const [initialWebAuth] = useState(() => {
// The slicer opens through a one-time app-password link. Never reuse a
// saved FarmControl login, which expires and replaces that session.
if (window.location.pathname === '/slicer') {
const hasAuthCode = new URLSearchParams(window.location.search).has(
'authCode'
)
const slicerSession = hasAuthCode ? null : getSlicerAuthSession()
if (slicerSession?.token) {
return {
retrieved: true,
token: slicerSession.token,
expiresAt: slicerSession.expiresAt,
user: slicerSession.user,
authenticated: true
}
}
return {
retrieved: true,
token: null,
expiresAt: null,
user: null,
authenticated: false
}
}
if (detectIsElectron()) {
return { retrieved: false }
}
@ -134,9 +161,13 @@ const AuthProvider = ({ children }) => {
})
sessionRef.current = { token, expiresAt, userProfile, authenticated }
const isDesktopHelperRoute = location.pathname.startsWith('/desktop')
const isSlicerRoute = location.pathname === '/slicer'
const slicerAppKey = getSlicerAppKey(location)
const suppressUnauthorizedModal =
isDesktopHelperRoute || Boolean(slicerAppKey) || slicerAuthPending
isDesktopHelperRoute ||
isSlicerRoute ||
Boolean(slicerAppKey) ||
slicerAuthPending
var redirectType = 'web'
@ -183,6 +214,13 @@ const AuthProvider = ({ children }) => {
const persistSession = useCallback(
async ({ token: nextToken, expiresAt: nextExpiresAt, user: nextUser }) => {
if (locationRef.current?.pathname === '/slicer') {
return setSlicerAuthSession({
access_token: nextToken,
expires_at: nextExpiresAt,
user: nextUser
})
}
if (isElectron) {
return await setAuthSession({
token: nextToken,
@ -200,6 +238,9 @@ const AuthProvider = ({ children }) => {
)
const clearPersistedSession = useCallback(async () => {
if (locationRef.current?.pathname === '/slicer') {
return clearSlicerAuthSession()
}
if (isElectron) {
return await clearAuthSession()
}
@ -283,13 +324,19 @@ const AuthProvider = ({ children }) => {
// An auth code represents a newly completed login and must take precedence
// over any existing persisted session.
if (
location.pathname === '/slicer' ||
location.pathname === '/applaunch' ||
location.pathname.startsWith('/desktop') ||
authCode != null
) {
if (location.pathname.startsWith('/desktop')) {
if (
location.pathname === '/slicer' ||
location.pathname.startsWith('/desktop')
) {
setRetreivedTokenFromCookies(true)
setInitialized(true)
if (location.pathname.startsWith('/desktop')) {
setInitialized(true)
}
}
return
}
@ -306,6 +353,7 @@ const AuthProvider = ({ children }) => {
useEffect(() => {
if (isElectron) return
const cleanupCookieSync = setupCookieSync(() => {
if (locationRef.current?.pathname === '/slicer') return
try {
if (validateAuthCookies()) {
const {
@ -658,6 +706,10 @@ const AuthProvider = ({ children }) => {
let intervalId
const tokenRefresh = () => {
if (locationRef.current?.pathname === '/slicer') {
notificationApi.destroy('token-expiration')
return
}
if (expiresAt) {
const now = new Date()
const expirationDate = new Date(expiresAt)
@ -878,6 +930,7 @@ const AuthProvider = ({ children }) => {
open={
showSessionExpiredModal &&
!isDesktopHelperRoute &&
!isSlicerRoute &&
!loading &&
!showAuthErrorModal
}

View File

@ -7,6 +7,7 @@ const COOKIE_OPTIONS = {
}
const AUTH_STORAGE_KEY = 'farmcontrol.auth'
const SLICER_AUTH_STORAGE_KEY = 'farmcontrol.slicerAuth'
const LEGACY_AUTH_COOKIE_NAMES = ['authToken', 'authExpiresAt', 'user']
const getAuthExpiryTime = (expiresAt) => {
@ -325,3 +326,63 @@ export const clearAuthCookies = () => {
}
clearLegacyAuthCookies()
}
const readStoredSession = (storageKey) => {
const raw = window.localStorage.getItem(storageKey)
if (!raw) return null
const session = parseStoredSession(raw)
const expirationTime = getAuthExpiryTime(session.expiresAt)
if (
!session.token ||
!Number.isFinite(expirationTime) ||
expirationTime <= Date.now()
) {
window.localStorage.removeItem(storageKey)
return null
}
return session
}
/**
* Slicer app-password sessions are stored apart from the normal login so a
* short-lived FarmControl session cannot replace or expire them.
*/
export const getSlicerAuthSession = () => {
try {
return readStoredSession(SLICER_AUTH_STORAGE_KEY)
} catch (error) {
console.error('Error reading slicer auth session:', error)
return null
}
}
export const setSlicerAuthSession = (authData) => {
try {
if (!authData?.access_token || !authData?.expires_at) {
console.warn('Auth data is required')
return false
}
const existing = getSlicerAuthSession()
const serialized = serializeAuthSession(authData, existing?.user)
if (window.localStorage.getItem(SLICER_AUTH_STORAGE_KEY) === serialized) {
return true
}
window.localStorage.setItem(SLICER_AUTH_STORAGE_KEY, serialized)
return true
} catch (error) {
console.error('Error setting slicer auth session:', error)
return false
}
}
export const clearSlicerAuthSession = () => {
try {
window.localStorage.removeItem(SLICER_AUTH_STORAGE_KEY)
return true
} catch (error) {
console.error('Error clearing slicer auth session:', error)
return false
}
}