AgentGraph / frontend /src /context /AuthContext.tsx
wu981526092's picture
Debug: Enhanced HF Spaces environment detection
121ec8d
raw
history blame
8.14 kB
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;
}