ae789318ba
- Teacher registration requires subject selection; account starts pending - Admin approves/rejects via existing admin panel - Teacher panel (Materialien, Ankündigungen, Prüfungen, Noten) visible only to approved teachers - Students see class materials and announcements via sidebar overlays - Teachers can assign grades to students (scoped to own subject) - New tables: teacher_materials, teacher_announcements, teacher_exams, teacher_assigned_grades - subject column added to users; included in JWT and /api/me - requireTeacher middleware fetches fresh status+subject from DB on every request - Login hint: username is the part of the school email before the @
359 lines
12 KiB
HTML
359 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>IFB · INFO1 – 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>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="page">
|
||
<div class="card">
|
||
|
||
<div class="brand-row">
|
||
<a class="brand" href="/">
|
||
<div class="brand-mark">i1</div>
|
||
<div class="brand-text">
|
||
<span class="brand-sub">IFB-Berufsfachschule Rosenheim</span>
|
||
<span class="brand-name">INFO1</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 class="field">
|
||
<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 class="notice notice-red" id="login-err"></div>
|
||
<button class="btn-submit" type="submit">Anmelden</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>
|
||
</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 6 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>
|
||
</div>
|
||
<div class="notice notice-red" id="reg-err"></div>
|
||
<div class="notice notice-green" id="reg-ok">Registrierung erfolgreich! Dein Lehrerkonto wird von einem Administrator geprüft und freigeschaltet.</div>
|
||
<button class="btn-submit" type="submit" id="reg-btn">Account erstellen</button>
|
||
</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'); }
|
||
|
||
async function doLogin(e) {
|
||
e.preventDefault(); clearErr('login-err');
|
||
const r = await fetch('/api/login', { method:'POST', headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({ username: document.getElementById('l-user').value, password: document.getElementById('l-pass').value }) });
|
||
const d = await r.json();
|
||
if (!r.ok) { showErr('login-err', d.error); return; }
|
||
window.location.href = '/';
|
||
}
|
||
|
||
async function doRegister(e) {
|
||
e.preventDefault(); clearErr('reg-err');
|
||
const body = {
|
||
email: document.getElementById('r-email').value,
|
||
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('reg-err', d.error); return; }
|
||
if (d.pending) {
|
||
document.getElementById('reg-ok').classList.add('show');
|
||
document.getElementById('reg-btn').disabled = true;
|
||
document.getElementById('reg-btn').textContent = 'Registriert';
|
||
} else {
|
||
window.location.href = '/';
|
||
}
|
||
}
|
||
|
||
function checkStrength(pw) {
|
||
let score = 0;
|
||
if (pw.length >= 6) 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');
|
||
|
||
fetch('/api/me').then(r => { if (r.ok) window.location.href = '/'; });
|
||
|
||
lucide.createIcons();
|
||
</script>
|
||
</body>
|
||
</html>
|