AgentGraph / frontend /src /context /AuthContext.tsx
wu981526092's picture
Fix: Login button debugging and YouTube iframe lazy-loading for HF Spaces
cceb270
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;
}