DeutschLernen/german-app-frontend/src/stores/authStore.tsx
Lasse Rune Hansen 50f1b8a8dc feat(backend): Implement mandatory authentication and admin module
- Add Role property to User entity with migration
- Create BootstrapController for first admin user creation
- Remove [AllowAnonymous] from all learning content controllers
- Create AdminController with admin-only endpoints
- Create AdminService for user management
- Create UserReportService for progress reports
- Add UserRepository implementation
- Update AuthService with role support

feat(frontend): Implement authentication system
- Add AuthStore with React context for auth state management
- Create Login, Register, Landing, and Home pages
- Add ProtectedRoute and AdminRoute components
- Create Auth API types and client
- Configure Vite with @/ path alias
- Add comprehensive CSS styles for auth and landing pages

BREAKING CHANGE: All learning content now requires authentication.
Users must register and sign in before accessing lessons, quizzes, and stories.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-14 12:42:15 +02:00

256 lines
6.7 KiB
TypeScript

/**
* 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<void>;
register: (username: string, email: string, password: string) => Promise<void>;
logout: () => void;
refreshAuthToken: () => Promise<void>;
clearError: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(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<AuthState>({
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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
// 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<string, string> {
if (!token) return {};
return { Authorization: `Bearer ${token}` };
}