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>
|
<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() {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user