From 0a5c899c5eebe7869d2bfd4f2c0de77cfb9b249a Mon Sep 17 00:00:00 2001 From: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:28:58 +0200 Subject: [PATCH] feat(auth): frontend login/setup pages + auth state in App.jsx --- frontend/src/App.jsx | 173 +++++++++++++++----------- frontend/src/api.js | 51 ++++++++ frontend/src/components/LoginPage.jsx | 54 ++++++++ frontend/src/components/SetupPage.jsx | 73 +++++++++++ 4 files changed, 280 insertions(+), 71 deletions(-) create mode 100644 frontend/src/components/LoginPage.jsx create mode 100644 frontend/src/components/SetupPage.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index f663efe..f6f119b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,8 @@ -import { useState, useEffect, useCallback, useRef } from 'react' -import { fetchArchives, fetchEntries, searchEntries, fetchRuns, fetchTags } from './api' +import { useState, useEffect, useCallback, useRef, createContext } from 'react' +import { fetchArchives, fetchEntries, searchEntries, fetchRuns, fetchTags, checkSetup, fetchMe } from './api' +import LoginPage from './components/LoginPage.jsx' +import SetupPage from './components/SetupPage.jsx' + import Topbar from './components/Topbar' import CaptureDialog from './components/CaptureDialog' import EntriesView from './components/EntriesView' @@ -8,7 +11,29 @@ import AdminView from './components/AdminView' import TagsView from './components/TagsView' import ContextRail from './components/ContextRail' +export const AuthContext = createContext(null); + export default function App() { + const [authState, setAuthState] = useState('loading'); + const [currentUser, setCurrentUser] = useState(null); + + useEffect(() => { + (async () => { + const needsSetup = await checkSetup(); + if (needsSetup) { setAuthState('setup'); return; } + const user = await fetchMe(); + if (!user) { setAuthState('login'); return; } + setCurrentUser(user); + setAuthState('authenticated'); + })(); + }, []); + + useEffect(() => { + const handler = () => { setCurrentUser(null); setAuthState('login'); }; + window.addEventListener('auth:expired', handler); + return () => window.removeEventListener('auth:expired', handler); + }, []); + const [archives, setArchives] = useState([]) const [archiveId, setArchiveId] = useState(null) const [entries, setEntries] = useState([]) @@ -127,77 +152,83 @@ export default function App() { ]) }, [archiveId, searchQuery, tagFilter, loadEntries]) + if (authState === 'loading') return
Loading\u2026
; + if (authState === 'setup') return setAuthState('login')} />; + if (authState === 'login') return { setCurrentUser(user); setAuthState('authenticated'); }} />; + return ( - <> - -
-
- {view === 'archive' && ( -
- setSearchQuery(e.target.value)} - /> -
- {resultCount} - {tagFilter && ( - - )} -
-
- )} - {view === 'archive' && ( - - )} - {view === 'runs' && } - {view === 'admin' && } - {view === 'tags' && ( - - )} -
- + <> + -
- - +
+
+ {view === 'archive' && ( +
+ setSearchQuery(e.target.value)} + /> +
+ {resultCount} + {tagFilter && ( + + )} +
+
+ )} + {view === 'archive' && ( + + )} + {view === 'runs' && } + {view === 'admin' && } + {view === 'tags' && ( + + )} +
+ +
+ + + ) } diff --git a/frontend/src/api.js b/frontend/src/api.js index 0ce034b..1af25c8 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -68,3 +68,54 @@ export async function submitCapture(archiveId, locator) { throw new Error(msg || `HTTP ${res.status}`); } } + +// ── Auth helpers ───────────────────────────────────────────────────────────── + +export async function checkSetup() { + const r = await fetch('/api/auth/setup'); + const data = await r.json(); + return data.setup_required === true; +} + +export async function doSetup(username, password) { + const r = await fetch('/api/auth/setup', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ username, password }), + }); + if (!r.ok) throw new Error((await r.json()).error || 'Setup failed'); + return r.json(); +} + +export async function login(username, password) { + const r = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ username, password }), + }); + if (!r.ok) throw new Error((await r.json()).error || 'Login failed'); + return r.json(); +} + +export async function logout() { + await fetch('/api/auth/logout', { method: 'POST' }); +} + +export async function fetchMe() { + const r = await fetch('/api/auth/me'); + if (r.status === 401) return null; + return r.json(); +} + +// ── 401 interceptor ─────────────────────────────────────────────────────────── +const _origFetch = window.fetch; +window.fetch = async (...args) => { + const r = await _origFetch(...args); + if (r.status === 401) { + const url = typeof args[0] === 'string' ? args[0] : args[0]?.url ?? ''; + if (!url.includes('/api/auth/')) { + window.dispatchEvent(new CustomEvent('auth:expired')); + } + } + return r; +}; diff --git a/frontend/src/components/LoginPage.jsx b/frontend/src/components/LoginPage.jsx new file mode 100644 index 0000000..105ed5a --- /dev/null +++ b/frontend/src/components/LoginPage.jsx @@ -0,0 +1,54 @@ +import { useState } from 'react'; +import { login } from '../api.js'; + +export default function LoginPage({ onLogin }) { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + async function handleSubmit(e) { + e.preventDefault(); + setError(null); + setLoading(true); + try { + const user = await login(username, password); + onLogin(user); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + } + + return ( +
+

Archivr

+
+ + + {error &&

{error}

} + +
+
+ ); +} diff --git a/frontend/src/components/SetupPage.jsx b/frontend/src/components/SetupPage.jsx new file mode 100644 index 0000000..aca4cb3 --- /dev/null +++ b/frontend/src/components/SetupPage.jsx @@ -0,0 +1,73 @@ +import { useState } from 'react'; +import { doSetup } from '../api.js'; + +export default function SetupPage({ onComplete }) { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [confirm, setConfirm] = useState(''); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + async function handleSubmit(e) { + e.preventDefault(); + if (password !== confirm) { + setError('Passwords do not match'); + return; + } + if (password.length < 8) { + setError('Password must be at least 8 characters'); + return; + } + setError(null); + setLoading(true); + try { + await doSetup(username, password); + onComplete(); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + } + + return ( +
+

Welcome to Archivr

+

Create your owner account to get started.

+
+ + + + {error &&

{error}

} + +
+
+ ); +}