File size: 7,726 Bytes
921d377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/**
 * 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>);
}