1e51ef7def
- 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>
123 lines
3.7 KiB
JavaScript
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 */ }
|
|
}
|
|
},
|
|
};
|