Files
ifb-schulapp/public/login.html
T

567 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#2563eb">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Klassenportal">
<title>Klassenportal · Anmelden</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: #f4f6f9;
min-height: 100vh;
display: flex;
flex-direction: column;
-webkit-font-smoothing: antialiased;
color: #111827;
}
.page {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 32px 16px;
}
.card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 14px;
box-shadow: 0 4px 16px rgba(0,0,0,.06), 0 1px 4px rgba(0,0,0,.04);
padding: 36px 32px 32px;
width: 100%;
max-width: 400px;
}
/* Brand */
.brand-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 28px;
}
.brand {
display: flex;
align-items: center;
gap: 9px;
text-decoration: none;
}
.brand-mark {
width: 36px; height: 36px;
background: #2563eb;
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 14px; font-weight: 800; color: #fff;
letter-spacing: -.5px; flex-shrink: 0;
}
.brand-text { display: flex; flex-direction: column; line-height: 1.2; }
.brand-sub { font-size: 10px; color: #9ca3af; font-weight: 500; letter-spacing: .2px; }
.brand-name { font-size: 16px; font-weight: 700; color: #111827; letter-spacing: -.3px; }
.back-link {
font-size: 12px;
color: #6b7280;
text-decoration: none;
display: flex; align-items: center; gap: 4px;
transition: color .12s;
padding: 4px 0;
}
.back-link:hover { color: #2563eb; }
/* Tabs */
.tabs {
display: flex;
background: #f3f4f6;
border-radius: 8px;
padding: 3px;
margin-bottom: 24px;
gap: 2px;
}
.tab {
flex: 1;
padding: 7px;
text-align: center;
font-size: 13px; font-weight: 600;
color: #6b7280;
border-radius: 6px;
cursor: pointer;
transition: background .12s, color .12s;
user-select: none;
}
.tab.active {
background: #fff;
color: #111827;
box-shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.04);
}
/* Forms */
.form { display: none; flex-direction: column; gap: 14px; }
.form.active { display: flex; }
.field { display: flex; flex-direction: column; gap: 5px; }
.field label { font-size: 12px; font-weight: 600; color: #374151; letter-spacing: .1px; }
.field input {
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, box-shadow .12s;
}
.field input:focus { border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,.1); }
/* Role toggle */
.role-toggle {
display: flex;
background: #f3f4f6;
border-radius: 8px;
padding: 3px;
gap: 2px;
}
.role-opt {
flex: 1; padding: 7px 10px;
text-align: center;
font-size: 13px; font-weight: 600;
color: #6b7280;
border-radius: 6px;
cursor: pointer;
transition: background .12s, color .12s;
user-select: none;
}
.role-opt.active { background: #fff; color: #111827; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
.role-opt.active.teacher { background: #fefce8; color: #854d0e; }
/* Notices */
.notice {
font-size: 12px;
border-radius: 7px;
padding: 9px 12px;
line-height: 1.5;
display: none;
}
.notice.show { display: block; }
.notice-amber { color: #92400e; background: #fffbeb; border: 1px solid #fde68a; }
.notice-red { color: #dc2626; background: #fef2f2; border: 1px solid #fecaca; }
.notice-green { color: #15803d; background: #f0fdf4; border: 1px solid #bbf7d0; }
/* Password strength */
.strength-wrap { margin-top: 4px; }
.strength-bar { height: 3px; border-radius: 99px; background: #e5e7eb; overflow: hidden; }
.strength-fill { height: 100%; border-radius: 99px; transition: width .25s, background .25s; width: 0; }
.strength-label { font-size: 11px; color: #9ca3af; margin-top: 3px; }
/* Submit button */
.btn-submit {
padding: 10px;
background: #2563eb; color: #fff;
border: none; border-radius: 8px;
font-size: 14px; font-weight: 600; font-family: inherit;
cursor: pointer;
transition: background .12s, transform .08s;
margin-top: 2px;
}
.btn-submit:hover { background: #1d4ed8; }
.btn-submit:active { transform: scale(.99); }
.btn-submit:disabled { background: #93c5fd; cursor: default; }
/* Footer */
footer {
text-align: center;
padding: 18px;
font-size: 12px;
color: #9ca3af;
}
footer a { color: #6b7280; text-decoration: none; transition: color .12s; }
footer a:hover { color: #2563eb; }
/* Icons */
.lucide { display: inline-block; vertical-align: -0.125em; flex-shrink: 0; width: 1em; height: 1em; stroke-width: 2; }
.notice .lucide { width: 14px; height: 14px; margin-right: 3px; }
</style>
<script>if ('serviceWorker' in navigator) navigator.serviceWorker.register('/sw.js');</script>
</head>
<body>
<div class="page">
<div class="card">
<div class="brand-row">
<a class="brand" href="/">
<div class="brand-mark">KP</div>
<div class="brand-text">
<span class="brand-sub">Klassenportal</span>
<span class="brand-name">Klassenportal</span>
</div>
</a>
<a class="back-link" href="/">← Dashboard</a>
</div>
<div class="tabs">
<div class="tab active" id="tab-login" onclick="switchTab('login')">Anmelden</div>
<div class="tab" id="tab-reg" onclick="switchTab('register')">Registrieren</div>
</div>
<form class="form active" id="form-login" onsubmit="doLogin(event)">
<div id="login-step-1">
<div class="field" style="margin-bottom:14px">
<label for="l-user">Benutzername</label>
<input type="text" id="l-user" autocomplete="username" placeholder="dein.name" required>
<span style="font-size:11px;color:#9ca3af;margin-top:2px">Der Teil deiner Schul-E-Mail vor dem @</span>
</div>
<div class="field">
<label for="l-pass">Passwort</label>
<input type="password" id="l-pass" autocomplete="current-password" placeholder="••••••" required>
</div>
</div>
<div id="login-step-2" style="display:none">
<div class="field">
<label for="l-totp">2FA-Code</label>
<input type="text" id="l-totp" autocomplete="one-time-code" placeholder="6-stelliger Code oder Wiederherstellungscode" maxlength="11">
<span style="font-size:11px;color:#9ca3af;margin-top:2px">Code aus der Authenticator-App oder einen Wiederherstellungscode (xxxxx-xxxxx) eingeben</span>
</div>
<button type="button" style="font-size:12px;color:#6b7280;background:none;border:none;cursor:pointer;padding:0;margin-top:4px" onclick="backToStep1()">← Zurück</button>
</div>
<div class="notice notice-green" id="login-verified-ok">E-Mail erfolgreich bestätigt. Du kannst dich jetzt anmelden.</div>
<div class="notice notice-green" id="login-reset-ok">Falls für diese Adresse ein Konto existiert, wurde eine E-Mail zum Zurücksetzen versendet.</div>
<div class="notice notice-red" id="login-err"></div>
<div class="notice notice-amber" id="login-resend" style="text-align:center">
<div style="margin-bottom:6px">E-Mail noch nicht bestätigt?</div>
<button type="button" id="resend-btn" onclick="resendVerify()" style="font-size:12px;color:#2563eb;background:none;border:none;cursor:pointer;padding:0;font-weight:600">Verifizierungsmail erneut senden</button>
</div>
<button class="btn-submit" type="submit" id="login-btn">Anmelden</button>
<button type="button" onclick="showForgot()" id="forgot-link" style="font-size:12px;color:#6b7280;background:none;border:none;cursor:pointer;padding:4px 0;margin-top:4px;text-align:center">Passwort vergessen?</button>
</form>
<form class="form" id="form-forgot" onsubmit="doForgot(event)">
<div style="font-size:14px;color:#374151;margin-bottom:4px">Gib deine Schul-E-Mail ein. Wir schicken dir einen Link zum Zurücksetzen.</div>
<div class="field">
<label for="f-email">Schul-E-Mail</label>
<input type="email" id="f-email" autocomplete="email" placeholder="dein.name@ifb-schulen.com" required>
</div>
<div class="notice notice-red" id="forgot-err"></div>
<button class="btn-submit" type="submit" id="forgot-btn">Link senden</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 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 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-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>
</form>
</div>
</div>
<footer>
<a href="/datenschutz">Datenschutzerklärung</a>
</footer>
<script>
let selectedRole = 'student';
function selectRole(r) {
selectedRole = r;
document.getElementById('role-student').classList.toggle('active', r === 'student');
document.getElementById('role-teacher').classList.toggle('active', r === 'teacher');
document.getElementById('role-teacher').classList.toggle('teacher', r === 'teacher');
document.getElementById('teacher-notice').classList.toggle('show', r === 'teacher');
document.getElementById('subject-field').style.display = r === 'teacher' ? '' : 'none';
document.getElementById('reg-btn').textContent = r === 'teacher' ? 'Als Lehrer/in registrieren' : 'Account erstellen';
}
function switchTab(t) {
document.getElementById('tab-login').classList.toggle('active', t==='login');
document.getElementById('tab-reg').classList.toggle('active', t==='register');
document.getElementById('form-login').classList.toggle('active', t==='login');
document.getElementById('form-reg').classList.toggle('active', t==='register');
}
function showErr(id, msg) {
const el = document.getElementById(id);
el.textContent = msg; el.classList.add('show');
}
function clearErr(id) { document.getElementById(id).classList.remove('show'); }
let totpPending = false;
function backToStep1() {
totpPending = false;
document.getElementById('login-step-1').style.display = '';
document.getElementById('login-step-2').style.display = 'none';
document.getElementById('login-btn').textContent = 'Anmelden';
clearErr('login-err');
}
async function doLogin(e) {
e.preventDefault(); clearErr('login-err'); clearErr('login-resend');
const body = { username: document.getElementById('l-user').value, password: document.getElementById('l-pass').value };
if (totpPending) body.totp_token = document.getElementById('l-totp').value;
const r = await fetch('/api/login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) });
const d = await r.json();
if (!r.ok) {
showErr('login-err', d.error);
if (d.needVerify) document.getElementById('login-resend').classList.add('show');
return;
}
if (d.requireTotp) {
totpPending = true;
document.getElementById('login-step-1').style.display = 'none';
document.getElementById('login-step-2').style.display = '';
document.getElementById('login-btn').textContent = 'Bestätigen';
document.getElementById('l-totp').value = '';
document.getElementById('l-totp').focus();
return;
}
window.location.href = '/app';
}
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.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 d = await r.json();
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');
btn.disabled = true; btn.textContent = 'Registriert ✓';
}
function showForgot() {
document.getElementById('form-login').classList.remove('active');
document.getElementById('form-reg').classList.remove('active');
document.getElementById('form-forgot').classList.add('active');
document.querySelector('.tabs').style.display = 'none';
}
function hideForgot() {
document.getElementById('form-forgot').classList.remove('active');
document.getElementById('form-login').classList.add('active');
document.querySelector('.tabs').style.display = '';
clearErr('forgot-err');
}
async function doForgot(e) {
e.preventDefault(); clearErr('forgot-err');
const email = document.getElementById('f-email').value.trim().toLowerCase();
const btn = document.getElementById('forgot-btn');
btn.disabled = true; btn.textContent = 'Wird gesendet...';
const r = await fetch('/api/forgot-password', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ email }) });
const d = await r.json().catch(()=>({}));
if (!r.ok) {
showErr('forgot-err', d.error || 'Fehler');
btn.disabled = false; btn.textContent = 'Link senden';
return;
}
hideForgot();
document.getElementById('login-reset-ok').classList.add('show');
btn.disabled = false; btn.textContent = 'Link senden';
document.getElementById('f-email').value = '';
}
async function resendVerify() {
const username = document.getElementById('l-user').value;
if (!username) return;
const email = username.toLowerCase() + '@ifb-schulen.com';
const btn = document.getElementById('resend-btn');
btn.disabled = true; btn.textContent = 'Wird gesendet...';
const r = await fetch('/api/resend-verify', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ email }) });
if (r.ok) btn.textContent = 'Mail gesendet ✓';
else { const d = await r.json().catch(()=>({})); btn.textContent = d.error || 'Fehler'; btn.disabled = false; }
}
function checkStrength(pw) {
let score = 0;
if (pw.length >= 8) score++;
if (pw.length >= 10) score++;
if (/[A-Z]/.test(pw)) score++;
if (/[0-9]/.test(pw)) score++;
if (/[^A-Za-z0-9]/.test(pw)) score++;
const levels = [
{ w: '0%', bg: '#e5e7eb', label: 'Passwort eingeben' },
{ w: '25%', bg: '#ef4444', label: 'Sehr schwach' },
{ w: '50%', bg: '#f97316', label: 'Schwach' },
{ w: '75%', bg: '#eab308', label: 'Mittel' },
{ w: '90%', bg: '#22c55e', label: 'Stark' },
{ w: '100%', bg: '#16a34a', label: 'Sehr stark' },
];
const l = levels[Math.min(score, 5)];
const fill = document.getElementById('strength-fill');
fill.style.width = l.w; fill.style.background = l.bg;
document.getElementById('strength-label').textContent = l.label;
}
const p = new URLSearchParams(location.search);
if (p.get('tab') === 'register') switchTab('register');
if (p.get('verified') === '1') document.getElementById('login-verified-ok').classList.add('show');
fetch('/api/me').then(r => { if (r.ok) window.location.href = '/app'; });
lucide.createIcons();
</script>
</body>
</html>