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
+153 -43
View File
@@ -258,45 +258,65 @@ 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>
</form>
<form class="form" id="form-reg" onsubmit="doRegister(event)">
<div class="field">
<label>Ich bin</label>
<div class="role-toggle">
<div class="role-opt active" id="role-student" onclick="selectRole('student')">Schüler/in</div>
<div class="role-opt" id="role-teacher" onclick="selectRole('teacher')">Lehrer/in</div>
<form class="form" id="form-reg" onsubmit="return false">
<div id="reg-step-1">
<div class="field">
<label>Ich bin</label>
<div class="role-toggle">
<div class="role-opt active" id="role-student" onclick="selectRole('student')">Schüler/in</div>
<div class="role-opt" id="role-teacher" onclick="selectRole('teacher')">Lehrer/in</div>
</div>
</div>
</div>
<div class="notice notice-amber" id="teacher-notice">
<i data-lucide="info" aria-hidden="true"></i> Lehrerkonten werden nach der Registrierung von einem Administrator geprüft und freigeschaltet.
</div>
<div class="field" id="subject-field" style="display:none">
<label for="r-subject">Lehrfach</label>
<select id="r-subject" style="padding:9px 12px;border:1px solid #e5e7eb;border-radius:7px;font-size:14px;font-family:inherit;color:#111827;background:#fff;outline:none;transition:border-color .12s">
<option value=""> Fach auswählen </option>
<option>Deutsch</option><option>Mathematik</option><option>Englisch</option>
<option>Informatik</option><option>Wirtschaft</option><option>Buchführung</option>
<option>BWL</option><option>VWL</option><option>Recht</option>
<option>Rechnungswesen</option><option>Sport</option><option>Religion</option>
<option>Geschichte</option><option>Gemeinschaftskunde</option><option>Physik</option>
<option>Chemie</option><option>Biologie</option><option>Sozialkunde</option>
<option>Ethik</option><option>Sonstiges</option>
</select>
</div>
<div class="field">
<label for="r-email">Schul E-Mail</label>
<input type="email" id="r-email" autocomplete="email" placeholder="Schul E-Mail-Adresse" required>
</div>
<div class="field">
<label for="r-pass">Passwort</label>
<input type="password" id="r-pass" autocomplete="new-password" placeholder="Mindestens 8 Zeichen" required oninput="checkStrength(this.value)">
<div class="strength-wrap">
<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="notice notice-amber" id="teacher-notice">
<i data-lucide="info" aria-hidden="true"></i> Lehrerkonten werden nach der Registrierung von einem Administrator geprüft und freigeschaltet.
</div>
<div class="field" id="subject-field" style="display:none">
<label for="r-subject">Lehrfach</label>
<select id="r-subject" style="padding:9px 12px;border:1px solid #e5e7eb;border-radius:7px;font-size:14px;font-family:inherit;color:#111827;background:#fff;outline:none;transition:border-color .12s">
<option value=""> Fach auswählen </option>
<option>Deutsch</option><option>Mathematik</option><option>Englisch</option>
<option>Informatik</option><option>Wirtschaft</option><option>Buchführung</option>
<option>BWL</option><option>VWL</option><option>Recht</option>
<option>Rechnungswesen</option><option>Sport</option><option>Religion</option>
<option>Geschichte</option><option>Gemeinschaftskunde</option><option>Physik</option>
<option>Chemie</option><option>Biologie</option><option>Sozialkunde</option>
<option>Ethik</option><option>Sonstiges</option>
</select>
</div>
<div class="field">
<label for="r-email">Schul E-Mail</label>
<input type="email" id="r-email" autocomplete="email" placeholder="Schul E-Mail-Adresse" required>
</div>
<div class="field">
<label for="r-pass">Passwort</label>
<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-bar"><div class="strength-fill" id="strength-fill"></div></div>
<div class="strength-label" id="strength-label">Passwort eingeben</div>
</div>
</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-red" id="reg-err"></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>
</div>
@@ -365,21 +385,111 @@ async function doLogin(e) {
window.location.href = '/app';
}
async function doRegister(e) {
e.preventDefault(); clearErr('reg-err');
let generatedPasswordUsed = false;
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 = {
email: document.getElementById('r-email').value,
email: document.getElementById('r-email').value.trim().toLowerCase(),
password: document.getElementById('r-pass').value,
role: selectedRole,
};
if (selectedRole === 'teacher') body.subject = document.getElementById('r-subject').value;
const r = await fetch('/api/register', { method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify(body) });
const r = await fetch('/api/register', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) });
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-btn').disabled = true;
document.getElementById('reg-btn').textContent = 'Registriert';
btn.disabled = true; btn.textContent = 'Registriert ✓';
}
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 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() {
const plain = Array.from({ length: 8 }, () => {
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 (password.length < 8) return res.status(400).json({ error: 'Passwort zu kurz (min. 8 Zeichen)' });
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 initialStatus = safeRole === 'teacher' ? 'pending' : 'active';