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>
This commit is contained in:
Till JS 2026-05-08 21:08:06 +02:00
parent a960d09e5b
commit 7119756ce6
6 changed files with 236 additions and 55 deletions

View file

@ -1,7 +1,12 @@
/**
* Cards-API-Client. Dünner Fetch-Wrapper, der `X-User-Id`-Header aus
* dem Dev-Auth-Stub setzt. Phase 2 ersetzt das durch ein Bearer-Token
* aus @mana/shared-auth.
* Cards-API-Client. Dünner Fetch-Wrapper.
*
* Phase 10c: schickt `Authorization: Bearer <jwt>` aus der echten
* mana-auth-Session. Wenn kein JWT da ist (Stub-Modus für Tests),
* fällt er auf den `X-User-Id`-Header zurück.
*
* 401 wir leeren die Session und werfen ApiError. Aufrufer kann
* darauf reagieren (z.B. Redirect auf `/`).
*/
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
@ -33,8 +38,10 @@ export async function api<T>(path: string, opts: RequestOptions = {}): Promise<T
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (devUser.id) {
headers['X-User-Id'] = devUser.id;
if (devUser.token) {
headers['Authorization'] = `Bearer ${devUser.token}`;
} else if (devUser.stubId) {
headers['X-User-Id'] = devUser.stubId;
}
const res = await fetch(`${API_BASE}${path}`, {
method: opts.method ?? 'GET',
@ -43,6 +50,11 @@ export async function api<T>(path: string, opts: RequestOptions = {}): Promise<T
signal: opts.signal,
});
if (!res.ok) {
if (res.status === 401 && devUser.token) {
// Token vermutlich abgelaufen — Session lokal aufräumen, sodass
// die Landing-Page einen Re-Login zeigt.
devUser.clear();
}
let body: unknown = null;
try {
body = await res.json();

View file

@ -21,7 +21,8 @@ export async function uploadMedia(file: File | Blob, filename?: string): Promise
form.append('file', wrapped);
const headers: Record<string, string> = {};
if (devUser.id) headers['X-User-Id'] = devUser.id;
if (devUser.token) headers['Authorization'] = `Bearer ${devUser.token}`;
else if (devUser.stubId) headers['X-User-Id'] = devUser.stubId;
const res = await fetch(`${API_BASE}/api/v1/media/upload`, {
method: 'POST',