diff --git a/src/App.js b/src/App.js index e348d88..9aa68b8 100644 --- a/src/App.js +++ b/src/App.js @@ -51,6 +51,7 @@ import ContactUs from './pages/ContactUs'; import TermsOfService from './pages/TermsOfService'; import PrivacyPolicy from './pages/PrivacyPolicy'; import AcceptInvitation from './pages/AcceptInvitation'; +import NotFound from './pages/NotFound'; const PrivateRoute = ({ children, adminOnly = false }) => { const { user, loading } = useAuth(); @@ -280,6 +281,9 @@ function App() { } /> + + {/* 404 - Catch all undefined routes */} + } /> diff --git a/src/context/AuthContext.js b/src/context/AuthContext.js index ca70795..24df9bf 100644 --- a/src/context/AuthContext.js +++ b/src/context/AuthContext.js @@ -3,7 +3,14 @@ import axios from 'axios'; const AuthContext = createContext(); -const API_URL = process.env.REACT_APP_BACKEND_URL; +const API_URL = process.env.REACT_APP_BACKEND_URL || window.location.origin; + +// Log environment on module load for debugging +console.log('[AuthContext] Module initialized with:', { + REACT_APP_BACKEND_URL: process.env.REACT_APP_BACKEND_URL, + REACT_APP_BASENAME: process.env.REACT_APP_BASENAME, + API_URL: API_URL +}); export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); @@ -54,21 +61,79 @@ export const AuthProvider = ({ children }) => { }; const login = async (email, password) => { - const response = await axios.post(`${API_URL}/api/auth/login`, { email, password }); - const { access_token, user: userData } = response.data; - localStorage.setItem('token', access_token); - setToken(access_token); - setUser(userData); - - // Fetch user permissions (don't let this fail the login) try { - await fetchPermissions(access_token); - } catch (error) { - console.error('Failed to fetch permissions during login, will retry later:', error); - // Don't throw - permissions can be fetched later if needed - } + console.log('[AuthContext] Starting login request...', { + API_URL: API_URL, + envBackendUrl: process.env.REACT_APP_BACKEND_URL, + fullUrl: `${API_URL}/api/auth/login` + }); - return userData; + const response = await axios.post( + `${API_URL}/api/auth/login`, + { email, password }, + { + timeout: 30000, // 30 second timeout + headers: { + 'Content-Type': 'application/json' + } + } + ); + + console.log('[AuthContext] Login response received:', { + status: response.status, + hasToken: !!response.data?.access_token, + hasUser: !!response.data?.user + }); + + const { access_token, user: userData } = response.data; + + // Store token first + localStorage.setItem('token', access_token); + console.log('[AuthContext] Token stored in localStorage'); + + // Update state + setToken(access_token); + setUser(userData); + console.log('[AuthContext] User state updated:', { + email: userData.email, + role: userData.role + }); + + // Fetch user permissions (don't let this fail the login) + // Use setTimeout to defer permission fetching slightly + setTimeout(async () => { + try { + console.log('[AuthContext] Fetching permissions...'); + await fetchPermissions(access_token); + console.log('[AuthContext] Permissions fetched successfully'); + } catch (error) { + console.error('[AuthContext] Failed to fetch permissions (non-critical):', { + message: error.message, + response: error.response?.data, + status: error.response?.status + }); + // Don't throw - permissions can be fetched later if needed + } + }, 100); // Small delay to ensure state is settled + + return userData; + } catch (error) { + // Enhanced error logging + console.error('[AuthContext] Login failed:', { + message: error.message, + response: error.response?.data, + status: error.response?.status, + code: error.code, + config: { + url: error.config?.url, + method: error.config?.method, + timeout: error.config?.timeout + } + }); + + // Re-throw to let Login component handle the error + throw error; + } }; const logout = () => { diff --git a/src/pages/AcceptInvitation.js b/src/pages/AcceptInvitation.js index 2be4be0..5410c0f 100644 --- a/src/pages/AcceptInvitation.js +++ b/src/pages/AcceptInvitation.js @@ -19,6 +19,8 @@ const AcceptInvitation = () => { const [invitation, setInvitation] = useState(null); const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); + const [success, setSuccess] = useState(false); + const [successUser, setSuccessUser] = useState(null); const [error, setError] = useState(null); const [formData, setFormData] = useState({ password: '', @@ -134,19 +136,23 @@ const AcceptInvitation = () => { const { access_token, user } = response.data; localStorage.setItem('token', access_token); - toast.success('Welcome to LOAF! Your account has been created successfully.'); - // Call login to update auth context if (login) { await login(invitation.email, formData.password); } - // Redirect based on role - if (user.role === 'admin' || user.role === 'superadmin') { - navigate('/admin/dashboard'); - } else { - navigate('/dashboard'); - } + // Show success state + setSuccessUser(user); + setSuccess(true); + + // Auto-redirect after 3 seconds + setTimeout(() => { + if (user.role === 'admin' || user.role === 'superadmin') { + navigate('/admin'); + } else { + navigate('/dashboard'); + } + }, 3000); } catch (error) { const errorMessage = error.response?.data?.detail || 'Failed to accept invitation'; toast.error(errorMessage); @@ -206,6 +212,83 @@ const AcceptInvitation = () => { ); } + if (success) { + const redirectPath = successUser?.role === 'admin' || successUser?.role === 'superadmin' ? '/admin' : '/dashboard'; + + return ( +
+ + {/* Success Animation */} +
+
+ +
+
+ + {/* Success Message */} +

