275 lines
8.0 KiB
JavaScript

// 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: (
<div>
<div style={{ marginBottom: 8 }}>
Your session will expire in {seconds} seconds
</div>
<Progress
percent={progress}
size='small'
status='active'
showInfo={false}
/>
</div>
),
duration: 0,
key: 'token-expiration',
icon: null,
placement: 'bottomRight',
style: {
width: 360
},
className: 'token-expiration-notification',
closeIcon: null,
onClose: () => {},
btn: (
<Button
type='primary'
size='small'
onClick={() => {
notificationApi.destroy('token-expiration')
refreshToken()
}}
>
Reload Session
</Button>
)
})
} 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 <AppError message={authError} showBack={false} />
}
return (
<>
{contextHolder}
{notificationContextHolder}
<AuthContext.Provider
value={{
authenticated,
loginWithSSO,
token,
loading,
userProfile,
logout
}}
>
{children}
</AuthContext.Provider>
<Modal
title={
<Space size={'middle'}>
<InfoCircleIcon />
Session Expired
</Space>
}
open={showSessionExpiredModal}
onOk={handleSessionExpiredModalOk}
okText='Log In'
style={{ maxWidth: 430 }}
closable={false}
centered
maskClosable={false}
footer={[
<Button
key='submit'
type='primary'
onClick={handleSessionExpiredModalOk}
>
Log In
</Button>
]}
>
Your session has expired. Please log in again to continue.
</Modal>
<Modal
title={
<Space size={'middle'}>
<ExclamationOctogonIcon />
Please log in to continue
</Space>
}
open={showUnauthorizedModal}
onOk={() => {
setShowUnauthorizedModal(false)
loginWithSSO()
}}
okText='Log In'
style={{ maxWidth: 430, top: '50%', transform: 'translateY(-50%)' }}
closable={false}
maskClosable={false}
footer={[
<Button
key='submit'
type='primary'
onClick={() => {
setShowUnauthorizedModal(false)
loginWithSSO()
}}
>
Log In
</Button>
]}
>
You need to be logged in to access FarmControl. Please log in with
tombutcher.work to continue.
</Modal>
</>
)
}
AuthProvider.propTypes = {
children: PropTypes.node.isRequired
}
export { AuthContext, AuthProvider }