/** * Authentication Hooks */ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { login, logout, getCurrentUser, masquerade, stopMasquerade, forgotPassword, LoginCredentials, User, MasqueradeStackEntry } from '../api/auth'; import { getCookie, setCookie, deleteCookie } from '../utils/cookies'; import { getBaseDomain, buildSubdomainUrl } from '../utils/domain'; /** * Helper hook to set auth tokens (used by invitation acceptance) */ export const useAuth = () => { const queryClient = useQueryClient(); const setTokens = (accessToken: string, refreshToken: string) => { setCookie('access_token', accessToken, 7); setCookie('refresh_token', refreshToken, 7); }; return { setTokens }; }; /** * Hook to get current user */ export const useCurrentUser = () => { return useQuery({ queryKey: ['currentUser'], queryFn: async () => { // Check if token exists before making request (from cookie) const token = getCookie('access_token'); if (!token) { return null; // No token, return null instead of making request } try { return await getCurrentUser(); } catch (error) { // If getCurrentUser fails (e.g., 401), return null // The API client interceptor will handle token refresh console.error('Failed to get current user:', error); return null; } }, retry: 1, // Retry once in case of token refresh staleTime: 5 * 60 * 1000, // 5 minutes refetchOnMount: true, // Always refetch when component mounts refetchOnWindowFocus: false, }); }; /** * Hook to login */ export const useLogin = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: login, onSuccess: (data) => { // Store tokens in cookies for cross-subdomain access setCookie('access_token', data.access, 7); setCookie('refresh_token', data.refresh, 7); // Clear any existing masquerade stack - this is a fresh login localStorage.removeItem('masquerade_stack'); // Set user in cache queryClient.setQueryData(['currentUser'], data.user); }, }); }; /** * Hook to logout */ export const useLogout = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: logout, onSuccess: () => { // Clear tokens (from cookies) deleteCookie('access_token'); deleteCookie('refresh_token'); // Clear masquerade stack localStorage.removeItem('masquerade_stack'); // Clear user cache queryClient.removeQueries({ queryKey: ['currentUser'] }); queryClient.clear(); // Redirect to login page on root domain const protocol = window.location.protocol; const baseDomain = getBaseDomain(); const port = window.location.port ? `:${window.location.port}` : ''; window.location.href = `${protocol}//${baseDomain}${port}/login`; }, }); }; /** * Check if user is authenticated */ export const useIsAuthenticated = (): boolean => { const { data: user, isLoading } = useCurrentUser(); return !isLoading && !!user; }; /** * Get the redirect path based on user role * Tenant users go to /dashboard/, platform users go to / * Note: Backend maps tenant_owner -> owner, tenant_staff -> staff, etc. */ const getRedirectPathForRole = (role: string): string => { // Tenant roles (as returned by backend after role mapping) const tenantRoles = ['owner', 'staff', 'customer']; if (tenantRoles.includes(role)) { return '/dashboard/'; } return '/'; }; /** * Hook to masquerade as another user */ export const useMasquerade = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (user_pk: number) => { // Get current masquerading stack from localStorage const stackJson = localStorage.getItem('masquerade_stack'); const currentStack: MasqueradeStackEntry[] = stackJson ? JSON.parse(stackJson) : []; // Call masquerade API with current stack return masquerade(user_pk, currentStack); }, onSuccess: async (data) => { // Store the updated masquerading stack if (data.masquerade_stack) { localStorage.setItem('masquerade_stack', JSON.stringify(data.masquerade_stack)); } const user = data.user; const currentHostname = window.location.hostname; const currentPort = window.location.port; const baseDomain = getBaseDomain(); let targetSubdomain: string | null = null; if (['superuser', 'platform_manager', 'platform_support'].includes(user.role)) { targetSubdomain = 'platform'; } else if (user.business_subdomain) { targetSubdomain = user.business_subdomain; } const needsRedirect = targetSubdomain && currentHostname !== `${targetSubdomain}.${baseDomain}`; const redirectPath = getRedirectPathForRole(user.role); if (needsRedirect) { // CRITICAL: Clear the session cookie BEFORE redirect // Call logout API to clear HttpOnly sessionid cookie try { const apiUrl = import.meta.env.VITE_API_URL || `${window.location.protocol}//${baseDomain}`; await fetch(`${apiUrl}/auth/logout/`, { method: 'POST', credentials: 'include', }); } catch (e) { // Continue anyway } // Pass tokens AND masquerading stack in URL (for cross-domain transfer) const stackEncoded = encodeURIComponent(JSON.stringify(data.masquerade_stack || [])); const redirectUrl = buildSubdomainUrl(targetSubdomain, `${redirectPath}?access_token=${data.access}&refresh_token=${data.refresh}&masquerade_stack=${stackEncoded}`); window.location.href = redirectUrl; return; } // If no redirect needed (same subdomain), we can just set cookies and navigate setCookie('access_token', data.access, 7); setCookie('refresh_token', data.refresh, 7); queryClient.setQueryData(['currentUser'], data.user); window.location.href = redirectPath; }, }); }; /** * Hook to stop masquerading and return to previous user */ export const useStopMasquerade = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async () => { // Get current masquerading stack from localStorage const stackJson = localStorage.getItem('masquerade_stack'); const currentStack: MasqueradeStackEntry[] = stackJson ? JSON.parse(stackJson) : []; if (currentStack.length === 0) { throw new Error('No masquerading session to stop'); } // Call stop_masquerade API with current stack return stopMasquerade(currentStack); }, onSuccess: async (data) => { // Update the masquerading stack if (data.masquerade_stack && data.masquerade_stack.length > 0) { localStorage.setItem('masquerade_stack', JSON.stringify(data.masquerade_stack)); } else { // Clear the stack if empty localStorage.removeItem('masquerade_stack'); } const user = data.user; const currentHostname = window.location.hostname; const currentPort = window.location.port; const baseDomain = getBaseDomain(); let targetSubdomain: string | null = null; if (['superuser', 'platform_manager', 'platform_support'].includes(user.role)) { targetSubdomain = 'platform'; } else if (user.business_subdomain) { targetSubdomain = user.business_subdomain; } const needsRedirect = targetSubdomain && currentHostname !== `${targetSubdomain}.${baseDomain}`; const redirectPath = getRedirectPathForRole(user.role); if (needsRedirect) { // CRITICAL: Clear the session cookie BEFORE redirect try { const apiUrl = import.meta.env.VITE_API_URL || `${window.location.protocol}//${baseDomain}`; await fetch(`${apiUrl}/auth/logout/`, { method: 'POST', credentials: 'include', }); } catch (e) { // Continue anyway } // Pass tokens AND masquerading stack in URL (for cross-domain transfer) const stackEncoded = encodeURIComponent(JSON.stringify(data.masquerade_stack || [])); const redirectUrl = buildSubdomainUrl(targetSubdomain, `${redirectPath}?access_token=${data.access}&refresh_token=${data.refresh}&masquerade_stack=${stackEncoded}`); window.location.href = redirectUrl; return; } // If no redirect needed (same subdomain), we can just set cookies and navigate setCookie('access_token', data.access, 7); setCookie('refresh_token', data.refresh, 7); queryClient.setQueryData(['currentUser'], data.user); window.location.href = redirectPath; }, }); }; /** * Hook to request password reset */ export const useForgotPassword = () => { return useMutation({ mutationFn: (data: { email: string }) => forgotPassword(data.email), }); };