app/components/AuthWrapper.tsx (570 lines of code) (raw):
import React, { useEffect, useState } from "react";
import { useAuth } from "~/lib/authContext";
import { LoginForm } from "./LoginForm";
import { AuthHeader } from "./AuthHeader";
import { ConfigService } from "~/lib/configService";
import { AuthService } from "~/lib/authService";
import { useTheme } from "~/lib/theme";
interface AuthWrapperProps {
children: React.ReactNode;
}
export const AuthWrapper: React.FC<AuthWrapperProps> = ({ children }) => {
// Use the centralized auth context instead of local state
const { authStatus, userInfo, isLoading, login, logout, refreshAuth } =
useAuth();
const [executionMode, setExecutionMode] = useState<string | null>(null);
const [executionModeLoading, setExecutionModeLoading] = useState(true);
const [autoLoginAttempted, setAutoLoginAttempted] = useState(false);
// Auto-login effect for development (GitHub only)
useEffect(() => {
const attemptGitHubAutoLogin = async () => {
if (autoLoginAttempted || process.env.NODE_ENV !== "development") {
return;
}
try {
// Only try GitHub auto-connect if we have a token
const response = await fetch("/api/auth/github/auto-connect", {
method: "POST",
headers: { "Content-Type": "application/json" },
});
if (response.ok) {
const result = await response.json();
console.log(`✅ GitHub auto-connected using ${result.source}`);
await refreshAuth();
}
} catch (error) {
// Silently fail - no need to log errors for missing tokens
} finally {
setAutoLoginAttempted(true);
}
};
attemptGitHubAutoLogin();
}, [autoLoginAttempted, refreshAuth]);
// Check execution mode on component mount
useEffect(() => {
const checkExecutionMode = async () => {
try {
const modeConfig = await ConfigService.getExecutionMode();
setExecutionMode(modeConfig.mode);
} catch (error) {
console.error("Failed to get execution mode:", error);
// Default to 'api' if we can't determine the mode
setExecutionMode("api");
} finally {
setExecutionModeLoading(false);
}
};
checkExecutionMode();
}, []);
const handleLoginSuccess = async () => {
console.log("Login successful, refreshing auth status...");
// Use the context's refreshAuth method
await refreshAuth();
};
const handleLogout = async () => {
console.log("Logging out...");
await logout();
};
// Show loading state while checking execution mode or auth
if (isLoading || executionModeLoading) {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900">
<div className="text-center">
{/* <div className="w-16 h-16 bg-blue-600 rounded-full flex items-center justify-center mx-auto mb-4 animate-pulse">
<span className="text-white text-2xl font-bold">H</span>
</div> */}
<div className="mx-auto mb-4 flex flex h-16 h-8 w-16 w-8 animate-pulse items-center items-center justify-center justify-center rounded-full rounded-full">
<img
src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
alt="Logo"
className="w-16 rounded-full"
/>
</div>
<p className="text-gray-600 dark:text-gray-400">Loading...</p>
</div>
</div>
);
}
// If using Docker execution mode, show simplified auth header
if (executionMode === "docker") {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<DockerModeHeader />
{children}
</div>
);
}
// For API mode, require authentication
// Show login form if not authenticated
if (!authStatus.isAuthenticated || !authStatus.hasHuggingFace) {
return <LoginForm onSuccess={handleLoginSuccess} login={login} />;
}
// Show authenticated app for API mode
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<AuthHeader
authStatus={authStatus}
userInfo={userInfo}
onLogout={handleLogout}
/>
{children}
</div>
);
};
// Docker Mode Header Component with GitHub authentication
const DockerModeHeader: React.FC = () => {
const { theme, setTheme } = useTheme();
const [dockerAuthStatus, setDockerAuthStatus] = useState({
hasGitHub: false,
githubUserInfo: null as any,
});
const [showGitHubModal, setShowGitHubModal] = useState(false);
const [githubConfig, setGithubConfig] = useState<any>(null);
useEffect(() => {
// Check GitHub configuration
checkGitHubConfig();
// Check current auth status for GitHub
checkDockerAuthStatus();
// Listen for window focus to refresh auth status
const handleFocus = () => {
checkDockerAuthStatus();
};
window.addEventListener("focus", handleFocus);
return () => {
window.removeEventListener("focus", handleFocus);
};
}, []);
const checkGitHubConfig = async () => {
try {
const response = await fetch("/api/auth/github/config");
if (response.ok) {
const config = await response.json();
setGithubConfig(config);
} else {
// Default config if endpoint doesn't exist yet
setGithubConfig({
methods: {
oauth: { available: false, recommended: false },
pat: { available: true, recommended: true },
},
defaultMethod: "pat",
showBothOptions: false,
isDevelopment: true,
});
}
} catch (error) {
console.error("Error checking GitHub config:", error);
// Default to PAT-only mode
setGithubConfig({
methods: {
oauth: { available: false, recommended: false },
pat: { available: true, recommended: true },
},
defaultMethod: "pat",
showBothOptions: false,
isDevelopment: true,
});
}
};
const checkDockerAuthStatus = async () => {
try {
const authStatus = await AuthService.getAuthStatus();
setDockerAuthStatus({
hasGitHub: authStatus.hasGitHub || false,
githubUserInfo: authStatus.githubUserInfo,
});
} catch (error) {
console.error("Error checking Docker auth status:", error);
}
};
const handleGitHubConnect = async (
method: "oauth" | "pat",
token?: string
) => {
if (method === "oauth") {
AuthService.startGitHubOAuth2Login();
// Refresh auth status after a delay to catch the GitHub connection
setTimeout(() => {
checkDockerAuthStatus();
}, 3000);
} else if (method === "pat" && token) {
// Handle PAT connection
connectWithPAT(token);
}
};
const tryAutoConnectGitHub = async () => {
try {
const response = await fetch("/api/auth/github/auto-connect", {
method: "POST",
headers: { "Content-Type": "application/json" },
});
if (response.ok) {
const result = await response.json();
console.log(`✅ GitHub auto-connected using ${result.source}`);
checkDockerAuthStatus();
return true;
}
} catch (error) {
console.warn("GitHub auto-connect failed:", error);
}
return false;
};
const handleGitHubConnectClick = async () => {
// First try auto-connect with environment variables
const autoConnected = await tryAutoConnectGitHub();
// If auto-connect failed, show the manual connection modal
if (!autoConnected) {
setShowGitHubModal(true);
}
};
const connectWithPAT = async (token: string) => {
try {
const response = await fetch("/api/auth/github/connect-pat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token }),
});
if (response.ok) {
setShowGitHubModal(false);
checkDockerAuthStatus(); // Refresh the auth status
alert("✅ GitHub connected successfully!");
} else {
const error = await response.json();
alert(`❌ Failed to connect: ${error.error || "Unknown error"}`);
}
} catch (error) {
console.error("PAT connection failed:", error);
alert("❌ Connection failed. Please try again.");
}
};
const handleGitHubDisconnect = async () => {
// Show confirmation dialog
const confirmed = confirm(
"Are you sure you want to disconnect GitHub? You will lose access to private repositories until you reconnect."
);
if (!confirmed) return;
try {
// Clear the GitHub auth by logging out and refreshing
await AuthService.logout();
// Refresh the auth status to reflect the change
checkDockerAuthStatus();
// Refresh the page to ensure all components update
window.location.reload();
} catch (error) {
console.error("Error disconnecting GitHub:", error);
alert("Failed to disconnect GitHub. Please try again.");
}
};
return (
<header className="border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800">
<div className="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex items-center">
<img
src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
alt="Logo"
className="mr-3 h-8 w-8 rounded-full"
/>
<h1 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
hugex
</h1>
<span className="ml-2 rounded-full bg-green-100 px-2 py-1 text-xs text-green-800 dark:bg-green-900 dark:text-green-200">
Docker Mode
</span>
</div>
</div>
<div className="flex items-center gap-4">
{/* Theme Toggle */}
<div className="flex items-center gap-1 rounded-lg bg-gray-100 p-1 dark:bg-gray-700">
<button
onClick={() => setTheme("light")}
className={`rounded p-1.5 text-xs transition-colors ${
theme === "light"
? "bg-white text-gray-900 shadow-sm dark:bg-gray-600 dark:text-gray-100"
: "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
}`}
title="Light mode"
>
<i className="fas fa-sun"></i>
</button>
<button
onClick={() => setTheme("dark")}
className={`rounded p-1.5 text-xs transition-colors ${
theme === "dark"
? "bg-white text-gray-900 shadow-sm dark:bg-gray-600 dark:text-gray-100"
: "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
}`}
title="Dark mode"
>
<i className="fas fa-moon"></i>
</button>
<button
onClick={() => setTheme("system")}
className={`rounded p-1.5 text-xs transition-colors ${
theme === "system"
? "bg-white text-gray-900 shadow-sm dark:bg-gray-600 dark:text-gray-100"
: "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
}`}
title="System preference"
>
<i className="fas fa-desktop"></i>
</button>
</div>
{/* GitHub Connection - ALWAYS SHOW */}
<div className="group relative">
{dockerAuthStatus.hasGitHub ? (
<button className="flex items-center gap-2 text-sm text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100">
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-green-100 dark:bg-green-900">
<svg
className="h-3 w-3 text-green-600 dark:text-green-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z"
clipRule="evenodd"
/>
</svg>
</div>
<span className="text-xs font-medium">
{dockerAuthStatus.githubUserInfo?.username || "GitHub"}
</span>
<svg
className="h-4 w-4 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
) : (
<button
onClick={handleGitHubConnectClick}
className="flex items-center gap-2 rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-700 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
>
<svg
className="h-4 w-4"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z"
clipRule="evenodd"
/>
</svg>
Connect GitHub
</button>
)}
{/* Dropdown for connected GitHub */}
{dockerAuthStatus.hasGitHub && (
<div className="invisible absolute right-0 top-full z-50 mt-2 w-64 rounded-lg border border-gray-200 bg-white opacity-0 shadow-lg transition-all duration-200 group-hover:visible group-hover:opacity-100 dark:border-gray-700 dark:bg-gray-800">
<div className="space-y-3 p-4">
<div className="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
GitHub Account
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">
Status:
</span>
<span className="font-medium text-green-600 dark:text-green-400">
Connected
</span>
</div>
{dockerAuthStatus.githubUserInfo && (
<>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">
Username:
</span>
<span className="font-medium text-gray-900 dark:text-gray-100">
@{dockerAuthStatus.githubUserInfo.username}
</span>
</div>
{dockerAuthStatus.githubUserInfo.name && (
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">
Name:
</span>
<span className="text-gray-900 dark:text-gray-100">
{dockerAuthStatus.githubUserInfo.name}
</span>
</div>
)}
</>
)}
</div>
<div className="border-t border-gray-200 pt-3 dark:border-gray-600">
<button
onClick={handleGitHubDisconnect}
className="mb-3 flex w-full items-center rounded px-2 py-2 text-left text-sm text-red-600 transition-colors hover:bg-red-50 hover:text-red-800 dark:text-red-400 dark:hover:bg-red-900/20 dark:hover:text-red-300"
>
<svg
className="mr-2 h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
Disconnect GitHub
</button>
<p className="text-xs text-gray-500 dark:text-gray-400">
Access private repositories for coding tasks
</p>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>
{/* GitHub Connection Modal */}
{showGitHubModal && (
<GitHubConnectionModal
onClose={() => setShowGitHubModal(false)}
onConnect={handleGitHubConnect}
githubConfig={githubConfig}
/>
)}
</header>
);
};
// Simple GitHub Connection Modal Component
const GitHubConnectionModal: React.FC<{
onClose: () => void;
onConnect: (method: "oauth" | "pat", token?: string) => void;
githubConfig: any;
}> = ({ onClose, onConnect, githubConfig }) => {
const [method, setMethod] = useState<"oauth" | "pat">(
githubConfig?.defaultMethod || "pat"
);
const [patToken, setPATToken] = useState("");
const [isValidating, setIsValidating] = useState(false);
const handlePATConnect = async () => {
if (!patToken.trim()) return;
setIsValidating(true);
try {
// Validate the PAT by making a test API call
const response = await fetch("/api/auth/github/validate-pat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: patToken }),
});
if (response.ok) {
onConnect("pat", patToken);
onClose();
} else {
const error = await response.json();
alert(
`Invalid GitHub token: ${error.error || "Please check your token and try again."}`
);
}
} catch (error) {
alert("Failed to validate token. Please try again.");
} finally {
setIsValidating(false);
}
};
const oauthAvailable = githubConfig?.methods?.oauth?.available || false;
const patAvailable = githubConfig?.methods?.pat?.available !== false; // Default to true
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 p-4">
<div className="w-full max-w-md rounded-lg bg-white p-6 dark:bg-gray-800">
<h2 className="mb-4 text-xl font-semibold text-gray-900 dark:text-gray-100">
Connect GitHub Account
</h2>
{/* Method Selection */}
{oauthAvailable && patAvailable && (
<div className="mb-4">
<div className="mb-3 flex space-x-2">
{oauthAvailable && (
<button
onClick={() => setMethod("oauth")}
className={`rounded px-3 py-2 text-sm ${
method === "oauth"
? "bg-blue-500 text-white"
: "bg-gray-200 text-gray-700 dark:bg-gray-600 dark:text-gray-300"
}`}
>
OAuth
</button>
)}
<button
onClick={() => setMethod("pat")}
className={`rounded px-3 py-2 text-sm ${
method === "pat"
? "bg-blue-500 text-white"
: "bg-gray-200 text-gray-700 dark:bg-gray-600 dark:text-gray-300"
}`}
>
Personal Access Token (Recommended)
</button>
</div>
</div>
)}
{/* OAuth Method */}
{method === "oauth" && oauthAvailable && (
<div>
<p className="mb-4 text-gray-600 dark:text-gray-400">
Use GitHub OAuth for secure authentication. This will redirect you
to GitHub to authorize access to your repositories.
</p>
<button
onClick={() => onConnect("oauth")}
className="w-full rounded bg-gray-900 px-4 py-2 text-white hover:bg-gray-800"
>
Sign in with GitHub
</button>
</div>
)}
{/* PAT Method */}
{method === "pat" && (
<div>
<p className="mb-3 text-gray-600 dark:text-gray-400">
Enter a GitHub Personal Access Token with{" "}
<code className="rounded bg-gray-100 px-1 dark:bg-gray-700">
repo
</code>{" "}
scope.
</p>
<div className="mb-3">
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
Personal Access Token
</label>
<input
type="password"
value={patToken}
onChange={(e) => setPATToken(e.target.value)}
placeholder="ghp_xxxxxxxxxxxxxxxxxxxx"
className="w-full rounded border border-gray-300 px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100"
/>
</div>
<div className="mb-4 text-xs text-gray-500 dark:text-gray-400">
<a
href="https://github.com/settings/tokens/new?scopes=repo&description=HugeX%20Local%20Development"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
Create a new token here
</a>{" "}
with "repo" scope selected.
</div>
<button
onClick={handlePATConnect}
disabled={!patToken.trim() || isValidating}
className="w-full rounded bg-green-600 px-4 py-2 text-white hover:bg-green-700 disabled:bg-gray-400"
>
{isValidating ? "Validating..." : "Connect with Token"}
</button>
</div>
)}
{/* OAuth Not Available Warning */}
{method === "oauth" && !oauthAvailable && (
<div className="mb-4 rounded bg-amber-50 p-3 text-amber-600 dark:bg-amber-900/20">
<p className="text-sm">
OAuth is not configured for this instance. Please use a Personal
Access Token or ask your administrator to configure GitHub OAuth.
</p>
</div>
)}
<button
onClick={onClose}
className="mt-3 w-full rounded bg-gray-200 px-4 py-2 text-gray-700 hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-300 dark:hover:bg-gray-500"
>
Cancel
</button>
</div>
</div>
);
};