import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { API_BASE_URL, TARGET_ENV, ALLOW_ACTIONPANEL_AI_REQUESTS, setAuthToken, clearAuthToken } from '../constants'; import { usePluginSettings } from './PluginSettingsContext'; interface UserInfo { name: string; email: string; theme: 'system' | 'light' | 'dark'; } interface AuthContextProps { isLoggedIn: boolean; userInfo: UserInfo | null; login: (email: string, password: string) => Promise; logout: () => Promise; signup: (name: string, email: string, password: string, verificationCode: string) => Promise; updateEmail: (newEmail: string, currentPassword: string) => Promise<{ requiresVerification: boolean }>; updatePassword: (currentPassword: string, newPassword: string) => Promise; setUserInfo: (info: UserInfo) => void; authError: string | null; setAuthError: (error: string | null) => void; requestVerification: (email: string, purpose: 'registration' | 'password_reset' | 'email_change') => Promise; verifyCode: (email: string, code: string) => Promise; resetPassword: (email: string, code: string, newPassword: string) => Promise; } const AuthContext = createContext(undefined); interface AuthProviderProps { children: ReactNode; } const reloadHomeRoute = () => { try { window.history.replaceState(null, '', '#/'); } catch { window.location.hash = '#/'; } window.location.reload(); }; const requestGateMessage = (purpose: string) => ( `Turn on "Allow WordPress to send requests to ActionPanel AI" in Permissions to ${purpose}.` ); export const AuthProvider = ({ children }: AuthProviderProps): JSX.Element => { const [isLoggedIn, setIsLoggedIn] = useState(false); const [userInfo, setUserInfo] = useState(null); const [authError, setAuthError] = useState(null); const queryClient = useQueryClient(); const pluginSettings = usePluginSettings(); const allowActionPanelAiRequests = TARGET_ENV === 'wordpress' ? pluginSettings.allowActionPanelAiRequests : ALLOW_ACTIONPANEL_AI_REQUESTS; const isPluginAuthBlocked = TARGET_ENV === 'wordpress' && !allowActionPanelAiRequests; // Check authentication status on mount useEffect(() => { const checkAuthStatus = async () => { if (isPluginAuthBlocked) { setIsLoggedIn(false); setUserInfo(null); return; } // console.log('checking auth status'); let hasRetried = false; const fetchStatus = async () => { try { const response = await fetch(`${API_BASE_URL}/auth-status`, { method: 'GET', credentials: 'include', }); if (response.status === 401 && !hasRetried) { hasRetried = true; console.log('Received 401. Retrying auth status check...'); return await fetchStatus(); } if (response.ok) { const data = await response.json(); setIsLoggedIn(data.isLoggedIn); } else { setIsLoggedIn(false); } } catch (error) { console.error('Error checking auth status:', error); setIsLoggedIn(false); } }; await fetchStatus(); }; checkAuthStatus(); }, [isPluginAuthBlocked]); // Fetch user info when logged in const fetchUserInfo = useCallback(async () => { if (isPluginAuthBlocked) { setIsLoggedIn(false); setUserInfo(null); return; } if (!isLoggedIn) return; try { const response = await fetch(`${API_BASE_URL}/user-info`, { method: 'GET', credentials: 'include', }); if (response.ok) { const data = await response.json(); setUserInfo({ name: data.name, email: data.email, theme: data.theme, }); } else { setIsLoggedIn(false); setUserInfo(null); } } catch (error) { console.error('Error fetching user info:', error); setIsLoggedIn(false); setUserInfo(null); } }, [isLoggedIn, isPluginAuthBlocked]); useEffect(() => { fetchUserInfo(); }, [fetchUserInfo]); // Login function const login = useCallback( async (email: string, password: string) => { if (isPluginAuthBlocked) { const message = requestGateMessage('use login and signup'); setAuthError(message); throw new Error(message); } setAuthError(null); try { const response = await fetch(`${API_BASE_URL}/login`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); if (!response.ok) { const errorData = await response.json(); setAuthError(errorData.detail || 'Failed to log in'); throw new Error(errorData.detail || 'Failed to log in'); } // Store bearer token in plugin mode to avoid 3rd-party cookie issues if (TARGET_ENV === 'wordpress') { try { const data = await response.json(); if (data?.access_token) setAuthToken(data.access_token); } catch {} } queryClient.clear(); window.location.reload(); } catch (error) { console.error('Error during login:', error); throw error; } }, [queryClient, isPluginAuthBlocked] ); // Logout function const logout = useCallback( async () => { if (isPluginAuthBlocked) { clearAuthToken(); setIsLoggedIn(false); setUserInfo(null); queryClient.clear(); reloadHomeRoute(); return; } setAuthError(null); try { const response = await fetch(`${API_BASE_URL}/logout`, { method: 'POST', credentials: 'include', }); if (!response.ok) { const errorData = await response.json(); setAuthError(errorData.detail || 'Failed to log out'); throw new Error(errorData.detail || 'Failed to log out'); } // Clear stored bearer token in plugin mode if (TARGET_ENV === 'wordpress') { clearAuthToken(); } queryClient.clear(); reloadHomeRoute(); } catch (error) { console.error('Error during logout:', error); throw error; } }, [queryClient, isPluginAuthBlocked] ); // Signup function const signup = useCallback( async (name: string, email: string, password: string, verificationCode: string) => { if (isPluginAuthBlocked) { const message = requestGateMessage('use login and signup'); setAuthError(message); throw new Error(message); } setAuthError(null); try { const response = await fetch(`${API_BASE_URL}/register`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user: { name, email, password, }, verification: { email, code: verificationCode } }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || 'Registration failed'); } if (TARGET_ENV === 'wordpress') { try { const data = await response.json(); if (data?.access_token) setAuthToken(data.access_token); } catch {} } queryClient.clear(); window.location.reload(); } catch (error) { const message = error instanceof Error ? error.message : 'Registration failed'; setAuthError(message); throw error; } }, [queryClient, isPluginAuthBlocked] ); // Update Email const updateEmail = useCallback( async (newEmail: string, currentPassword: string) => { if (isPluginAuthBlocked) { const message = requestGateMessage('use account features'); setAuthError(message); throw new Error(message); } setAuthError(null); try { const response = await fetch(`${API_BASE_URL}/account/email`, { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ new_email: newEmail, current_password: currentPassword }), }); if (!response.ok) { const errorData = await response.json(); setAuthError(errorData.detail || 'Failed to update email'); throw new Error(errorData.detail || 'Failed to update email'); } const data = await response.json(); // Only update user info immediately if verification is not required if (!data.requires_verification) { await fetchUserInfo(); } return { requiresVerification: data.requires_verification }; } catch (error) { console.error('Error updating email:', error); throw error; } }, [fetchUserInfo, isPluginAuthBlocked] ); // Update Password const updatePassword = useCallback( async (currentPassword: string, newPassword: string) => { if (isPluginAuthBlocked) { const message = requestGateMessage('use account features'); setAuthError(message); throw new Error(message); } setAuthError(null); try { const response = await fetch(`${API_BASE_URL}/account/password`, { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }), }); if (!response.ok) { const errorData = await response.json(); setAuthError(errorData.detail || 'Failed to update password'); throw new Error(errorData.detail || 'Failed to update password'); } await fetchUserInfo(); } catch (error) { console.error('Error updating password:', error); throw error; } }, [fetchUserInfo, isPluginAuthBlocked] ); // Send verification code const requestVerification = useCallback( async (email: string, purpose: 'registration' | 'password_reset' | 'email_change') => { if (isPluginAuthBlocked) { const message = requestGateMessage('use login and signup'); setAuthError(message); throw new Error(message); } try { const response = await fetch(`${API_BASE_URL}/request-verification`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email.toLowerCase(), purpose: purpose }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || 'Failed to send verification code'); } } catch (error) { const message = error instanceof Error ? error.message : 'Failed to request verification'; console.error('Error requesting verification:', message); throw new Error(message); } }, [isPluginAuthBlocked] ); // Verify code const verifyCode = useCallback(async (email: string, code: string) => { if (isPluginAuthBlocked) { const message = requestGateMessage('use account features'); setAuthError(message); return false; } try { const response = await fetch(`${API_BASE_URL}/account/email/verify`, { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, code }), }); const data = await response.json(); if (!response.ok) { setAuthError(data.detail || 'Failed to verify code'); throw new Error(data.detail || 'Failed to verify code'); } await fetchUserInfo(); return true; } catch (error) { const message = error instanceof Error ? error.message : 'Verification failed'; setAuthError(message); return false; } }, [fetchUserInfo, isPluginAuthBlocked]); const resetPassword = useCallback( async (email: string, code: string, newPassword: string) => { if (isPluginAuthBlocked) { const message = requestGateMessage('use login and signup'); setAuthError(message); throw new Error(message); } setAuthError(null); try { const response = await fetch(`${API_BASE_URL}/reset-password`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, code, new_password: newPassword }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || 'Password reset failed'); } return true; } catch (error) { const message = error instanceof Error ? error.message : 'Password reset failed'; setAuthError(message); throw error; } }, [isPluginAuthBlocked] ); const value: AuthContextProps = { isLoggedIn, userInfo, login, logout, setAuthError, signup, updateEmail, updatePassword, setUserInfo, authError, requestVerification, verifyCode, resetPassword, }; return {children}; }; export const useAuth = (): AuthContextProps => { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context; };