From 345d995b9627b082b400e3d0db03bbe3de1a4087 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 22 Apr 2026 23:04:46 +0200 Subject: [PATCH] feat: password generator, weak-password block, confirm step for generated passwords --- public/login.html | 196 ++++++++++++++++++++++++++++++++++++---------- src/routes.js | 11 +++ 2 files changed, 164 insertions(+), 43 deletions(-) diff --git a/public/login.html b/public/login.html index a22cfa3..3908cbd 100644 --- a/public/login.html +++ b/public/login.html @@ -258,45 +258,65 @@ footer a:hover { color: #2563eb; } -
-
- -
-
Schüler/in
-
Lehrer/in
+ +
+
+ +
+
Schüler/in
+
Lehrer/in
+
-
-
- Lehrerkonten werden nach der Registrierung von einem Administrator geprüft und freigeschaltet. -
- -
- - -
-
- - -
-
-
Passwort eingeben
+
+ Lehrerkonten werden nach der Registrierung von einem Administrator geprüft und freigeschaltet.
+ +
+ + +
+
+ + + + +
+
+
Passwort eingeben
+
+
+
+ +
+ -
Registrierung erfolgreich! Wir haben dir eine Bestätigungs-Mail geschickt. Bitte prüfe dein Postfach und klicke auf den Link.
-
@@ -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() { diff --git a/src/routes.js b/src/routes.js index 93eb824..4f514cb 100644 --- a/src/routes.js +++ b/src/routes.js @@ -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';