bytelyst-devops-tools/dashboard/web/src/lib/auth.tsx
root dcf7ecbb32 fix(devops-web): resolve rendering issues and improve error handling
- 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>
2026-05-11 03:04:36 +00:00

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>
);
}