import React, { createContext, useContext, useReducer, useEffect, ReactNode, useMemo, } from "react"; // Removed @huggingface/hub dependency - using direct OAuth redirect instead import { AuthState, AuthContextType, OAuthResult, UserInfo, } from "@/types/auth"; import { useModal } from "./ModalContext"; type AuthAction = | { type: "AUTH_START" } | { type: "AUTH_SUCCESS"; payload: OAuthResult } | { type: "AUTH_ERROR"; payload: string } | { type: "AUTH_LOGOUT" } | { type: "SET_LOADING"; payload: boolean }; const initialState: AuthState = { isAuthenticated: false, user: null, accessToken: null, accessTokenExpiresAt: null, scope: null, isLoading: true, error: null, }; function authReducer(state: AuthState, action: AuthAction): AuthState { switch (action.type) { case "AUTH_START": return { ...state, isLoading: true, error: null, }; case "AUTH_SUCCESS": return { ...state, isAuthenticated: true, user: action.payload.userInfo, accessToken: action.payload.accessToken, accessTokenExpiresAt: action.payload.accessTokenExpiresAt instanceof Date ? action.payload.accessTokenExpiresAt.toISOString() : action.payload.accessTokenExpiresAt, scope: action.payload.scope, isLoading: false, error: null, }; case "AUTH_ERROR": return { ...state, isAuthenticated: false, user: null, accessToken: null, accessTokenExpiresAt: null, scope: null, isLoading: false, error: action.payload, }; case "AUTH_LOGOUT": return { ...initialState, isLoading: false, }; case "SET_LOADING": return { ...state, isLoading: action.payload, }; default: return state; } } const AuthContext = createContext(undefined); const STORAGE_KEY = "agentgraph_oauth"; // OAuth result handling is now done directly in checkAuthStatus export function AuthProvider({ children }: { children: ReactNode }) { const [authState, dispatch] = useReducer(authReducer, initialState); const { openModal } = useModal(); // Check for existing auth on mount useEffect(() => { checkAuthStatus(); }, []); const checkAuthStatus = async () => { try { dispatch({ type: "SET_LOADING", payload: true }); // First check backend session for existing authentication try { const response = await fetch("/auth/status", { credentials: "include", // Include session cookies }); if (response.ok) { const statusData = await response.json(); if (statusData.authenticated && statusData.user) { console.log("🔓 Found existing backend session authentication"); // Convert backend user format to our OAuth format const oauthResult: OAuthResult = { userInfo: { id: statusData.user.id, name: statusData.user.name || statusData.user.username, fullname: statusData.user.name || statusData.user.username, email: statusData.user.email, emailVerified: false, avatarUrl: statusData.user.avatar_url, orgs: [], isPro: false, }, accessToken: statusData.user.access_token || "session_token", accessTokenExpiresAt: new Date( Date.now() + 24 * 60 * 60 * 1000 ).toISOString(), // 24 hours scope: "openid profile read-repos", // Backend authenticated users have these scopes }; dispatch({ type: "AUTH_SUCCESS", payload: oauthResult }); // Also save to localStorage for consistency localStorage.setItem(STORAGE_KEY, JSON.stringify(oauthResult)); return; } } } catch (error) { console.log("🔍 No backend session found, checking localStorage"); } // Second, check localStorage for existing oauth data const stored = localStorage.getItem(STORAGE_KEY); if (stored) { try { const oauthResult = JSON.parse(stored) as OAuthResult; // Check if token is still valid const expiresAt = new Date(oauthResult.accessTokenExpiresAt); if (expiresAt > new Date()) { dispatch({ type: "AUTH_SUCCESS", payload: oauthResult }); return; } else { // Token expired, remove from storage localStorage.removeItem(STORAGE_KEY); } } catch (error) { localStorage.removeItem(STORAGE_KEY); } } // Check for OAuth redirect (standard OAuth code parameter) const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get("code"); const state = urlParams.get("state"); const storedState = localStorage.getItem("oauth_state"); if (code && state) { console.log( "🔐 OAuth callback detected - backend should have handled this" ); // Clear OAuth state from localStorage localStorage.removeItem("oauth_state"); // Clean up URL parameters since backend handled the OAuth window.history.replaceState( {}, document.title, window.location.pathname ); // Recheck auth status to pick up the backend session setTimeout(() => { checkAuthStatus(); }, 100); return; } else { // No OAuth redirect found, check if we need to show auth modal // Enhanced HF Spaces detection with debugging const isHFSpaces = // Method 1: Check for window.huggingface (window.huggingface && window.huggingface.variables) || // Method 2: Check for HF-specific URL patterns window.location.hostname.includes("hf.space") || window.location.hostname.includes("huggingface.co") || // Method 3: Check for HF-specific environment indicators document.querySelector('meta[name="hf:space"]') !== null; // Debug logging (remove in production) console.log("🔍 HF Spaces Environment Check:", { hostname: window.location.hostname, hasHuggingface: !!window.huggingface, hasVariables: !!(window.huggingface && window.huggingface.variables), hasHfMeta: !!document.querySelector('meta[name="hf:space"]'), isHFSpaces, href: window.location.href, huggingfaceObject: window.huggingface, variables: window.huggingface?.variables, oauthScopes: window.huggingface?.variables?.OAUTH_SCOPES, }); if (isHFSpaces) { // In HF Spaces, we require authentication - show the modal console.log("🔐 HF Spaces detected - showing auth modal"); dispatch({ type: "SET_LOADING", payload: false }); openModal("auth-login", "Sign in to AgentGraph"); } else { // In local development, no auth required console.log("🏠 Local development detected - no auth required"); dispatch({ type: "SET_LOADING", payload: false }); } } } catch (error) { console.error("Auth check failed:", error); dispatch({ type: "AUTH_ERROR", payload: "Failed to check authentication status", }); } }; const login = async () => { console.log("🚀 login() function called"); try { dispatch({ type: "AUTH_START" }); console.log("📤 Dispatched AUTH_START"); // Check if we're in HF Spaces environment const isHFSpaces = (window.huggingface && window.huggingface.variables) || window.location.hostname.includes("hf.space") || window.location.hostname.includes("huggingface.co") || document.querySelector('meta[name="hf:space"]') !== null; console.log("🔍 HF Spaces detection in login():", { isHFSpaces, hostname: window.location.hostname, hasHuggingface: !!window.huggingface, }); if (isHFSpaces) { // In HF Spaces, get OAuth config from backend API console.log("🔐 Getting OAuth config from backend"); try { console.log("📡 Fetching /auth/oauth-config..."); const response = await fetch("/auth/oauth-config"); console.log("📡 Fetch response status:", response.status); const oauthConfig = await response.json(); console.log("📡 OAuth config response:", oauthConfig); if (!oauthConfig.oauth_enabled) { throw new Error("OAuth not enabled or configured on backend"); } console.log("✅ OAuth config received:", { client_id: oauthConfig.client_id?.substring(0, 8) + "...", scopes: oauthConfig.scopes, provider_url: oauthConfig.provider_url, }); // Direct OAuth URL construction with backend-provided client_id const baseUrl = window.location.origin; const redirectUri = `${baseUrl}/auth/oauth-callback`; // Create state for CSRF protection const state = Math.random().toString(36).substring(2, 15); localStorage.setItem("oauth_state", state); // Use HF OAuth endpoint with proper client_id from backend const oauthUrl = `${oauthConfig.provider_url}/oauth/authorize?` + `response_type=code&` + `client_id=${encodeURIComponent(oauthConfig.client_id)}&` + `redirect_uri=${encodeURIComponent(redirectUri)}&` + `scope=${encodeURIComponent(oauthConfig.scopes)}&` + `state=${state}&` + `prompt=consent`; console.log("🔄 Redirecting to HF OAuth:", oauthUrl.substring(0, 100) + "..."); window.location.href = oauthUrl; } catch (configError) { console.error( "❌ Failed to get OAuth config from backend:", configError ); throw new Error("OAuth configuration not available from backend"); } } else { // For local development, show a message or redirect to HF console.log("⚠️ Not in HF Spaces, dispatching AUTH_ERROR"); dispatch({ type: "AUTH_ERROR", payload: "Authentication is only available when deployed to Hugging Face Spaces", }); } } catch (error) { console.error("❌ Login failed:", error); dispatch({ type: "AUTH_ERROR", payload: "Failed to initiate login", }); } }; const logout = () => { localStorage.removeItem(STORAGE_KEY); dispatch({ type: "AUTH_LOGOUT" }); // Optionally redirect to clear URL params const url = new URL(window.location.href); url.search = ""; window.history.replaceState({}, "", url.toString()); }; const requireAuth = () => { if (!authState.isAuthenticated) { openModal("auth-login", "Sign in to AgentGraph"); } }; const contextValue = useMemo( () => ({ authState, login, logout, requireAuth, checkAuthStatus, }), [authState] ); return ( {children} ); } export function useAuth() { const context = useContext(AuthContext); if (context === undefined) { throw new Error("useAuth must be used within an AuthProvider"); } return context; }