/** * Auth store for managing authentication state * Uses React context for state management */ import { createContext, useContext, useState, useEffect, useCallback } from 'react'; import type { ReactNode } from 'react'; import type { CurrentUser } from '@/types/api/auth'; import * as authApi from '@/lib/api/auth'; // Token storage keys const TOKEN_KEY = 'deutschlernen_token'; const REFRESH_TOKEN_KEY = 'deutschlernen_refresh_token'; const TOKEN_EXPIRY_KEY = 'deutschlernen_token_expiry'; interface AuthState { token: string | null; refreshToken: string | null; user: CurrentUser | null; isAuthenticated: boolean; isLoading: boolean; error: string | null; } interface AuthContextType extends AuthState { login: (email: string, password: string) => Promise; register: (username: string, email: string, password: string) => Promise; logout: () => void; refreshAuthToken: () => Promise; clearError: () => void; } const AuthContext = createContext(undefined); // Helper to check if token is expired function isTokenExpired(expiry: string | null): boolean { if (!expiry) return true; const expiryDate = new Date(expiry); // Add 30 second buffer return expiryDate.getTime() - 30000 < Date.now(); } // Helper to get token from storage function getTokenFromStorage(): { token: string | null; refreshToken: string | null; expiry: string | null } { const token = localStorage.getItem(TOKEN_KEY); const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY); const expiry = localStorage.getItem(TOKEN_EXPIRY_KEY); return { token, refreshToken, expiry }; } // Helper to store token function storeToken(token: string, refreshToken: string, expiry: string): void { localStorage.setItem(TOKEN_KEY, token); localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); localStorage.setItem(TOKEN_EXPIRY_KEY, expiry); } // Helper to clear token function clearToken(): void { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(REFRESH_TOKEN_KEY); localStorage.removeItem(TOKEN_EXPIRY_KEY); } // Provider component interface AuthProviderProps { children: ReactNode; } export function AuthProvider({ children }: AuthProviderProps) { const [state, setState] = useState({ token: null, refreshToken: null, user: null, isAuthenticated: false, isLoading: true, error: null, }); // Initialize from storage useEffect(() => { const initializeAuth = async () => { const { token, refreshToken, expiry } = getTokenFromStorage(); if (!token || isTokenExpired(expiry)) { clearToken(); setState({ token: null, refreshToken: null, user: null, isAuthenticated: false, isLoading: false, error: null, }); return; } try { // Try to get current user const user = await authApi.getCurrentUser(token); setState({ token, refreshToken, user, isAuthenticated: true, isLoading: false, error: null, }); } catch (err) { clearToken(); setState({ token: null, refreshToken: null, user: null, isAuthenticated: false, isLoading: false, error: null, }); } }; initializeAuth(); }, []); // Login function const login = useCallback(async (email: string, password: string) => { setState(prev => ({ ...prev, isLoading: true, error: null })); try { const response = await authApi.login({ email, password }); storeToken(response.token, response.refreshToken, response.expiresAt); // Get current user info const user = await authApi.getCurrentUser(response.token); setState({ token: response.token, refreshToken: response.refreshToken, user, isAuthenticated: true, isLoading: false, error: null, }); } catch (err) { setState(prev => ({ ...prev, isLoading: false, error: err instanceof Error ? err.message : 'Login failed', })); throw err; } }, []); // Register function const register = useCallback(async (username: string, email: string, password: string) => { setState(prev => ({ ...prev, isLoading: true, error: null })); try { const response = await authApi.register({ username, email, password }); storeToken(response.token, response.refreshToken, response.expiresAt); // Get current user info const user = await authApi.getCurrentUser(response.token); setState({ token: response.token, refreshToken: response.refreshToken, user, isAuthenticated: true, isLoading: false, error: null, }); } catch (err) { setState(prev => ({ ...prev, isLoading: false, error: err instanceof Error ? err.message : 'Registration failed', })); throw err; } }, []); // Logout function const logout = useCallback(() => { clearToken(); setState({ token: null, refreshToken: null, user: null, isAuthenticated: false, isLoading: false, error: null, }); }, []); // Refresh token function const refreshAuthToken = useCallback(async () => { const { refreshToken } = getTokenFromStorage(); if (!refreshToken) { logout(); return; } try { const response = await authApi.refreshToken({ refreshToken }); storeToken(response.token, response.refreshToken, response.expiresAt); // Update state with new token setState(prev => ({ ...prev, token: response.token, refreshToken: response.refreshToken, })); } catch (err) { logout(); } }, [logout]); // Clear error function const clearError = useCallback(() => { setState(prev => ({ ...prev, error: null })); }, []); const value: AuthContextType = { ...state, login, register, logout, refreshAuthToken, clearError, }; return {children}; } // Hook to use auth context export function useAuth(): AuthContextType { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context; } // Helper hook to check if user is admin export function useIsAdmin(): boolean { const { user } = useAuth(); return user?.role === 'Admin'; } // Helper to get auth header export function getAuthHeader(token: string | null): Record { if (!token) return {}; return { Authorization: `Bearer ${token}` }; }