| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import React, { createContext, useCallback, useContext, useEffect, useState } from 'react' |
| import AuthScreen from './AuthScreen' |
| import OnboardingWizard from './OnboardingWizard' |
| import { resolveBackendUrl } from '../lib/backendUrl' |
|
|
| export interface AuthUser { |
| id: string |
| username: string |
| display_name: string |
| email: string |
| avatar_url: string |
| onboarding_complete: boolean |
| } |
|
|
| |
| export interface RecentUser { |
| username: string |
| display_name: string |
| avatar_url: string |
| lastLogin: number |
| } |
|
|
| const LS_TOKEN_KEY = 'homepilot_auth_token' |
| const LS_USER_KEY = 'homepilot_auth_user' |
| const LS_RECENT_USERS_KEY = 'homepilot_recent_users' |
|
|
| function getBackendUrl(): string { |
| return resolveBackendUrl() |
| } |
|
|
| function loadRecentUsers(): RecentUser[] { |
| try { |
| const raw = localStorage.getItem(LS_RECENT_USERS_KEY) |
| if (!raw) return [] |
| const parsed = JSON.parse(raw) as RecentUser[] |
| |
| return parsed.sort((a, b) => b.lastLogin - a.lastLogin).slice(0, 5) |
| } catch { |
| return [] |
| } |
| } |
|
|
| function saveRecentUser(user: AuthUser) { |
| const existing = loadRecentUsers().filter(u => u.username !== user.username) |
| const entry: RecentUser = { |
| 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)) |
| } |
|
|
| |
| interface AuthContextValue { |
| user: AuthUser | null |
| token: string |
| logout: () => Promise<void> |
| |
| updateUser: (partial: Partial<AuthUser>) => void |
| } |
|
|
| const AuthContext = createContext<AuthContextValue>({ |
| user: null, |
| token: '', |
| logout: async () => {}, |
| updateUser: () => {}, |
| }) |
|
|
| export function useAuth(): AuthContextValue { |
| return useContext(AuthContext) |
| } |
|
|
| |
| interface AuthGateProps { |
| children: React.ReactNode |
| } |
|
|
| export default function AuthGate({ children }: AuthGateProps) { |
| const [state, setState] = useState<'loading' | 'login' | 'onboarding' | 'ready'>('loading') |
| const [user, setUser] = useState<AuthUser | null>(null) |
| const [token, setToken] = useState<string>('') |
| const [loggedOutMessage, setLoggedOutMessage] = useState<string>('') |
| const [recentUsers, setRecentUsers] = useState<RecentUser[]>([]) |
| 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) { |
| |
| |
| setState('ready') |
| return |
| } |
|
|
| const data = await res.json() |
|
|
| if (data.needs_setup) { |
| |
| setState('login') |
| return |
| } |
|
|
| if (data.needs_login) { |
| |
| setRecentUsers(loadRecentUsers()) |
| setState('login') |
| return |
| } |
|
|
| if (data.user) { |
| const u = data.user as AuthUser |
| 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 |
| } |
|
|
| |
| setState('ready') |
| } catch { |
| |
| setState('ready') |
| } |
| } |
|
|
| function handleAuthenticated(u: AuthUser, t: string) { |
| 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: string) { |
| if (user) { |
| const updated = { ...user, display_name: displayName, onboarding_complete: true } |
| setUser(updated) |
| localStorage.setItem(LS_USER_KEY, JSON.stringify(updated)) |
| } |
| setState('ready') |
| } |
|
|
| |
| const updateUser = useCallback((partial: Partial<AuthUser>) => { |
| setUser(prev => { |
| if (!prev) return prev |
| const updated = { ...prev, ...partial } |
| localStorage.setItem(LS_USER_KEY, JSON.stringify(updated)) |
| |
| saveRecentUser(updated) |
| return updated |
| }) |
| }, []) |
|
|
| |
| const logout = useCallback(async () => { |
| const savedToken = localStorage.getItem(LS_TOKEN_KEY) || '' |
| const currentDisplayName = user?.display_name || user?.username || '' |
|
|
| |
| if (savedToken) { |
| try { |
| await fetch(`${backendUrl}/v1/auth/logout`, { |
| method: 'POST', |
| headers: { 'Authorization': `Bearer ${savedToken}` }, |
| credentials: 'include', |
| }) |
| } catch { } |
| } |
|
|
| |
| localStorage.removeItem(LS_TOKEN_KEY) |
| localStorage.removeItem(LS_USER_KEY) |
|
|
| |
| setUser(null) |
| setToken('') |
| setLoggedOutMessage( |
| currentDisplayName |
| ? `Signed out as ${currentDisplayName}` |
| : 'Signed out successfully' |
| ) |
| setRecentUsers(loadRecentUsers()) |
| setState('login') |
| }, [user, backendUrl]) |
|
|
| |
| 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> |
| ) |
| } |
|
|
| |
| if (state === 'login') { |
| return ( |
| <AuthScreen |
| backendUrl={backendUrl} |
| onAuthenticated={handleAuthenticated} |
| logoutMessage={loggedOutMessage} |
| recentUsers={recentUsers} |
| /> |
| ) |
| } |
|
|
| |
| if (state === 'onboarding' && user) { |
| return ( |
| <OnboardingWizard |
| backendUrl={backendUrl} |
| token={token} |
| username={user.username} |
| onComplete={handleOnboardingComplete} |
| /> |
| ) |
| } |
|
|
| |
| return ( |
| <AuthContext.Provider value={{ user, token, logout, updateUser }}> |
| {children} |
| </AuthContext.Provider> |
| ) |
| } |
|
|