| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- // Authentication context for sharing auth state across the app
- import { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react'
- import * as authApi from '@/api/auth'
- import apiClient from '@/api/client'
- import type { AuthTokens, User } from '@/types'
- const ACCESS_TOKEN_KEY = 'smartbotic_access_token'
- const REFRESH_TOKEN_KEY = 'smartbotic_refresh_token'
- const TOKEN_EXPIRY_KEY = 'smartbotic_token_expiry'
- interface AuthContextValue {
- isAuthenticated: boolean
- isLoading: boolean
- user: User | null
- error: string | null
- login: (email: string, password: string) => Promise<void>
- logout: () => Promise<void>
- clearError: () => void
- }
- const AuthContext = createContext<AuthContextValue | null>(null)
- interface AuthProviderProps {
- children: ReactNode
- }
- export function AuthProvider({ children }: AuthProviderProps) {
- const [isAuthenticated, setIsAuthenticated] = useState(() => {
- return !!localStorage.getItem(ACCESS_TOKEN_KEY)
- })
- const [isLoading, setIsLoading] = useState(true)
- const [user, setUser] = useState<User | null>(null)
- const [error, setError] = useState<string | null>(null)
- const clearError = useCallback(() => {
- setError(null)
- }, [])
- const saveTokens = useCallback((tokens: AuthTokens) => {
- localStorage.setItem(ACCESS_TOKEN_KEY, tokens.access_token)
- localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refresh_token)
- // Calculate expiry time (subtract 60 seconds for buffer)
- const expiryTime = Date.now() + (tokens.expires_in - 60) * 1000
- localStorage.setItem(TOKEN_EXPIRY_KEY, expiryTime.toString())
- apiClient.setAccessToken(tokens.access_token)
- }, [])
- const clearTokens = useCallback(() => {
- localStorage.removeItem(ACCESS_TOKEN_KEY)
- localStorage.removeItem(REFRESH_TOKEN_KEY)
- localStorage.removeItem(TOKEN_EXPIRY_KEY)
- apiClient.setAccessToken(null)
- }, [])
- const refreshTokens = useCallback(async (): Promise<boolean> => {
- const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY)
- if (!refreshToken) {
- return false
- }
- try {
- const tokens = await authApi.refreshToken(refreshToken)
- saveTokens(tokens)
- return true
- } catch {
- clearTokens()
- setIsAuthenticated(false)
- setUser(null)
- return false
- }
- }, [saveTokens, clearTokens])
- const login = useCallback(
- async (email: string, password: string) => {
- setIsLoading(true)
- setError(null)
- try {
- const tokens = await authApi.login({ email, password })
- saveTokens(tokens)
- setIsAuthenticated(true)
- // Fetch user info after successful login
- try {
- const userInfo = await apiClient.get<User>('/auth/me')
- setUser(userInfo)
- } catch {
- // User info fetch failed but login succeeded
- }
- } catch (err) {
- const message = err instanceof Error ? err.message : 'Login failed'
- setError(message)
- throw err
- } finally {
- setIsLoading(false)
- }
- },
- [saveTokens]
- )
- const logout = useCallback(async () => {
- setIsLoading(true)
- try {
- const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY)
- if (refreshToken) {
- await authApi.logout(refreshToken).catch(() => {
- // Ignore logout API errors
- })
- }
- } finally {
- clearTokens()
- setIsAuthenticated(false)
- setUser(null)
- setIsLoading(false)
- }
- }, [clearTokens])
- // Initialize auth state on mount
- useEffect(() => {
- const initAuth = async () => {
- const accessToken = localStorage.getItem(ACCESS_TOKEN_KEY)
- if (!accessToken) {
- setIsLoading(false)
- return
- }
- apiClient.setAccessToken(accessToken)
- // Check if token is expired or about to expire
- const expiryStr = localStorage.getItem(TOKEN_EXPIRY_KEY)
- const expiry = expiryStr ? parseInt(expiryStr, 10) : 0
- if (Date.now() >= expiry) {
- // Token expired, try to refresh
- const refreshed = await refreshTokens()
- if (!refreshed) {
- setIsLoading(false)
- return
- }
- }
- // Fetch user info
- try {
- const userInfo = await apiClient.get<User>('/auth/me')
- setUser(userInfo)
- setIsAuthenticated(true)
- } catch {
- // Token invalid, try to refresh
- const refreshed = await refreshTokens()
- if (refreshed) {
- try {
- const userInfo = await apiClient.get<User>('/auth/me')
- setUser(userInfo)
- setIsAuthenticated(true)
- } catch {
- clearTokens()
- setIsAuthenticated(false)
- }
- }
- } finally {
- setIsLoading(false)
- }
- }
- initAuth()
- }, [refreshTokens, clearTokens])
- // Auto-refresh token before expiry
- useEffect(() => {
- if (!isAuthenticated) {
- return
- }
- const checkAndRefresh = () => {
- const expiryStr = localStorage.getItem(TOKEN_EXPIRY_KEY)
- const expiry = expiryStr ? parseInt(expiryStr, 10) : 0
- // Refresh if token expires in less than 2 minutes
- if (Date.now() >= expiry - 120000) {
- refreshTokens()
- }
- }
- // Check every minute
- const interval = setInterval(checkAndRefresh, 60000)
- return () => clearInterval(interval)
- }, [isAuthenticated, refreshTokens])
- return (
- <AuthContext.Provider
- value={{
- isAuthenticated,
- isLoading,
- user,
- error,
- login,
- logout,
- clearError,
- }}
- >
- {children}
- </AuthContext.Provider>
- )
- }
- export function useAuth() {
- const context = useContext(AuthContext)
- if (!context) {
- throw new Error('useAuth must be used within an AuthProvider')
- }
- return context
- }
- export default AuthContext
|