Files
MAIA/interfaces/nativeapp/src/contexts/AuthContext.tsx
2025-04-23 20:53:40 +02:00

231 lines
8.5 KiB
TypeScript

// src/contexts/AuthContext.tsx
import React, { createContext, useState, useEffect, useContext, useMemo, useCallback } from 'react';
import { Platform, ActivityIndicator, View, StyleSheet } from 'react-native'; // Import Platform
import apiClient from '../api/client';
import { useTheme } from 'react-native-paper';
import { getAccessToken, getRefreshToken, storeTokens, clearTokens } from '../api/client';
// Define UserRole enum matching the backend
enum UserRole {
USER = 'user',
ADMIN = 'admin',
}
interface UserData {
username: string;
name: string;
role: UserRole;
// Add other user fields if needed
}
interface AuthContextData {
isAuthenticated: boolean;
isLoading: boolean;
user: UserData | null; // Add user data to context
login: (username: string, password: string) => Promise<void>;
logout: () => Promise<void>;
register: (username: string, password: string, name: string) => Promise<void>; // Add register function
}
const AuthContext = createContext<AuthContextData>({
isAuthenticated: false,
isLoading: true,
user: null, // Initialize user as null
login: async () => { throw new Error('AuthContext not initialized'); },
logout: async () => { throw new Error('AuthContext not initialized'); },
register: async () => { throw new Error('AuthContext not initialized'); }, // Add register initializer
});
interface AuthProviderProps {
children: React.ReactNode;
}
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isAuthenticatedState, setIsAuthenticatedState] = useState<boolean>(false);
const [userState, setUserState] = useState<UserData | null>(null); // State for user data
// Function to fetch user data
const fetchUserData = useCallback(async () => {
try {
console.log("[AuthContext] fetchUserData: Fetching /user/me");
const response = await apiClient.get<UserData>('/user/me');
console.log("[AuthContext] fetchUserData: User data received:", response.data);
setUserState(response.data);
return response.data;
} catch (error: any) {
console.error("[AuthContext] fetchUserData: Error fetching user data:", error.response?.data || error.message);
// If fetching user fails (e.g., token expired mid-session), log out
await clearTokens();
setIsAuthenticatedState(false);
setUserState(null);
return null;
}
}, []);
const checkAuthStatus = useCallback(async () => {
const token = await getAccessToken();
const hasToken = !!token;
if (hasToken) {
console.log("[AuthContext] checkAuthStatus: Token found, fetching user data.");
const userData = await fetchUserData();
if (userData) {
setIsAuthenticatedState(true);
} else {
// Fetch failed, already handled logout in fetchUserData
setIsAuthenticatedState(false);
}
} else {
console.log("[AuthContext] checkAuthStatus: No token found.");
setIsAuthenticatedState(false);
setUserState(null);
}
return isAuthenticatedState; // Return the updated state
}, [fetchUserData, isAuthenticatedState]); // Added isAuthenticatedState dependency
const loadInitialAuth = useCallback(async () => {
setIsLoading(true);
try {
console.log("[AuthContext] loadInitialAuth: Checking initial auth status");
await checkAuthStatus();
console.log("[AuthContext] loadInitialAuth: Initial check complete.");
} catch (error) {
console.error("[AuthContext] loadInitialAuth: Error during initial auth check:", error);
await clearTokens();
setIsAuthenticatedState(false);
setUserState(null);
} finally {
setIsLoading(false);
}
}, [checkAuthStatus]);
useEffect(() => {
loadInitialAuth();
}, [loadInitialAuth]);
const login = useCallback(async (username: string, password: string) => {
console.log("[AuthContext] login: Function called with:", username);
try {
// ... (existing login API call) ...
const response = await apiClient.post(
'/auth/login',
'grant_type=password&username=' + encodeURIComponent(username) + '&password=' + encodeURIComponent(password) + '&scope=&client_id=&client_secret=',
{
headers: {
'accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
const { access_token, refresh_token } = response.data;
// ... (existing token validation) ...
if (!access_token || typeof access_token !== 'string' || !refresh_token) {
console.error("[AuthContext] login: Invalid token structure received:", response.data);
throw new Error('Invalid token received from server.');
}
console.log('[AuthContext] login: Login successful, storing tokens.');
await storeTokens(access_token, refresh_token);
// Fetch user data immediately after successful login
const userData = await fetchUserData();
if (userData) {
setIsAuthenticatedState(true);
} else {
// Should not happen if login succeeded, but handle defensively
throw new Error('Failed to fetch user data after login.');
}
} catch (error: any) {
// ... (existing error handling) ...
console.error("[AuthContext] login: Caught Error Object:", error);
await clearTokens();
setIsAuthenticatedState(false);
setUserState(null);
throw error;
}
}, [fetchUserData]); // Added fetchUserData dependency
const register = useCallback(async (username: string, password: string, name: string) => {
console.log("[AuthContext] register: Function called with:", username, name);
try {
// Call the backend register endpoint
const response = await apiClient.post('/auth/register', {
username,
password,
name,
}, {
headers: {
'accept': 'application/json',
'Content-Type': 'application/json',
},
});
console.log('[AuthContext] register: Registration successful:', response.data);
// Optionally, you could automatically log the user in here
// For now, we'll just let the user log in manually after registering
// Or display a success message and navigate back to login
} catch (error: any) {
console.error("[AuthContext] register: Caught Error Object:", error);
// Rethrow the error so the UI can handle it (e.g., display specific messages)
throw error;
}
}, []); // No dependencies needed for register itself
const logout = useCallback(async () => {
console.log('[AuthContext] logout: Logging out.');
const refreshToken = await getRefreshToken();
// ... (existing backend logout call) ...
try {
if (refreshToken) {
console.log('[AuthContext] logout: Calling backend /auth/logout');
await apiClient.post("/auth/logout", { refresh_token: refreshToken });
console.log('[AuthContext] logout: Backend logout call successful (or ignored error).');
}
} catch (error: any) {
console.error('[AuthContext] logout: Error calling backend logout:', error.response?.data || error.message);
} finally {
await clearTokens();
setIsAuthenticatedState(false);
setUserState(null); // Clear user data on logout
console.log('[AuthContext] logout: Local tokens cleared and state updated.');
}
}, []);
const contextValue = useMemo(() => ({
isAuthenticated: isAuthenticatedState,
isLoading,
user: userState, // Provide user state
login,
logout,
register, // Add register to context value
}), [isAuthenticatedState, isLoading, userState, login, logout, register]); // Added register dependency
// ... (rest of the component: Provider, useAuth, AuthLoadingScreen) ...
return (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
export const AuthLoadingScreen: React.FC = () => {
const theme = useTheme();
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: theme.colors.background }
});
return (<View style={styles.container}><ActivityIndicator size="large" color={theme.colors.primary} /></View>);
}
// Export UserRole if needed elsewhere
export { UserRole };