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(() => {
|
const connectToServer = useCallback(() => {
|
||||||
if (token && authenticated == true) {
|
if (token && authenticated == true) {
|
||||||
if (socketRef.current) {
|
if (socketRef.current?.authToken === token) {
|
||||||
if (!socketRef.current.connected) {
|
if (!socketRef.current.connected) {
|
||||||
socketRef.current.connect()
|
socketRef.current.connect()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (socketRef.current) {
|
||||||
|
const previousSocket = socketRef.current
|
||||||
|
socketRef.current = null
|
||||||
|
previousSocket.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug('Token is available, connecting to api server...')
|
logger.debug('Token is available, connecting to api server...')
|
||||||
|
|
||||||
@ -738,6 +743,7 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
if (typeof newSocket.io?.reconnection === 'function') {
|
if (typeof newSocket.io?.reconnection === 'function') {
|
||||||
newSocket.io.reconnection(false)
|
newSocket.io.reconnection(false)
|
||||||
}
|
}
|
||||||
|
newSocket.authToken = token
|
||||||
|
|
||||||
setConnecting(true)
|
setConnecting(true)
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,9 @@ import {
|
|||||||
getAuthCookies,
|
getAuthCookies,
|
||||||
setAuthCookies,
|
setAuthCookies,
|
||||||
clearAuthCookies,
|
clearAuthCookies,
|
||||||
|
getSlicerAuthSession,
|
||||||
|
setSlicerAuthSession,
|
||||||
|
clearSlicerAuthSession,
|
||||||
areCookiesEnabled,
|
areCookiesEnabled,
|
||||||
validateAuthCookies,
|
validateAuthCookies,
|
||||||
setupCookieSync,
|
setupCookieSync,
|
||||||
@ -68,6 +71,30 @@ const AuthProvider = ({ children }) => {
|
|||||||
const [notificationApi, notificationContextHolder] =
|
const [notificationApi, notificationContextHolder] =
|
||||||
notification.useNotification()
|
notification.useNotification()
|
||||||
const [initialWebAuth] = useState(() => {
|
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()) {
|
if (detectIsElectron()) {
|
||||||
return { retrieved: false }
|
return { retrieved: false }
|
||||||
}
|
}
|
||||||
@ -134,9 +161,13 @@ const AuthProvider = ({ children }) => {
|
|||||||
})
|
})
|
||||||
sessionRef.current = { token, expiresAt, userProfile, authenticated }
|
sessionRef.current = { token, expiresAt, userProfile, authenticated }
|
||||||
const isDesktopHelperRoute = location.pathname.startsWith('/desktop')
|
const isDesktopHelperRoute = location.pathname.startsWith('/desktop')
|
||||||
|
const isSlicerRoute = location.pathname === '/slicer'
|
||||||
const slicerAppKey = getSlicerAppKey(location)
|
const slicerAppKey = getSlicerAppKey(location)
|
||||||
const suppressUnauthorizedModal =
|
const suppressUnauthorizedModal =
|
||||||
isDesktopHelperRoute || Boolean(slicerAppKey) || slicerAuthPending
|
isDesktopHelperRoute ||
|
||||||
|
isSlicerRoute ||
|
||||||
|
Boolean(slicerAppKey) ||
|
||||||
|
slicerAuthPending
|
||||||
|
|
||||||
var redirectType = 'web'
|
var redirectType = 'web'
|
||||||
|
|
||||||
@ -183,6 +214,13 @@ const AuthProvider = ({ children }) => {
|
|||||||
|
|
||||||
const persistSession = useCallback(
|
const persistSession = useCallback(
|
||||||
async ({ token: nextToken, expiresAt: nextExpiresAt, user: nextUser }) => {
|
async ({ token: nextToken, expiresAt: nextExpiresAt, user: nextUser }) => {
|
||||||
|
if (locationRef.current?.pathname === '/slicer') {
|
||||||
|
return setSlicerAuthSession({
|
||||||
|
access_token: nextToken,
|
||||||
|
expires_at: nextExpiresAt,
|
||||||
|
user: nextUser
|
||||||
|
})
|
||||||
|
}
|
||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
return await setAuthSession({
|
return await setAuthSession({
|
||||||
token: nextToken,
|
token: nextToken,
|
||||||
@ -200,6 +238,9 @@ const AuthProvider = ({ children }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const clearPersistedSession = useCallback(async () => {
|
const clearPersistedSession = useCallback(async () => {
|
||||||
|
if (locationRef.current?.pathname === '/slicer') {
|
||||||
|
return clearSlicerAuthSession()
|
||||||
|
}
|
||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
return await clearAuthSession()
|
return await clearAuthSession()
|
||||||
}
|
}
|
||||||
@ -283,13 +324,19 @@ const AuthProvider = ({ children }) => {
|
|||||||
// An auth code represents a newly completed login and must take precedence
|
// An auth code represents a newly completed login and must take precedence
|
||||||
// over any existing persisted session.
|
// over any existing persisted session.
|
||||||
if (
|
if (
|
||||||
|
location.pathname === '/slicer' ||
|
||||||
location.pathname === '/applaunch' ||
|
location.pathname === '/applaunch' ||
|
||||||
location.pathname.startsWith('/desktop') ||
|
location.pathname.startsWith('/desktop') ||
|
||||||
authCode != null
|
authCode != null
|
||||||
) {
|
) {
|
||||||
if (location.pathname.startsWith('/desktop')) {
|
if (
|
||||||
|
location.pathname === '/slicer' ||
|
||||||
|
location.pathname.startsWith('/desktop')
|
||||||
|
) {
|
||||||
setRetreivedTokenFromCookies(true)
|
setRetreivedTokenFromCookies(true)
|
||||||
setInitialized(true)
|
if (location.pathname.startsWith('/desktop')) {
|
||||||
|
setInitialized(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -306,6 +353,7 @@ const AuthProvider = ({ children }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isElectron) return
|
if (isElectron) return
|
||||||
const cleanupCookieSync = setupCookieSync(() => {
|
const cleanupCookieSync = setupCookieSync(() => {
|
||||||
|
if (locationRef.current?.pathname === '/slicer') return
|
||||||
try {
|
try {
|
||||||
if (validateAuthCookies()) {
|
if (validateAuthCookies()) {
|
||||||
const {
|
const {
|
||||||
@ -658,6 +706,10 @@ const AuthProvider = ({ children }) => {
|
|||||||
let intervalId
|
let intervalId
|
||||||
|
|
||||||
const tokenRefresh = () => {
|
const tokenRefresh = () => {
|
||||||
|
if (locationRef.current?.pathname === '/slicer') {
|
||||||
|
notificationApi.destroy('token-expiration')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (expiresAt) {
|
if (expiresAt) {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const expirationDate = new Date(expiresAt)
|
const expirationDate = new Date(expiresAt)
|
||||||
@ -878,6 +930,7 @@ const AuthProvider = ({ children }) => {
|
|||||||
open={
|
open={
|
||||||
showSessionExpiredModal &&
|
showSessionExpiredModal &&
|
||||||
!isDesktopHelperRoute &&
|
!isDesktopHelperRoute &&
|
||||||
|
!isSlicerRoute &&
|
||||||
!loading &&
|
!loading &&
|
||||||
!showAuthErrorModal
|
!showAuthErrorModal
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ const COOKIE_OPTIONS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AUTH_STORAGE_KEY = 'farmcontrol.auth'
|
const AUTH_STORAGE_KEY = 'farmcontrol.auth'
|
||||||
|
const SLICER_AUTH_STORAGE_KEY = 'farmcontrol.slicerAuth'
|
||||||
const LEGACY_AUTH_COOKIE_NAMES = ['authToken', 'authExpiresAt', 'user']
|
const LEGACY_AUTH_COOKIE_NAMES = ['authToken', 'authExpiresAt', 'user']
|
||||||
|
|
||||||
const getAuthExpiryTime = (expiresAt) => {
|
const getAuthExpiryTime = (expiresAt) => {
|
||||||
@ -325,3 +326,63 @@ export const clearAuthCookies = () => {
|
|||||||
}
|
}
|
||||||
clearLegacyAuthCookies()
|
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