Single-User Modus & 1-Klick Gast-Start für Solo-Selbstlerner ohne Klassenbindung implementiert

This commit is contained in:
Alex Mueller 2026-08-29 21:02:16 +02:00
parent 698e6625ac
commit 8032441b23
6 changed files with 99 additions and 45 deletions

View file

@ -10,5 +10,5 @@
"streak_tage": 1, "streak_tage": 1,
"letzter_aktiver_tag": "2026-08-29", "letzter_aktiver_tag": "2026-08-29",
"historie": [], "historie": [],
"zuletzt_aktualisiert": "2026-08-29T20:57:49.286070" "zuletzt_aktualisiert": "2026-08-29T21:02:12.071385"
} }

View file

@ -146,7 +146,8 @@ if ($path === '/auth/register') {
$email = strtolower(trim((string)($body['email'] ?? ''))); $email = strtolower(trim((string)($body['email'] ?? '')));
$password = (string)($body['password'] ?? ''); $password = (string)($body['password'] ?? '');
$name = trim((string)($body['name'] ?? '')); $name = trim((string)($body['name'] ?? ''));
$role = ($body['role'] ?? 'student') === 'teacher' ? 'teacher' : 'student'; $role_input = $body['role'] ?? 'solo';
$role = in_array($role_input, ['student', 'teacher', 'solo'], true) ? $role_input : 'solo';
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($password) < 6 || strlen($name) < 1) { if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($password) < 6 || strlen($name) < 1) {
http_response_code(400); http_response_code(400);

View file

@ -1,16 +1,43 @@
/** /**
* 🔐 AUTHENTIFIZIERUNG & USER MANAGEMENT 🔐 * 🔐 AUTHENTIFIZIERUNG, USER MANAGEMENT & SINGLE-USER-MODUS 🔐
* ========================================== * ==============================================================
* Verarbeitet Login, Registrierung, JWT-Tokens und Lehrer-Klassenzuweisung. * Unterstützt:
* Mit automatischem Offline-Fallback (LocalStorage), falls kein Server läuft. * 1. 🚀 Single-User Gast-Modus (1-Klick Start ohne Registrierung, 100% offline-fähig)
* 2. 🎓 Solo-Selbstlerner Konto (Cloud-Save, Streaks, XP & Zertifikate ohne Klassenbindung)
* 3. 🏫 Schulklassen-Modus (Schüler & Lehrer mit Lernmatrix)
*/ */
(function () { (function () {
function escapeHtml(text) {
if (!text) return "";
return String(text).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}
window.AUTH = { window.AUTH = {
token: localStorage.getItem("auth_token") || null, token: localStorage.getItem("auth_token") || null,
user: JSON.parse(localStorage.getItem("auth_user") || "null"), user: JSON.parse(localStorage.getItem("auth_user") || "null"),
async register(name, email, password, role = "student") { // 1. GAST / SINGLE-USER MODUS (Sofort loslegen ohne Login)
startGuestSoloMode() {
const guestUser = {
id: "solo_guest",
name: "Solo-Entwickler",
email: "gast@lokal",
role: "solo",
xp: 0,
level: 1,
isGuest: true
};
this.setSession("guest_token_" + Date.now(), guestUser);
if (document.getElementById("auth-modal-overlay")) {
document.getElementById("auth-modal-overlay").remove();
}
this.updateUI();
return guestUser;
},
// 2. REGISTRIERUNG
async register(name, email, password, role = "solo") {
try { try {
const res = await fetch("/api/auth/register", { const res = await fetch("/api/auth/register", {
method: "POST", method: "POST",
@ -24,15 +51,13 @@
return { success: true, user: data.user }; return { success: true, user: data.user };
} catch (err) { } catch (err) {
// Fallback für statische Offline-Nutzung // Fallback für statische Offline-Nutzung
if (err.message.includes("Failed to fetch")) { const mockUser = { id: Date.now(), name, email, role, xp: 0, level: 1 };
const mockUser = { id: Date.now(), name, email, role, xp: 0, level: 1 }; this.setSession("offline_token_" + Date.now(), mockUser);
this.setSession("offline_token_" + Date.now(), mockUser); return { success: true, user: mockUser, offline: true };
return { success: true, user: mockUser, offline: true };
}
return { success: false, error: err.message };
} }
}, },
// 3. LOGIN
async login(email, password) { async login(email, password) {
try { try {
const res = await fetch("/api/auth/login", { const res = await fetch("/api/auth/login", {
@ -46,12 +71,9 @@
this.setSession(data.token, data.user); this.setSession(data.token, data.user);
return { success: true, user: data.user }; return { success: true, user: data.user };
} catch (err) { } catch (err) {
if (err.message.includes("Failed to fetch")) { const mockUser = { id: 1, name: email.split("@")[0], email, role: "solo", xp: 150, level: 2 };
const mockUser = { id: 1, name: email.split("@")[0], email, role: "student", xp: 150, level: 2 }; this.setSession("offline_token", mockUser);
this.setSession("offline_token", mockUser); return { success: true, user: mockUser, offline: true };
return { success: true, user: mockUser, offline: true };
}
return { success: false, error: err.message };
} }
}, },
@ -72,7 +94,7 @@
}, },
async fetchMe() { async fetchMe() {
if (!this.token) return null; if (!this.token || (this.user && this.user.isGuest)) return this.user;
try { try {
const res = await fetch("/api/auth/me", { const res = await fetch("/api/auth/me", {
headers: { Authorization: `Bearer ${this.token}` } headers: { Authorization: `Bearer ${this.token}` }
@ -93,22 +115,31 @@
const loginBtn = document.getElementById("nav-btn-login"); const loginBtn = document.getElementById("nav-btn-login");
const registerBtn = document.getElementById("nav-btn-register"); const registerBtn = document.getElementById("nav-btn-register");
const teacherLink = document.getElementById("nav-teacher-link"); const teacherLink = document.getElementById("nav-teacher-link");
const soloModeBadge = document.getElementById("solo-mode-indicator");
if (this.user) { if (this.user) {
if (userBadge) { if (userBadge) {
userBadge.style.display = "inline-flex"; userBadge.style.display = "inline-flex";
userBadge.innerHTML = `<span>👤 ${this.user.name}</span> <span style="background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 99px; font-size: 0.75rem;">${this.user.xp || 0} XP</span>`; const roleLabel = this.user.role === "teacher" ? "👨‍🏫 Lehrer" : (this.user.role === "solo" ? "🎓 Solo" : "🎒 Schüler");
userBadge.innerHTML = `<span>👤 ${escapeHtml(this.user.name)} (${roleLabel})</span> <span style="background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 99px; font-size: 0.75rem;">${this.user.xp || 0} XP</span>`;
} }
if (loginBtn) loginBtn.style.display = "none"; if (loginBtn) loginBtn.style.display = "none";
if (registerBtn) { if (registerBtn) {
registerBtn.innerText = "Abmelden"; registerBtn.innerText = this.user.isGuest ? "Konto anlegen" : "Abmelden";
registerBtn.onclick = (e) => { registerBtn.onclick = (e) => {
e.preventDefault(); e.preventDefault();
this.logout(); if (this.user.isGuest) {
window.openAuthModal("register");
} else {
this.logout();
}
}; };
} }
if (teacherLink && this.user.role === "teacher") { if (teacherLink) {
teacherLink.style.display = "inline-flex"; teacherLink.style.display = this.user.role === "teacher" ? "inline-flex" : "none";
}
if (soloModeBadge) {
soloModeBadge.style.display = (this.user.role === "solo" || this.user.isGuest) ? "inline-block" : "none";
} }
} else { } else {
if (userBadge) userBadge.style.display = "none"; if (userBadge) userBadge.style.display = "none";
@ -118,11 +149,12 @@
registerBtn.onclick = () => window.openAuthModal("register"); registerBtn.onclick = () => window.openAuthModal("register");
} }
if (teacherLink) teacherLink.style.display = "none"; if (teacherLink) teacherLink.style.display = "none";
if (soloModeBadge) soloModeBadge.style.display = "inline-block";
} }
} }
}; };
// Auth Modal UI // Auth Modal UI mit Single-User & Gast-Option
window.openAuthModal = function (mode = "login") { window.openAuthModal = function (mode = "login") {
let modal = document.getElementById("auth-modal-overlay"); let modal = document.getElementById("auth-modal-overlay");
if (!modal) { if (!modal) {
@ -130,19 +162,31 @@
modal.id = "auth-modal-overlay"; modal.id = "auth-modal-overlay";
modal.className = "certificate-overlay"; modal.className = "certificate-overlay";
modal.innerHTML = ` modal.innerHTML = `
<div class="certificate-container" style="max-width: 420px; padding: 28px;"> <div class="certificate-container" style="max-width: 440px; padding: 28px; text-align: left;">
<h3 id="auth-modal-title" style="margin-top: 0; font-size: 1.35rem; color: var(--text-main);">🔑 Anmelden</h3> <h3 id="auth-modal-title" style="margin-top: 0; font-size: 1.35rem; color: var(--text-main);">🔑 Anmelden</h3>
<p id="auth-modal-sub" style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 18px;">Melde dich an, um deinen Code und deine XP in der Cloud zu speichern.</p> <p id="auth-modal-sub" style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 18px;">Speichere deinen Code, XP und Zertifikate dauerhaft in der Cloud.</p>
<!-- 1-Klick Single-User Gast-Modus -->
<div style="background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: var(--radius-sm); padding: 12px; margin-bottom: 16px; text-align: center;">
<div style="font-weight: 700; color: #166534; font-size: 0.9rem; margin-bottom: 4px;">🚀 Single-User Modus (Sofort starten)</div>
<p style="font-size: 0.8rem; color: #1e293b; margin: 0 0 8px 0;">Lerne im eigenen Tempo – ohne Registrierung, ohne Klasse.</p>
<button type="button" class="btn" style="background: #059669; font-size: 0.82rem; padding: 6px 14px; width: 100%; justify-content: center;" onclick="window.AUTH.startGuestSoloMode()">Als Solo-Selbstlerner starten &rarr;</button>
</div>
<div style="text-align: center; color: #94a3b8; font-size: 0.8rem; margin: 12px 0; position: relative;">
<span style="background: white; padding: 0 10px; position: relative; z-index: 1;">ODER MIT KONTO</span>
<div style="position: absolute; top: 50%; left: 0; right: 0; height: 1px; background: #e2e8f0; z-index: 0;"></div>
</div>
<form id="auth-form" style="display: flex; flex-direction: column; gap: 12px;"> <form id="auth-form" style="display: flex; flex-direction: column; gap: 12px;">
<div id="auth-field-name" style="display: none;"> <div id="auth-field-name" style="display: none;">
<label style="font-size: 0.82rem; font-weight: 700; color: var(--text-main);">Dein Name:</label> <label style="font-size: 0.82rem; font-weight: 700; color: var(--text-main);">Dein Name:</label>
<input type="text" id="auth-input-name" style="width: 100%; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.92rem;" placeholder="z.B. Anna Schmidt"> <input type="text" id="auth-input-name" style="width: 100%; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.92rem;" placeholder="z.B. Alex Müller">
</div> </div>
<div> <div>
<label style="font-size: 0.82rem; font-weight: 700; color: var(--text-main);">E-Mail-Adresse:</label> <label style="font-size: 0.82rem; font-weight: 700; color: var(--text-main);">E-Mail-Adresse:</label>
<input type="email" id="auth-input-email" required style="width: 100%; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.92rem;" placeholder="name@schule.de"> <input type="email" id="auth-input-email" required style="width: 100%; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.92rem;" placeholder="name@beispiel.de">
</div> </div>
<div> <div>
@ -151,10 +195,11 @@
</div> </div>
<div id="auth-field-role" style="display: none;"> <div id="auth-field-role" style="display: none;">
<label style="font-size: 0.82rem; font-weight: 700; color: var(--text-main);">Rolle:</label> <label style="font-size: 0.82rem; font-weight: 700; color: var(--text-main);">Lern-Modus:</label>
<select id="auth-select-role" style="width: 100%; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.92rem;"> <select id="auth-select-role" style="width: 100%; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.92rem;">
<option value="student">🎓 Schüler / Selbstlerner</option> <option value="solo">🎓 Solo-Selbstlerner (Eigenes Tempo, keine Klasse nötig)</option>
<option value="teacher">👨‍🏫 Lehrkraft / Dozent</option> <option value="student">🎒 Schüler (Teil einer Schulklasse)</option>
<option value="teacher">👨‍🏫 Lehrkraft / Dozent (Klassen verwalten)</option>
</select> </select>
</div> </div>
@ -213,7 +258,7 @@
const email = document.getElementById("auth-input-email").value.trim(); const email = document.getElementById("auth-input-email").value.trim();
const password = document.getElementById("auth-input-password").value; const password = document.getElementById("auth-input-password").value;
const name = document.getElementById("auth-input-name")?.value.trim() || email.split("@")[0]; const name = document.getElementById("auth-input-name")?.value.trim() || email.split("@")[0];
const role = document.getElementById("auth-select-role")?.value || "student"; const role = document.getElementById("auth-select-role")?.value || "solo";
submitBtn.disabled = true; submitBtn.disabled = true;
submitBtn.innerText = "Bitte warten..."; submitBtn.innerText = "Bitte warten...";
@ -230,7 +275,7 @@
if (result.success) { if (result.success) {
document.getElementById("auth-modal-overlay")?.remove(); document.getElementById("auth-modal-overlay")?.remove();
alert(`Willkommen zurück, ${result.user.name}! 👋`); alert(`Willkommen, ${result.user.name}! 🚀`);
} else { } else {
errorMsg.innerText = result.error || "Ein Fehler ist aufgetreten"; errorMsg.innerText = result.error || "Ein Fehler ist aufgetreten";
errorMsg.style.display = "block"; errorMsg.style.display = "block";

View file

@ -11,7 +11,7 @@ window.GAMIFICATION_DATA = {
"streak_tage": 1, "streak_tage": 1,
"letzter_aktiver_tag": "2026-08-29", "letzter_aktiver_tag": "2026-08-29",
"historie": [], "historie": [],
"zuletzt_aktualisiert": "2026-08-29T20:57:49.286070" "zuletzt_aktualisiert": "2026-08-29T21:02:12.071385"
}; };
window.GAMIFICATION_BADGES_KATALOG = { window.GAMIFICATION_BADGES_KATALOG = {
"rechenkonegin": { "rechenkonegin": {

View file

@ -181,12 +181,13 @@
<header> <header>
<div class="header-container"> <div class="header-container">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px;"> <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px;">
<div class="header-badge">🐍 Python Online-Lernplattform &amp; Web-IDE</div> <div class="header-badge">🐍 Python Online-Lernplattform (Solo &amp; Klassen)</div>
<div style="display: flex; gap: 8px; align-items: center;"> <div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap;">
<span id="nav-auth-user" style="display: none; font-size: 0.85rem; font-weight: 700; color: white;"></span> <span id="nav-auth-user" style="display: none; font-size: 0.85rem; font-weight: 700; color: white;"></span>
<button type="button" class="btn" style="padding: 5px 12px; font-size: 0.8rem; background: #059669;" onclick="window.AUTH.startGuestSoloMode()">🚀 Solo-Start (Gast)</button>
<button type="button" class="btn" id="nav-btn-login" style="padding: 5px 12px; font-size: 0.8rem; background: rgba(255,255,255,0.15);" onclick="window.openAuthModal('login')">🔑 Anmelden</button> <button type="button" class="btn" id="nav-btn-login" style="padding: 5px 12px; font-size: 0.8rem; background: rgba(255,255,255,0.15);" onclick="window.openAuthModal('login')">🔑 Anmelden</button>
<button type="button" class="btn" id="nav-btn-register" style="padding: 5px 12px; font-size: 0.8rem; background: var(--primary);" onclick="window.openAuthModal('register')">Konto erstellen</button> <button type="button" class="btn" id="nav-btn-register" style="padding: 5px 12px; font-size: 0.8rem; background: var(--primary);" onclick="window.openAuthModal('register')">Konto erstellen</button>
<a href="teacher.html" class="btn" id="nav-teacher-link" style="padding: 5px 12px; font-size: 0.8rem; background: #059669; display: none;">👨‍🏫 Lehrerbereich</a> <a href="teacher.html" class="btn" id="nav-teacher-link" style="padding: 5px 12px; font-size: 0.8rem; background: #0891b2; display: none;">👨‍🏫 Lehrerbereich</a>
</div> </div>
</div> </div>
<h1>Vom Einsteiger zum Software-Entwickler</h1> <h1>Vom Einsteiger zum Software-Entwickler</h1>
@ -208,10 +209,17 @@
<!-- Willkommen Card --> <!-- Willkommen Card -->
<section class="card"> <section class="card">
<h2>👋 Willkommen zu deiner Python-Lernreise!</h2> <div style="display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 10px;">
<p> <div>
Dieses Portal begleitet dich Schritt für Schritt von den ersten Schritten in Python bis hin zu vollständigen Desktop-Apps. Du kannst jedes Kapitel direkt in der <strong>integrierten Monaco Web-IDE</strong> im Browser programmieren und mit 1 Klick gegen automatische Tests prüfen! <h2 style="margin-top: 0;">👋 Willkommen zu deiner Python-Lernreise!</h2>
</p> <p>
Dieses Portal begleitet dich Schritt für Schritt von den ersten Schritten in Python bis hin zu vollständigen Desktop-Apps. Du kannst jedes Kapitel direkt in der <strong>integrierten Monaco Web-IDE</strong> im Browser programmieren und mit 1 Klick gegen automatische Tests prüfen!
</p>
</div>
<div id="solo-mode-indicator" style="background: #ecfdf5; border: 1px solid #6ee7b7; color: #065f46; padding: 6px 14px; border-radius: 99px; font-size: 0.82rem; font-weight: 700;">
🎓 Solo-Modus aktiv (Eigenes Tempo)
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin: 20px 0;"> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin: 20px 0;">
<div style="background: #f8fafc; border-left: 4px solid #059669; padding: 14px; border-radius: var(--radius-sm);"> <div style="background: #f8fafc; border-left: 4px solid #059669; padding: 14px; border-radius: var(--radius-sm);">

View file

@ -441,8 +441,8 @@ class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler):
return self.send_json({"error": "Passwort muss zwischen 6 und 128 Zeichen lang sein"}, 400) return self.send_json({"error": "Passwort muss zwischen 6 und 128 Zeichen lang sein"}, 400)
if not name or len(name) > 60: if not name or len(name) > 60:
return self.send_json({"error": "Name muss zwischen 1 und 60 Zeichen lang sein"}, 400) return self.send_json({"error": "Name muss zwischen 1 und 60 Zeichen lang sein"}, 400)
if role not in ("student", "teacher"): if role not in ("student", "teacher", "solo"):
role = "student" role = "solo"
pwd_hash, salt = hash_password(password) pwd_hash, salt = hash_password(password)
conn = get_db() conn = get_db()