// src/contexts/AuthContext.js import React, { createContext, useState, useCallback, useEffect } from 'react' import axios from 'axios' import { message, Modal, notification, Progress, Button, Space } from 'antd' import PropTypes from 'prop-types' import ExclamationOctogonIcon from '../../Icons/ExclamationOctagonIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon' import config from '../../../config' import AppError from '../../App/AppError' import loglevel from 'loglevel' const logger = loglevel.getLogger('ApiServerContext') logger.setLevel(config.logLevel) const AuthContext = createContext() const AuthProvider = ({ children }) => { const [messageApi, contextHolder] = message.useMessage() const [notificationApi, notificationContextHolder] = notification.useNotification() const [authenticated, setAuthenticated] = useState(false) const [loading, setLoading] = useState(false) const [token, setToken] = useState(null) const [expiresAt, setExpiresAt] = useState(null) const [userProfile, setUserProfile] = useState(null) const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false) const [showUnauthorizedModal, setShowUnauthorizedModal] = useState(false) const [authError, setAuthError] = useState(null) const logout = useCallback((redirectUri = '/login') => { setAuthenticated(false) setToken(null) setExpiresAt(null) setUserProfile(null) window.location.href = `${config.backendUrl}/auth/logout?redirect_uri=${encodeURIComponent(redirectUri)}` }, []) // Login using query parameters const loginWithSSO = useCallback( (redirectUri = window.location.pathname + window.location.search) => { messageApi.info('Logging in with tombutcher.work') window.location.href = `${config.backendUrl}/auth/login?redirect_uri=${encodeURIComponent(redirectUri)}` }, [messageApi] ) // Function to check if the user is logged in const checkAuthStatus = useCallback(async () => { setLoading(true) setAuthError(null) try { // Make a call to your backend to check auth status const response = await axios.get(`${config.backendUrl}/auth/user`, { withCredentials: true // Important for including cookies }) if (response.status === 200 && response.data) { logger.debug('Got auth token!') setToken(response.data.access_token) setExpiresAt(response.data.expires_at) setUserProfile(response.data) } else { setAuthenticated(false) setAuthError('Failed to authenticate user.') } } catch (error) { logger.debug('Auth check failed', error) if (error.response?.status === 401) { setShowUnauthorizedModal(true) } else { setAuthError('Error connecting to authentication service.') } setAuthenticated(false) } finally { setLoading(false) } }, []) const refreshToken = useCallback(async () => { try { const response = await axios.get(`${config.backendUrl}/auth/refresh`, { withCredentials: true }) if (response.status === 200 && response.data) { setToken(response.data.access_token) setExpiresAt(response.data.expires_at) } } catch (error) { console.error('Token refresh failed', error) } }, []) const handleSessionExpiredModalOk = () => { setShowSessionExpiredModal(false) loginWithSSO() } // Initialize on component mount useEffect(() => { let intervalId const tokenRefresh = () => { if (expiresAt) { const now = new Date() const expirationDate = new Date(expiresAt) const timeRemaining = expirationDate - now if (timeRemaining <= 0) { if (authenticated == true) { setAuthenticated(false) } setShowSessionExpiredModal(true) notificationApi.destroy('token-expiration') } else { if (authenticated == false) { setAuthenticated(true) } const minutes = Math.floor(timeRemaining / 60000) const seconds = Math.floor((timeRemaining % 60000) / 1000) // Only show notification in the final minute if (minutes === 0) { const totalSeconds = 60 const remainingSeconds = totalSeconds - seconds const progress = (remainingSeconds / totalSeconds) * 100 notificationApi.info({ message: 'Session Expiring Soon', description: (
Your session will expire in {seconds} seconds
), duration: 0, key: 'token-expiration', icon: null, placement: 'bottomRight', style: { width: 360 }, className: 'token-expiration-notification', closeIcon: null, onClose: () => {}, btn: ( ) }) } else if (minutes === 1) { // Clear any existing notification when we enter the final minute notificationApi.destroy('token-expiration') } } } } intervalId = setInterval(tokenRefresh, 1000) console.log('fresh', authenticated) tokenRefresh() return () => { if (intervalId) { clearInterval(intervalId) } } }, [expiresAt, authenticated, notificationApi, refreshToken]) useEffect(() => { checkAuthStatus() }, [checkAuthStatus]) if (authError) { return } return ( <> {contextHolder} {notificationContextHolder} {children} Session Expired } open={showSessionExpiredModal} onOk={handleSessionExpiredModalOk} okText='Log In' style={{ maxWidth: 430 }} closable={false} centered maskClosable={false} footer={[ ]} > Your session has expired. Please log in again to continue. Please log in to continue } open={showUnauthorizedModal} onOk={() => { setShowUnauthorizedModal(false) loginWithSSO() }} okText='Log In' style={{ maxWidth: 430, top: '50%', transform: 'translateY(-50%)' }} closable={false} maskClosable={false} footer={[ ]} > You need to be logged in to access FarmControl. Please log in with tombutcher.work to continue. ) } AuthProvider.propTypes = { children: PropTypes.node.isRequired } export { AuthContext, AuthProvider }