glam/apps/archief-assistent/src/context/AuthContext.tsx
2025-12-21 00:01:54 +01:00

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'