- Add timeout to auth check to prevent hanging on API failures - Add timeout to API requests to prevent infinite loading - Add proper error state and error messages to dashboard - Show empty states when no services/deployments are available - Update E2E tests to handle authentication properly - Improve user feedback when API is unavailable This fixes the "Loading..." hang issue when backend APIs are unavailable and provides better user experience with clear error messages and retry options. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
169 lines
4.7 KiB
TypeScript
169 lines
4.7 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState, createContext, useContext } from 'react';
|
|
import { getAccessTokenFromStorage, authApi, setAccessToken, setRefreshToken, clearAuthTokens, type MeResponse } from './api';
|
|
import { productId } from './product-config';
|
|
import { useRouter, usePathname } from 'next/navigation';
|
|
|
|
interface User {
|
|
id: string;
|
|
email: string;
|
|
role: string;
|
|
plan: string;
|
|
displayName: string;
|
|
products?: Array<{
|
|
productId: string;
|
|
plan: string;
|
|
role: string;
|
|
}>;
|
|
}
|
|
|
|
interface AuthContextType {
|
|
user: User | null;
|
|
loading: boolean;
|
|
login: (email: string, password: string, productId: string) => Promise<void>;
|
|
logout: () => void;
|
|
isAdmin: boolean;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
|
|
export function useAuth() {
|
|
const context = useContext(AuthContext);
|
|
if (context === undefined) {
|
|
throw new Error('useAuth must be used within an AuthProvider');
|
|
}
|
|
return context;
|
|
}
|
|
|
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [isAdmin, setIsAdmin] = useState(false);
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
// Skip auth check for login page
|
|
if (pathname === '/login') {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
checkAuth();
|
|
}, [pathname]);
|
|
|
|
useEffect(() => {
|
|
// Skip redirect for login page
|
|
if (pathname === '/login') {
|
|
return;
|
|
}
|
|
if (!loading && mounted && !user) {
|
|
router.push('/login');
|
|
}
|
|
}, [loading, mounted, user, router, pathname]);
|
|
|
|
// If on login page, render children without auth context
|
|
if (pathname === '/login') {
|
|
return <>{children}</>;
|
|
}
|
|
|
|
async function checkAuth() {
|
|
try {
|
|
const token = getAccessTokenFromStorage();
|
|
if (!token) {
|
|
console.log('No token found in storage');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
console.log('Checking auth with token...');
|
|
|
|
// Add timeout to prevent hanging
|
|
const timeoutPromise = new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error('Auth check timeout')), 10000)
|
|
);
|
|
|
|
const userData = await Promise.race([
|
|
authApi.me(token),
|
|
timeoutPromise
|
|
]) as MeResponse;
|
|
|
|
console.log('User data received:', userData);
|
|
setUser(userData);
|
|
|
|
// Simplified admin check - just check global admin role
|
|
const globalRole = userData.role;
|
|
const hasAdminAccess = globalRole === 'admin';
|
|
setIsAdmin(hasAdminAccess);
|
|
|
|
console.log('Admin access:', hasAdminAccess);
|
|
} catch (error) {
|
|
console.error('Auth check failed:', error);
|
|
clearAuthTokens();
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function login(email: string, password: string, productId: string) {
|
|
try {
|
|
console.log('Attempting login for:', email, 'with productId:', productId);
|
|
const response = await authApi.login({ email, password, productId });
|
|
console.log('Login response received:', response);
|
|
|
|
setAccessToken(response.accessToken);
|
|
setRefreshToken(response.refreshToken);
|
|
setUser(response.user);
|
|
|
|
// Check if user has admin access (global admin role)
|
|
const hasAdminAccess = response.user.role === 'admin';
|
|
setIsAdmin(hasAdminAccess);
|
|
|
|
console.log('Login successful, admin access:', hasAdminAccess);
|
|
} catch (error) {
|
|
console.error('Login failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function logout() {
|
|
clearAuthTokens();
|
|
setUser(null);
|
|
setIsAdmin(false);
|
|
router.push('/');
|
|
}
|
|
|
|
// Prevent hydration mismatch by not rendering until mounted
|
|
if (!mounted) {
|
|
return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
|
|
}
|
|
|
|
if (loading) {
|
|
return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
|
|
}
|
|
|
|
if (!user) {
|
|
return <div className="min-h-screen flex items-center justify-center">Redirecting to login...</div>;
|
|
}
|
|
|
|
if (user && !isAdmin) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<div className="text-center">
|
|
<h1 className="text-2xl font-bold text-red-600 mb-2">Access Denied</h1>
|
|
<p className="text-gray-600">You need admin privileges to access the DevOps dashboard.</p>
|
|
<p className="text-sm text-gray-500 mt-2">Your role: {user.role}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AuthContext.Provider value={{ user, loading, login, logout, isAdmin }}>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
}
|