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 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 && state === storedState) { console.log("🔐 OAuth callback detected, exchanging code for token"); // Clear OAuth state localStorage.removeItem("oauth_state"); // For HF Spaces direct OAuth, we'll simulate a successful auth // In a real implementation, you'd exchange the code for a token const mockUserInfo: UserInfo = { id: "hf_user_" + Date.now(), name: "Hugging Face User", fullname: "HF User", email: "user@example.com", emailVerified: false, avatarUrl: undefined, isPro: false, orgs: [], }; const normalizedResult: OAuthResult = { accessToken: `hf_mock_token_${Date.now()}`, accessTokenExpiresAt: new Date(Date.now() + 3600000).toISOString(), // 1 hour userInfo: mockUserInfo, scope: "openid profile read-repos", }; localStorage.setItem(STORAGE_KEY, JSON.stringify(normalizedResult)); dispatch({ type: "AUTH_SUCCESS", payload: normalizedResult }); // Clean up URL window.history.replaceState( {}, document.title, window.location.pathname ); } 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 () => { try { dispatch({ type: "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; if (isHFSpaces) { // In HF Spaces, get OAuth config from backend API console.log("🔐 Getting OAuth config from backend"); try { const response = await fetch('/api/auth/oauth-config'); const oauthConfig = await response.json(); 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}/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 with client_id"); 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 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; }