feat: password generator, weak-password block, confirm step for generated passwords
This commit is contained in:
+121
-11
@@ -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>
|
||||
</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">
|
||||
<label>Ich bin</label>
|
||||
<div class="role-toggle">
|
||||
@@ -288,15 +289,34 @@ footer a:hover { color: #2563eb; }
|
||||
</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)">
|
||||
<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-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() {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user