203 lines
6.6 KiB
Markdown
203 lines
6.6 KiB
Markdown
# 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:
|
|
|
|
1. **AuthContext**: Managed session state and loading flag
|
|
2. **AuthorizationBlocker**: Determined access reasons based on session state
|
|
3. **AuthGateway**: Combined context and blocker state
|
|
4. **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**:
|
|
```typescript
|
|
// 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
|
|
```typescript
|
|
// 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
|
|
```typescript
|
|
// 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
|
|
```typescript
|
|
// 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
|
|
```typescript
|
|
// 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:
|
|
1. User visits `/dashboard`
|
|
2. Middleware checks for `gp_session` cookie → not found
|
|
3. Middleware redirects to `/auth/login?returnTo=/dashboard`
|
|
4. User logs in
|
|
5. Session created, cookie set
|
|
6. Redirected back to `/dashboard`
|
|
7. AuthGuard verifies session exists
|
|
8. Dashboard loads
|
|
|
|
### Authenticated User:
|
|
1. User visits `/dashboard`
|
|
2. Middleware checks for `gp_session` cookie → found
|
|
3. Request proceeds to page rendering
|
|
4. AuthGuard shows "Verifying authentication..." (briefly)
|
|
5. Session verified via AuthContext
|
|
6. AuthGuard shows "Redirecting to login..." (if unauthorized)
|
|
7. 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
|
|
|
|
1. `apps/website/lib/auth/AuthContext.tsx` - Fixed loading state management
|
|
2. `apps/website/lib/gateways/AuthGateway.ts` - Simplified isLoading logic
|
|
3. `apps/website/lib/blockers/AuthorizationBlocker.ts` - Removed 'loading' reason
|
|
4. `apps/website/lib/gateways/RouteGuard.tsx` - Improved user feedback
|
|
5. `apps/website/app/dashboard/page.tsx` - Removed redundant auth checks
|
|
6. `apps/website/app/dashboard/layout.tsx` - Added AuthGuard protection
|
|
7. `apps/website/app/profile/layout.tsx` - Added AuthGuard protection
|
|
8. `apps/website/app/sponsor/layout.tsx` - Added AuthGuard protection
|
|
9. `apps/website/app/onboarding/layout.tsx` - Added AuthGuard protection
|
|
10. `apps/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:
|
|
1. Clear cookies, visit `/dashboard` → Should redirect to login
|
|
2. Login, visit `/dashboard` → Should show dashboard
|
|
3. Login, clear cookies, refresh → Should redirect to login
|
|
4. 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
|
|
|
|
1. Add role-based access to SessionViewModel
|
|
2. Implement proper backend role system
|
|
3. Add session refresh mechanism
|
|
4. Implement proper token validation
|
|
5. Add authentication state persistence |