Zum Inhalt springen

Frontend-API

Svelte-Komponenten rufen niemals fetch() direkt auf. Stattdessen gibt es pro Backend-Ressource eine Service-Klasse in frontend/src/api/, gebündelt im zentralen api-Objekt:

<script>
import { api, ApiError } from '../api';
let tenants = $state([]);
async function load() {
tenants = await api.tenants.getAll();
}
</script>

Verfügbar: api.auth, api.users, api.apiKeys, api.tenants (inkl. Submissions/Quarantäne/Stats), api.routes (SMTP-Routen, Test-Versand), api.blocklist, api.system (Version/Build) sowie api.client (reaktiver Zustand).

frontend/src/api/client.svelte.js ist die einzige Stelle mit fetch:

  • JWT automatisch anhängen: Ist eine Session aktiv, bekommt jeder Request den Authorization: Bearer …-Header.
  • Fehler normalisieren: Jede Fehlerantwort wird zu einer ApiError mit status und message (aus dem {"error": "..."}-Body).
  • Auto-Logout bei 401: Session verwerfen + Redirect auf /login — genau einmal implementiert, gilt überall.
  • Session-Persistenz: Token + Profil in localStorage; ein Reload behält den Login.

Der Auth-Zustand ist ein $state-Feld in einer .svelte.js-Datei — api.client.isLoggedIn, api.client.user und hasPermission() sind dadurch reaktiv.

Für Komponenten gibt es den schlanken Wrapper frontend/src/stores/auth.svelte.js:

<script>
import { auth } from '../stores/auth.svelte.js';
</script>
{#if auth.isLoggedIn}
Hallo, {auth.user.display_name}!
{/if}
{#if auth.can('tenants:write')}
<button>Neuer Mandant</button>
{/if}

auth.can() ist reine UI-Kosmetik (Buttons ausblenden) — die verbindliche Prüfung macht immer der Server per RBAC-Middleware.

1. Klassefrontend/src/api/xyz.js (Vorlage: routes.js):

export class XyzApi {
#client;
constructor(client) { this.#client = client; }
getAll() { return this.#client.get('/xyz'); }
create(input) { return this.#client.post('/xyz', input); }
remove(id) { return this.#client.delete(`/xyz/${id}`); }
}

2. Registrieren — in frontend/src/api/index.js importieren und ans api-Objekt hängen.

3. Verwendenawait api.xyz.getAll() in jeder Komponente.

Lokaler Zustand + Laden (Vorlage: pages/Tenants.svelte):

<script>
let tenants = $state([]);
let error = $state('');
async function load() {
try {
tenants = await api.tenants.getAll();
error = '';
} catch (e) {
error = e instanceof ApiError ? e.message : String(e);
}
}
$effect(() => {
load();
});
</script>

Abhängigkeitsgetriggertes Neuladen (Vorlage: pages/TenantDetail.svelte — Statusfilter):

$effect(() => {
statusFilter; // getrackte Abhängigkeit: Filterwechsel lädt neu
load();
});

Props (svelte-spa-router übergibt Routen-Parameter):

<script>
let { params = {} } = $props(); // params.id aus '/tenants/:id'
</script>

Fehlerbehandlung: immer ApiError abfangen und e.message anzeigen — so wird ein 403 („fehlende Berechtigung: tenants:write”) zur verständlichen UI-Meldung statt zu einer Konsolen-Exception.

svelte-spa-router mit Hash-Routing (/#/tenants): funktioniert im Single-Binary ohne Server-Konfiguration. Routen sind in App.svelte definiert; neue Seite = neue Datei in pages/ + Eintrag im routes-Objekt. Links mit use:link, programmatisch mit push('/pfad').

Terminal-Fenster
make dev

startet das Go-Backend auf :8080 und Vite auf :5173 (mit /api-Proxy aufs Backend). Frontend-Änderungen erscheinen per Hot-Reload sofort; fürs finale Testen make build und das Binary starten. Die Public API (/v1) testet man am einfachsten mit der Beispielseite client/example.html gegen das laufende Binary (Origin des Testservers beim Mandanten eintragen).