1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 10:09:35 +01:00
effigenix/frontend/apps/cli/src/state/auth-context.tsx

151 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { createContext, useCallback, useContext, useEffect, useReducer } from 'react';
import { RefreshTokenExpiredError } from '@effigenix/api-client';
import { tokenStorage, getStoredUsername, saveUsername } from '../utils/token-storage.js';
import { client } from '../utils/api-client.js';
export interface AuthUser {
username: string;
}
interface AuthState {
isAuthenticated: boolean;
user: AuthUser | null;
loading: boolean;
error: string | null;
}
type AuthAction =
| { type: 'SET_LOADING'; loading: boolean }
| { type: 'SET_USER'; user: AuthUser }
| { type: 'SET_ERROR'; error: string }
| { type: 'CLEAR_ERROR' }
| { type: 'LOGOUT' };
function authReducer(state: AuthState, action: AuthAction): AuthState {
switch (action.type) {
case 'SET_LOADING':
return { ...state, loading: action.loading };
case 'SET_USER':
return { ...state, isAuthenticated: true, user: action.user, loading: false, error: null };
case 'SET_ERROR':
return { ...state, loading: false, error: action.error };
case 'CLEAR_ERROR':
return { ...state, error: null };
case 'LOGOUT':
return { isAuthenticated: false, user: null, loading: false, error: null };
}
}
interface AuthContextValue {
isAuthenticated: boolean;
user: AuthUser | null;
loading: boolean;
error: string | null;
login: (username: string, password: string) => Promise<boolean>;
logout: () => Promise<void>;
clearError: () => void;
}
const AuthContext = createContext<AuthContextValue | null>(null);
interface AuthProviderProps {
children: React.ReactNode;
onAuthRequired?: () => void;
onLogout?: () => void;
}
export function AuthProvider({ children, onLogout }: AuthProviderProps) {
const [state, dispatch] = useReducer(authReducer, {
isAuthenticated: false,
user: null,
loading: true,
error: null,
});
// Beim Start: prüfen ob bereits gültige Session vorhanden
useEffect(() => {
void (async () => {
try {
const token = await tokenStorage.getAccessToken();
const refreshToken = await tokenStorage.getRefreshToken();
if (token && refreshToken) {
// Token existiert und ist noch gültig
const username = await getStoredUsername() ?? '?';
dispatch({ type: 'SET_USER', user: { username } });
} else if (refreshToken) {
// Access Token abgelaufen, aber Refresh Token vorhanden → Refresh versuchen
try {
const storedUsername = await getStoredUsername();
const refreshed = await client.auth.refresh({ refreshToken });
await tokenStorage.saveTokens(
refreshed.accessToken,
refreshed.refreshToken,
refreshed.expiresAt,
);
dispatch({ type: 'SET_USER', user: { username: storedUsername ?? '?' } });
} catch {
await tokenStorage.clearTokens();
dispatch({ type: 'LOGOUT' });
}
} else {
dispatch({ type: 'SET_LOADING', loading: false });
}
} catch {
dispatch({ type: 'SET_LOADING', loading: false });
}
})();
}, []);
const login = useCallback(async (username: string, password: string): Promise<boolean> => {
dispatch({ type: 'SET_LOADING', loading: true });
dispatch({ type: 'CLEAR_ERROR' });
try {
const response = await client.auth.login({ username, password });
await tokenStorage.saveTokens(response.accessToken, response.refreshToken, response.expiresAt);
await saveUsername(username);
dispatch({ type: 'SET_USER', user: { username } });
return true;
} catch (err) {
if (err instanceof RefreshTokenExpiredError) {
dispatch({ type: 'SET_ERROR', error: 'Session abgelaufen. Bitte neu anmelden.' });
} else if (err instanceof Error) {
const message = err.message.includes('401')
? 'Ungültiger Benutzername oder Passwort.'
: err.message;
dispatch({ type: 'SET_ERROR', error: message });
} else {
dispatch({ type: 'SET_ERROR', error: 'Anmeldung fehlgeschlagen.' });
}
return false;
}
}, []);
const logout = useCallback(async (): Promise<void> => {
try {
await client.auth.logout();
} catch {
// Logout-Request kann fehlschlagen wenn Token abgelaufen trotzdem lokale Session löschen
}
await tokenStorage.clearTokens();
dispatch({ type: 'LOGOUT' });
onLogout?.();
}, [onLogout]);
const clearError = useCallback(() => dispatch({ type: 'CLEAR_ERROR' }), []);
const value: AuthContextValue = {
...state,
login,
logout,
clearError,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
return ctx;
}