6.6 KiB
Authentication Loading State Fixes
Problem
Users were experiencing an infinite "loading" state when accessing protected routes like /dashboard. The page would show "Loading..." indefinitely instead of either displaying the content or redirecting to login.
Root Cause Analysis
The issue was caused by a mismatch between multiple authentication state management systems:
- AuthContext: Managed session state and loading flag
- AuthorizationBlocker: Determined access reasons based on session state
- AuthGateway: Combined context and blocker state
- RouteGuard: Handled UI rendering and redirects
The Problem Flow:
1. User visits /dashboard
2. AuthContext initializes: session = null, loading = false
3. AuthGuard checks access
4. AuthorizationBlocker sees session = null → returns 'loading'
5. AuthGateway sees blocker.reason = 'loading' → sets isLoading = true
6. RouteGuard shows loading state
7. Session fetch completes: session = null, loading = false
8. But blocker still returns 'loading' because session is null
9. Infinite loading state
Fixes Applied
1. AuthContext.tsx
Problem: Initial loading state was false, but session fetch wasn't tracked
Fix:
// Before
const [loading, setLoading] = useState(false);
const fetchSession = useCallback(async () => {
try {
const current = await sessionService.getSession();
setSession(current);
} catch {
setSession(null);
}
}, [sessionService]);
// After
const [loading, setLoading] = useState(true); // Start with loading = true
const fetchSession = useCallback(async () => {
setLoading(true); // Set loading when starting fetch
try {
const current = await sessionService.getSession();
setSession(current);
} catch {
setSession(null);
} finally {
setLoading(false); // Clear loading when done
}
}, [sessionService]);
2. AuthGateway.ts
Problem: Was checking both authContext.loading AND blocker.reason === 'loading'
Fix: Only check authContext.loading for the isLoading state
// Before
isLoading: this.authContext.loading || reason === 'loading',
// After
isLoading: this.authContext.loading,
3. AuthorizationBlocker.ts
Problem: Returned 'loading' when session was null, creating confusion Fix: Treat null session as unauthenticated, not loading
// Before
getReason(): AuthorizationBlockReason {
if (!this.currentSession) {
return 'loading';
}
// ...
}
// After
getReason(): AuthorizationBlockReason {
if (!this.currentSession) {
return 'unauthenticated'; // Null = unauthenticated
}
// ...
}
canExecute(): boolean {
const reason = this.getReason();
return reason === 'enabled'; // Only enabled grants access
}
4. RouteGuard.tsx
Problem: Generic loading message, unclear redirect flow Fix: Better user feedback during authentication flow
// Loading state shows verification message
if (accessState.isLoading) {
return loadingComponent || (
<div className="flex items-center justify-center min-h-screen">
<LoadingState message="Verifying authentication..." className="min-h-screen" />
</div>
);
}
// Unauthorized shows redirect message before redirecting
if (!accessState.canAccess && config.redirectOnUnauthorized !== false) {
return (
<div className="flex items-center justify-center min-h-screen">
<LoadingState message="Redirecting to login..." className="min-h-screen" />
</div>
);
}
5. Dashboard Page
Problem: Had redundant auth checks that conflicted with layout protection Fix: Simplified to only handle data loading
// Before: Had auth checks, useEffect for redirects, etc.
export default function DashboardPage() {
const router = useRouter();
const { session, loading: authLoading } = useAuth();
// ... complex auth logic
// After: Only handles data loading
export default function DashboardPage() {
const { data: dashboardData, isLoading, error } = useDashboardOverview();
// ... simple data loading
}
New Authentication Flow
Unauthenticated User:
- User visits
/dashboard - Middleware checks for
gp_sessioncookie → not found - Middleware redirects to
/auth/login?returnTo=/dashboard - User logs in
- Session created, cookie set
- Redirected back to
/dashboard - AuthGuard verifies session exists
- Dashboard loads
Authenticated User:
- User visits
/dashboard - Middleware checks for
gp_sessioncookie → found - Request proceeds to page rendering
- AuthGuard shows "Verifying authentication..." (briefly)
- Session verified via AuthContext
- AuthGuard shows "Redirecting to login..." (if unauthorized)
- Or renders dashboard content
Loading State Resolution:
Initial: session=null, loading=true → AuthGuard shows "Verifying..."
Fetch completes: session=null, loading=false → AuthGuard redirects to login
Files Modified
apps/website/lib/auth/AuthContext.tsx- Fixed loading state managementapps/website/lib/gateways/AuthGateway.ts- Simplified isLoading logicapps/website/lib/blockers/AuthorizationBlocker.ts- Removed 'loading' reasonapps/website/lib/gateways/RouteGuard.tsx- Improved user feedbackapps/website/app/dashboard/page.tsx- Removed redundant auth checksapps/website/app/dashboard/layout.tsx- Added AuthGuard protectionapps/website/app/profile/layout.tsx- Added AuthGuard protectionapps/website/app/sponsor/layout.tsx- Added AuthGuard protectionapps/website/app/onboarding/layout.tsx- Added AuthGuard protectionapps/website/app/admin/layout.tsx- Added RouteGuard protection
Testing the Fix
Expected Behavior:
- Unauthenticated access: Redirects to login within 500ms
- Authenticated access: Shows dashboard after brief verification
- No infinite loading: Loading states resolve properly
Test Scenarios:
- Clear cookies, visit
/dashboard→ Should redirect to login - Login, visit
/dashboard→ Should show dashboard - Login, clear cookies, refresh → Should redirect to login
- Login as non-admin, visit
/admin→ Should redirect to login
Security Notes
- Defense in depth: Multiple protection layers (middleware + layout + page)
- No security bypass: All fixes maintain security requirements
- User experience: Clear feedback during authentication flow
- Performance: Minimal overhead, only necessary checks
Future Improvements
- Add role-based access to SessionViewModel
- Implement proper backend role system
- Add session refresh mechanism
- Implement proper token validation
- Add authentication state persistence