231 lines
8.5 KiB
TypeScript
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 }; |