Ein extrem ressourceneffizientes Web-App-Template, optimiert für den Betrieb auf schwachen VPS (Virtual Private Servern) mit minimalem RAM-Verbrauch (< 100MB im Idle).
- Runtime: Bun - Extrem schneller JavaScript-All-in-One-Runtime.
- Framework: Hono - Ultrafast, web-standardsbasiertes Framework.
- Database:
bun:sqlite- Native SQLite-Anbindung ohne schwere ORMs oder externe Prozesse. - Auth:
hono/jwtMiddleware &Bun.passwordfür sicheres Argon2/bcrypt Hashing. - Validation: Zod +
@hono/zod-validator- Typsichere Eingabe-Validierung. - Security:
secureHeaders()Middleware für XSS, HSTS, Clickjacking-Schutz. - WebSocket: Native Bun WebSockets für Echtzeit-Chat.
- Framework: React 19 (SPA) - Als statische Dateien serviert.
- Build-Tool: Vite 6 - Schnelle Development-Experience und optimierte Builds.
- State Management: TanStack Query v5 - Server-State mit Caching, Offline-Support & Optimistic Updates.
- PWA: vite-plugin-pwa - Offline-Support und Installierbarkeit.
- CSS: Tailwind CSS v4 - Modernstes CSS-Framework via
@tailwindcss/vite. - Linting: ESLint + Prettier - Konsistente Code-Formatierung.
- Query Cache Persistenz: TanStack Query Cache wird im localStorage gespeichert.
- Optimistic Updates: UI wird sofort aktualisiert, Rollback bei Fehler.
- Offline-Sync: Mutationen werden bei fehlender Verbindung pausiert und automatisch synchronisiert.
- Service Worker: PWA-Support mit Workbox für Asset-Caching.
/
├── backend/
│ ├── index.ts # Haupteinstiegspunkt (App-Setup, Static Serving, Security Headers)
│ ├── db/
│ │ └── index.ts # Datenbankverbindung & Repositories
│ ├── middleware/
│ │ ├── index.ts # Middleware-Exports
│ │ └── rateLimit.ts # Rate-Limiting Middleware
│ ├── routes/
│ │ ├── index.ts # Route-Exports
│ │ ├── auth.ts # Authentifizierungs-Routen (Login, Signup) mit Zod-Validierung
│ │ ├── chat.ts # WebSocket-Chat-Route mit Pub/Sub
│ │ ├── entries.ts # Einträge-Routen (CRUD) mit Zod-Validierung
│ │ ├── files.ts # Datei-Upload-Routen (Upload, Download, Delete)
│ │ ├── health.ts # Health-Check-Route
│ │ └── password-reset.ts # Passwort-Reset-Routen
│ ├── types/
│ │ └── index.ts # TypeScript Type-Definitionen
│ └── validation/
│ ├── index.ts # Eingabe-Validierungsfunktionen
│ └── schemas.ts # Zod-Schemas für API-Validierung
│
├── frontend/
│ ├── src/
│ │ ├── App.tsx # Haupt-App-Komponente mit Tab-Navigation
│ │ ├── main.tsx # React-Einstiegspunkt mit TanStack Query Provider
│ │ ├── index.css # Globale Styles (Tailwind)
│ │ ├── components/
│ │ │ ├── index.ts # Komponenten-Barrel-Export
│ │ │ ├── ui/ # Wiederverwendbare UI-Komponenten
│ │ │ │ ├── Alert.tsx
│ │ │ │ ├── Button.tsx
│ │ │ │ ├── Card.tsx
│ │ │ │ ├── Input.tsx
│ │ │ │ └── OfflineBanner.tsx # Offline-Status Anzeige
│ │ │ ├── auth/ # Authentifizierungs-Komponenten
│ │ │ │ ├── AuthForm.tsx
│ │ │ │ ├── ForgotPasswordForm.tsx
│ │ │ │ └── ResetPasswordForm.tsx
│ │ │ ├── entries/ # Einträge-Komponenten
│ │ │ │ ├── EntryForm.tsx
│ │ │ │ └── EntryList.tsx
│ │ │ ├── files/ # Datei-Upload-Komponenten
│ │ │ │ ├── FileUpload.tsx
│ │ │ │ └── FileList.tsx
│ │ │ ├── chat/ # Chat-Komponenten
│ │ │ │ └── Chat.tsx
│ │ │ └── layout/ # Layout-Container
│ │ │ └── PageLayout.tsx
│ │ ├── hooks/ # Custom React Hooks mit TanStack Query
│ │ │ ├── index.ts
│ │ │ ├── useAuth.ts
│ │ │ ├── useEntries.ts # Mit Optimistic Updates
│ │ │ ├── useFiles.ts # Mit Optimistic Updates
│ │ │ ├── useChat.ts # WebSocket-Chat
│ │ │ └── useOnlineStatus.ts # Offline-Erkennung
│ │ ├── lib/ # Hilfsfunktionen & API-Client
│ │ │ ├── api.ts # Zentralisierter API-Client
│ │ │ ├── storage.ts # LocalStorage-Wrapper
│ │ │ └── queryClient.ts # TanStack Query Konfiguration
│ │ └── types/ # TypeScript Type-Definitionen
│ │ └── index.ts
│ ├── vite.config.ts
│ └── eslint.config.js # ESLint + Prettier Konfiguration
│
├── data/
│ ├── sqlite/ # SQLite-Datenbank
│ └── uploads/ # Datei-Uploads (nach User-ID)
│
├── .prettierrc # Prettier Konfiguration
└── dist/ # Build-Output (vom Backend serviert)
Das Projekt ist in eine klare Trennung von Frontend und Backend unterteilt, wobei das Backend zur Produktionszeit als Single-Server fungiert:
/frontend: Der Quellcode der React-App. Während der Entwicklung läuft hier Vite als Dev-Server./backend: Die API-Logik und DB-Anbindung./dist: Der Build-Output des Frontends. Das Backend serviert diesen Ordner statisch auf der Root-Route (/).
Das Frontend folgt einer klaren Schichtenarchitektur:
| Schicht | Zweck | Beispiele |
|---|---|---|
| Components | UI-Darstellung | Button, Card, AuthForm, OfflineBanner |
| Hooks | Business-Logik & State (TanStack Query) | useAuth, useEntries, useOnlineStatus |
| Lib | Infrastruktur | api.ts, storage.ts, queryClient.ts |
| Types | TypeScript-Definitionen | Entry, User, FileMetadata |
┌─────────────────────────────────────────────────────────────┐
│ PersistQueryClientProvider │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ QueryClient │ │
│ │ ┌─────────────────┐ ┌─────────────────────────────┐ │ │
│ │ │ Query Cache │ │ Mutation Cache │ │ │
│ │ │ (entries,files)│ │ (add,update,delete) │ │ │
│ │ └────────┬────────┘ └─────────────┬───────────────┘ │ │
│ │ │ │ │ │
│ │ └─────────┬───────────────┘ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────┐ │ │
│ │ │ SyncStoragePersister│ │ │
│ │ │ (localStorage) │ │ │
│ │ └──────────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
networkMode: 'offlineFirst'
├── Online: Request → Server → Cache → UI
└── Offline: Cache → UI, Mutation paused → Sync when online
Das Backend ist modular aufgebaut:
| Modul | Zweck |
|---|---|
| routes/ | HTTP-Endpunkte nach Domäne gruppiert |
| middleware/ | Request-Processing (Rate Limiting) |
| db/ | Datenbankzugriff & Repositories |
| validation/ | Eingabe-Validierung |
| types/ | Gemeinsame TypeScript-Definitionen |
- TanStack Query Persistenz: Query- und Mutation-Cache werden im localStorage gespeichert.
- Optimistic Updates: UI wird sofort aktualisiert, automatischer Rollback bei Fehlern.
- Offline-First Modus: Mutationen werden bei fehlender Verbindung pausiert.
- Auto-Sync: Pausierte Mutationen werden automatisch synchronisiert wenn online.
- Offline-Banner: Informiert User über Offline-Status und pausierte Änderungen.
- Asset-Caching: Workbox cachet statische Assets für schnellen Offline-Zugriff.
- Smart Updates: Service Worker wird mit
Cache-Control: no-cacheserviert.
Vorteile dieser Architektur:
- Zero-Downtime DB: SQLite ist eine Datei, kein extra Dienst, der abstürzen kann.
- Minimaler Footprint: Bun kombiniert HTTP-Server, Paketmanager und Runtime in einer Binärdatei.
- CPU-Effizienz: Kein Server-Side-Rendering (SSR). Die CPU des VPS wird nur für API-Logik und Datei-Serving genutzt.
- Wiederverwendbarkeit: Modulare Komponenten, Hooks und API-Clients können leicht erweitert werden.
- Bun (v1.0+)
# Im Root-Verzeichnis
cd frontend && bun install
cd ../backend && bun install
cd ..
bun install # Root-Dependencies (Prettier)export JWT_SECRET="dein-sicheres-secret"bun run dev- Frontend:
http://localhost:5173(Vite mit Proxy zu API) - Backend:
http://localhost:3000(Hono API)
| Script | Beschreibung |
|---|---|
bun run dev |
Frontend + Backend gleichzeitig starten |
bun run dev:frontend |
Nur Vite Dev-Server |
bun run dev:backend |
Nur Backend mit Watch-Mode |
bun run build:frontend |
Frontend für Produktion bauen |
bun run lint |
ESLint ausführen |
bun run format |
Code mit Prettier formatieren |
bun run format:check |
Prüfen ob Code formatiert ist |
Dieses Projekt ist darauf ausgelegt, mit minimalem Aufwand auf einem Linux-Server zu laufen.
bun run build:frontendNur diese Ordner/Dateien auf den VPS kopieren:
/backend/dist
Erstelle eine .env Datei im backend/ Ordner:
# Pflicht: JWT Secret für Token-Signierung
JWT_SECRET=dein-sicheres-secret-hier
# Für Passwort-Reset Funktion (optional aber empfohlen)
RESEND_API_KEY=re_xxxxxxxxxxxx
[email protected]
APP_URL=https://deine-domain.decd backend
bun install --production
bun run index.tsEmpfehlung: Nutze pm2 oder systemd für Prozess-Management.
Die Passwort-vergessen-Funktion verwendet Resend für den E-Mail-Versand. Resend ist ein moderner E-Mail-Dienst mit großzügigem Free-Tier (3.000 E-Mails/Monat).
| Vorteil | Beschreibung |
|---|---|
| Kein SMTP-Server | Keine eigene Mail-Infrastruktur nötig |
| Minimaler RAM | Nur eine HTTP-Anfrage, Resend übernimmt Queueing |
| Hohe Zustellrate | Professionelle Infrastruktur, weniger Spam-Probleme |
| Einfache API | Native fetch() von Bun, kein nodemailer |
- Gehe zu resend.com/signup
- Erstelle einen Account (kostenlos)
- Im Dashboard: API Keys → Create API Key
- Kopiere den Key (beginnt mit
re_)
Ohne Domain-Verifizierung landen E-Mails im Spam oder werden abgelehnt.
- Im Resend Dashboard: Domains → Add Domain
- Gib deine Domain ein (z.B.
deine-domain.de) - Füge die angezeigten DNS-Einträge bei deinem Domain-Provider hinzu:
| Typ | Name | Wert |
|---|---|---|
| TXT | resend._domainkey |
p=MIGf... (Resend zeigt den vollständigen Wert) |
| TXT | @ oder _dmarc |
v=DMARC1; p=none; |
| CNAME | send |
send.resend.com |
Beispiel für Cloudflare/Hetzner DNS:
# SPF Record (falls nicht vorhanden)
TXT @ "v=spf1 include:_spf.resend.com ~all"
# DKIM Record
TXT resend._domainkey "p=MIGf..."
# DMARC Record
TXT _dmarc "v=DMARC1; p=none;"
- Warte auf Verifizierung (kann bis zu 24h dauern, meist schneller)
- Status sollte auf "Verified" wechseln ✅
# backend/.env
RESEND_API_KEY=re_123456789abcdef
[email protected]
APP_URL=https://deine-domain.deWichtig:
EMAIL_FROMmuss eine Adresse deiner verifizierten Domain seinAPP_URLwird für den Reset-Link in der E-Mail verwendet- Ohne
APP_URLwirdhttp://localhost:5173als Fallback genutzt
# Backend starten
cd backend
export RESEND_API_KEY=re_xxx
export [email protected]
export APP_URL=http://localhost:5173
bun run index.ts
# Test-Request (in neuem Terminal)
curl -X POST http://localhost:3000/api/forgot-password \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]"}'| Problem | Lösung |
|---|---|
| E-Mail kommt nicht an | DNS-Einträge prüfen, Spam-Ordner checken |
RESEND_API_KEY not configured |
.env Datei erstellen oder export verwenden |
Invalid API Key |
Key im Resend Dashboard neu generieren |
| E-Mail im Spam | Domain vollständig verifizieren (SPF, DKIM, DMARC) |
Das Projekt verwendet ESLint mit:
- TypeScript-Support (
typescript-eslint) - React Hooks Rules (
eslint-plugin-react-hooks) - React Refresh (
eslint-plugin-react-refresh) - Prettier-Kompatibilität (
eslint-config-prettier)
Konfiguriert in .prettierrc:
- Keine Semikolons
- Single Quotes
- 2 Spaces Einrückung
- 100 Zeichen Zeilenlänge
Empfehlung: Beide Tools sollten in der CI/CD Pipeline verwendet werden:
bun run lint && bun run format:check- JWT (JSON Web Tokens) mit HS256-Algorithmus und 24h Ablaufzeit.
- Passwörter werden mit Bun.password (Argon2/bcrypt) sicher gehasht.
- Rate Limiting schützt Login (10/min) und Signup (5/min) vor Brute-Force.
- Zod-Schemas validieren alle API-Eingaben mit
@hono/zod-validator. - Strikte Typ-Validierung für Username, Passwort, Entry-Text und Datei-Uploads.
- Alle Datenbankabfragen verwenden Prepared Statements mit
?-Platzhaltern. - Werte werden nie direkt in SQL-Strings konkateniert.
- Beispiel:
db.query('SELECT * FROM users WHERE username = ?').get(username)
Die secureHeaders() Middleware aktiviert:
X-XSS-Protection: 1; mode=blockX-Content-Type-Options: nosniffX-Frame-Options: SAMEORIGIN(Clickjacking-Schutz)Strict-Transport-Security(HSTS)Referrer-Policy: strict-origin-when-cross-origin
- Erlaubte MIME-Types: Bilder (JPG, PNG, GIF, WebP), PDF, TXT, CSV.
- Maximale Dateigröße: 5 MB.
- Dateien werden mit UUID umbenannt (verhindert Path-Traversal).
- User können nur eigene Dateien sehen/löschen.
Die folgenden Diagramme zeigen die wichtigsten Abläufe in der Anwendung.
sequenceDiagram
participant User as 👤 User
participant UI as 🖥️ AuthForm
participant Hook as 🪝 useAuth
participant API as 📡 api.ts
participant Server as 🖧 Backend
participant DB as 💾 SQLite
User->>UI: Füllt Formular aus
UI->>Hook: signup(username, password)
Hook->>Hook: setLoading(true)
Hook->>API: authApi.signup()
API->>Server: POST /api/signup
Note over Server: Rate Limit Check (5/min)
Server->>Server: validateAuth()
alt Validierung fehlgeschlagen
Server-->>API: 400 { error }
API-->>Hook: { error }
Hook-->>UI: setError(message)
UI-->>User: ❌ Zeigt Fehler
end
Server->>Server: Bun.password.hash()
Server->>DB: INSERT INTO users
alt Username existiert
DB-->>Server: UNIQUE constraint error
Server-->>API: 400 "User already exists"
API-->>Hook: { error }
Hook-->>UI: setError()
UI-->>User: ❌ Zeigt Fehler
end
DB-->>Server: ✓ User erstellt
Server-->>API: 200 { success: true }
API-->>Hook: { success }
Hook-->>UI: setError("Erfolgreich!")
UI-->>User: ✅ Wechselt zu Login
sequenceDiagram
participant User as 👤 User
participant UI as 🖥️ AuthForm
participant Hook as 🪝 useAuth
participant API as 📡 api.ts
participant Server as 🖧 Backend
participant DB as 💾 SQLite
participant Storage as 💾 LocalStorage
User->>UI: Gibt Credentials ein
UI->>Hook: login(username, password)
Hook->>API: authApi.login()
API->>Server: POST /api/login
Note over Server: Rate Limit Check (10/min)
Server->>Server: validateAuth()
Server->>DB: SELECT * FROM users WHERE username = ?
alt User nicht gefunden
DB-->>Server: null
Server-->>API: 401 "Invalid credentials"
API-->>Hook: { error, status: 401 }
Hook-->>UI: setError()
UI-->>User: ❌ Zeigt Fehler
end
DB-->>Server: User { id, password_hash }
Server->>Server: Bun.password.verify()
alt Passwort falsch
Server-->>API: 401 "Invalid credentials"
API-->>Hook: { error }
Hook-->>UI: setError()
UI-->>User: ❌ Zeigt Fehler
end
Server->>Server: jwt.sign({ id, username }, secret)
Server-->>API: 200 { token }
API-->>Hook: { data: { token } }
Hook->>Storage: tokenStorage.set(token)
Hook->>Hook: setToken(token)
Hook-->>UI: isAuthenticated = true
UI-->>User: ✅ Zeigt Dashboard
sequenceDiagram
participant User as 👤 User
participant UI as 🖥️ Button
participant Hook as 🪝 useAuth
participant Storage as 💾 LocalStorage
participant Entries as 🪝 useEntries
User->>UI: Klickt "Logout"
UI->>Hook: logout()
Hook->>Storage: tokenStorage.remove()
Storage-->>Hook: ✓ Token gelöscht
Hook->>Hook: setToken('')
Note over Hook,Entries: Token ist leer →<br/>isAuthenticated = false
Hook-->>UI: State Update
UI-->>User: 🔄 Zeigt Login-Formular
Note over User,Storage: Kein Server-Request nötig!<br/>JWT ist stateless - Token<br/>wird einfach verworfen.
sequenceDiagram
participant User as 👤 User
participant Form as 🖥️ EntryForm
participant Hook as 🪝 useEntries
participant API as 📡 api.ts
participant Server as 🖧 Backend
participant JWT as 🔐 JWT Middleware
participant DB as 💾 SQLite
User->>Form: Gibt Text ein, klickt "Senden"
Form->>Form: Validiert (nicht leer)
Form->>Hook: addEntry(text)
Hook->>API: entriesApi.create(token, text)
API->>Server: POST /api/entries<br/>Header: Authorization: Bearer {token}
Server->>JWT: jwt({ secret, alg: 'HS256' })
alt Token ungültig/abgelaufen
JWT-->>Server: 401 Unauthorized
Server-->>API: 401
API-->>Hook: { status: 401 }
Hook->>Hook: onUnauthorized() → logout()
Hook-->>Form: Redirect zu Login
end
JWT-->>Server: payload { id, username }
Server->>Server: validateEntryText(text)
alt Text ungültig
Server-->>API: 400 { error }
API-->>Hook: { error }
Hook-->>Form: setError()
Form-->>User: ❌ Zeigt Fehler
end
Server->>DB: INSERT INTO entries (text, userId)
DB-->>Server: ✓ Entry erstellt
Server-->>API: 200 { success: true }
API-->>Hook: { success }
Hook->>Hook: fetchEntries() → Refresh
Hook-->>Form: ✓ Success
Form->>Form: setInputText('')
Form-->>User: ✅ Neuer Eintrag sichtbar
sequenceDiagram
participant Browser as 🌐 Browser
participant App as 🖥️ App.tsx
participant Auth as 🪝 useAuth
participant Entries as 🪝 useEntries
participant Storage as 💾 LocalStorage
participant API as 📡 api.ts
participant Server as 🖧 Backend
Browser->>App: Lädt Seite
App->>Auth: useAuth()
Auth->>Storage: tokenStorage.get()
alt Kein Token
Storage-->>Auth: null
Auth-->>App: isAuthenticated = false
App-->>Browser: 🔐 Zeigt Login
end
Storage-->>Auth: token
Auth->>Auth: setToken(token)
Auth-->>App: isAuthenticated = true
App->>Entries: useEntries(token, logout)
Note over Entries: useEffect() bei<br/>Token-Änderung
Entries->>API: entriesApi.getAll(token)
API->>Server: GET /api/entries
Server->>Server: JWT validieren
Server->>Server: DB Query für userId
Server-->>API: 200 [ entries... ]
API-->>Entries: { data: entries }
Entries->>Entries: setEntries(data)
Entries-->>App: entries = [...]
App-->>Browser: 📋 Zeigt Einträge
graph TB
subgraph "🖥️ Frontend - React SPA"
UI["📦 Components<br/>(Button, Card, AuthForm...)"]
Hooks["🪝 Custom Hooks<br/>(useAuth, useEntries, useFiles)"]
APIClient["📡 api.ts<br/>(Fetch Wrapper)"]
Storage["💾 storage.ts<br/>(LocalStorage)"]
end
subgraph "🖧 Backend - Hono + Bun"
Routes["🛤️ Routes<br/>(auth, entries, files, health)"]
MW["🛡️ Middleware<br/>(JWT, RateLimit, SecureHeaders)"]
Val["✅ Validation<br/>(Zod Schemas)"]
Repo["📚 Repositories"]
DB[("💾 SQLite")]
FS[("📁 Filesystem<br/>/data/uploads")]
end
subgraph "📧 External Services"
Resend["📨 Resend API<br/>(E-Mail Versand)"]
end
UI --> Hooks
Hooks --> APIClient
Hooks --> Storage
APIClient -->|"HTTP/JSON"| Routes
Routes --> MW
Routes --> Val
Routes --> Repo
Routes -->|"Password Reset"| Resend
Repo --> DB
Routes -->|"File Storage"| FS
style UI fill:#61dafb,color:#000
style Hooks fill:#61dafb,color:#000
style APIClient fill:#61dafb,color:#000
style Storage fill:#61dafb,color:#000
style Routes fill:#ff6b6b,color:#000
style MW fill:#ff6b6b,color:#000
style Val fill:#ff6b6b,color:#000
style Repo fill:#ff6b6b,color:#000
style DB fill:#ffd93d,color:#000
style FS fill:#ffd93d,color:#000
style Resend fill:#9333ea,color:#fff
sequenceDiagram
participant User as 👤 User
participant UI as 🖥️ FileUpload
participant Hook as 🪝 useFiles
participant API as 📡 api.ts
participant Server as 🖧 Backend
participant FS as 📁 Filesystem
participant DB as 💾 SQLite
User->>UI: Wählt Datei aus / Drag & Drop
UI->>UI: Validiert Dateityp & Größe
alt Validierung fehlgeschlagen
UI-->>User: ❌ Zeigt Fehler (Typ/Größe)
end
UI->>Hook: uploadFile(file, description?)
Hook->>Hook: setLoading(true)
Hook->>API: filesApi.upload(token, file)
Note over API: multipart/form-data
API->>Server: POST /api/files
Server->>Server: JWT validieren
alt Token ungültig
Server-->>API: 401 Unauthorized
API-->>Hook: { status: 401 }
Hook->>Hook: onUnauthorized()
Hook-->>UI: Redirect zu Login
end
Server->>Server: MIME-Type prüfen
Server->>Server: Dateigröße prüfen (max 5MB)
alt Validierung fehlgeschlagen
Server-->>API: 400 { error }
API-->>Hook: { error }
Hook-->>UI: setError()
UI-->>User: ❌ Zeigt Fehler
end
Server->>Server: crypto.randomUUID()
Server->>FS: mkdir /data/uploads/{userId}
Server->>FS: Bun.write({uuid}.ext)
Server->>DB: INSERT INTO files (metadata)
DB-->>Server: ✓ FileMetadata
Server-->>API: 200 { success, file }
API-->>Hook: { data }
Hook->>Hook: fetchFiles() → Refresh
Hook-->>UI: ✓ Success
UI-->>User: ✅ Datei in Liste sichtbar
sequenceDiagram
participant User as 👤 User
participant UI as 🖥️ ForgotPasswordForm
participant API as 📡 api.ts
participant Server as 🖧 Backend
participant DB as 💾 SQLite
participant Resend as 📨 Resend API
participant Email as 📧 E-Mail Client
Note over User,Email: Phase 1: Passwort-Reset anfordern
User->>UI: Gibt E-Mail ein
UI->>API: authApi.forgotPassword(email)
API->>Server: POST /api/forgot-password
Note over Server: Rate Limit Check (3/min)
Server->>Server: Zod-Validierung
Server->>DB: SELECT * FROM users WHERE email = ?
alt E-Mail nicht gefunden
DB-->>Server: null
Server-->>API: 200 { success, message }
Note over Server: Keine Info preisgeben!
API-->>UI: "Falls Konto existiert..."
UI-->>User: ✅ Generische Erfolgsmeldung
end
DB-->>Server: User { id, username }
Server->>Server: crypto.randomUUID()
Server->>Server: expires = now + 1h
Server->>DB: UPDATE users SET reset_token, reset_expires
Server->>Server: generateResetEmail(url, username)
Server->>Resend: POST /emails (HTML)
Resend-->>Server: 200 OK
Server-->>API: 200 { success, message }
API-->>UI: "Falls Konto existiert..."
UI-->>User: ✅ Generische Erfolgsmeldung
Note over Resend,Email: E-Mail Zustellung
Resend->>Email: Reset-Link E-Mail
Note over User,Email: Phase 2: Passwort zurücksetzen
Email->>User: Öffnet E-Mail
User->>UI: Klickt Reset-Link
Note over UI: ResetPasswordForm
UI->>API: authApi.validateResetToken(token)
API->>Server: GET /api/reset-password/{token}
Server->>DB: SELECT * FROM users WHERE reset_token = ?
alt Token ungültig/abgelaufen
Server-->>API: 400 { valid: false }
API-->>UI: { valid: false }
UI-->>User: ❌ "Link abgelaufen"
end
Server-->>API: 200 { valid: true }
API-->>UI: Token gültig
UI-->>User: Zeigt Passwort-Formular
User->>UI: Gibt neues Passwort ein
UI->>API: authApi.resetPassword(token, password)
API->>Server: POST /api/reset-password
Server->>DB: SELECT * FROM users WHERE reset_token = ?
Server->>Server: Prüfe reset_expires
Server->>Server: Bun.password.hash(newPassword)
Server->>DB: UPDATE users SET password = ?
Server->>DB: UPDATE users SET reset_token = NULL
Server-->>API: 200 { success, message }
API-->>UI: "Passwort zurückgesetzt!"
UI-->>User: ✅ Zum Login-Button
MIT