Download frontend/src/ui/components/AuthGate.jsx from ruslanmv/HomePilot: direct link, hf CLI and curl.
- Browser
- Download file 7.73 kB
-
https://huggingface.co/spaces/ruslanmv/HomePilot/resolve/main/frontend/src/ui/components/AuthGate.jsx
- Command line
-
hf download hf://spaces/ruslanmv/HomePilot/frontend/src/ui/components/AuthGate.jsx
-
curl -L -o AuthGate.jsx https://huggingface.co/spaces/ruslanmv/HomePilot/resolve/main/frontend/src/ui/components/AuthGate.jsx
7.73 kB
| /** | |
| * AuthGate β wraps the entire app with authentication + onboarding. | |
| * | |
| * Flow: | |
| * 1. On mount, call GET /v1/auth/me to check auth status | |
| * 2. If needs_setup (no users) β show AuthScreen in register mode | |
| * 3. If needs_login (multi-user, no token) β show AuthScreen | |
| * 4. If authenticated but !onboarding_complete β show OnboardingWizard | |
| * 5. If authenticated and onboarded β render children (the main App) | |
| * | |
| * Single-user with no password: auto-login, no friction. | |
| * | |
| * Provides AuthContext so child components (e.g. App.tsx) can call | |
| * `logout()` for a smooth transition back to the login screen | |
| * without a hard page reload. | |
| */ | |
| import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; | |
| import AuthScreen from './AuthScreen'; | |
| import OnboardingWizard from './OnboardingWizard'; | |
| import { resolveBackendUrl } from '../lib/backendUrl'; | |
| const LS_TOKEN_KEY = 'homepilot_auth_token'; | |
| const LS_USER_KEY = 'homepilot_auth_user'; | |
| const LS_RECENT_USERS_KEY = 'homepilot_recent_users'; | |
| function getBackendUrl() { | |
| return resolveBackendUrl(); | |
| } | |
| function loadRecentUsers() { | |
| try { | |
| const raw = localStorage.getItem(LS_RECENT_USERS_KEY); | |
| if (!raw) | |
| return []; | |
| const parsed = JSON.parse(raw); | |
| // Sort by most recent first, keep max 5 | |
| return parsed.sort((a, b) => b.lastLogin - a.lastLogin).slice(0, 5); | |
| } | |
| catch { | |
| return []; | |
| } | |
| } | |
| function saveRecentUser(user) { | |
| const existing = loadRecentUsers().filter(u => u.username !== user.username); | |
| const entry = { | |
| username: user.username, | |
| display_name: user.display_name, | |
| avatar_url: user.avatar_url, | |
| lastLogin: Date.now(), | |
| }; | |
| const updated = [entry, ...existing].slice(0, 5); | |
| localStorage.setItem(LS_RECENT_USERS_KEY, JSON.stringify(updated)); | |
| } | |
| const AuthContext = createContext({ | |
| user: null, | |
| token: '', | |
| logout: async () => { }, | |
| updateUser: () => { }, | |
| }); | |
| export function useAuth() { | |
| return useContext(AuthContext); | |
| } | |
| export default function AuthGate({ children }) { | |
| const [state, setState] = useState('loading'); | |
| const [user, setUser] = useState(null); | |
| const [token, setToken] = useState(''); | |
| const [loggedOutMessage, setLoggedOutMessage] = useState(''); | |
| const [recentUsers, setRecentUsers] = useState([]); | |
| const backendUrl = getBackendUrl(); | |
| useEffect(() => { | |
| checkAuth(); | |
| }, []); | |
| async function checkAuth() { | |
| const savedToken = localStorage.getItem(LS_TOKEN_KEY) || ''; | |
| try { | |
| const res = await fetch(`${backendUrl}/v1/auth/me`, { | |
| headers: savedToken ? { 'Authorization': `Bearer ${savedToken}` } : {}, | |
| credentials: 'include', | |
| }); | |
| if (!res.ok) { | |
| // Backend might not have the /v1/auth routes yet (old version) | |
| // β just let through (backward compatible) | |
| setState('ready'); | |
| return; | |
| } | |
| const data = await res.json(); | |
| if (data.needs_setup) { | |
| // First boot β no users exist | |
| setState('login'); | |
| return; | |
| } | |
| if (data.needs_login) { | |
| // Multi-user, need to log in | |
| setRecentUsers(loadRecentUsers()); | |
| setState('login'); | |
| return; | |
| } | |
| if (data.user) { | |
| const u = data.user; | |
| const t = data.token || savedToken; | |
| setUser(u); | |
| setToken(t); | |
| if (t) | |
| localStorage.setItem(LS_TOKEN_KEY, t); | |
| localStorage.setItem(LS_USER_KEY, JSON.stringify(u)); | |
| saveRecentUser(u); | |
| if (!u.onboarding_complete) { | |
| setState('onboarding'); | |
| } | |
| else { | |
| setState('ready'); | |
| } | |
| return; | |
| } | |
| // Fallback: let through | |
| setState('ready'); | |
| } | |
| catch { | |
| // Backend unreachable β skip auth (backward compatible with pre-auth setups) | |
| setState('ready'); | |
| } | |
| } | |
| function handleAuthenticated(u, t) { | |
| setUser(u); | |
| setToken(t); | |
| setLoggedOutMessage(''); | |
| localStorage.setItem(LS_TOKEN_KEY, t); | |
| localStorage.setItem(LS_USER_KEY, JSON.stringify(u)); | |
| saveRecentUser(u); | |
| if (!u.onboarding_complete) { | |
| setState('onboarding'); | |
| } | |
| else { | |
| setState('ready'); | |
| } | |
| } | |
| function handleOnboardingComplete(displayName) { | |
| if (user) { | |
| const updated = { ...user, display_name: displayName, onboarding_complete: true }; | |
| setUser(updated); | |
| localStorage.setItem(LS_USER_KEY, JSON.stringify(updated)); | |
| } | |
| setState('ready'); | |
| } | |
| // Update user fields live (e.g. after avatar upload or display name change) | |
| const updateUser = useCallback((partial) => { | |
| setUser(prev => { | |
| if (!prev) | |
| return prev; | |
| const updated = { ...prev, ...partial }; | |
| localStorage.setItem(LS_USER_KEY, JSON.stringify(updated)); | |
| // Also update recent-users cache so the login screen shows fresh data | |
| saveRecentUser(updated); | |
| return updated; | |
| }); | |
| }, []); | |
| // Smooth logout β clears state and transitions to login screen without reload | |
| const logout = useCallback(async () => { | |
| const savedToken = localStorage.getItem(LS_TOKEN_KEY) || ''; | |
| const currentDisplayName = user?.display_name || user?.username || ''; | |
| // Fire-and-forget backend invalidation | |
| if (savedToken) { | |
| try { | |
| await fetch(`${backendUrl}/v1/auth/logout`, { | |
| method: 'POST', | |
| headers: { 'Authorization': `Bearer ${savedToken}` }, | |
| credentials: 'include', | |
| }); | |
| } | |
| catch { /* non-fatal */ } | |
| } | |
| // Clear stored credentials | |
| localStorage.removeItem(LS_TOKEN_KEY); | |
| localStorage.removeItem(LS_USER_KEY); | |
| // Transition smoothly | |
| setUser(null); | |
| setToken(''); | |
| setLoggedOutMessage(currentDisplayName | |
| ? `Signed out as ${currentDisplayName}` | |
| : 'Signed out successfully'); | |
| setRecentUsers(loadRecentUsers()); | |
| setState('login'); | |
| }, [user, backendUrl]); | |
| // Loading spinner β uses same neutral bg as login page | |
| if (state === 'loading') { | |
| return (<div style={{ | |
| minHeight: '100vh', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| background: '#050506', | |
| color: 'rgba(255, 255, 255, 0.30)', | |
| fontFamily: 'Inter, system-ui, sans-serif', | |
| fontSize: 14, | |
| }}> | |
| Loading... | |
| </div>); | |
| } | |
| // Login / Register screen | |
| if (state === 'login') { | |
| return (<AuthScreen backendUrl={backendUrl} onAuthenticated={handleAuthenticated} logoutMessage={loggedOutMessage} recentUsers={recentUsers}/>); | |
| } | |
| // Onboarding wizard | |
| if (state === 'onboarding' && user) { | |
| return (<OnboardingWizard backendUrl={backendUrl} token={token} username={user.username} onComplete={handleOnboardingComplete}/>); | |
| } | |
| // Authenticated and onboarded β render the main app with auth context | |
| return (<AuthContext.Provider value={{ user, token, logout, updateUser }}> | |
| {children} | |
| </AuthContext.Provider>); | |
| } | |