/** * Hugging Face OAuth Authentication and Space Management * Uses @huggingface/hub library for OAuth flow * * Note: The public space now supports secure multi-user connections with per-user isolation. * Cloning a private space is optional - most users can use the public space safely. */ import { oauthLoginUrl, oauthHandleRedirectIfPresent, uploadFile, createRepo } from "https://cdn.jsdelivr.net/npm/@huggingface/hub@0.15.2/+esm"; const HF_API = 'https://huggingface.co/api'; const NEW_SPACE_NAME = 'reachy_mini_remote_control'; // State management let currentUser = null; let userToken = null; let oauthResult = null; /** * Initialize OAuth and check authentication status */ async function initAuth() { try { // Check if we're returning from OAuth redirect oauthResult = await oauthHandleRedirectIfPresent(); if (oauthResult) { // User just authenticated via OAuth console.log('OAuth result:', oauthResult); console.log('User info:', oauthResult.userInfo); console.log('All userInfo keys:', Object.keys(oauthResult.userInfo)); // Extract username from fullname field (HuggingFace uses fullname for the username) currentUser = oauthResult.userInfo.fullname; userToken = oauthResult.accessToken; console.log('Extracted username:', currentUser); console.log('Extracted token:', userToken ? 'present' : 'missing'); // Store for session (not persistent across page reloads by design) sessionStorage.setItem('hf_token', userToken); sessionStorage.setItem('hf_username', currentUser); sessionStorage.setItem('hf_token_expires', oauthResult.accessTokenExpiresAt); console.log('OAuth authentication successful:', currentUser); showAuthenticatedView(); await checkForExistingSpace(); } else { // Check if we have a stored session const storedToken = sessionStorage.getItem('hf_token'); const storedUser = sessionStorage.getItem('hf_username'); const tokenExpires = sessionStorage.getItem('hf_token_expires'); if (storedToken && storedUser && tokenExpires && new Date(tokenExpires) > new Date()) { // Valid session exists userToken = storedToken; currentUser = storedUser; showAuthenticatedView(); await checkForExistingSpace(); } } } catch (error) { console.error('OAuth initialization error:', error); // Show login view on error showLoginView(); } } /** * Redirect to Hugging Face OAuth login */ async function loginToHuggingFace() { try { // Generate OAuth login URL and redirect const loginUrl = await oauthLoginUrl(); window.location.href = loginUrl; } catch (error) { console.error('OAuth login error:', error); alert('Failed to initiate login. Please try again.'); } } /** * Show the login view */ function showLoginView() { document.getElementById('login-view').style.display = 'block'; document.getElementById('authenticated-view').style.display = 'none'; } /** * Show authenticated user view */ function showAuthenticatedView() { console.log('Showing authenticated view for user:', currentUser); document.getElementById('login-view').style.display = 'none'; document.getElementById('authenticated-view').style.display = 'block'; // Display username (from fullname field) document.getElementById('username').textContent = `@${currentUser}`; } /** * Check if user already has the space */ async function checkForExistingSpace() { try { const response = await fetch(`${HF_API}/spaces/${currentUser}/${NEW_SPACE_NAME}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); if (response.ok) { showExistingSpaceView(); } else { showNoSpaceView(); } } catch (error) { console.error('Error checking for space:', error); showNoSpaceView(); } } /** * Show view for users who don't have the space yet */ function showNoSpaceView() { document.getElementById('no-space-view').style.display = 'block'; document.getElementById('has-space-view').style.display = 'none'; } /** * Show view for users who already have the space */ function showExistingSpaceView() { document.getElementById('no-space-view').style.display = 'none'; document.getElementById('has-space-view').style.display = 'block'; // Update space URL displays const spaceUrl = `${currentUser}/${NEW_SPACE_NAME}`; // HuggingFace converts underscores to hyphens in subdomain URLs // Example: andito/reachy_mini_remote_control -> andito-reachy-mini-remote-control.hf.space const wsUri = `wss://${currentUser}-${NEW_SPACE_NAME.replace(/_/g, '-')}.hf.space`; document.getElementById('space-url-display').textContent = spaceUrl; document.getElementById('private-uri').textContent = wsUri; document.getElementById('space-link').href = `https://huggingface.co/spaces/${spaceUrl}`; } /** * Create a new space and upload files */ async function cloneSpace() { const btn = document.querySelector('#no-space-view .btn'); const btnText = document.getElementById('clone-btn-text'); const originalText = btnText.textContent; try { // Verify we have user credentials if (!currentUser || !userToken) { throw new Error('Not authenticated. Please sign in again.'); } // Update button state btn.disabled = true; btnText.textContent = '⏳ Creating space...'; console.log('Creating space for user:', currentUser); console.log('Creating repo:', `${currentUser}/${NEW_SPACE_NAME}`); // Step 1: Create the space repository using @huggingface/hub const createResult = await createRepo({ repo: { type: 'space', name: `${currentUser}/${NEW_SPACE_NAME}` }, accessToken: userToken, credentials: { accessToken: userToken }, hubUrl: 'https://huggingface.co', spaceHardware: 'cpu-basic', spaceSdk: 'gradio', private: true }); console.log('Repo created successfully:', createResult); btnText.textContent = '⏳ Uploading files...'; // Step 2: Upload files to the space from local directory const files = ['README.md', 'app.py', 'requirements.txt', 'packages.txt', 'dist.zip']; for (const fileName of files) { console.log(`Fetching ${fileName}...`); // Fetch file content from local remote_control_space directory const fileResponse = await fetch(`remote_control_space/${fileName}`); if (!fileResponse.ok) { console.error(`Failed to fetch ${fileName}`); continue; } const content = await fileResponse.blob(); console.log(`Uploading ${fileName}...`); // Upload to space using @huggingface/hub const uploadResult = await uploadFile({ repo: { type: 'space', name: `${currentUser}/${NEW_SPACE_NAME}` }, accessToken: userToken, credentials: { accessToken: userToken }, file: { path: fileName, content: content } }); console.log(`${fileName} uploaded:`, uploadResult); } btnText.textContent = '✓ Space created successfully!'; // Wait a moment then show the space view setTimeout(() => { showExistingSpaceView(); }, 1500); } catch (error) { console.error('Error creating space:', error); alert(`Failed to create space: ${error.message}\n\nPlease try again or create the space manually.`); btn.disabled = false; btnText.textContent = originalText; } } /** * Logout and clear stored credentials */ function logout() { if (confirm('Are you sure you want to sign out?')) { localStorage.removeItem('hf_token'); localStorage.removeItem('hf_username'); currentUser = null; userToken = null; // Reset UI document.getElementById('login-view').style.display = 'block'; document.getElementById('authenticated-view').style.display = 'none'; } } /** * Copy text to clipboard */ function copyToClipboard(text, buttonElement) { navigator.clipboard.writeText(text).then(() => { // Visual feedback if (buttonElement) { const originalText = buttonElement.textContent; buttonElement.textContent = '✓ Copied!'; setTimeout(() => { buttonElement.textContent = originalText; }, 2000); } }).catch(err => { console.error('Failed to copy:', err); alert('Failed to copy to clipboard'); }); } /** * Copy private URI to clipboard */ function copyPrivateUri(event) { const uri = document.getElementById('private-uri').textContent; copyToClipboard(uri, event.currentTarget); } // Expose functions globally for onclick handlers window.loginToHuggingFace = loginToHuggingFace; window.cloneSpace = cloneSpace; window.logout = logout; window.copyToClipboard = copyToClipboard; window.copyPrivateUri = copyPrivateUri; // Initialize on page load document.addEventListener('DOMContentLoaded', initAuth);