HomePilot / frontend /src /ui /components /AuthGate.jsx
HomePilot Deploy Bot
chore(hf): sync HomePilot to HF Space
921d377
Raw
History Blame Contribute Delete
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>);
}