feat: add TOTP 2FA with QR code and manual secret entry

This commit is contained in:
Simon
2026-04-17 22:56:39 +02:00
parent ae789318ba
commit 8f75bc6a10
7 changed files with 959 additions and 26 deletions
+40 -10
View File
@@ -209,17 +209,27 @@ footer a:hover { color: #2563eb; }
</div>
<form class="form active" id="form-login" onsubmit="doLogin(event)">
<div class="field">
<label for="l-user">Benutzername</label>
<input type="text" id="l-user" autocomplete="username" placeholder="dein.name" required>
<span style="font-size:11px;color:#9ca3af;margin-top:2px">Der Teil deiner Schul-E-Mail vor dem @</span>
<div id="login-step-1">
<div class="field" style="margin-bottom:14px">
<label for="l-user">Benutzername</label>
<input type="text" id="l-user" autocomplete="username" placeholder="dein.name" required>
<span style="font-size:11px;color:#9ca3af;margin-top:2px">Der Teil deiner Schul-E-Mail vor dem @</span>
</div>
<div class="field">
<label for="l-pass">Passwort</label>
<input type="password" id="l-pass" autocomplete="current-password" placeholder="••••••" required>
</div>
</div>
<div class="field">
<label for="l-pass">Passwort</label>
<input type="password" id="l-pass" autocomplete="current-password" placeholder="••••••" required>
<div id="login-step-2" style="display:none">
<div class="field">
<label for="l-totp">2FA-Code</label>
<input type="text" id="l-totp" autocomplete="one-time-code" placeholder="6-stelliger Code" maxlength="6" inputmode="numeric">
<span style="font-size:11px;color:#9ca3af;margin-top:2px">Code aus deiner Authenticator-App eingeben</span>
</div>
<button type="button" style="font-size:12px;color:#6b7280;background:none;border:none;cursor:pointer;padding:0;margin-top:4px" onclick="backToStep1()">← Zurück</button>
</div>
<div class="notice notice-red" id="login-err"></div>
<button class="btn-submit" type="submit">Anmelden</button>
<button class="btn-submit" type="submit" id="login-btn">Anmelden</button>
</form>
<form class="form" id="form-reg" onsubmit="doRegister(event)">
@@ -296,12 +306,32 @@ function showErr(id, msg) {
}
function clearErr(id) { document.getElementById(id).classList.remove('show'); }
let totpPending = false;
function backToStep1() {
totpPending = false;
document.getElementById('login-step-1').style.display = '';
document.getElementById('login-step-2').style.display = 'none';
document.getElementById('login-btn').textContent = 'Anmelden';
clearErr('login-err');
}
async function doLogin(e) {
e.preventDefault(); clearErr('login-err');
const r = await fetch('/api/login', { method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ username: document.getElementById('l-user').value, password: document.getElementById('l-pass').value }) });
const body = { username: document.getElementById('l-user').value, password: document.getElementById('l-pass').value };
if (totpPending) body.totp_token = document.getElementById('l-totp').value;
const r = await fetch('/api/login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) });
const d = await r.json();
if (!r.ok) { showErr('login-err', d.error); return; }
if (d.requireTotp) {
totpPending = true;
document.getElementById('login-step-1').style.display = 'none';
document.getElementById('login-step-2').style.display = '';
document.getElementById('login-btn').textContent = 'Bestätigen';
document.getElementById('l-totp').value = '';
document.getElementById('l-totp').focus();
return;
}
window.location.href = '/';
}