bytelyst-devops-tools/dashboard/web/src/lib/auth.tsx
Hermes VM 31b414d62b fix: systematic bug fixes — code-quality parser, env key, config warnings, auth cleanup, deployment safety
- code-quality/repository.ts: fix tsErrorMatch[3] → [4] for type field (group 3 is column, 4 is error|warning)
- code-quality/repository.ts: fix ESLint regex to make rule brackets optional (not all formatters include them)
- code-quality/repository.ts: fix Vitest test count — parse 'Tests' line (individual tests) instead of 'Test Files' (file count); improve Jest regex to capture pass/fail independently
- env/repository.ts: replace raw process.env.ENCRYPTION_KEY with config.ENCRYPTION_KEY so the validated default flows through a single source of truth
- config.ts: add startup console.warn when CSRF_SECRET or ENCRYPTION_KEY are using insecure defaults
- deployments/orchestrator.ts: refactor runDeploymentScript to use try/catch/finally — deployment record is now always written in the finally block, preventing zombie 'running' states if updateDeployment itself throws
- auth.tsx: remove dead 'user &&' guard (user is always truthy after the !user check above); remove debug console.log calls, keep console.error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 18:53:20 +00:00

159 lines
4.2 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) {
setLoading(false);
return;
}
// 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;
setUser(userData);
// Simplified admin check - just check global admin role
const globalRole = userData.role;
const hasAdminAccess = globalRole === 'admin';
setIsAdmin(hasAdminAccess);
} catch (error) {
console.error('Auth check failed:', error);
clearAuthTokens();
} finally {
setLoading(false);
}
}
async function login(email: string, password: string, productId: string) {
try {
const response = await authApi.login({ email, password, productId });
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);
} 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 (!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>
);
}