/** * Login page. * * Username + password form, plus "Sign in with passkey" button. * On success: stores tokens and navigates to dashboard. */ import { h, definePage, html, apiFetch, toast, setAuthToken, getAuthToken, handleLoginSuccess, webauthnSupported, startAuthentication, esc, } from '/static/hoover/index.js'; function LoginPage() { const hasWebAuthn = webauthnSupported(); return html`
${hasWebAuthn ? html` ` : ''}
`; } function handleLogin() { const form = document.getElementById('loginForm'); if (!form) return; form.removeEventListener('submit', loginFormHandler); form.addEventListener('submit', loginFormHandler); } const loginFormHandler = 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, ' + esc(username), 'success'); } else { errEl.textContent = res.error || 'Login failed'; } } function setupPasskeyButton() { const passkeyBtn = document.getElementById('passkeyBtn'); if (!passkeyBtn) return; passkeyBtnRef.btn = passkeyBtn; passkeyBtnRef.usernameInput = document.getElementById('loginUsername'); passkeyBtnRef.passwordGroup = document.getElementById('loginPasswordGroup'); passkeyBtnRef.errEl = document.getElementById('loginError'); passkeyBtn.removeEventListener('click', passkeyClickHandler); passkeyBtn.addEventListener('click', passkeyClickHandler); passkeyBtn.removeEventListener('mouseenter', passkeyMouseEnterHandler); passkeyBtn.removeEventListener('mouseleave', passkeyMouseLeaveHandler); passkeyBtn.addEventListener('mouseenter', passkeyMouseEnterHandler); passkeyBtn.addEventListener('mouseleave', passkeyMouseLeaveHandler); } const passkeyBtnRef = { btn: null, usernameInput: null, passwordGroup: null, errEl: null, }; const passkeyClickHandler = async () => { const { btn, usernameInput, passwordGroup, errEl } = passkeyBtnRef; if (!btn) return; errEl.textContent = ''; const username = usernameInput.value.trim(); if (!username) { errEl.textContent = 'Enter your username first'; usernameInput.focus(); return; } btn.disabled = true; btn.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'; btn.disabled = false; btn.textContent = 'Sign in with passkey'; return; } if (beginRes.data && beginRes.data.no_webauthn) { errEl.textContent = 'No passkey registered for this account'; btn.disabled = false; btn.textContent = 'Sign in with passkey'; return; } const authOptions = beginRes.data; btn.textContent = 'Waiting for authenticator...'; const assertionResponse = await startAuthentication(authOptions); btn.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, ' + esc(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 { btn.disabled = false; btn.textContent = 'Sign in with passkey'; } }; const passkeyMouseEnterHandler = () => { const { passwordGroup } = passkeyBtnRef; if (passwordGroup) { passwordGroup.style.display = 'none'; } }; const passkeyMouseLeaveHandler = () => { const { passwordGroup } = passkeyBtnRef; if (passwordGroup) { passwordGroup.style.display = ''; } }; const Page = definePage({ init() { document.title = 'Login — Vacuum Wall'; }, load() { handleLogin(); setupPasskeyButton(); }, render() { return h('div', null, LoginPage()); }, }); export default Page;