Enhance Authentication Handling for Slicer Integration
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
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:
parent
763bbf6f6e
commit
15f7a86c47
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user