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:
144
src/hooks/useAuth.jsx
Normal file
144
src/hooks/useAuth.jsx
Normal 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>
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user