perf/ops: Auth-Latenz, Zähl-View, DB-Backup, Schreib-Limit, Asset-Cache

- auth: Supabase-JWT lokal verifizieren (hono/jwt, HS256) statt GoTrue-
  Roundtrip pro Request; JWT_SECRET in cms-env, Remote-Fallback wenn ungesetzt
- dialog: comment_stats-View (group by thread) ersetzt Full-Table-Scan +
  JS-Aggregation bei jedem Forum-Aufruf
- ops: scripts/backup-db.sh (pg_dump, rotiert) + täglicher Cron im Proxmox-
  Script — Dialog-Daten liegen nur in Postgres, nicht in Git
- security: Rate-Limit auf Schreib-Endpunkte (/api non-GET, 60/min je Nutzer)
- perf: Cache-Control (1 Woche) auf statische Assets, HTML bleibt frisch

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 23:01:12 +02:00
parent d0b5c6f670
commit 8404165f5c
9 changed files with 112 additions and 12 deletions
+27 -5
View File
@@ -1,5 +1,27 @@
import { verify } from 'hono/jwt';
import { supabaseAuth } from './supabase.js';
// Supabase-Tokens sind HS256-signiert. Mit dem JWT_SECRET verifizieren wir sie
// lokal (Signatur + Ablauf) — das spart pro Request den Roundtrip zu GoTrue.
// Ohne JWT_SECRET (z.B. Alt-Deploy) fällt requireAuth auf die Remote-Prüfung
// zurück. Tokens sind kurzlebig (1h) und Self-Signup ist aus → kein
// Sperr-Check nötig.
const JWT_SECRET = process.env.JWT_SECRET || '';
// Liefert ein User-Objekt {id,email,app_metadata} oder null.
async function verifyToken(token) {
if (JWT_SECRET) {
try {
const p = await verify(token, JWT_SECRET, 'HS256');
if (!p?.sub) return null;
return { id: p.sub, email: p.email || '', app_metadata: p.app_metadata || {} };
} catch { return null; }
}
const { data, error } = await supabaseAuth.auth.getUser(token);
if (error || !data?.user) return null;
return data.user;
}
// Rollen-Hierarchie: admin > editor (Redakteur) > user.
// - admin: alles (Foren verwalten, moderieren, Nutzer/Rollen, Inhalte)
// - editor: moderieren (Wortmeldungen ausblenden/löschen, Threads sperren)
@@ -24,12 +46,12 @@ export async function requireAuth(c, next) {
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
if (!token) return c.json({ error: 'Nicht eingeloggt' }, 401);
const { data, error } = await supabaseAuth.auth.getUser(token);
if (error || !data?.user) return c.json({ error: 'Ungültiges Token' }, 401);
const user = await verifyToken(token);
if (!user) return c.json({ error: 'Ungültiges Token' }, 401);
const email = (data.user.email || '').toLowerCase();
const role = roleOf(data.user);
c.set('user', data.user);
const email = (user.email || '').toLowerCase();
const role = roleOf(user);
c.set('user', user);
c.set('email', email);
c.set('role', role);
c.set('isAdmin', role === 'admin');