/** * Login page. * * Username + password form, plus "Sign in with passkey" button. * On success: stores tokens and navigates to dashboard. */ import { h, definePage } from '/static/hoover/index.js?v=11'; import { apiFetch, toast, setAuthToken, getAuthToken } from '/static/hoover/api.js?v=12'; import { handleLoginSuccess, webauthnSupported, startAuthentication, } from '/static/hoover/components/auth.js'; import { html } from '/static/hoover/html.js?v=9'; function LoginPage() { const hasWebAuthn = webauthnSupported(); return html`
${hasWebAuthn ? html` ` : ''}
`; } let _loginFormBound = false; let _passkeyBound = false; function handleLogin() { if (_loginFormBound) return; _loginFormBound = true; const form = document.getElementById('loginForm'); if (!form) return; form.addEventListener('submit', async (e) => { e.preventDefault(); await doPasswordLogin(); }); } async function doPasswordLogin() { const username = document.getElementById('loginUsername').value.trim(); const password = document.getElementById('loginPassword').value; const errEl = document.getElementById('loginError'); if (!username || !password) { errEl.textContent = 'Username and password are required'; return; } errEl.textContent = ''; const res = await apiFetch('/api/auth/login', { method: 'POST', body: { username, password }, }); if (res.ok) { handleLoginSuccess(res.data); toast('Welcome, ' + username, 'success'); } else { errEl.textContent = res.error || 'Login failed'; } } function setupPasskeyButton() { if (_passkeyBound) return; _passkeyBound = true; const passkeyBtn = document.getElementById('passkeyBtn'); if (!passkeyBtn) return; const usernameInput = document.getElementById('loginUsername'); const passwordGroup = document.getElementById('loginPasswordGroup'); const loginBtn = document.getElementById('loginBtn'); const errEl = document.getElementById('loginError'); passkeyBtn.addEventListener('click', async () => { errEl.textContent = ''; const username = usernameInput.value.trim(); if (!username) { errEl.textContent = 'Enter your username first'; usernameInput.focus(); return; } passkeyBtn.disabled = true; passkeyBtn.textContent = 'Checking...'; try { const beginRes = await apiFetch('/api/auth/webauthn/authenticate-begin', { method: 'POST', body: { username }, }); if (!beginRes.ok) { errEl.textContent = beginRes.error || 'Failed to start authentication'; passkeyBtn.disabled = false; passkeyBtn.textContent = 'Sign in with passkey'; return; } if (beginRes.data && beginRes.data.no_webauthn) { errEl.textContent = 'No passkey registered for this account'; passkeyBtn.disabled = false; passkeyBtn.textContent = 'Sign in with passkey'; return; } const authOptions = beginRes.data; passkeyBtn.textContent = 'Waiting for authenticator...'; const assertionResponse = await startAuthentication(authOptions); passkeyBtn.textContent = 'Verifying...'; const finishRes = await apiFetch('/api/auth/webauthn/authenticate-finish', { method: 'POST', body: { username, assertion_response: assertionResponse, auth_options: authOptions, }, }); if (finishRes.ok) { handleLoginSuccess(finishRes.data); toast('Welcome, ' + username, 'success'); } else { errEl.textContent = finishRes.error || 'Passkey authentication failed'; } } catch (err) { if (err.message && err.message.toLowerCase().includes('user cancelled')) { errEl.textContent = 'Authentication cancelled'; } else { errEl.textContent = err.message || 'Passkey authentication failed'; } } finally { passkeyBtn.disabled = false; passkeyBtn.textContent = 'Sign in with passkey'; } }); passkeyBtn.addEventListener('mouseenter', () => { if (passwordGroup) { passwordGroup.style.display = 'none'; } }); passkeyBtn.addEventListener('mouseleave', () => { if (passwordGroup) { passwordGroup.style.display = ''; } }); } const Page = definePage({ init() { document.title = 'Login — Vacuum Wall'; }, async load(state, abortController) { if (getAuthToken()) { try { const res = await apiFetch('/api/auth/session'); if (res.ok) { window.location.hash = '/dashboard'; return; } } catch { // auth check failed, show login } } }, render() { return h('div', null, LoginPage()); }, }); handleLogin(); setupPasskeyButton(); export default Page;