feat: password generator, weak-password block, confirm step for generated passwords

This commit is contained in:
Simon
2026-04-22 23:04:46 +02:00
parent 480c3d09e5
commit 345d995b96
2 changed files with 164 additions and 43 deletions
+121 -11
View File
@@ -258,7 +258,8 @@ footer a:hover { color: #2563eb; }
<button type="button" onclick="hideForgot()" style="font-size:12px;color:#6b7280;background:none;border:none;cursor:pointer;padding:4px 0;margin-top:4px;text-align:center">← Zurück zum Login</button> <button type="button" onclick="hideForgot()" style="font-size:12px;color:#6b7280;background:none;border:none;cursor:pointer;padding:4px 0;margin-top:4px;text-align:center">← Zurück zum Login</button>
</form> </form>
<form class="form" id="form-reg" onsubmit="doRegister(event)"> <form class="form" id="form-reg" onsubmit="return false">
<div id="reg-step-1">
<div class="field"> <div class="field">
<label>Ich bin</label> <label>Ich bin</label>
<div class="role-toggle"> <div class="role-toggle">
@@ -288,15 +289,34 @@ footer a:hover { color: #2563eb; }
</div> </div>
<div class="field"> <div class="field">
<label for="r-pass">Passwort</label> <label for="r-pass">Passwort</label>
<input type="password" id="r-pass" autocomplete="new-password" placeholder="Mindestens 8 Zeichen" required oninput="checkStrength(this.value)"> <input type="password" id="r-pass" autocomplete="new-password" placeholder="Mindestens 8 Zeichen" required oninput="onPassInput(this.value)">
<button type="button" onclick="useGeneratedPassword()" style="margin-top:7px;font-size:12px;font-weight:600;color:#2563eb;background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;padding:5px 10px;cursor:pointer;font-family:inherit;width:100%;text-align:center">🔀 Sicheres Passwort generieren</button>
<div id="gen-display" style="display:none;margin-top:6px;background:#f0fdf4;border:1px solid #86efac;border-radius:6px;padding:8px 10px;font-size:12px;color:#166534">
<div style="margin-bottom:4px;font-weight:600">Generiertes Passwort unbedingt speichern:</div>
<div style="display:flex;align-items:center;gap:8px">
<span id="gen-pw-text" style="font-family:monospace;font-size:13px;word-break:break-all;flex:1;letter-spacing:.5px"></span>
<button type="button" id="copy-btn" onclick="copyGenPw()" style="flex-shrink:0;font-size:11px;font-weight:600;color:#166534;background:#dcfce7;border:1px solid #86efac;border-radius:5px;padding:3px 8px;cursor:pointer;font-family:inherit">Kopieren</button>
</div>
</div>
<div class="strength-wrap"> <div class="strength-wrap">
<div class="strength-bar"><div class="strength-fill" id="strength-fill"></div></div> <div class="strength-bar"><div class="strength-fill" id="strength-fill"></div></div>
<div class="strength-label" id="strength-label">Passwort eingeben</div> <div class="strength-label" id="strength-label">Passwort eingeben</div>
</div> </div>
</div> </div>
<div class="notice notice-red" id="reg-err"></div> <div class="notice notice-red" id="reg-err"></div>
<button class="btn-submit" type="button" id="reg-weiter-btn" onclick="regWeiter()">Weiter</button>
</div>
<div id="reg-step-2" style="display:none">
<p style="font-size:14px;color:#374151;margin-bottom:14px;line-height:1.5">Gib das generierte Passwort nochmal ein, um sicherzustellen, dass du es dir gespeichert hast.</p>
<div class="field">
<label for="r-pass-confirm">Passwort bestätigen</label>
<input type="password" id="r-pass-confirm" autocomplete="new-password" placeholder="••••••">
</div>
<div class="notice notice-red" id="reg-confirm-err"></div>
<button class="btn-submit" type="button" id="reg-confirm-btn" onclick="doRegister()">Account erstellen</button>
<button type="button" onclick="regBack()" style="display:block;width:100%;text-align:center;font-size:12px;color:#6b7280;background:none;border:none;cursor:pointer;padding:6px 0;margin-top:4px">← Zurück</button>
</div>
<div class="notice notice-green" id="reg-ok">Registrierung erfolgreich! Wir haben dir eine Bestätigungs-Mail geschickt. Bitte prüfe dein Postfach und klicke auf den Link.</div> <div class="notice notice-green" id="reg-ok">Registrierung erfolgreich! Wir haben dir eine Bestätigungs-Mail geschickt. Bitte prüfe dein Postfach und klicke auf den Link.</div>
<button class="btn-submit" type="submit" id="reg-btn">Account erstellen</button>
</form> </form>
</div> </div>
@@ -365,21 +385,111 @@ async function doLogin(e) {
window.location.href = '/app'; window.location.href = '/app';
} }
async function doRegister(e) { let generatedPasswordUsed = false;
e.preventDefault(); clearErr('reg-err'); let lastGeneratedPw = '';
const WEAK_PW = new Set([
'passwort','password','passwort1','password1','passwort123','password123',
'123456','1234567','12345678','123456789','1234567890','qwerty','qwertz',
'abc123','iloveyou','admin','admin123','letmein','welcome','111111',
'000000','123123','hallo','hallo123','test','test1234','schueler','schule',
'klassenportal','ifbschule','ifb',
]);
function onPassInput(val) {
generatedPasswordUsed = false;
document.getElementById('gen-display').style.display = 'none';
checkStrength(val);
}
function useGeneratedPassword() {
const chars = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%&*';
const arr = new Uint8Array(16);
crypto.getRandomValues(arr);
const pw = Array.from(arr, b => chars[b % chars.length]).join('');
lastGeneratedPw = pw;
generatedPasswordUsed = true;
const input = document.getElementById('r-pass');
input.value = pw;
input.type = 'text';
checkStrength(pw);
document.getElementById('gen-pw-text').textContent = pw;
document.getElementById('gen-display').style.display = '';
document.getElementById('copy-btn').textContent = 'Kopieren';
}
function copyGenPw() {
navigator.clipboard.writeText(lastGeneratedPw).catch(() => {
const t = document.createElement('textarea');
t.value = lastGeneratedPw; document.body.appendChild(t); t.select();
document.execCommand('copy'); document.body.removeChild(t);
});
const btn = document.getElementById('copy-btn');
btn.textContent = '✓ Kopiert';
setTimeout(() => { btn.textContent = 'Kopieren'; }, 2000);
}
function validateRegForm() {
const email = document.getElementById('r-email').value.trim().toLowerCase();
const pw = document.getElementById('r-pass').value;
clearErr('reg-err');
if (!email) { showErr('reg-err', 'E-Mail-Adresse erforderlich'); return false; }
if (!pw) { showErr('reg-err', 'Passwort erforderlich'); return false; }
if (pw.length < 8) { showErr('reg-err', 'Passwort muss mindestens 8 Zeichen lang sein'); return false; }
const uname = email.split('@')[0].toLowerCase();
if (pw.toLowerCase() === uname) { showErr('reg-err', 'Passwort darf nicht dem Benutzernamen entsprechen'); return false; }
if (WEAK_PW.has(pw.toLowerCase())) { showErr('reg-err', 'Dieses Passwort ist zu leicht zu erraten'); return false; }
if (selectedRole === 'teacher' && !document.getElementById('r-subject').value) {
showErr('reg-err', 'Bitte ein Lehrfach auswählen'); return false;
}
return true;
}
function regWeiter() {
if (!validateRegForm()) return;
if (generatedPasswordUsed) {
document.getElementById('reg-step-1').style.display = 'none';
document.getElementById('reg-step-2').style.display = '';
document.getElementById('r-pass-confirm').value = '';
document.getElementById('r-pass-confirm').focus();
clearErr('reg-confirm-err');
} else {
doRegister();
}
}
function regBack() {
document.getElementById('reg-step-2').style.display = 'none';
document.getElementById('reg-step-1').style.display = '';
clearErr('reg-confirm-err');
}
async function doRegister() {
if (generatedPasswordUsed) {
const confirm = document.getElementById('r-pass-confirm').value;
if (confirm !== document.getElementById('r-pass').value) {
showErr('reg-confirm-err', 'Passwörter stimmen nicht überein'); return;
}
clearErr('reg-confirm-err');
}
const btn = document.getElementById(generatedPasswordUsed ? 'reg-confirm-btn' : 'reg-weiter-btn');
btn.disabled = true; btn.textContent = 'Wird gesendet…';
const body = { const body = {
email: document.getElementById('r-email').value, email: document.getElementById('r-email').value.trim().toLowerCase(),
password: document.getElementById('r-pass').value, password: document.getElementById('r-pass').value,
role: selectedRole, role: selectedRole,
}; };
if (selectedRole === 'teacher') body.subject = document.getElementById('r-subject').value; if (selectedRole === 'teacher') body.subject = document.getElementById('r-subject').value;
const r = await fetch('/api/register', { method:'POST', headers:{'Content-Type':'application/json'}, const r = await fetch('/api/register', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) });
body: JSON.stringify(body) });
const d = await r.json(); const d = await r.json();
if (!r.ok) { showErr('reg-err', d.error); return; } if (!r.ok) {
showErr(generatedPasswordUsed ? 'reg-confirm-err' : 'reg-err', d.error);
btn.disabled = false;
btn.textContent = generatedPasswordUsed ? 'Account erstellen' : 'Weiter';
return;
}
document.getElementById('reg-ok').classList.add('show'); document.getElementById('reg-ok').classList.add('show');
document.getElementById('reg-btn').disabled = true; btn.disabled = true; btn.textContent = 'Registriert ✓';
document.getElementById('reg-btn').textContent = 'Registriert';
} }
function showForgot() { function showForgot() {
+11
View File
@@ -18,6 +18,14 @@ const RESET_TTL_MS = 60 * 60 * 1000;
const DUMMY_PASSWORD_HASH = bcrypt.hashSync('dummy-placeholder-value', 12); const DUMMY_PASSWORD_HASH = bcrypt.hashSync('dummy-placeholder-value', 12);
const WEAK_PASSWORDS = new Set([
'passwort','password','passwort1','password1','passwort123','password123',
'123456','1234567','12345678','123456789','1234567890','qwerty','qwertz',
'abc123','iloveyou','admin','admin123','letmein','welcome','111111',
'000000','123123','hallo','hallo123','test','test1234','schueler','schule',
'klassenportal','ifbschule','ifb',
]);
function generateRecoveryCodes() { function generateRecoveryCodes() {
const plain = Array.from({ length: 8 }, () => { const plain = Array.from({ length: 8 }, () => {
const h = crypto.randomBytes(5).toString('hex'); const h = crypto.randomBytes(5).toString('hex');
@@ -108,6 +116,9 @@ router.post('/register', registerLimiter, async (req, res) => {
if (!IFB_EMAIL_RE.test(email)) return res.status(403).json({ error: 'Ungültige E-Mail-Adresse' }); if (!IFB_EMAIL_RE.test(email)) return res.status(403).json({ error: 'Ungültige E-Mail-Adresse' });
if (password.length < 8) return res.status(400).json({ error: 'Passwort zu kurz (min. 8 Zeichen)' }); if (password.length < 8) return res.status(400).json({ error: 'Passwort zu kurz (min. 8 Zeichen)' });
const username = email.split('@')[0].toLowerCase(); const username = email.split('@')[0].toLowerCase();
const pwLower = password.toLowerCase();
if (pwLower === username) return res.status(400).json({ error: 'Passwort darf nicht dem Benutzernamen entsprechen' });
if (WEAK_PASSWORDS.has(pwLower)) return res.status(400).json({ error: 'Dieses Passwort ist zu leicht zu erraten. Bitte wähle ein sichereres.' });
const safeRole = (role === 'teacher') ? 'teacher' : 'student'; const safeRole = (role === 'teacher') ? 'teacher' : 'student';
const initialStatus = safeRole === 'teacher' ? 'pending' : 'active'; const initialStatus = safeRole === 'teacher' ? 'pending' : 'active';