+ Welcome to LOAF! 🎉 +

+

+ Your account has been created successfully. +

+ + {/* User Info Card */} +
+
+
+

+ Name +

+

+ {successUser?.first_name} {successUser?.last_name} +

+
+
+

+ Email +

+

+ {successUser?.email} +

+
+
+

+ Role +

+
{getRoleBadge(successUser?.role)}
+
+
+

+ Status +

+ + {successUser?.status} + +
+
+
+ + {/* Redirect Info */} +
+

+ + Redirecting you to your dashboard in 3 seconds... +

+
+ + {/* Manual Continue Button */} + +
+
+ ); + } + return (
diff --git a/src/pages/NotFound.js b/src/pages/NotFound.js new file mode 100644 index 0000000..21d68b0 --- /dev/null +++ b/src/pages/NotFound.js @@ -0,0 +1,81 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Button } from '../components/ui/button'; +import { Card } from '../components/ui/card'; +import { Home, ArrowLeft, Search } from 'lucide-react'; + +const NotFound = () => { + const navigate = useNavigate(); + + return ( +
+ + {/* 404 Illustration */} +
+
+

+ 404 +

+
+ +
+
+
+ + {/* Message */} +

+ Page Not Found +

+

+ Oops! The page you're looking for doesn't exist. It might have been moved or deleted. +

+ + {/* Action Buttons */} +
+ + +
+ + {/* Help Text */} +
+

+ Need help? Contact us at{' '} + + support@loaftx.org + +

+
+
+
+ ); +}; + +export default NotFound; diff --git a/src/utils/api.js b/src/utils/api.js index 7a1a797..b79e35b 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -4,14 +4,60 @@ const API_URL = process.env.REACT_APP_BACKEND_URL; export const api = axios.create({ baseURL: `${API_URL}/api`, + timeout: 30000, // 30 second timeout for all requests }); -api.interceptors.request.use((config) => { - const token = localStorage.getItem('token'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; +// Request interceptor - add auth token +api.interceptors.request.use( + (config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + console.error('[API] Request error:', error); + return Promise.reject(error); } - return config; -}); +); + +// Response interceptor - handle errors and retries +api.interceptors.response.use( + (response) => { + return response; + }, + async (error) => { + const config = error.config; + + // Don't retry if we've already retried or if it's a client error (4xx) + if (!config || config.__isRetry || (error.response && error.response.status < 500)) { + console.error('[API] Request failed:', { + url: config?.url, + method: config?.method, + status: error.response?.status, + message: error.message, + data: error.response?.data + }); + return Promise.reject(error); + } + + // Mark as retry to prevent infinite loops + config.__isRetry = true; + + // Retry after 1 second for server errors or network issues + console.warn('[API] Retrying request after 1s:', { + url: config.url, + method: config.method, + error: error.message + }); + + return new Promise((resolve) => { + setTimeout(() => { + resolve(api.request(config)); + }, 1000); + }); + } +); export default api;