Commit graph

3 commits

Author SHA1 Message Date
Till JS
1b840a95f9 Phase 10d: Token-Refresh + 401-Retry im Cards-Web
Some checks are pending
CI / validate (push) Waiting to run
Cards-Web läuft jetzt auch über die 15min-JWT-TTL hinaus weiter:

- Login schickt credentials:'include' → SSO-Cookie auf .mana.how
  wird gesetzt (mana-auth-Standard).
- tryRefresh() ruft mana-auth POST /api/v1/auth/refresh mit Cookie-
  Auth, holt frischen accessToken, legt ihn in localStorage. Multi-
  Caller werden zu einer Promise gecoalesced (kein Refresh-Storm).
- ensureFreshToken() prüft beim Konstruktor + vor jedem API-Request,
  ob der Token noch ≥60s gültig ist. Wenn nicht, proaktiver Refresh.
- API-Client: 401 → tryRefresh() → exactly-once Retry. Erst wenn das
  auch 401 gibt, wird die Session lokal geleert (führt User zurück
  auf /).
- uploadMedia (multipart) parallel mit demselben Pattern.
- Logout ruft mana-auth /logout mit Cookie-Auth, damit das SSO-Cookie
  für andere *.mana.how-Apps auch weg ist (best-effort, nicht-fatal).
- Boot-Pfad: bei abgelaufenem Token im localStorage behalten wir das
  User-Profil temporär und versuchen einen still-Refresh — User sieht
  beim Reload kein Login-Flackern, wenn die Cookie-Session noch lebt.

svelte-check 384 files 0 errors, 7 Web-Tests grün, prod-Build sauber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:50:12 +02:00
Till JS
7119756ce6 Phase 10c: Cards-Web SSO-Login gegen mana-auth
Some checks are pending
CI / validate (push) Waiting to run
Echte Anmeldung gegen auth.mana.how/api/v1/auth/login statt
Dev-Stub-User-ID. accessToken (EdDSA-JWT, 15 min TTL) + Profil
(email, name, tier) leben in localStorage; jeder API-Call schickt
`Authorization: Bearer <jwt>`. Bei 401 wird die Session lokal
geleert — User landet beim nächsten Render auf der Login-Page.

`devUser.id` bleibt eine Vereinfachte UI-Sentinel (gibt id wenn
JWT ODER Dev-Stub aktiv) — alle existierenden Importer
funktionieren unverändert. Dev-Stub-Pfad bleibt als Fallback für
Tests + Anki-Importer-Migration. Filename `dev-stub.svelte.ts`
behalten, Inhalt komplett umgebaut (Sprint 10d wäre der Rename).

Account-Page zeigt Email + Name + Tier statt nur UUID. Header
zeigt Email statt UUID. Login-Form auf Landing-Page mit Email +
Passwort, error-Anzeige, autocomplete-Hints für Browser-Manager.

uploadMedia (multipart) angepasst: Bearer first, X-User-Id-Stub
als Fallback.

svelte-check 384 files 0 errors, 7 Web-Tests grün, prod-Build
sauber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:08:06 +02:00
Till JS
c9eb0a6f80 Phase 9k: Media-Upload via MinIO-Container
Eigener cards-minio-Container im docker-compose (9100/9101 — Plattform
auf 9000/9001 bleibt isoliert). cardsadmin/cardsadmin als Dev-Default,
prod via env-Vars (CARDS_S3_*).

apps/api/src/services/storage.ts — schmaler StorageService um den
minio-Client. ensureBucket() ist idempotent (auto-create beim ersten
Upload). removeObjectsByPrefix() implementiert den DSGVO-Bucket-Sweep,
weil die S3-API kein Cascade kennt.

Neue Tabelle media_files in pgSchema('cards'):
  id, user_id, object_key, mime_type, original_filename, size_bytes,
  kind, created_at — kein FK auf cards (ein File kann mehreren Karten
  gehören). objectKey-Format <userId>/<ulid>.<ext> für Bucket-Prefix-
  Sweep beim DSGVO-Delete. Legacy mediaRefs bleibt als Slot.

Neuer Router /api/v1/media:
  POST /upload   — multipart, 25 MiB Default-Limit, image/audio/video
                   only (415 sonst), schreibt media_files-Row + speichert
                   in MinIO unter <userId>/<ulid>.<ext>
  GET  /:id      — streamt aus MinIO mit Cache-Control: private,
                   immutable. Cross-User → 404 (nicht 403, anti-enumeration).
  GET  /         — listet alle eigenen Files

DSGVO-Pfade (Service-Key + /me/delete) räumen jetzt auch media_files
+ MinIO-Bucket-Prefix mit ab. Storage-Sweep ist non-fatal — DB ist erst
konsistent gelöscht, dead bytes wären die schlimmstmögliche Folge.

Anki-Import: parse.ts sanitizeAnkiHtml akzeptiert wieder eine
Filename→URL-Map (war in Phase 8c gedroppt). import.ts lädt vor den
Karten alle referenzierten Media-Files via uploadMedia() in MinIO,
sammelt URLs, ersetzt Anki-Filenames durch /api/v1/media/<id>-Pfade
in `<img>` (Markdown) und `[sound:…]` (HTML <audio>). 4-fache Worker-
Concurrency.

apps/web/src/lib/markdown.ts: DOMPurify lässt jetzt <audio>/<video>/
<source> mit src/controls/preload-Attributen durch — sonst würden die
Audio-Tags aus dem Anki-Import gestrippt.

i18n-Strings (DE/EN) auf Media-Stage erweitert: stage_media,
done_media, what_works_media, dropzone_hint, preview_media.
import.what_skipped_media wird zur Bestätigung dass Media seit
Sprint 9k mit übernommen wird.

Manueller E2E-Smoke gegen lokale MinIO (cards-minio :9100):
- 1×1-PNG hochgeladen → 201 mit ID + URL
- /api/v1/media/<id> streamt 200 image/png 69 bytes (file-Identifikation
  bestätigt)
- Cross-User → 404, ohne X-User-Id → 401, text/plain → 415

53 API-Tests grün (+4 neue media-Auth-Gate-Tests), 7 Web-Tests,
51 Domain-Tests, type-check + svelte-check 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:42:56 +02:00