Single-User Modus & 1-Klick Gast-Start für Solo-Selbstlerner ohne Klassenbindung implementiert
This commit is contained in:
parent
698e6625ac
commit
8032441b23
6 changed files with 99 additions and 45 deletions
|
|
@ -10,5 +10,5 @@
|
|||
"streak_tage": 1,
|
||||
"letzter_aktiver_tag": "2026-08-29",
|
||||
"historie": [],
|
||||
"zuletzt_aktualisiert": "2026-08-29T20:57:49.286070"
|
||||
"zuletzt_aktualisiert": "2026-08-29T21:02:12.071385"
|
||||
}
|
||||
3
api.php
3
api.php
|
|
@ -146,7 +146,8 @@ if ($path === '/auth/register') {
|
|||
$email = strtolower(trim((string)($body['email'] ?? '')));
|
||||
$password = (string)($body['password'] ?? '');
|
||||
$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) {
|
||||
http_response_code(400);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,43 @@
|
|||
/**
|
||||
* 🔐 AUTHENTIFIZIERUNG & USER MANAGEMENT 🔐
|
||||
* ==========================================
|
||||
* Verarbeitet Login, Registrierung, JWT-Tokens und Lehrer-Klassenzuweisung.
|
||||
* Mit automatischem Offline-Fallback (LocalStorage), falls kein Server läuft.
|
||||
* 🔐 AUTHENTIFIZIERUNG, USER MANAGEMENT & SINGLE-USER-MODUS 🔐
|
||||
* ==============================================================
|
||||
* Unterstützt:
|
||||
* 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 escapeHtml(text) {
|
||||
if (!text) return "";
|
||||
return String(text).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
|
||||
window.AUTH = {
|
||||
token: localStorage.getItem("auth_token") || 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 {
|
||||
const res = await fetch("/api/auth/register", {
|
||||
method: "POST",
|
||||
|
|
@ -24,15 +51,13 @@
|
|||
return { success: true, user: data.user };
|
||||
} catch (err) {
|
||||
// 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 };
|
||||
this.setSession("offline_token_" + Date.now(), mockUser);
|
||||
return { success: true, user: mockUser, offline: true };
|
||||
}
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
},
|
||||
|
||||
// 3. LOGIN
|
||||
async login(email, password) {
|
||||
try {
|
||||
const res = await fetch("/api/auth/login", {
|
||||
|
|
@ -46,13 +71,10 @@
|
|||
this.setSession(data.token, data.user);
|
||||
return { success: true, user: data.user };
|
||||
} catch (err) {
|
||||
if (err.message.includes("Failed to fetch")) {
|
||||
const mockUser = { id: 1, name: email.split("@")[0], email, role: "student", xp: 150, level: 2 };
|
||||
const mockUser = { id: 1, name: email.split("@")[0], email, role: "solo", xp: 150, level: 2 };
|
||||
this.setSession("offline_token", mockUser);
|
||||
return { success: true, user: mockUser, offline: true };
|
||||
}
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
},
|
||||
|
||||
logout() {
|
||||
|
|
@ -72,7 +94,7 @@
|
|||
},
|
||||
|
||||
async fetchMe() {
|
||||
if (!this.token) return null;
|
||||
if (!this.token || (this.user && this.user.isGuest)) return this.user;
|
||||
try {
|
||||
const res = await fetch("/api/auth/me", {
|
||||
headers: { Authorization: `Bearer ${this.token}` }
|
||||
|
|
@ -93,22 +115,31 @@
|
|||
const loginBtn = document.getElementById("nav-btn-login");
|
||||
const registerBtn = document.getElementById("nav-btn-register");
|
||||
const teacherLink = document.getElementById("nav-teacher-link");
|
||||
const soloModeBadge = document.getElementById("solo-mode-indicator");
|
||||
|
||||
if (this.user) {
|
||||
if (userBadge) {
|
||||
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 (registerBtn) {
|
||||
registerBtn.innerText = "Abmelden";
|
||||
registerBtn.innerText = this.user.isGuest ? "Konto anlegen" : "Abmelden";
|
||||
registerBtn.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
if (this.user.isGuest) {
|
||||
window.openAuthModal("register");
|
||||
} else {
|
||||
this.logout();
|
||||
}
|
||||
};
|
||||
}
|
||||
if (teacherLink && this.user.role === "teacher") {
|
||||
teacherLink.style.display = "inline-flex";
|
||||
if (teacherLink) {
|
||||
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 {
|
||||
if (userBadge) userBadge.style.display = "none";
|
||||
|
|
@ -118,11 +149,12 @@
|
|||
registerBtn.onclick = () => window.openAuthModal("register");
|
||||
}
|
||||
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") {
|
||||
let modal = document.getElementById("auth-modal-overlay");
|
||||
if (!modal) {
|
||||
|
|
@ -130,19 +162,31 @@
|
|||
modal.id = "auth-modal-overlay";
|
||||
modal.className = "certificate-overlay";
|
||||
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>
|
||||
<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 →</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;">
|
||||
<div id="auth-field-name" style="display: none;">
|
||||
<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>
|
||||
<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>
|
||||
|
|
@ -151,10 +195,11 @@
|
|||
</div>
|
||||
|
||||
<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;">
|
||||
<option value="student">🎓 Schüler / Selbstlerner</option>
|
||||
<option value="teacher">👨🏫 Lehrkraft / Dozent</option>
|
||||
<option value="solo">🎓 Solo-Selbstlerner (Eigenes Tempo, keine Klasse nötig)</option>
|
||||
<option value="student">🎒 Schüler (Teil einer Schulklasse)</option>
|
||||
<option value="teacher">👨🏫 Lehrkraft / Dozent (Klassen verwalten)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
|
@ -213,7 +258,7 @@
|
|||
const email = document.getElementById("auth-input-email").value.trim();
|
||||
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 || "student";
|
||||
const role = document.getElementById("auth-select-role")?.value || "solo";
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerText = "Bitte warten...";
|
||||
|
|
@ -230,7 +275,7 @@
|
|||
|
||||
if (result.success) {
|
||||
document.getElementById("auth-modal-overlay")?.remove();
|
||||
alert(`Willkommen zurück, ${result.user.name}! 👋`);
|
||||
alert(`Willkommen, ${result.user.name}! 🚀`);
|
||||
} else {
|
||||
errorMsg.innerText = result.error || "Ein Fehler ist aufgetreten";
|
||||
errorMsg.style.display = "block";
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ window.GAMIFICATION_DATA = {
|
|||
"streak_tage": 1,
|
||||
"letzter_aktiver_tag": "2026-08-29",
|
||||
"historie": [],
|
||||
"zuletzt_aktualisiert": "2026-08-29T20:57:49.286070"
|
||||
"zuletzt_aktualisiert": "2026-08-29T21:02:12.071385"
|
||||
};
|
||||
window.GAMIFICATION_BADGES_KATALOG = {
|
||||
"rechenkonegin": {
|
||||
|
|
|
|||
16
index.html
16
index.html
|
|
@ -181,12 +181,13 @@
|
|||
<header>
|
||||
<div class="header-container">
|
||||
<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 & Web-IDE</div>
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<div class="header-badge">🐍 Python Online-Lernplattform (Solo & Klassen)</div>
|
||||
<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>
|
||||
<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-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>
|
||||
<h1>Vom Einsteiger zum Software-Entwickler</h1>
|
||||
|
|
@ -208,10 +209,17 @@
|
|||
|
||||
<!-- Willkommen 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;">
|
||||
<div>
|
||||
<h2 style="margin-top: 0;">👋 Willkommen zu deiner Python-Lernreise!</h2>
|
||||
<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="background: #f8fafc; border-left: 4px solid #059669; padding: 14px; border-radius: var(--radius-sm);">
|
||||
|
|
|
|||
|
|
@ -441,8 +441,8 @@ class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|||
return self.send_json({"error": "Passwort muss zwischen 6 und 128 Zeichen lang sein"}, 400)
|
||||
if not name or len(name) > 60:
|
||||
return self.send_json({"error": "Name muss zwischen 1 und 60 Zeichen lang sein"}, 400)
|
||||
if role not in ("student", "teacher"):
|
||||
role = "student"
|
||||
if role not in ("student", "teacher", "solo"):
|
||||
role = "solo"
|
||||
|
||||
pwd_hash, salt = hash_password(password)
|
||||
conn = get_db()
|
||||
|
|
|
|||
Loading…
Reference in a new issue