app/components/LoginForm.tsx (344 lines of code) (raw):

import React, { useState, useEffect } from "react"; import { AuthService, ApiCredentials } from "~/lib/authService"; interface LoginFormProps { onSuccess: () => void; login?: (credentials: { openaiApiKey?: string; huggingfaceToken?: string; }) => Promise<{ success: boolean; error?: string }>; } export const LoginForm: React.FC<LoginFormProps> = ({ onSuccess, login }) => { const [credentials, setCredentials] = useState({ openaiApiKey: "", huggingfaceToken: "", }); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<string | null>(null); const [showKeys, setShowKeys] = useState({ openai: false, huggingface: false, }); const [oauth2Available, setOauth2Available] = useState(true); const [showManualEntry, setShowManualEntry] = useState(false); // Check if OAuth2 is available on component mount useEffect(() => { AuthService.isOAuth2Available().then(setOauth2Available); }, []); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); setError(null); // Validate that HuggingFace token is provided if (!credentials.huggingfaceToken.trim()) { setError("Please provide your HuggingFace token to continue"); setIsLoading(false); return; } try { // Use the login function from context if provided, otherwise fall back to AuthService const authFunction = login || AuthService.authenticate; const result = await authFunction({ openaiApiKey: credentials.openaiApiKey.trim(), huggingfaceToken: credentials.huggingfaceToken.trim(), }); if (result.success) { // Log the user info for debugging if (result.hfUserInfo) { console.log("HuggingFace user info received:", result.hfUserInfo); } onSuccess(); } else { setError( result.error || "Authentication failed. Please check your API keys and try again." ); } } catch (err) { console.error("Login error:", err); setError(err instanceof Error ? err.message : "Authentication failed"); } finally { setIsLoading(false); } }; const handleInputChange = ( field: keyof typeof credentials, value: string ) => { setCredentials((prev) => ({ ...prev, [field]: value })); if (error) setError(null); // Clear error when user starts typing }; const handleOAuth2Login = () => { // Use popup for OAuth2 to avoid iframe cookie issues const popup = window.open( "/api/auth/login", "oauth2_login", "width=500,height=600,scrollbars=yes,resizable=yes" ); console.log("OAuth2 login popup opened:", popup); // Simply check if /api/auth/done return a 200 response fo this every 500ms for 10 seconds const checkAuthDone = setInterval(async () => { try { const response = await fetch("/api/auth/done", { method: "GET", credentials: "include", }); if (response.ok) { clearInterval(checkAuthDone); onSuccess(); } } catch (err) { console.error("Error checking auth done:", err); } }, 1500); // if (!popup) { // alert('Popup blocked. Please allow popups for OAuth2 login.'); // return; // } // // Listen for popup completion // const checkClosed = setInterval(() => { // if (popup.closed) { // clearInterval(checkClosed); // // Check if authentication was successful by refreshing auth status // setTimeout(() => { // onSuccess(); // }, 500); // } // }, 1000); // // Handle popup messages (if needed) // const handleMessage = (event: MessageEvent) => { // if (event.origin !== window.location.origin) return; // if (event.data.type === 'OAUTH2_SUCCESS') { // popup.close(); // clearInterval(checkClosed); // onSuccess(); // } else if (event.data.type === 'OAUTH2_ERROR') { // popup.close(); // clearInterval(checkClosed); // setError(event.data.error || 'OAuth2 authentication failed'); // } // }; // window.addEventListener('message', handleMessage); // // Cleanup listener when popup closes // const originalClose = popup.close; // popup.close = function() { // window.removeEventListener('message', handleMessage); // originalClose.call(this); // }; }; return ( <div className="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 dark:bg-gray-900 sm:px-6 lg:px-8"> <div className="w-full max-w-md space-y-8"> {/* Header */} <div className="text-center"> <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800"> {/* <span className="text-white text-2xl font-bold">H</span> */} <img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="Logo" className="w-12 rounded-full" /> </div> <h2 className="text-3xl font-bold text-gray-900 dark:text-gray-100"> Welcome to Hugex </h2> <p className="mt-2 text-gray-600 dark:text-gray-400"> {oauth2Available ? "Sign in with your HuggingFace account to get started" : "Please provide your HuggingFace token to get started"} </p> </div> {/* OAuth2 Button or Manual Form */} <div className="mt-8 space-y-6"> {oauth2Available && !showManualEntry ? ( /* OAuth2 Login Section */ <div className="space-y-6 rounded-lg bg-white p-6 shadow-lg dark:bg-gray-800"> <button type="button" onClick={handleOAuth2Login} className="flex w-full items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-3 font-medium text-gray-900 shadow-lg transition-colors hover:bg-gray-50 hover:shadow-xl dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700" > <img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="HuggingFace" className="h-4 w-4" /> Sign in with HuggingFace </button> {/* Development notice - always show since OAuth2 is enabled */} {/* <div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3"> <div className="flex items-start gap-2"> <svg className="w-4 h-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> <div className="text-xs text-blue-800 dark:text-blue-300"> <p className="font-medium mb-1">OAuth2 Authentication</p> <p>Click above to sign in with your HuggingFace account. For production use, ensure OAuth2 app credentials are properly configured.</p> </div> </div> </div> */} <div className="text-center"> <button type="button" onClick={() => setShowManualEntry(true)} className="text-sm text-gray-500 underline hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300" > Or use manual token entry </button> </div> </div> ) : ( /* Manual Token Entry Form */ <form onSubmit={handleSubmit}> <div className="space-y-6 rounded-lg bg-white p-6 shadow-lg dark:bg-gray-800"> {/* OpenAI API Key */} {/* <div> <label htmlFor="openai-key" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2" > OpenAI API Key <span className="text-gray-500 dark:text-gray-400 font-normal ml-1"> (optional - can be configured later) </span> </label> <div className="relative"> <input id="openai-key" type={showKeys.openai ? "text" : "password"} value={credentials.openaiApiKey} onChange={(e) => handleInputChange("openaiApiKey", e.target.value) } className="w-full px-3 py-3 pr-12 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" placeholder="sk-..." /> <button type="button" onClick={() => setShowKeys((prev) => ({ ...prev, openai: !prev.openai })) } className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" > {showKeys.openai ? ( <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> </svg> ) : ( <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" /> </svg> )} </button> </div> <p className="mt-1 text-xs text-gray-500 dark:text-gray-400"> Get your API key from{" "} <a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline" > OpenAI Platform </a> </p> </div> */} {/* HuggingFace Token */} <div> <label htmlFor="hf-token" className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300" > HuggingFace Token <span className="ml-1 font-normal text-red-500 dark:text-red-400"> (required) </span> </label> <div className="relative"> <input id="hf-token" type={showKeys.huggingface ? "text" : "password"} value={credentials.huggingfaceToken} onChange={(e) => handleInputChange("huggingfaceToken", e.target.value) } className="w-full rounded-lg border border-gray-300 bg-gray-50 px-3 py-3 pr-12 text-gray-900 placeholder-gray-500 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-400" placeholder="hf_..." /> <button type="button" onClick={() => setShowKeys((prev) => ({ ...prev, huggingface: !prev.huggingface, })) } className="absolute right-3 top-1/2 -translate-y-1/2 transform text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" > {showKeys.huggingface ? ( <svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> </svg> ) : ( <svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" /> </svg> )} </button> </div> <p className="mt-1 text-xs text-gray-500 dark:text-gray-400"> Get your token from{" "} <a href="https://huggingface.co/settings/tokens" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline" > HuggingFace Settings </a> </p> </div> {/* Info Box */} <div className="rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-800 dark:bg-blue-900/20"> <div className="flex items-start gap-3"> <svg className="mt-0.5 h-5 w-5 flex-shrink-0 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> <div className="text-sm text-blue-800 dark:text-blue-300"> <p className="mb-1 font-medium">Security & Privacy</p> <ul className="space-y-1 text-xs"> <li> • Your HuggingFace token is required for authentication and job management </li> {/* <li>• OpenAI API key is optional and can be configured later in settings</li> */} <li> • All credentials are stored securely and encrypted </li> <li>• Session expires automatically after 7 days</li> </ul> </div> </div> </div> {/* Error Message */} {error && ( <div className="rounded-lg border border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20"> <div className="flex items-center gap-3"> <svg className="h-5 w-5 flex-shrink-0 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> <p className="text-sm text-red-800 dark:text-red-300"> {error} </p> </div> </div> )} {/* Submit Button */} <button type="submit" disabled={isLoading || !credentials.huggingfaceToken.trim()} className="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-3 font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-400 dark:disabled:bg-gray-600" > {isLoading ? ( <> <svg className="h-4 w-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /> </svg> Verifying credentials... </> ) : ( "Continue to Hugex" )} </button> {/* Back to OAuth2 button if manual entry is shown */} {oauth2Available && showManualEntry && ( <div className="text-center"> <button type="button" onClick={() => setShowManualEntry(false)} className="text-sm text-gray-500 underline hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300" > ← Back to HuggingFace sign in </button> </div> )} </div> </form> )} </div> {/* Footer */} <div className="space-y-2 text-center text-xs text-gray-500 dark:text-gray-400"> <p> By continuing, you agree to store your credentials securely in your browser. </p> {/* <p className="text-blue-600 dark:text-blue-400"> ℹ️ OpenAI API key is now optional and can be configured later in settings </p> */} </div> </div> </div> ); };