/* Login page logic for the Work Package Suite. Three views on one page: • sign in posts to /api/auth/login. On success the server sets an HttpOnly session cookie (not readable here — that's the point) and we redirect to ?next= or the home page. • forgot password posts to /api/auth/forgot-password, which emails a single-use link. Only offered when the server reports email is actually configured (/api/auth/reset-available); otherwise we say to ask an admin. • set a new password shown when the page is opened as login.html?reset= from that email. Posts to /api/auth/reset-password. The reset token stays in the URL only until it's used; on success we strip it from the address bar so it isn't left in history or copied out of the bar. */ (function () { 'use strict'; var errorBox = document.getElementById('error'); var okBox = document.getElementById('ok'); function show(el) { if (el) el.style.display = ''; } function hide(el) { if (el) el.style.display = 'none'; } function byId(id) { return document.getElementById(id); } function showError(msg) { okBox.classList.remove('show'); errorBox.textContent = msg; errorBox.classList.add('show'); } function showOk(msg) { errorBox.classList.remove('show'); okBox.textContent = msg; okBox.classList.add('show'); } function clearBanners() { errorBox.classList.remove('show'); okBox.classList.remove('show'); } // Where to go after signing in: the ?next= param if it's a safe same-site // path, otherwise the home page. (Reject absolute/scheme URLs to avoid an // open-redirect.) function nextTarget() { try { var next = new URLSearchParams(location.search).get('next') || ''; if (next && next.charAt(0) === '/' && next.charAt(1) !== '/') return next; } catch (e) {} return 'index.html'; } function resetToken() { try { return new URLSearchParams(location.search).get('reset') || ''; } catch (e) { return ''; } } function postJson(url, payload) { return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).then(function (r) { return r.json().catch(function () { return null; }).then(function (j) { return { status: r.status, ok: r.ok, json: j }; }); }); } function detail(res, fallback) { var d = res && res.json && res.json.detail; return (typeof d === 'string' && d) ? d : fallback; } function view(which) { clearBanners(); ['login', 'forgot', 'reset'].forEach(function (v) { (which === v ? show : hide)(byId('view-' + v)); }); } // ── sign in ──────────────────────────────────────────────────────────────── var form = byId('login-form'); var submitBtn = byId('submit'); // Guarded because a cached older login.html may not have the reset views; an // unguarded addEventListener on null would break sign-in itself. if (!form || !submitBtn) return; form.addEventListener('submit', function (e) { e.preventDefault(); clearBanners(); var username = byId('username').value.trim(); var password = byId('password').value; if (!username || !password) { showError('Enter your username and password.'); return; } submitBtn.disabled = true; submitBtn.textContent = 'Signing in…'; postJson('/api/auth/login', { username: username, password: password }) .then(function (res) { if (res.ok) { location.replace(nextTarget()); return; } if (res.status === 401) showError('Invalid username or password.'); else if (res.status === 403) showError(detail(res, 'Your account is disabled.')); else if (res.status === 429) showError(detail(res, 'Too many failed attempts. Try again later.')); else showError(detail(res, 'Sign-in failed (HTTP ' + res.status + ').')); submitBtn.disabled = false; submitBtn.textContent = 'Sign in'; }) .catch(function () { showError('Could not reach the server. Check your connection and try again.'); submitBtn.disabled = false; submitBtn.textContent = 'Sign in'; }); }); // ── forgot password ──────────────────────────────────────────────────────── var resetAvailable = null; // null = not checked yet function checkResetAvailable() { if (resetAvailable !== null) return Promise.resolve(resetAvailable); return fetch('/api/auth/reset-available') .then(function (r) { return r.ok ? r.json() : null; }) .then(function (j) { resetAvailable = !!(j && j.enabled); return resetAvailable; }) .catch(function () { resetAvailable = false; return false; }); } (byId('forgot-link') || {addEventListener: function(){}}).addEventListener('click', function (e) { e.preventDefault(); view('forgot'); // Prefill from the sign-in box so nobody types their username twice. var u = byId('username').value.trim(); if (u) byId('forgot-username').value = u; checkResetAvailable().then(function (enabled) { // With email off there's nothing to submit — say so and hide the form. (enabled ? hide : show)(byId('forgot-unavailable')); (enabled ? show : hide)(byId('forgot-form')); if (enabled) byId('forgot-username').focus(); }); }); (byId('back-to-login') || {addEventListener: function(){}}).addEventListener('click', function (e) { e.preventDefault(); view('login'); }); var forgotForm = byId('forgot-form') || document.createElement('form'); var forgotBtn = byId('forgot-submit') || document.createElement('button'); forgotForm.addEventListener('submit', function (e) { e.preventDefault(); clearBanners(); var who = byId('forgot-username').value.trim(); if (!who) { showError('Enter your username or email.'); return; } forgotBtn.disabled = true; forgotBtn.textContent = 'Sending…'; postJson('/api/auth/forgot-password', { username: who }) .then(function (res) { if (res.status === 503) { showError(detail(res, "Password reset by email isn't available. Ask an administrator.")); } else if (res.ok) { // Deliberately the same message whether or not the account exists. showOk('If that account exists, a reset link is on its way. The link expires in an hour.'); hide(forgotForm); } else { showError(detail(res, 'Could not send the reset email (HTTP ' + res.status + ').')); } forgotBtn.disabled = false; forgotBtn.textContent = 'Email me a reset link'; }) .catch(function () { showError('Could not reach the server. Check your connection and try again.'); forgotBtn.disabled = false; forgotBtn.textContent = 'Email me a reset link'; }); }); // ── set a new password (from the emailed link) ────────────────────────────── (byId('reset-to-login') || {addEventListener: function(){}}).addEventListener('click', function (e) { e.preventDefault(); view('login'); }); var resetForm = byId('reset-form') || document.createElement('form'); var resetBtn = byId('reset-submit') || document.createElement('button'); resetForm.addEventListener('submit', function (e) { e.preventDefault(); clearBanners(); var token = resetToken(); var pw = byId('new-password').value; var pw2 = byId('new-password2').value; if (!token) { showError('This reset link is incomplete. Request a new one.'); return; } if (pw !== pw2) { showError('The two passwords do not match.'); return; } if (pw.length < 12) { showError('Password must be at least 12 characters.'); return; } resetBtn.disabled = true; resetBtn.textContent = 'Saving…'; postJson('/api/auth/reset-password', { token: token, new_password: pw }) .then(function (res) { if (res.ok) { // Take the token out of the URL before anything else — it's spent. try { history.replaceState(null, '', 'login.html'); } catch (err) {} view('login'); showOk('Password updated. Sign in with your new password.'); byId('username').focus(); return; } showError(detail(res, 'Could not set your password (HTTP ' + res.status + ').')); resetBtn.disabled = false; resetBtn.textContent = 'Set password & sign in'; }) .catch(function () { showError('Could not reach the server. Check your connection and try again.'); resetBtn.disabled = false; resetBtn.textContent = 'Set password & sign in'; }); }); // Arriving from the reset email opens straight into the new-password view. if (resetToken()) view('reset'); })();