/** * Modul: Login-Seite * Zweck: Anmeldeformular mit Username/Passwort, Fehlerbehandlung, Session-Start * Abhängigkeiten: /api.js */ import { auth } from '/api.js'; import { t } from '/i18n.js'; const VERSION_URL = '/api/v1/version'; /** * Rendert die Login-Seite in den gegebenen Container. * @param {HTMLElement} container */ export async function render(container) { container.innerHTML = `

Oikos

`; const form = container.querySelector('#login-form'); const errorEl = container.querySelector('#login-error'); const submitBtn = container.querySelector('#login-btn'); const versionEl = container.querySelector('#login-version'); fetch(VERSION_URL) .then((r) => r.json()) .then((d) => { versionEl.textContent = t('login.version', { version: d.version }); }) .catch(() => {}); form.addEventListener('submit', async (e) => { e.preventDefault(); errorEl.hidden = true; const username = form.username.value.trim(); const password = form.password.value; const usernameInput = form.querySelector('#username'); const passwordInput = form.querySelector('#password'); const usernameGroup = usernameInput.closest('.form-group'); const passwordGroup = passwordInput.closest('.form-group'); usernameGroup.classList.toggle('form-group--error', !username); passwordGroup.classList.toggle('form-group--error', !password); usernameInput.setAttribute('aria-invalid', String(!username)); passwordInput.setAttribute('aria-invalid', String(!password)); if (!username || !password) { if (!username) usernameInput.focus(); else passwordInput.focus(); return; } const labelEl = submitBtn.querySelector('.login-btn__label'); submitBtn.disabled = true; labelEl.textContent = t('login.loggingIn'); const spinner = document.createElement('span'); spinner.className = 'login-spinner'; spinner.setAttribute('aria-hidden', 'true'); submitBtn.insertBefore(spinner, labelEl); try { const result = await auth.login(username, password); window.oikos.navigate('/', result.user); } catch (err) { showError(errorEl, err.status === 429 ? t('login.tooManyAttempts') : t('login.invalidCredentials') ); } finally { submitBtn.disabled = false; labelEl.textContent = t('login.loginButton'); spinner.remove(); } }); form.querySelector('#username').addEventListener('input', (e) => { e.currentTarget.closest('.form-group').classList.remove('form-group--error'); e.currentTarget.removeAttribute('aria-invalid'); }); form.querySelector('#password').addEventListener('input', (e) => { e.currentTarget.closest('.form-group').classList.remove('form-group--error'); e.currentTarget.removeAttribute('aria-invalid'); }); } function showError(el, message) { el.textContent = message; el.hidden = false; }