AuthContext.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. // Authentication context for sharing auth state across the app
  2. import { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react'
  3. import * as authApi from '@/api/auth'
  4. import apiClient from '@/api/client'
  5. import type { AuthTokens, User } from '@/types'
  6. const ACCESS_TOKEN_KEY = 'smartbotic_access_token'
  7. const REFRESH_TOKEN_KEY = 'smartbotic_refresh_token'
  8. const TOKEN_EXPIRY_KEY = 'smartbotic_token_expiry'
  9. interface AuthContextValue {
  10. isAuthenticated: boolean
  11. isLoading: boolean
  12. user: User | null
  13. error: string | null
  14. login: (email: string, password: string) => Promise<void>
  15. logout: () => Promise<void>
  16. clearError: () => void
  17. }
  18. const AuthContext = createContext<AuthContextValue | null>(null)
  19. interface AuthProviderProps {
  20. children: ReactNode
  21. }
  22. export function AuthProvider({ children }: AuthProviderProps) {
  23. const [isAuthenticated, setIsAuthenticated] = useState(() => {
  24. return !!localStorage.getItem(ACCESS_TOKEN_KEY)
  25. })
  26. const [isLoading, setIsLoading] = useState(true)
  27. const [user, setUser] = useState<User | null>(null)
  28. const [error, setError] = useState<string | null>(null)
  29. const clearError = useCallback(() => {
  30. setError(null)
  31. }, [])
  32. const saveTokens = useCallback((tokens: AuthTokens) => {
  33. localStorage.setItem(ACCESS_TOKEN_KEY, tokens.access_token)
  34. localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refresh_token)
  35. // Calculate expiry time (subtract 60 seconds for buffer)
  36. const expiryTime = Date.now() + (tokens.expires_in - 60) * 1000
  37. localStorage.setItem(TOKEN_EXPIRY_KEY, expiryTime.toString())
  38. apiClient.setAccessToken(tokens.access_token)
  39. }, [])
  40. const clearTokens = useCallback(() => {
  41. localStorage.removeItem(ACCESS_TOKEN_KEY)
  42. localStorage.removeItem(REFRESH_TOKEN_KEY)
  43. localStorage.removeItem(TOKEN_EXPIRY_KEY)
  44. apiClient.setAccessToken(null)
  45. }, [])
  46. const refreshTokens = useCallback(async (): Promise<boolean> => {
  47. const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY)
  48. if (!refreshToken) {
  49. return false
  50. }
  51. try {
  52. const tokens = await authApi.refreshToken(refreshToken)
  53. saveTokens(tokens)
  54. return true
  55. } catch {
  56. clearTokens()
  57. setIsAuthenticated(false)
  58. setUser(null)
  59. return false
  60. }
  61. }, [saveTokens, clearTokens])
  62. const login = useCallback(
  63. async (email: string, password: string) => {
  64. setIsLoading(true)
  65. setError(null)
  66. try {
  67. const tokens = await authApi.login({ email, password })
  68. saveTokens(tokens)
  69. setIsAuthenticated(true)
  70. // Fetch user info after successful login
  71. try {
  72. const userInfo = await apiClient.get<User>('/auth/me')
  73. setUser(userInfo)
  74. } catch {
  75. // User info fetch failed but login succeeded
  76. }
  77. } catch (err) {
  78. const message = err instanceof Error ? err.message : 'Login failed'
  79. setError(message)
  80. throw err
  81. } finally {
  82. setIsLoading(false)
  83. }
  84. },
  85. [saveTokens]
  86. )
  87. const logout = useCallback(async () => {
  88. setIsLoading(true)
  89. try {
  90. const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY)
  91. if (refreshToken) {
  92. await authApi.logout(refreshToken).catch(() => {
  93. // Ignore logout API errors
  94. })
  95. }
  96. } finally {
  97. clearTokens()
  98. setIsAuthenticated(false)
  99. setUser(null)
  100. setIsLoading(false)
  101. }
  102. }, [clearTokens])
  103. // Initialize auth state on mount
  104. useEffect(() => {
  105. const initAuth = async () => {
  106. const accessToken = localStorage.getItem(ACCESS_TOKEN_KEY)
  107. if (!accessToken) {
  108. setIsLoading(false)
  109. return
  110. }
  111. apiClient.setAccessToken(accessToken)
  112. // Check if token is expired or about to expire
  113. const expiryStr = localStorage.getItem(TOKEN_EXPIRY_KEY)
  114. const expiry = expiryStr ? parseInt(expiryStr, 10) : 0
  115. if (Date.now() >= expiry) {
  116. // Token expired, try to refresh
  117. const refreshed = await refreshTokens()
  118. if (!refreshed) {
  119. setIsLoading(false)
  120. return
  121. }
  122. }
  123. // Fetch user info
  124. try {
  125. const userInfo = await apiClient.get<User>('/auth/me')
  126. setUser(userInfo)
  127. setIsAuthenticated(true)
  128. } catch {
  129. // Token invalid, try to refresh
  130. const refreshed = await refreshTokens()
  131. if (refreshed) {
  132. try {
  133. const userInfo = await apiClient.get<User>('/auth/me')
  134. setUser(userInfo)
  135. setIsAuthenticated(true)
  136. } catch {
  137. clearTokens()
  138. setIsAuthenticated(false)
  139. }
  140. }
  141. } finally {
  142. setIsLoading(false)
  143. }
  144. }
  145. initAuth()
  146. }, [refreshTokens, clearTokens])
  147. // Auto-refresh token before expiry
  148. useEffect(() => {
  149. if (!isAuthenticated) {
  150. return
  151. }
  152. const checkAndRefresh = () => {
  153. const expiryStr = localStorage.getItem(TOKEN_EXPIRY_KEY)
  154. const expiry = expiryStr ? parseInt(expiryStr, 10) : 0
  155. // Refresh if token expires in less than 2 minutes
  156. if (Date.now() >= expiry - 120000) {
  157. refreshTokens()
  158. }
  159. }
  160. // Check every minute
  161. const interval = setInterval(checkAndRefresh, 60000)
  162. return () => clearInterval(interval)
  163. }, [isAuthenticated, refreshTokens])
  164. return (
  165. <AuthContext.Provider
  166. value={{
  167. isAuthenticated,
  168. isLoading,
  169. user,
  170. error,
  171. login,
  172. logout,
  173. clearError,
  174. }}
  175. >
  176. {children}
  177. </AuthContext.Provider>
  178. )
  179. }
  180. export function useAuth() {
  181. const context = useContext(AuthContext)
  182. if (!context) {
  183. throw new Error('useAuth must be used within an AuthProvider')
  184. }
  185. return context
  186. }
  187. export default AuthContext