All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 14m41s
150 lines
5.9 KiB
TypeScript
150 lines
5.9 KiB
TypeScript
// 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 { 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);
|
|
|
|
// 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(() => {
|
|
// Only process once the query has completed at least once
|
|
if (!isFetched && isQueryLoading) {
|
|
return;
|
|
}
|
|
|
|
const token = getToken();
|
|
|
|
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 && isFetched && !fetchedProfile) {
|
|
// Token exists, query completed, but profile is null - sign out
|
|
logger.warn('[AuthProvider] Token was present but profile is null. 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');
|
|
}
|
|
|
|
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) => {
|
|
logger.info(`[AuthProvider-Login] Attempting login.`);
|
|
setToken(token);
|
|
|
|
if (profileData) {
|
|
// If profile is provided (e.g., from credential login), use it directly.
|
|
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,
|
|
});
|
|
} else {
|
|
// 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 {
|
|
// 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(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);
|
|
logger.error('Failed to fetch user data after login. Rolling back auth state.', {
|
|
error: errorMessage,
|
|
});
|
|
logout(); // Log the user out to prevent an inconsistent state.
|
|
// Re-throw the error so the calling component can handle it (e.g., show a notification)
|
|
throw new Error(`Login succeeded, but failed to fetch your data: ${errorMessage}`);
|
|
}
|
|
}
|
|
},
|
|
[logout, queryClient],
|
|
);
|
|
|
|
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(
|
|
() => ({
|
|
userProfile,
|
|
authStatus,
|
|
isLoading,
|
|
login,
|
|
logout,
|
|
updateProfile,
|
|
}),
|
|
[userProfile, authStatus, isLoading, login, logout, updateProfile],
|
|
);
|
|
|
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
};
|