From 9b14a74fcc7f3b2ba705d39b10a11df75b143706 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 10:29:36 +0000 Subject: [PATCH] =?UTF-8?q?Portal-Login:=20E-Mail-Code=20f=C3=BCr=20Mitarb?= =?UTF-8?q?eiter=20mit=202FA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- public/app.js | 57 +++++++++++++++ public/login.css | 15 ++++ public/login.html | 25 +++++++ server/routes/auth.js | 118 ++++++++++++++++++++++---------- server/services/staffAccount.js | 22 ++++++ test/staffLogin.test.js | 103 ++++++++++++++++++++++++++++ 6 files changed, 304 insertions(+), 36 deletions(-) create mode 100644 server/services/staffAccount.js create mode 100644 test/staffLogin.test.js diff --git a/public/app.js b/public/app.js index cef369a..61b6cc6 100644 --- a/public/app.js +++ b/public/app.js @@ -548,6 +548,10 @@ async function initLoginPage() { password: document.getElementById('password').value, }), }) + if (result.mfaRequired) { + showMfaStep(result.email) + return + } if (result.role === 'admin') { window.location.href = adminLoginTarget() return @@ -568,6 +572,59 @@ async function initLoginPage() { }) } +/* Mitarbeiter-2FA: Code aus der E-Mail (POST /api/auth/login/mfa) */ +function showMfaStep(maskedEmail) { + const loginForm = document.getElementById('login-form') + const form = document.getElementById('mfa-form') + const input = document.getElementById('mfa-code') + const errorEl = document.getElementById('mfa-error') + const btn = document.getElementById('mfa-btn') + const resend = document.getElementById('mfa-resend') + loginForm?.classList.add('hidden') + form?.classList.remove('hidden') + document.getElementById('mfa-hint').textContent = `Wir haben dir einen Code an ${maskedEmail} geschickt.` + input?.focus() + + form?.addEventListener('submit', async (e) => { + e.preventDefault() + if (btn.disabled) return + hideError(errorEl) + btn.disabled = true + btn.classList.add('is-loading') + try { + await api('/api/auth/login/mfa', { method: 'POST', body: JSON.stringify({ otp: input.value.trim() }) }) + window.location.href = adminLoginTarget() + } catch (err) { + if (err.status === 410) { + window.location.href = `/login.html?error=${encodeURIComponent(err.message)}` + return + } + showError(errorEl, err.message) + input.select() + } finally { + btn.disabled = false + btn.classList.remove('is-loading') + } + }) + + resend?.addEventListener('click', async () => { + hideError(errorEl) + resend.disabled = true + try { + await api('/api/auth/login/mfa/resend', { method: 'POST' }) + document.getElementById('mfa-hint').textContent = `Neuer Code ist unterwegs an ${maskedEmail}.` + } catch (err) { + if (err.status === 410) { + window.location.href = `/login.html?error=${encodeURIComponent(err.message)}` + return + } + showError(errorEl, err.message) + } finally { + resend.disabled = false + } + }) +} + /* --------------------------------------------------------------- View-as --- */ function renderViewAsBanner(me) { const banner = document.getElementById('viewas-banner') diff --git a/public/login.css b/public/login.css index 7729bfe..9f99b76 100644 --- a/public/login.css +++ b/public/login.css @@ -647,3 +647,18 @@ } } } + +/* Mitarbeiter-2FA: Code-Schritt */ +#mfa-form.hidden { display: none; } +.mfa-hint { margin: 0 0 4px; text-align: center; } +#mfa-code { letter-spacing: 0.3em; font-variant-numeric: tabular-nums; } +.mfa-actions { margin-top: 12px; } +.mfa-link { + background: none; + border: 0; + padding: 0; + font: inherit; + color: inherit; + text-decoration: underline; + cursor: pointer; +} diff --git a/public/login.html b/public/login.html index 4aab939..bced9c3 100644 --- a/public/login.html +++ b/public/login.html @@ -72,6 +72,31 @@ + +