Spaces:
Running
Running
| 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<AuthContextType | undefined>(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 ( | |
| <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider> | |
| ); | |
| } | |
| export function useAuth() { | |
| const context = useContext(AuthContext); | |
| if (context === undefined) { | |
| throw new Error("useAuth must be used within an AuthProvider"); | |
| } | |
| return context; | |
| } | |