Support Refresh token and improve authentication and security.

Refactor authentication handling and API client integration

- Updated UserProfileAvatar to use 'access_token' instead of 'ca_token' for logout.
- Removed SSEProvider from Contexts and adjusted related imports.
- Introduced useAuth hook for centralized authentication logic, including login, logout, and token management.
- Refactored useSSE to utilize the new useAuth hook for token validation.
- Updated UserQueries to check for token validity using the new method.
- Deleted AuthenticationService as its functionality is now handled by useAuth.
- Created a new ApiClient utility for handling API requests and token management.
- Updated various components and views to use the new ApiClient for API interactions.
- Removed TokenManager and migrated its functionality to the new ApiClient.
- Adjusted LoginView and related components to utilize the new authentication flow.
- Cleaned up unused variables and improved code consistency across components.
This commit is contained in:
Mo Tarbin
2025-12-25 22:54:39 -05:00
parent 1123fb3ca6
commit 8edd3774d2
20 changed files with 493 additions and 428 deletions

144
src/hooks/useAuth.jsx Normal file
View File

@@ -0,0 +1,144 @@
import { createContext, useContext, useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { API_URL } from '../Config'
import { apiClient } from '../utils/ApiClient'
const AuthContext = createContext(null)
export const useAuth = () => {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
export const AuthProvider = ({ children }) => {
const [token, setToken] = useState(() => localStorage.getItem('token'))
const [user, setUser] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const navigate = useNavigate()
const baseURL = `${API_URL}/api/v1`
const isAuthenticated = !!token
const isTokenExpired = () => {
const expiry = localStorage.getItem('token_expiry')
if (!expiry) return false
return new Date() >= new Date(expiry)
}
const clearAuth = () => {
setToken(null)
setUser(null)
localStorage.removeItem('token')
localStorage.removeItem('token_expiry')
localStorage.removeItem('ca_token')
localStorage.removeItem('ca_expiration')
localStorage.removeItem('access_token')
}
const login = async credentials => {
setIsLoading(true)
try {
const response = await fetch(`${baseURL}/auth/login`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
})
if (!response.ok) {
const error = await response.json()
return { success: false, error: error.message || 'Login failed' }
}
const data = await response.json()
const userToken = data.token || data.access_token
if (userToken) {
setToken(userToken)
localStorage.setItem('token', userToken)
if (data.expire || data.access_token_expiry) {
localStorage.setItem(
'token_expiry',
data.expire || data.access_token_expiry,
)
}
}
setIsLoading(false)
return { success: true, data }
} catch (error) {
setIsLoading(false)
return { success: false, error: 'Network error' }
}
}
const logout = async () => {
setIsLoading(true)
try {
await fetch(`${baseURL}/auth/logout`, {
method: 'POST',
credentials: 'include',
})
} catch (error) {
console.warn('Logout API call failed:', error)
} finally {
clearAuth()
setIsLoading(false)
navigate('/login')
}
}
const fetchUser = async () => {
if (!token) return null
try {
const response = await apiClient.get('/users/profile')
if (!response.ok) {
return null
}
const userData = await response.json()
setUser(userData)
return userData
} catch (error) {
console.error('Fetch user error:', error)
return null
}
}
useEffect(() => {
const initAuth = async () => {
if (token && !isTokenExpired()) {
await fetchUser()
} else if (token && isTokenExpired()) {
// Token is expired, but don't refresh here
// Let the first API call handle refresh via ApiClient
// Just try to fetch user - if it fails, ApiClient will handle refresh
await fetchUser()
} else {
clearAuth()
navigate('/login')
}
setIsLoading(false)
}
initAuth()
}, [token, navigate])
const value = {
token,
user,
isLoading,
isAuthenticated,
login,
logout,
fetchUser,
}
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}

View File

@@ -4,7 +4,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { useUserProfile } from '../queries/UserQueries'
import { useAlerts } from '../service/AlertsProvider'
import { useNotification } from '../service/NotificationProvider'
import { apiManager, isTokenValid } from '../utils/TokenManager'
import { apiClient } from '../utils/apiClient'
import { useAuth } from './useAuth.jsx'
const SSE_STATES = {
CONNECTING: 0,
OPEN: 1,
@@ -17,6 +18,7 @@ const CIRCUIT_BREAKER_RESET_TIME = 600000 // 10 minutes
export const useSSE = () => {
const { data: userProfile } = useUserProfile()
const { isAuthenticated, token } = useAuth()
const [connectionState, setConnectionState] = useState(SSE_STATES.CLOSED)
const [lastEvent, setLastEvent] = useState(null)
const [error, setError] = useState(null)
@@ -34,14 +36,14 @@ export const useSSE = () => {
const { showAlert } = useAlerts()
const getSSEUrl = useCallback(() => {
const token = localStorage.getItem('ca_token')
if (!token || !isTokenValid()) {
const authToken = token
if (!authToken || !isAuthenticated) {
console.log('SSE: No valid authentication token')
return null
}
// Get the API URL from apiManager
const apiUrl = apiManager.getApiURL() // e.g., "http://localhost:8080/api/v1"
const apiUrl = apiClient.baseURL // e.g., "http://localhost:8080/api/v1"
// Build SSE URL - let backend determine circle from authenticated user
const sseUrl = `${apiUrl}/realtime/sse`
@@ -302,6 +304,7 @@ export const useSSE = () => {
'Cache-Control': 'no-cache',
Accept: 'text/event-stream',
},
withCredentials: true,
// Increase timeout to prevent premature disconnections
// Default is 45000ms (45s), increasing to 2 minutes
// TODO: send this in the resource object so it can be configured per instance
@@ -447,10 +450,10 @@ export const useSSE = () => {
enabled => {
console.log('SSE toggleSSEEnabled called:', {
enabled,
isTokenValid: isTokenValid(),
isTokenValid: isAuthenticated,
})
localStorage.setItem('sse_enabled', enabled.toString())
if (enabled && isTokenValid()) {
if (enabled && isAuthenticated) {
console.log('SSE toggleSSEEnabled: Calling connect()')
connect()
} else {
@@ -468,13 +471,13 @@ export const useSSE = () => {
// Auto-connect when SSE is enabled and token is valid
useEffect(() => {
console.log('SSE auto-connect effect triggered')
console.log('Token valid:', isTokenValid())
console.log('Token valid:', isAuthenticated)
// Check if SSE is enabled in settings
const isSSEEnabledSetting = localStorage.getItem('sse_enabled') === 'true'
console.log('SSE enabled in settings:', isSSEEnabledSetting)
if (isTokenValid() && isSSEEnabledSetting) {
if (isAuthenticated && isSSEEnabledSetting) {
console.log('SSE: Conditions met, attempting to connect')
connect()
} else {
@@ -514,7 +517,7 @@ export const useSSE = () => {
const isSSEEnabledSetting =
localStorage.getItem('sse_enabled') === 'true'
if (
isTokenValid() &&
isAuthenticated &&
isSSEEnabledSetting &&
connectionState !== SSE_STATES.OPEN
) {