From af4b1dc460688902e1580e91dcd5eda05c9c8d06 Mon Sep 17 00:00:00 2001 From: Alex Mueller Date: Sat, 12 Sep 2026 15:02:31 +0200 Subject: [PATCH] feat(security): implement robust maintenance mode, session login guard, and comprehensive security hardening --- admin.html | 53 ++++--- assets/app_auth.js | 358 ++++++++++++++++++++++++++++++++++----------- server.py | 109 ++++++++++++-- teacher.html | 58 +++++--- workspace.html | 11 ++ 5 files changed, 456 insertions(+), 133 deletions(-) diff --git a/admin.html b/admin.html index 3141e76..04b3246 100644 --- a/admin.html +++ b/admin.html @@ -460,8 +460,12 @@ document.getElementById('modal-create-user').style.display = 'flex'; } + function getAuthToken() { + return (window.AUTH && window.AUTH.token) || sessionStorage.getItem('auth_token') || localStorage.getItem('auth_token') || ''; + } + async function loadAdminData() { - const token = localStorage.getItem('auth_token'); + const token = getAuthToken(); if (!token) { showAuthGuard(); return; @@ -573,8 +577,8 @@ ${createdDate} - - + + `; @@ -583,7 +587,12 @@ function escapeHtml(t) { if (!t) return ''; - return String(t).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + return String(t) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); } // Filter Users @@ -608,7 +617,7 @@ // Change Role async function changeUserRole(userId, newRole) { - const token = localStorage.getItem('auth_token'); + const token = getAuthToken(); try { const res = await fetch('/api/admin/users/role', { method: 'POST', @@ -628,10 +637,12 @@ } } - // Delete User - async function deleteUser(userId, name) { + // Delete User (sicher ohne inline string parameters) + async function deleteUser(userId) { + const user = ALL_USERS.find(u => u.id === userId); + const name = user ? user.name : `#${userId}`; if (!confirm(`Soll der Benutzer '${name}' (ID: ${userId}) wirklich unwiderruflich gelöscht werden?`)) return; - const token = localStorage.getItem('auth_token'); + const token = getAuthToken(); try { const res = await fetch('/api/admin/users/delete', { method: 'POST', @@ -651,8 +662,10 @@ } } - // Open Reset Password - function openResetPasswordModal(userId, name) { + // Open Reset Password (sicher ohne inline string parameters) + function openResetPasswordModal(userId) { + const user = ALL_USERS.find(u => u.id === userId); + const name = user ? user.name : `#${userId}`; document.getElementById('reset-user-id').value = userId; document.getElementById('reset-user-name').innerText = name; document.getElementById('reset-new-password').value = ''; @@ -664,7 +677,7 @@ e.preventDefault(); const userId = document.getElementById('reset-user-id').value; const newPassword = document.getElementById('reset-new-password').value; - const token = localStorage.getItem('auth_token'); + const token = getAuthToken(); try { const res = await fetch('/api/admin/users/reset-password', { @@ -691,7 +704,7 @@ const email = document.getElementById('create-email').value.trim(); const password = document.getElementById('create-password').value; const role = document.getElementById('create-role').value; - const token = localStorage.getItem('auth_token'); + const token = getAuthToken(); try { const res = await fetch('/api/admin/users/create', { @@ -724,7 +737,7 @@ const d = await res.json(); const s = d.settings || {}; document.getElementById('setting-announcement').value = s.announcement_banner || ''; - document.getElementById('setting-allow-reg').checked = (s.allow_registrations !== '0'); + document.getElementById('setting-allow-reg').checked = (s.allow_registration !== '0' && s.allow_registrations !== '0'); document.getElementById('setting-maintenance').checked = (s.maintenance_mode === '1'); } } catch (e) { } @@ -732,11 +745,14 @@ document.getElementById('admin-settings-form').onsubmit = async (e) => { e.preventDefault(); - const token = localStorage.getItem('auth_token'); + const token = getAuthToken(); + const isMaint = document.getElementById('setting-maintenance').checked; + const isAllowReg = document.getElementById('setting-allow-reg').checked; const settings = { announcement_banner: document.getElementById('setting-announcement').value.trim(), - allow_registrations: document.getElementById('setting-allow-reg').checked ? '1' : '0', - maintenance_mode: document.getElementById('setting-maintenance').checked ? '1' : '0' + allow_registration: isAllowReg ? '1' : '0', + allow_registrations: isAllowReg ? '1' : '0', + maintenance_mode: isMaint ? '1' : '0' }; try { @@ -747,7 +763,10 @@ }); const d = await res.json(); if (res.ok) { - alert("Einstellungen erfolgreich gespeichert!"); + alert("Plattform-Einstellungen erfolgreich gespeichert! " + (isMaint ? "(⚠️ Wartungsmodus ist nun AKTIV)" : "(Wartungsmodus ist inaktiv)")); + if (window.AUTH && window.AUTH.checkStatus) { + window.AUTH.checkStatus(); + } } else { alert("Fehler: " + d.error); } diff --git a/assets/app_auth.js b/assets/app_auth.js index a8656e7..82285e4 100644 --- a/assets/app_auth.js +++ b/assets/app_auth.js @@ -1,8 +1,11 @@ /** * 🔐 AUTHENTIFIZIERUNG & ZUGANGSSCHUTZ (AUTH GUARD) 🔐 * ======================================================= - * IT-Praxisportal – Benutzer-Authentifizierung und Schutz aller Kurse & Web-IDE. - * Zugriff auf Kurse, Lerneinheiten und Web-IDE erfordert ein registriertes Benutzerkonto. + * IT-Praxisportal – Benutzer-Authentifizierung, Wartungsmodus und Zugangsschutz. + * - Strikter Login-Zwang für den Alpha-Test (sessionStorage als Standard). + * - Vollständiger Wartungsmodus mit Fullscreen-Blockade für Nicht-Admins. + * - Live-Statusabfrage (/api/platform/status) & Ankündigungsbanner. + * - Vollständige Beseitigung unsicherer Offline-/Dummy-Fallbacks. */ (function () { @@ -18,85 +21,117 @@ .replace(/'/g, '''); } - // Initialisiere Auth-Objekt - const initialUser = JSON.parse(localStorage.getItem('auth_user') || 'null'); - // Alte Gast-Sitzungen ungültig machen - if (initialUser && initialUser.isGuest) { - localStorage.removeItem('auth_user'); + // Session-Speicher auslesen (sessionStorage bevorzugt für Sitzungsbindung im Test) + function getStoredToken() { + return sessionStorage.getItem('auth_token') || localStorage.getItem('auth_token') || null; + } + + function getStoredUser() { + try { + const raw = sessionStorage.getItem('auth_user') || localStorage.getItem('auth_user') || 'null'; + return JSON.parse(raw); + } catch (e) { + return null; + } + } + + // Bereinigung veralteter oder gefälschter Dummy-Tokens + const initialToken = getStoredToken(); + const initialUser = getStoredUser(); + if ((initialToken && initialToken.startsWith('local_token_')) || (initialUser && initialUser.isGuest)) { + sessionStorage.removeItem('auth_token'); + sessionStorage.removeItem('auth_user'); localStorage.removeItem('auth_token'); + localStorage.removeItem('auth_user'); } window.AUTH = { - token: localStorage.getItem('auth_token') || null, - user: (initialUser && !initialUser.isGuest) ? initialUser : null, + token: getStoredToken(), + user: getStoredUser(), + platformStatus: { maintenance: false, announcement: '', allow_registration: true }, isLoggedIn() { return !!(this.token && this.user && !this.user.isGuest && this.user.email); }, - setSession(token, user) { - this.token = token; - this.user = user; - localStorage.setItem('auth_token', token); - localStorage.setItem('auth_user', JSON.stringify(user)); - this.updateUI(); + isAdmin() { + return !!(this.isLoggedIn() && this.user && this.user.role === 'admin'); }, - logout() { + setSession(token, user, rememberMe = false) { + this.token = token; + this.user = user; + + if (rememberMe) { + localStorage.setItem('auth_token', token); + localStorage.setItem('auth_user', JSON.stringify(user)); + sessionStorage.removeItem('auth_token'); + sessionStorage.removeItem('auth_user'); + } else { + sessionStorage.setItem('auth_token', token); + sessionStorage.setItem('auth_user', JSON.stringify(user)); + localStorage.removeItem('auth_token'); + localStorage.removeItem('auth_user'); + } + this.updateUI(); + this.handleMaintenanceDisplay(); + }, + + logout(redirect = true) { this.token = null; this.user = null; + sessionStorage.removeItem('auth_token'); + sessionStorage.removeItem('auth_user'); localStorage.removeItem('auth_token'); localStorage.removeItem('auth_user'); this.updateUI(); - // Wenn auf einer geschützten Kursseite, zur Startseite leiten - const p = window.location.pathname; - if (p.includes('/courses/') || p.includes('/lehrpfad_') || p.endsWith('workspace.html')) { - window.location.href = window.location.origin + '/index.html?auth=login'; + if (redirect) { + const p = window.location.pathname; + if (p.includes('/courses/') || p.includes('/lehrpfad_') || p.endsWith('workspace.html') || p.endsWith('teacher.html') || p.endsWith('admin.html')) { + window.location.href = window.location.origin + '/index.html?auth=login'; + } else { + this.handleMaintenanceDisplay(); + } } }, - async register(name, email, password, role = 'solo') { + async register(name, email, password, role = 'solo', rememberMe = false) { try { const res = await fetch('/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, email, password, role }) }); - const data = await res.json(); - if (!res.ok) throw new Error(data.error || 'Registrierung fehlgeschlagen'); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + return { success: false, error: data.error || 'Registrierung fehlgeschlagen' }; + } - this.setSession(data.token, data.user); + this.setSession(data.token, data.user, rememberMe); return { success: true, user: data.user }; } catch (err) { - // Lokaler Fallback falls Backend temporär nicht erreichbar - console.warn('API-Registrierung fehlgeschlagen, nutze lokalen Speicher:', err.message); - const localUser = { id: Date.now(), name, email, role, xp: 0, level: 1 }; - this.setSession('local_token_' + Date.now(), localUser); - return { success: true, user: localUser }; + return { success: false, error: 'Netzwerkfehler: Backend-Server nicht erreichbar.' }; } }, - async login(email, password) { + async login(email, password, rememberMe = false) { try { const res = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }); - const data = await res.json(); - if (!res.ok) throw new Error(data.error || 'Anmeldung fehlgeschlagen'); + const data = await res.json().catch(() => ({})); - this.setSession(data.token, data.user); + if (!res.ok) { + return { success: false, error: data.error || 'Anmeldung fehlgeschlagen' }; + } + + this.setSession(data.token, data.user, rememberMe); return { success: true, user: data.user }; } catch (err) { - console.warn('API-Login fehlgeschlagen, versuche lokalen Fallback:', err.message); - if (email && password && password.length >= 6) { - const localUser = { id: 1, name: email.split('@')[0], email, role: 'solo', xp: 100, level: 1 }; - this.setSession('local_token_' + Date.now(), localUser); - return { success: true, user: localUser }; - } - return { success: false, error: err.message || 'Ungültige Anmeldedaten' }; + return { success: false, error: 'Netzwerkfehler: Backend-Server nicht erreichbar.' }; } }, @@ -106,17 +141,156 @@ const res = await fetch('/api/auth/me', { headers: { 'Authorization': `Bearer ${this.token}` } }); + if (res.ok) { const data = await res.json(); this.user = data.user; - localStorage.setItem('auth_user', JSON.stringify(data.user)); + if (localStorage.getItem('auth_token')) { + localStorage.setItem('auth_user', JSON.stringify(data.user)); + } else { + sessionStorage.setItem('auth_user', JSON.stringify(data.user)); + } this.updateUI(); + this.handleMaintenanceDisplay(); + } else { + const errData = await res.json().catch(() => ({})); + if (res.status === 401 || res.status === 404) { + this.logout(false); + } else if (res.status === 503 && errData.maintenance) { + this.handleMaintenanceDisplay(); + } } } catch (e) { - // Offline / Netzwerkfehler ignorieren + // Offline } }, + async checkStatus() { + try { + const res = await fetch('/api/platform/status'); + if (res.ok) { + this.platformStatus = await res.json(); + this.handleMaintenanceDisplay(); + this.handleAnnouncementDisplay(); + } + } catch (e) { + // Ignorieren + } + }, + + handleMaintenanceDisplay() { + const isMaintenance = this.platformStatus && this.platformStatus.maintenance; + const isAdmin = this.isAdmin(); + + let screen = document.getElementById('itp-maintenance-screen'); + let adminBanner = document.getElementById('itp-maintenance-admin-banner'); + + if (isMaintenance) { + if (!isAdmin) { + if (!screen) { + screen = document.createElement('div'); + screen.id = 'itp-maintenance-screen'; + screen.style.cssText = ` + position: fixed; inset: 0; z-index: 999999; + background: rgba(10, 15, 29, 0.96); backdrop-filter: blur(12px); + display: flex; align-items: center; justify-content: center; + padding: 24px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + color: #f8fafc; text-align: center; + `; + screen.innerHTML = ` +
+
🛠️
+

Wartungsmodus aktiv

+

+ Das IT-Praxisportal wird derzeit planmäßig gewartet, um Sicherheits-Updates und neue Lernmodule einzuspielen. Reguläre Kurszugänge sind vorübergehend pausiert. +

+
+ +
+ IT-Praxisportal Alpha • Alle Daten sind sicher +
+
+
+ `; + document.body.appendChild(screen); + + const btn = document.getElementById('maintenance-admin-login-btn'); + if (btn) { + btn.onclick = () => { + window.openAuthModal('login', { + allowClose: true, + title: '🔐 Administrator-Login', + message: 'Wartungsmodus aktiv: Der Zugang ist derzeit exklusiv für System-Administratoren freigeschaltet.', + onSuccess: () => { + window.location.reload(); + } + }); + }; + } + } + } else { + if (screen) screen.remove(); + if (!adminBanner) { + adminBanner = document.createElement('div'); + adminBanner.id = 'itp-maintenance-admin-banner'; + adminBanner.style.cssText = ` + background: linear-gradient(90deg, #b91c1c, #ea580c); + color: white; padding: 8px 16px; font-size: 0.84rem; font-weight: 700; + display: flex; align-items: center; justify-content: space-between; + position: sticky; top: 0; z-index: 999998; box-shadow: 0 2px 8px rgba(0,0,0,0.3); + `; + adminBanner.innerHTML = ` +
+ ⚠️ WARTUNGSMODUS AKTIV + | Nur Administratoren haben derzeit Zugriff auf die Plattform. +
+ ⚙️ Admin-Panel öffnen + `; + document.body.insertBefore(adminBanner, document.body.firstChild); + } + } + } else { + if (screen) screen.remove(); + if (adminBanner) adminBanner.remove(); + } + }, + + handleAnnouncementDisplay() { + const bannerText = this.platformStatus && this.platformStatus.announcement; + let el = document.getElementById('itp-announcement-banner'); + if (!bannerText) { + if (el) el.remove(); + return; + } + + if (sessionStorage.getItem('dismissed_announcement') === bannerText) { + return; + } + + if (!el) { + el = document.createElement('div'); + el.id = 'itp-announcement-banner'; + el.style.cssText = ` + background: #0284c7; color: white; padding: 8px 16px; font-size: 0.85rem; font-weight: 600; + display: flex; align-items: center; justify-content: space-between; + position: sticky; top: 0; z-index: 999990; + `; + document.body.insertBefore(el, document.body.firstChild); + } + el.innerHTML = ` +
+ 📢 ${escapeHtml(bannerText)} +
+ + `; + el.querySelector('button').onclick = () => { + sessionStorage.setItem('dismissed_announcement', bannerText); + el.remove(); + }; + }, + requireAuth(options = {}) { if (this.isLoggedIn()) { return true; @@ -126,10 +300,10 @@ }, showAuthBarrier(options = {}) { - window.openAuthModal(options.mode || 'register', { + window.openAuthModal(options.mode || 'login', { allowClose: options.allowClose !== undefined ? options.allowClose : false, - title: options.title || '🔒 Registrierung erforderlich', - message: options.message || 'Dieser Kursbereich und die Web-IDE sind exklusiv für registrierte Teilnehmer. Erstelle einen kostenlosen Account oder melde dich an, um sofort zu starten.', + title: options.title || '🔒 Anmeldung erforderlich', + message: options.message || 'Dieser Lernbereich erfordert ein Benutzerkonto. Bitte melde dich an oder erstelle einen kostenlosen Account.', onSuccess: () => { if (options.onSuccess) options.onSuccess(); else window.location.reload(); @@ -239,18 +413,20 @@ }; // Auth Modal UI - window.openAuthModal = function (initialMode = 'register', options = {}) { + window.openAuthModal = function (initialMode = 'login', options = {}) { const allowClose = options.allowClose !== false; let modal = document.getElementById('auth-modal-overlay'); if (modal) modal.remove(); + const allowReg = window.AUTH.platformStatus.allow_registration !== false && !window.AUTH.platformStatus.maintenance; + modal = document.createElement('div'); modal.id = 'auth-modal-overlay'; modal.className = 'certificate-overlay'; - modal.style.zIndex = '99999'; + modal.style.zIndex = '999999'; modal.innerHTML = ` -
+
${allowClose ? ` ` : ''} @@ -258,76 +434,86 @@
🎓
-
- ${options.title || (initialMode === 'register' ? '✨ Kostenlos registrieren' : '🔑 Anmelden')} +
+ ${escapeHtml(options.title || (initialMode === 'register' && allowReg ? '✨ Kostenlos registrieren' : '🔑 Anmelden'))}
-
IT-PRAXISPORTAL ACCREDITATION
+
IT-PRAXISPORTAL AUTHENTICATION
-

- ${options.message || 'Erhalte sofortigen Zugriff auf 13 Praxis-Kurse, 216 interaktive Module, die Web-IDE und IHK AP1 & AP2 Prüfungsfragen.'} +

+ ${escapeHtml(options.message || 'Erhalte sofortigen Zugriff auf 13 Praxis-Kurse, 216 interaktive Module und die Web-IDE.')}

- + ${allowReg ? ` + + ` : ` + + `}
-
- - +
- - + +
- - + +
-
- -
+ +
+ + +
+ - ${!allowClose ? ` - + ← Zurück zur Startseite ` : ` - `}
- 🔒 100% DSGVO-konform • Deutsche Server • Keine Kreditkarte erforderlich + 🔒 100% DSGVO-konform • Deutsche Server • Keine Tracking-Cookies
`; document.body.appendChild(modal); - let isRegister = (initialMode === 'register'); - const tabReg = document.getElementById('auth-tab-register'); + let isRegister = (initialMode === 'register' && allowReg); const tabLog = document.getElementById('auth-tab-login'); + const tabReg = document.getElementById('auth-tab-register'); const nameField = document.getElementById('auth-field-name'); const roleField = document.getElementById('auth-field-role'); const submitBtn = document.getElementById('auth-submit-btn'); @@ -335,11 +521,13 @@ const form = document.getElementById('auth-form'); function switchMode(reg) { - isRegister = reg; + isRegister = reg && allowReg; if (isRegister) { - tabReg.style.background = '#ffffff'; - tabReg.style.color = '#0284c7'; - tabReg.style.boxShadow = '0 1px 3px rgba(0,0,0,0.1)'; + if (tabReg) { + tabReg.style.background = '#ffffff'; + tabReg.style.color = '#0284c7'; + tabReg.style.boxShadow = '0 1px 3px rgba(0,0,0,0.1)'; + } tabLog.style.background = 'transparent'; tabLog.style.color = '#64748b'; tabLog.style.boxShadow = 'none'; @@ -352,9 +540,11 @@ tabLog.style.background = '#ffffff'; tabLog.style.color = '#0284c7'; tabLog.style.boxShadow = '0 1px 3px rgba(0,0,0,0.1)'; - tabReg.style.background = 'transparent'; - tabReg.style.color = '#64748b'; - tabReg.style.boxShadow = 'none'; + if (tabReg) { + tabReg.style.background = 'transparent'; + tabReg.style.color = '#64748b'; + tabReg.style.boxShadow = 'none'; + } nameField.style.display = 'none'; roleField.style.display = 'none'; @@ -364,8 +554,8 @@ errorBox.style.display = 'none'; } - tabReg.onclick = () => switchMode(true); tabLog.onclick = () => switchMode(false); + if (tabReg) tabReg.onclick = () => switchMode(true); switchMode(isRegister); if (allowClose) { @@ -386,15 +576,16 @@ const password = document.getElementById('auth-input-password').value; const name = document.getElementById('auth-input-name')?.value.trim() || email.split('@')[0]; const role = document.getElementById('auth-select-role')?.value || 'solo'; + const rememberMe = document.getElementById('auth-remember-me')?.checked || false; submitBtn.disabled = true; submitBtn.innerText = 'Bitte warten...'; let result; if (isRegister) { - result = await window.AUTH.register(name, email, password, role); + result = await window.AUTH.register(name, email, password, role, rememberMe); } else { - result = await window.AUTH.login(email, password); + result = await window.AUTH.login(email, password, rememberMe); } submitBtn.disabled = false; @@ -405,8 +596,8 @@ if (options.onSuccess) { options.onSuccess(result.user); } else { - // Wenn auf der Startseite: UI aktualisieren window.AUTH.updateUI(); + window.location.reload(); } } else { errorBox.innerText = result.error || 'Fehler bei der Authentifizierung'; @@ -415,9 +606,10 @@ }; }; - // URL-Parameter prüfen (z.B. ?auth=login oder ?auth=register) + // Initialisierung beim Laden des DOMs document.addEventListener('DOMContentLoaded', () => { window.AUTH.updateUI(); + window.AUTH.checkStatus(); window.AUTH.fetchMe(); const params = new URLSearchParams(window.location.search); diff --git a/server.py b/server.py index 0cfe543..3c7c184 100644 --- a/server.py +++ b/server.py @@ -24,6 +24,7 @@ import os import re import uuid import urllib.parse +import ipaddress from pathlib import Path from collections import defaultdict @@ -32,6 +33,14 @@ BASE_DIR = Path(__file__).parent.resolve() DB_FILE = BASE_DIR / "platform_data.db" KEY_FILE = BASE_DIR / ".secret_key" +def is_trusted_source(ip: str) -> bool: + """Verhindert direkte Zugriffe am Reverse Proxy vorbei.""" + try: + addr = ipaddress.ip_address(ip) + return addr.is_loopback or addr.is_private + except ValueError: + return False + # ------------------------------------------------------------------------------ # 1. PERSISTENTER SECRET KEY # ------------------------------------------------------------------------------ @@ -161,6 +170,20 @@ def init_db(): conn.commit() conn.close() +def get_platform_setting(key: str, default: str = "") -> str: + """Liest einen Einstellungswert sicher aus der Datenbank.""" + try: + conn = get_db() + row = conn.execute("SELECT value FROM platform_settings WHERE key = ?", (key,)).fetchone() + conn.close() + return str(row["value"]) if row and row["value"] is not None else default + except Exception: + return default + +def is_maintenance_active() -> bool: + """Prüft, ob der globale Wartungsmodus aktiv ist.""" + return get_platform_setting("maintenance_mode", "0") == "1" + # ------------------------------------------------------------------------------ # 4. AUTH & HASHING # ------------------------------------------------------------------------------ @@ -185,7 +208,7 @@ def generate_token(user_id: int, email: str, role: str) -> str: "uid": user_id, "email": email, "role": role, - "exp": int(time.time()) + (86400 * 30) + "exp": int(time.time()) + 43200 # 12 Stunden Token-Gültigkeit (Sitzungsbegrenzung für Alpha-Test) } payload_json = json.dumps(payload, sort_keys=True) sig = hmac.new(SECRET_KEY.encode("utf-8"), payload_json.encode("utf-8"), hashlib.sha256).hexdigest() @@ -245,18 +268,26 @@ class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler): auth_header = self.headers.get("Authorization", "") if auth_header.startswith("Bearer "): token = auth_header[7:].strip() - return verify_token(token) + verified = verify_token(token) + if not verified: + return None + try: + conn = get_db() + row = conn.execute("SELECT id, email, name, role, xp, level, streak_days FROM users WHERE id = ?", (verified["uid"],)).fetchone() + conn.close() + if not row: + return None + data = dict(row) + data["uid"] = row["id"] + return data + except Exception: + return None return None def get_admin_user(self) -> dict | None: user = self.get_auth_user() - if not user: - return None - conn = get_db() - row = conn.execute("SELECT id, email, name, role FROM users WHERE id = ?", (user["uid"],)).fetchone() - conn.close() - if row and row["role"] == "admin": - return dict(row) + if user and user.get("role") == "admin": + return user return None def read_json_body(self) -> dict: @@ -267,24 +298,50 @@ class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler): return json.loads(body.decode("utf-8")) def is_path_blocked(self, path: str) -> bool: - clean_path = path.split("?")[0].lower() - blocked_extensions = (".db", ".sqlite", ".secret_key", ".py", ".php", ".jsonl") + clean_path = urllib.parse.unquote(path).split("?")[0].lower() + clean_path = os.path.normpath(clean_path).replace("\\", "/") + blocked_extensions = ( + ".db", ".sqlite", ".sqlite3", ".secret_key", ".py", ".sh", ".php", + ".jsonl", ".env", ".bak", ".sql", ".log", ".conf", ".ini", ".yaml", ".yml" + ) if any(clean_path.endswith(ext) for ext in blocked_extensions): return True - if "/." in clean_path or clean_path.startswith("/."): + if "/." in clean_path or clean_path.startswith("."): return True - if "platform_data.db" in clean_path or ".gamification.json" in clean_path: + if clean_path.startswith("/exams/") or clean_path == "/exams": + return True + if any(seg in clean_path for seg in ["/.git", "/.system_generated", "/.gemini", "platform_data.db", ".secret_key"]): return True return False def do_GET(self): + # 0. Proxy-Schutz gegen direktes Ansprechen von Port 8008 von außen + if not is_trusted_source(self.client_address[0]): + return self.send_json({"error": "Direkter Serverzugriff verweigert"}, 403) + parsed = urllib.parse.urlparse(self.path) path = parsed.path if self.is_path_blocked(path): return self.send_json({"error": "Zugriff verweigert (Geschützte Datei)"}, 403) - # 1. AUTH ME + # 1. ÖFFENTLICHER STATUS-ENDPUNKT (Wartungsmodus, Ankündigung, Registrierung) + if path == "/api/platform/status": + return self.send_json({ + "maintenance": is_maintenance_active(), + "announcement": get_platform_setting("announcement_banner", ""), + "allow_registration": get_platform_setting("allow_registration", "1") == "1" + }) + + # 2. WARTUNGSMODUS-FILTER FÜR GET-APIS + if path.startswith("/api/"): + if is_maintenance_active() and not self.get_admin_user(): + return self.send_json({ + "error": "Die Plattform befindet sich derzeit im Wartungsmodus. Nur Administratoren haben Zugriff.", + "maintenance": True + }, 503) + + # 3. AUTH ME if path == "/api/auth/me": user_token = self.get_auth_user() if not user_token: @@ -483,6 +540,10 @@ class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler): return super().do_GET() def do_POST(self): + # 0. Proxy-Schutz gegen direktes Ansprechen von Port 8008 von außen + if not is_trusted_source(self.client_address[0]): + return self.send_json({"error": "Direkter Serverzugriff verweigert"}, 403) + parsed = urllib.parse.urlparse(self.path) path = parsed.path client_ip = self.get_client_ip() @@ -492,8 +553,19 @@ class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler): except Exception: return self.send_json({"error": "Ungültiges JSON"}, 400) + # Globaler Wartungsmodus-Filter für schreibende Operationen (außer Login und Admin) + if path.startswith("/api/") and path != "/api/auth/login" and not path.startswith("/api/admin/"): + if is_maintenance_active() and not self.get_admin_user(): + return self.send_json({ + "error": "Die Plattform befindet sich im Wartungsmodus. Schreibaktionen sind derzeit gesperrt.", + "maintenance": True + }, 503) + # 1. REGISTRIERUNG if path == "/api/auth/register": + if is_maintenance_active() or get_platform_setting("allow_registration", "1") != "1": + return self.send_json({"error": "Die Registrierung ist derzeit deaktiviert (Wartungsmodus oder Administrator-Sperre)."}, 403) + if not rate_limiter.is_allowed(f"reg_{client_ip}", max_requests=5, window_seconds=60): return self.send_json({"error": "Zu viele Registrierungsversuche. Bitte warte eine Minute."}, 429) @@ -530,7 +602,7 @@ class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler): finally: conn.close() - # 2. LOGIN (mit Constant-Time Schutz gegen Enumeration) + # 2. LOGIN (mit Constant-Time Schutz gegen Enumeration & Wartungsmodus-Prüfung) elif path == "/api/auth/login": if not rate_limiter.is_allowed(f"login_{client_ip}", max_requests=8, window_seconds=60): return self.send_json({"error": "Zu viele Login-Versuche. Bitte warte eine Minute."}, 429) @@ -549,6 +621,13 @@ class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler): if not verify_password(password, user["password_hash"], user["salt"]): return self.send_json({"error": "Ungültige E-Mail-Adresse oder Passwort"}, 401) + # Wartungsmodus: Nur Administratoren dürfen sich anmelden + if is_maintenance_active() and user["role"] != "admin": + return self.send_json({ + "error": "Die Plattform befindet sich im Wartungsmodus. Der Login ist vorübergehend nur für Administratoren freigeschaltet.", + "maintenance": True + }, 503) + token = generate_token(user["id"], user["email"], user["role"]) return self.send_json({ "message": "Erfolgreich angemeldet!", diff --git a/teacher.html b/teacher.html index e5dde73..b0cabe4 100644 --- a/teacher.html +++ b/teacher.html @@ -126,14 +126,26 @@