Spaces:
Running
Running
| import React, { | |
| createContext, | |
| useContext, | |
| useReducer, | |
| useEffect, | |
| ReactNode, | |
| useMemo, | |
| } from "react"; | |
| import { oauthLoginUrl, oauthHandleRedirectIfPresent } from "@huggingface/hub"; | |
| import type { OAuthResult as HFOAuthResult } from "@huggingface/hub"; | |
| 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"; | |
| // Convert HF OAuth result to our internal format | |
| function convertHFOAuthResult(hfResult: HFOAuthResult): OAuthResult { | |
| const userInfo = hfResult.userInfo as any; // Type assertion for flexibility | |
| return { | |
| accessToken: hfResult.accessToken, | |
| accessTokenExpiresAt: | |
| hfResult.accessTokenExpiresAt instanceof Date | |
| ? hfResult.accessTokenExpiresAt.toISOString() | |
| : hfResult.accessTokenExpiresAt, | |
| userInfo: { | |
| id: userInfo.sub || userInfo.id || "unknown", | |
| name: userInfo.name || userInfo.preferred_username || "Unknown User", | |
| fullname: userInfo.preferred_username, | |
| email: userInfo.email, | |
| emailVerified: userInfo.email_verified, | |
| avatarUrl: userInfo.picture, | |
| isPro: userInfo.isPro, | |
| orgs: userInfo.orgs, | |
| }, | |
| scope: hfResult.scope, | |
| }; | |
| } | |
| 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 | |
| const hfOauthResult = await oauthHandleRedirectIfPresent(); | |
| if (hfOauthResult) { | |
| // Convert HF result to our internal format | |
| const normalizedResult = convertHFOAuthResult(hfOauthResult); | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(normalizedResult)); | |
| dispatch({ type: "AUTH_SUCCESS", payload: normalizedResult }); | |
| } 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 | |
| }); | |
| 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) { | |
| // Use HF OAuth | |
| const scopes = | |
| window.huggingface?.variables?.OAUTH_SCOPES || | |
| "openid profile read-repos"; | |
| const loginUrl = await oauthLoginUrl({ scopes }); | |
| window.location.href = loginUrl + "&prompt=consent"; | |
| } 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; | |
| } | |