/** * V2 Auth-Client — Token-Management im Browser. * * Speichert Token + Profil in localStorage. * Bietet api(path, opts) als zentralen Fetch-Wrapper mit Auto-401-Redirect. * * Verwendung in Cockpit-Seiten: * import { auth } from './assets/js/auth-client.js'; * if (!auth.token) location.href = './login.html'; * const me = await auth.api('GET', 'student/me.php'); */ const STORAGE_KEY = 'ggs_v2_session'; function read() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null'); } catch { return null; } } function write(data) { if (data === null) localStorage.removeItem(STORAGE_KEY); else localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } // API-Base: bei /mock/ im Pfad nutzen wir Mock-Endpoints const apiBase = location.pathname.includes('/mock/') ? './mock/api' : './php/api'; export const auth = { get session() { return read(); }, get token() { return read()?.token || null; }, get role() { return read()?.role || null; }, get user() { return read()?.user || null; }, async login(role, credentials) { const r = await fetch(`${apiBase}/auth/login.php`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role, ...credentials }), }); const data = await r.json(); if (!r.ok) throw new Error(data.message || `Login fehlgeschlagen (${r.status})`); write({ token: data.token, role: data.role, issuedAt: Date.now(), expiresInS: data.expiresIn, user: { teacherId: data.teacherId, studentId: data.studentId, classId: data.classId, displayName: data.displayName, className: data.className, email: data.email, schoolName: data.schoolName, }, }); return data; }, async logout() { try { await this.api('POST', 'auth/logout.php'); } catch { /* ignore */ } write(null); }, async refresh() { const r = await fetch(`${apiBase}/auth/refresh.php`, { method: 'POST', headers: { Authorization: `Bearer ${this.token || ''}` }, }); if (!r.ok) { write(null); throw new Error('Refresh fehlgeschlagen'); } const data = await r.json(); const cur = read(); if (cur) { cur.token = data.token; cur.issuedAt = Date.now(); write(cur); } return data.token; }, /** * Zentraler Fetch-Wrapper: * await auth.api('GET', 'student/me.php') * await auth.api('POST', 'auth/logout.php') * await auth.api('PUT', 'module/state.php', { moduleSlug:'x', state:{} }) */ async api(method, path, body = null) { if (!this.token) { location.href = './login.html'; throw new Error('Kein Token — Redirect zu Login.'); } const headers = { Authorization: `Bearer ${this.token}` }; if (body) headers['Content-Type'] = 'application/json'; const r = await fetch(`${apiBase}/${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined, }); if (r.status === 401) { write(null); location.href = './login.html'; throw new Error('Token abgelaufen — Redirect zu Login.'); } if (!r.ok) { const err = await r.json().catch(() => ({})); throw new Error(err.message || `HTTP ${r.status}`); } return r.json(); }, /** * Beim Cockpit-Start: Token-Refresh wenn Token bald abläuft. */ async maybeRefresh() { const s = this.session; if (!s) return; const ageMs = Date.now() - (s.issuedAt || 0); const ttlMs = (s.expiresInS || 3600) * 1000; // Refresh wenn weniger als 5 Min Lebensdauer übrig if (ageMs > ttlMs - 5 * 60 * 1000) { try { await this.refresh(); } catch { /* sleep — beim nächsten 401 wird redirected */ } } }, };