149 lines
4 KiB
TypeScript
149 lines
4 KiB
TypeScript
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react'
|
|
import {
|
|
type User,
|
|
loginApi,
|
|
logoutApi,
|
|
getStoredTokens,
|
|
getStoredUser,
|
|
isTokenExpired,
|
|
refreshTokenApi,
|
|
clearAuthStorage,
|
|
changePasswordApi,
|
|
} from '../services/authApi'
|
|
import { TOKEN_REFRESH_BUFFER_MS } from '../config/api'
|
|
|
|
interface AuthContextType {
|
|
user: User | null
|
|
isAuthenticated: boolean
|
|
isLoading: boolean
|
|
login: (email: string, password: string) => Promise<{ success: boolean; error?: string }>
|
|
logout: () => Promise<void>
|
|
changePassword: (currentPassword: string, newPassword: string) => Promise<{ success: boolean; error?: string }>
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
|
|
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
const [user, setUser] = useState<User | null>(null)
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
|
|
// Refresh token before expiry
|
|
const scheduleTokenRefresh = useCallback((expiresInMs: number) => {
|
|
const refreshIn = Math.max(expiresInMs - TOKEN_REFRESH_BUFFER_MS, 10000)
|
|
|
|
const timeoutId = setTimeout(async () => {
|
|
const tokens = await refreshTokenApi()
|
|
if (tokens) {
|
|
// Schedule next refresh
|
|
scheduleTokenRefresh(tokens.expiresIn * 1000)
|
|
} else {
|
|
// Refresh failed - logout
|
|
setUser(null)
|
|
}
|
|
}, refreshIn)
|
|
|
|
return () => clearTimeout(timeoutId)
|
|
}, [])
|
|
|
|
// Check for existing session on mount
|
|
useEffect(() => {
|
|
const initAuth = async () => {
|
|
const { accessToken, refreshToken } = getStoredTokens()
|
|
|
|
if (!accessToken && !refreshToken) {
|
|
setIsLoading(false)
|
|
return
|
|
}
|
|
|
|
// Check if access token is still valid
|
|
if (accessToken && !isTokenExpired(accessToken)) {
|
|
const storedUser = getStoredUser()
|
|
if (storedUser) {
|
|
setUser(storedUser)
|
|
|
|
// Schedule token refresh
|
|
const payload = JSON.parse(atob(accessToken.split('.')[1]))
|
|
const expiresInMs = (payload.exp * 1000) - Date.now()
|
|
scheduleTokenRefresh(expiresInMs)
|
|
}
|
|
setIsLoading(false)
|
|
return
|
|
}
|
|
|
|
// Access token expired - try to refresh
|
|
if (refreshToken && !isTokenExpired(refreshToken)) {
|
|
try {
|
|
const tokens = await refreshTokenApi()
|
|
if (tokens) {
|
|
const storedUser = getStoredUser()
|
|
if (storedUser) {
|
|
setUser(storedUser)
|
|
scheduleTokenRefresh(tokens.expiresIn * 1000)
|
|
}
|
|
}
|
|
} catch {
|
|
clearAuthStorage()
|
|
}
|
|
} else {
|
|
clearAuthStorage()
|
|
}
|
|
|
|
setIsLoading(false)
|
|
}
|
|
|
|
initAuth()
|
|
}, [scheduleTokenRefresh])
|
|
|
|
const login = async (email: string, password: string): Promise<{ success: boolean; error?: string }> => {
|
|
try {
|
|
const response = await loginApi(email, password)
|
|
setUser(response.user)
|
|
|
|
// Schedule token refresh
|
|
scheduleTokenRefresh(response.tokens.expiresIn * 1000)
|
|
|
|
return { success: true }
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Login failed'
|
|
return { success: false, error: message }
|
|
}
|
|
}
|
|
|
|
const logout = async () => {
|
|
await logoutApi()
|
|
setUser(null)
|
|
}
|
|
|
|
const changePassword = async (
|
|
currentPassword: string,
|
|
newPassword: string
|
|
): Promise<{ success: boolean; error?: string }> => {
|
|
return changePasswordApi(currentPassword, newPassword)
|
|
}
|
|
|
|
return (
|
|
<AuthContext.Provider
|
|
value={{
|
|
user,
|
|
isAuthenticated: !!user,
|
|
isLoading,
|
|
login,
|
|
logout,
|
|
changePassword,
|
|
}}
|
|
>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
)
|
|
}
|
|
|
|
export function useAuth() {
|
|
const context = useContext(AuthContext)
|
|
if (context === undefined) {
|
|
throw new Error('useAuth must be used within an AuthProvider')
|
|
}
|
|
return context
|
|
}
|
|
|
|
// Re-export User type for convenience
|
|
export type { User } from '../services/authApi'
|