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