feat: Schritte 14–15 — Google Calendar OAuth + Apple CalDAV Sync + Settings-Seite

- server/services/google-calendar.js: OAuth 2.0, bidirektionaler Sync via
  Google Calendar API v3, inkrementeller syncToken, 410-Fallback auf Vollsync
- server/services/apple-calendar.js: CalDAV via tsdav (dynamic ESM import),
  minimaler ICS-Parser + ICS-Builder, bidirektionaler Sync
- server/routes/calendar.js: 7 neue Sync-Routen (google/auth, google/callback,
  google/sync, google/status, google/disconnect, apple/status, apple/sync)
- server/db.js: Migration 2 — sync_config Tabelle + idx_calendar_external_id
- server/db-schema-test.js: MIGRATIONS_SQL[2] für Tests synchronisiert
- server/auth.js: PATCH /me/password Endpoint
- server/index.js: Auto-Sync-Scheduler (setInterval, SYNC_INTERVAL_MINUTES)
- public/pages/settings.js: vollständige Settings-Seite (Konto, Passwort,
  Kalender-Sync-Status + Aktionen, Familienmitglieder-Verwaltung)
- public/styles/settings.css: neue Stylesheet-Datei
- public/index.html + public/sw.js: settings.css eingebunden und gecacht
- .env.example: SYNC_INTERVAL_MINUTES ergänzt
- README.md: vollständige Setup-Anleitung, Google/Apple-Sync-Dokumentation,
  modernes GitHub-Layout mit Badges und aufklappbaren Abschnitten

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ulsklyc
2026-03-24 22:53:44 +01:00
parent 81d4000ee1
commit 72d6d5126e
13 changed files with 1693 additions and 131 deletions
+33
View File
@@ -239,6 +239,39 @@ router.post('/users', requireAuth, requireAdmin, async (req, res) => {
}
});
/**
* PATCH /api/v1/auth/me/password
* Ändert das eigene Passwort.
* Body: { current_password: string, new_password: string }
* Response: { ok: true }
*/
router.patch('/me/password', requireAuth, async (req, res) => {
try {
const { current_password, new_password } = req.body;
if (!current_password || !new_password) {
return res.status(400).json({ error: 'Aktuelles und neues Passwort erforderlich.', code: 400 });
}
if (new_password.length < 8) {
return res.status(400).json({ error: 'Neues Passwort muss mindestens 8 Zeichen haben.', code: 400 });
}
const user = db.get().prepare('SELECT password_hash FROM users WHERE id = ?').get(req.session.userId);
if (!user) return res.status(404).json({ error: 'Benutzer nicht gefunden.', code: 404 });
const valid = await bcrypt.compare(current_password, user.password_hash);
if (!valid) return res.status(401).json({ error: 'Aktuelles Passwort falsch.', code: 401 });
const hash = await bcrypt.hash(new_password, 12);
db.get().prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(hash, req.session.userId);
res.json({ ok: true });
} catch (err) {
console.error('[Auth] Passwort-Ändern-Fehler:', err);
res.status(500).json({ error: 'Interner Serverfehler.', code: 500 });
}
});
/**
* DELETE /api/v1/auth/users/:id
* Admin only. Löscht ein Familienmitglied.