Spaces:
Running
Running
// src/hooks/useAuth.js | |
import { useState, useContext, createContext, useCallback, useEffect } from 'react'; | |
import { useNavigate } from 'react-router-dom'; | |
import { login as apiLogin, logout as apiLogout, refreshToken as apiRefreshToken } from '../services/auth'; | |
import { useLocalStorage } from './useLocalStorage'; | |
const AuthContext = createContext(); | |
export const AuthProvider = ({ children }) => { | |
const [user, setUser] = useState(null); | |
const [authTokens, setAuthTokens] = useLocalStorage('authTokens', null); | |
const [userType, setUserType] = useLocalStorage('userType', 'Enthusiast'); | |
const [isRefreshing, setIsRefreshing] = useState(false); | |
const navigate = useNavigate(); | |
const parseJwt = (token) => { | |
try { | |
return JSON.parse(atob(token.split('.')[1])); | |
} catch (e) { | |
return null; | |
} | |
}; | |
const login = useCallback(async (username, password) => { | |
try { | |
const { access_token, refresh_token } = await apiLogin(username, password); | |
setAuthTokens({ | |
access: access_token, | |
refresh: refresh_token | |
}); | |
setUser(username); | |
return { success: true }; | |
} catch (error) { | |
return { success: false, error: error.message }; | |
} | |
}, [setAuthTokens]); | |
const logout = useCallback(async () => { | |
try { | |
if (authTokens?.access) { | |
await apiLogout(authTokens.access); | |
} | |
setAuthTokens(null); | |
setUser(null); | |
navigate('/login'); | |
} catch (error) { | |
console.error('Logout error:', error); | |
// Even if logout API fails, clear local tokens | |
setAuthTokens(null); | |
setUser(null); | |
} | |
}, [authTokens, setAuthTokens, navigate]); | |
const refreshToken = useCallback(async () => { | |
if (!authTokens?.refresh || isRefreshing) return; | |
setIsRefreshing(true); | |
try { | |
const { access_token, refresh_token } = await apiRefreshToken({ | |
refresh_token: authTokens.refresh | |
}); | |
setAuthTokens({ | |
access: access_token, | |
refresh: refresh_token | |
}); | |
return access_token; | |
} catch (error) { | |
console.error('Token refresh failed:', error); | |
logout(); // Full logout if refresh fails | |
throw error; | |
} finally { | |
setIsRefreshing(false); | |
} | |
}, [authTokens, isRefreshing, setAuthTokens, logout]); | |
// Auto-refresh token when it's about to expire | |
useEffect(() => { | |
const refreshInterval = setInterval(() => { | |
if (authTokens?.access) { | |
const { exp } = parseJwt(authTokens.access); | |
// Refresh token if it expires in less than 5 minutes | |
if (exp * 1000 - Date.now() < 300000) { | |
refreshToken(); | |
} | |
} | |
}, 60000); // Check every minute | |
return () => clearInterval(refreshInterval); | |
}, [authTokens, refreshToken]); | |
const getAccessToken = useCallback(async () => { | |
if (!authTokens?.access) return null; | |
const { exp } = parseJwt(authTokens.access); | |
if (exp * 1000 - Date.now() < 30000) { // If expires in <30 seconds | |
return await refreshToken(); | |
} | |
return authTokens.access; | |
}, [authTokens, refreshToken]); | |
const value = { | |
user, | |
authTokens, | |
userType, | |
setUserType, | |
isAuthenticated: !!authTokens?.access, | |
isRefreshing, | |
login, | |
logout, | |
refreshToken, | |
getAccessToken | |
}; | |
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; | |
}; | |
export const useAuth = () => { | |
const context = useContext(AuthContext); | |
if (!context) { | |
throw new Error('useAuth must be used within an AuthProvider'); | |
} | |
return context; | |
}; |