This commit is contained in:
@@ -1,89 +1,66 @@
|
||||
// src/providers/AuthProvider.tsx
|
||||
import React, { useState, useEffect, useCallback, ReactNode, useMemo } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { AuthContext, AuthContextType } from '../contexts/AuthContext';
|
||||
import type { UserProfile } from '../types';
|
||||
import * as apiClient from '../services/apiClient';
|
||||
import { useApi } from '../hooks/useApi';
|
||||
import { useAuthProfileQuery, AUTH_PROFILE_QUERY_KEY } from '../hooks/queries/useAuthProfileQuery';
|
||||
import { getToken, setToken, removeToken } from '../services/tokenStorage';
|
||||
import { logger } from '../services/logger.client';
|
||||
|
||||
/**
|
||||
* AuthProvider component that manages authentication state.
|
||||
*
|
||||
* Refactored to use TanStack Query (ADR-0005 Phase 7).
|
||||
*/
|
||||
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||
const [authStatus, setAuthStatus] = useState<AuthContextType['authStatus']>('Determining...');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// FIX: Stabilize the apiFunction passed to useApi.
|
||||
// By wrapping this in useCallback, we ensure the same function instance is passed to
|
||||
// useApi on every render. This prevents the `execute` function returned by `useApi`
|
||||
// from being recreated, which in turn breaks the infinite re-render loop in the useEffect.
|
||||
const getProfileCallback = useCallback(() => apiClient.getAuthenticatedUserProfile(), []);
|
||||
|
||||
const { execute: checkTokenApi } = useApi<UserProfile, []>(getProfileCallback);
|
||||
const { execute: fetchProfileApi } = useApi<UserProfile, []>(getProfileCallback);
|
||||
// Use TanStack Query to fetch the authenticated user's profile
|
||||
const {
|
||||
data: fetchedProfile,
|
||||
isLoading: isQueryLoading,
|
||||
isError,
|
||||
isFetched,
|
||||
} = useAuthProfileQuery();
|
||||
|
||||
// Effect to sync query result with auth state
|
||||
useEffect(() => {
|
||||
// This flag prevents state updates if the component unmounts or if another
|
||||
// auth operation (like login/logout) occurs before this initial check completes.
|
||||
let isMounted = true;
|
||||
logger.info('[AuthProvider-Effect] Starting initial authentication check.');
|
||||
// Only process once the query has completed at least once
|
||||
if (!isFetched && isQueryLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkAuthToken = async () => {
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
logger.info('[AuthProvider-Effect] Found auth token. Validating...');
|
||||
try {
|
||||
const fetchedProfile = await checkTokenApi();
|
||||
const token = getToken();
|
||||
|
||||
if (isMounted && fetchedProfile) {
|
||||
logger.info('[AuthProvider-Effect] Profile received, setting state to AUTHENTICATED.');
|
||||
setUserProfile(fetchedProfile);
|
||||
setAuthStatus('AUTHENTICATED');
|
||||
} else if (isMounted) {
|
||||
logger.warn(
|
||||
'[AuthProvider-Effect] Token was present but validation returned no profile. Signing out.',
|
||||
);
|
||||
removeToken();
|
||||
setUserProfile(null);
|
||||
setAuthStatus('SIGNED_OUT');
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
// This catch block is now primarily for unexpected errors, as useApi handles API errors.
|
||||
logger.warn('Auth token validation failed. Clearing token.', { error: e });
|
||||
if (isMounted) {
|
||||
removeToken();
|
||||
setUserProfile(null);
|
||||
setAuthStatus('SIGNED_OUT');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.info('[AuthProvider-Effect] No auth token found. Setting state to SIGNED_OUT.');
|
||||
if (isMounted) {
|
||||
setAuthStatus('SIGNED_OUT');
|
||||
}
|
||||
}
|
||||
if (fetchedProfile) {
|
||||
logger.info('[AuthProvider] Profile received from query, setting state to AUTHENTICATED.');
|
||||
setUserProfile(fetchedProfile);
|
||||
setAuthStatus('AUTHENTICATED');
|
||||
} else if (token && isError) {
|
||||
logger.warn('[AuthProvider] Token was present but validation failed. Signing out.');
|
||||
removeToken();
|
||||
setUserProfile(null);
|
||||
setAuthStatus('SIGNED_OUT');
|
||||
} else if (!token) {
|
||||
logger.info('[AuthProvider] No auth token found. Setting state to SIGNED_OUT.');
|
||||
setAuthStatus('SIGNED_OUT');
|
||||
}
|
||||
|
||||
if (isMounted) {
|
||||
logger.info(
|
||||
'[AuthProvider-Effect] Initial auth check finished. Setting isLoading to false.',
|
||||
);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkAuthToken();
|
||||
|
||||
return () => {
|
||||
logger.info('[AuthProvider-Effect] Component unmounting, cleaning up.');
|
||||
isMounted = false;
|
||||
};
|
||||
}, [checkTokenApi]);
|
||||
setIsLoading(false);
|
||||
}, [fetchedProfile, isQueryLoading, isError, isFetched]);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
logger.info('[AuthProvider-Logout] Clearing user data and auth token.');
|
||||
removeToken();
|
||||
setUserProfile(null);
|
||||
setAuthStatus('SIGNED_OUT');
|
||||
}, []);
|
||||
// Clear the auth profile cache on logout
|
||||
queryClient.removeQueries({ queryKey: AUTH_PROFILE_QUERY_KEY });
|
||||
}, [queryClient]);
|
||||
|
||||
const login = useCallback(
|
||||
async (token: string, profileData?: UserProfile) => {
|
||||
@@ -95,6 +72,8 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
logger.info('[AuthProvider-Login] Profile data received directly.');
|
||||
setUserProfile(profileData);
|
||||
setAuthStatus('AUTHENTICATED');
|
||||
// Update the query cache with the provided profile
|
||||
queryClient.setQueryData(AUTH_PROFILE_QUERY_KEY, profileData);
|
||||
logger.info('[AuthProvider-Login] Login successful. State set to AUTHENTICATED.', {
|
||||
user: profileData.user,
|
||||
});
|
||||
@@ -102,12 +81,23 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
// If no profile is provided (e.g., from OAuth or token refresh), fetch it.
|
||||
logger.info('[AuthProvider-Login] Auth token set in storage. Fetching profile...');
|
||||
try {
|
||||
const fetchedProfile = await fetchProfileApi();
|
||||
if (!fetchedProfile) {
|
||||
// Directly fetch the profile (not using the query hook since we need immediate results)
|
||||
const response = await apiClient.getAuthenticatedUserProfile();
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({
|
||||
message: `Request failed with status ${response.status}`,
|
||||
}));
|
||||
throw new Error(error.message || 'Failed to fetch profile');
|
||||
}
|
||||
const fetchedProfileData: UserProfile = await response.json();
|
||||
|
||||
if (!fetchedProfileData) {
|
||||
throw new Error('Received null or undefined profile from API.');
|
||||
}
|
||||
setUserProfile(fetchedProfile);
|
||||
setUserProfile(fetchedProfileData);
|
||||
setAuthStatus('AUTHENTICATED');
|
||||
// Update the query cache with the fetched profile
|
||||
queryClient.setQueryData(AUTH_PROFILE_QUERY_KEY, fetchedProfileData);
|
||||
logger.info('[AuthProvider-Login] Profile fetch successful. State set to AUTHENTICATED.');
|
||||
} catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||
@@ -120,16 +110,22 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
}
|
||||
}
|
||||
},
|
||||
[fetchProfileApi, logout],
|
||||
[logout, queryClient],
|
||||
);
|
||||
|
||||
const updateProfile = useCallback((updatedProfileData: Partial<UserProfile>) => {
|
||||
logger.info('[AuthProvider-UpdateProfile] Updating profile state.', { updatedProfileData });
|
||||
setUserProfile((prevProfile) => {
|
||||
if (!prevProfile) return null;
|
||||
return { ...prevProfile, ...updatedProfileData };
|
||||
});
|
||||
}, []);
|
||||
const updateProfile = useCallback(
|
||||
(updatedProfileData: Partial<UserProfile>) => {
|
||||
logger.info('[AuthProvider-UpdateProfile] Updating profile state.', { updatedProfileData });
|
||||
setUserProfile((prevProfile) => {
|
||||
if (!prevProfile) return null;
|
||||
const newProfile = { ...prevProfile, ...updatedProfileData };
|
||||
// Keep the query cache in sync
|
||||
queryClient.setQueryData(AUTH_PROFILE_QUERY_KEY, newProfile);
|
||||
return newProfile;
|
||||
});
|
||||
},
|
||||
[queryClient],
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
|
||||
Reference in New Issue
Block a user