Files
Adminator 1e51ef7def Nachtrag: alle bisher untracked Ordner + hängende Änderungen mit-committen
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/
- 12 code-workspace-Files
- STATUS-*.md
- viele M/D/R-Änderungen an bereits getrackten Files
- .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-08 02:27:02 +02:00

123 lines
3.7 KiB
JavaScript

/**
* 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 */ }
}
},
};