Profilseite mit Avatar, Passwort-Änderung und TOTP 2FA

- Neues Profil-Modal: Avatar-Upload, E-Mail bearbeiten, Kalender-Übersicht
- Passwort ändern mit Validierung des aktuellen Passworts
- TOTP 2FA: QR-Code + manueller Schlüssel, Aktivierung/Deaktivierung
- Login-Flow unterstützt 2FA-Code (neuer JSON-Endpoint /auth/login)
- User-Dropdown mit Profil-Link statt confirm()-Dialog
- Kalenderfarben in Sidebar editierbar (Color-Picker auf Farbpunkt)
- Monatsansicht nutzt volle Höhe (#view-container flex fix)
- requirements.txt: passlib durch bcrypt ersetzt, pyotp/qrcode/Pillow hinzugefügt
This commit is contained in:
2026-03-26 14:10:53 +01:00
parent 8e200e9d11
commit 128f1b468a
10 changed files with 738 additions and 18 deletions

View File

@@ -19,7 +19,7 @@ async function request(method, path, body = null, formEncoded = false) {
const res = await fetch(`${BASE}${path}`, { method, headers, body: bodyStr });
if (res.status === 401) {
if (res.status === 401 && !path.startsWith('/auth/login')) {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.reload();
@@ -35,14 +35,36 @@ async function request(method, path, body = null, formEncoded = false) {
return res.json();
}
async function uploadRequest(path, formData) {
const token = localStorage.getItem('token');
const headers = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`${BASE}${path}`, { method: 'POST', headers, body: formData });
if (res.status === 401) {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.reload();
return null;
}
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: 'Unbekannter Fehler' }));
throw new Error(err.detail || `HTTP ${res.status}`);
}
if (res.status === 204) return null;
return res.json();
}
export const api = {
get: (path) => request('GET', path),
post: (path, body) => request('POST', path, body),
put: (path, body) => request('PUT', path, body),
delete: (path) => request('DELETE', path),
upload: (path, form) => uploadRequest(path, form),
login: (username, password) =>
request('POST', '/auth/token', { username, password }, true),
login: (username, password, totp_code = null) =>
request('POST', '/auth/login', { username, password, totp_code }),
setupRequired: () => request('GET', '/auth/setup-required'),
setup: (data) => request('POST', '/auth/setup', data),