wannpassts/README.md
Tronax bfae47f584
fix(db): auto-create DB directory and improve open error messages
Open() now creates the parent directory of the database path itself,
so e.g. DB_PATH=/data/app.db works on first start without a prior
mkdir. Open, ping, and migration errors are wrapped with context and
hints about writable directories and Docker bind-mount ownership
(chown 1000:1000 or use a named volume).

Also add a README troubleshooting section for the "database could
not be opened" error.
2026-08-27 19:07:07 +02:00

191 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WannPassts
**Deine freien Zeiten geteilt per Link. Ohne deine Termine preiszugeben.**
WannPassts ist eine Self-Hosted-Buchungs-App (à la Cal.com / Calendly, aber privat):
Du verbindest deine Kalender, teilst einen Link, und andere sehen nur **frei / belegt**
und stellen Buchungsanfragen. Termintitel, Orte und Beschreibungen verlassen deine
Kalender nie.
## Features
- 🔐 **Registrierung/Login** (JWT, bcrypt-Passwort-Hashes)
- 📆 **Kalender-Connect:**
- **Google Kalender** per OAuth nutzt die FreeBusy-API, die *strukturell* keine Termindetails liefert
- **iCloud** & andere (Fastmail, Nextcloud, …) per **CalDAV** iCloud mit App-spezifischem Passwort
- **Beliebige ICS-/webcal-Links** (z. B. Google-„Private Adresse“, Ferien-/Schichtpläne)
- 🔗 **Öffentliche Buchungsseite** (`/b/<slug>`): freie Slots im von dir definierten Zeitfenster, Wochentage, Slot-Raster und Dauern **niemals Termindetails**
- 📨 **Buchungsanfragen** annehmen/ablehnen; angenommene Zeiten werden belegt und verhindern Doppelbuchungen
- ⚙️ Buchungsregeln: Zeitfenster, Slot-Raster (1060 Min), Dauern, Wochentage, Horizont (1120 Tage), Zeitzone
- 🧊 **Liquid-Glass-UI** mit **Dark-/Light-Mode**: Umschalter (Auto / Dunkel / Hell) auf jeder Seite, automatische Systemerkennung (`prefers-color-scheme`) inkl. Live-Wechsel, Auswahl wird pro Browser gespeichert
- 📱 **Voll mobilfähig**: responsive Layouts ab ~320 px, 44px-Touch-Targets, kein iOS-Fokus-Zoom (16px-Inputs), Safe-Area-Unterstützung (Notch), `dvh`-Höhen, `touch-action` ohne Doppeltipp-Zoom
- 📲 **PWA**: installierbar (Android/Desktop: „App installieren“, iOS: „Zum Home-Bildschirm“), Manifest mit Maskable-Icons, App-Shortcuts; Service Worker cached die App-Shell → Startseite funktioniert offline (API-Aufrufe brauchen natürlich Netz)
- 🔒 Provider-Tokens werden **AES-256-GCM-verschlüsselt** in der SQLite-DB gespeichert
## Tech Stack
| Teil | Technologie |
| -------- | ------------------------------------------------------------------ |
| Frontend | Vue 3 + TypeScript, Vite, Pinia, Vue Router (kein UI-Framework) |
| Backend | Go, chi, JWT, SQLite (modernc, CGO-frei), x/oauth2, go-webdav |
## Schnellstart (Entwicklung)
```bash
# Terminal 1 Backend (http://localhost:8080)
cd backend
cp .env.example .env # optional anpassen; ohne .env läuft es mit Defaults
go run ./cmd/server
# Terminal 2 Frontend (http://localhost:5173, Proxied /api → 8080)
cd frontend
npm install
npm run dev
```
Dann http://localhost:5173 öffnen, Konto erstellen, Kalender verbinden, Link teilen. 🎉
## Produktion (ein einziger Go-Prozess)
```bash
cd frontend && npm run build && cd ..
cd backend
STATIC_DIR=../frontend/dist \
APP_URL=https://deine-domain.de \
FRONTEND_URL=https://deine-domain.de \
JWT_SECRET=$(openssl rand -hex 32) \
ENCRYPTION_KEY=$(openssl rand -hex 32) \
go run ./cmd/server
```
Der Go-Server liefert dann das Frontend (`dist/`) **und** die API über einen Port.
## Docker (alles in einem Image)
Multi-Stage-Build: Node baut das Frontend, Go ein statisches Binary (CGO-frei dank modernc-SQLite), Laufzeit ist Alpine mit CA-Zertifikaten (für Google-/CalDAV-/ICS-HTTPS-Aufrufe). Frontend liegt im Image unter `/app/dist` und wird vom Go-Server mit ausgeliefert — **ein Port, ein Container, eine URL.**
```bash
# Image bauen
docker build -t wannpassts .
# Starten (Datenbank im Volume, damit sie Updates überlebt)
docker run -d --name wannpassts -p 8080:8080 \
-e JWT_SECRET=$(openssl rand -hex 32) \
-e ENCRYPTION_KEY=$(openssl rand -hex 32) \
-v wannpassts-data:/data \
wannpassts
```
Oder mit Compose (liest Secrets aus `.env` im Projektroot bzw. `backend/.env`):
```bash
cp backend/.env.example backend/.env # Secrets & Google-Zugänge eintragen
docker compose up -d --build
```
Hinter einem Reverse-Proxy / mit Domain zusätzlich setzen:
```bash
-e APP_URL=https://termine.deinedomain.de \
-e FRONTEND_URL=https://termine.deinedomain.de
```
Wichtig: `JWT_SECRET` und `ENCRYPTION_KEY` dauerhaft setzen (Container-Neustarts sonst neue Secrets → Logins/Token ungültig), und die Google-Redirect-URI auf `{APP_URL}/api/calendars/google/callback` konfigurieren. Das SQLite-File liegt im Volume `/data`.
**Troubleshooting „Datenbank konnte nicht geöffnet werden“:** Der Server legt Verzeichnis und Datei selbst an, braucht dafür aber Schreibrechte. Der Container läuft als User 1000 (`app`). Bei einem **Bind-Mount** (`-v /pfad/auf/host:/data`) muss das Host-Verzeichnis diesem User gehören:
```bash
sudo chown -R 1000:1000 /pfad/auf/host
```
Docker erstellt nicht-existierende Bind-Mount-Pfade automatisch als root deshalb entweder den Pfad vorher anlegen und `chown`en, oder den **Named Volume** nutzen (`-v wannpassts-data:/data` bzw. die docker-compose.yml), wo das automatisch passt.
### PWA-Hinweise
- Der Service Worker wird nur im Production-Build registriert (`npm run build` + Auslieferung über `STATIC_DIR`), nicht im Vite-Dev-Modus.
- Die Cache-Version des Service Workers wird pro Build automatisch neu gesetzt (Build-Zeitstempel) — nach einem Deploy holen Clients die neue Version und alte Caches werden geräumt.
- `/api/*` wird niemals gecached; offline funktioniert die App-Shell (Startseite/Navigation), Kalender- und Buchungsdaten benötigen eine Verbindung.
- Icons liegen als SVG-Quellen in `frontend/public/icons/` und werden per `rsvg-convert` zu PNGs konvertiert (Neuerzeugung: siehe Skript unten im Abschnitt „Tests“).
## Kalender-Anbindung einrichten
### Google
1. [Google Cloud Console](https://console.cloud.google.com): Projekt anlegen, **Google Calendar API** aktivieren.
2. OAuth-Zustimmungsbildschirm (External) einrichten, Scope `calendar.readonly`.
3. **OAuth-Client-ID** (Webanwendung) erstellen; als autorisierte Redirect-URI
`{APP_URL}/api/calendars/google/callback` eintragen (lokal: `http://localhost:8080/api/calendars/google/callback`).
4. `GOOGLE_CLIENT_ID` + `GOOGLE_CLIENT_SECRET` in `backend/.env` setzen.
Beim Verbinden wird nur der **Primärkalender** angebunden; gelesen wird ausschließlich Free/Busy.
### iCloud
1. Auf [appleid.apple.com](https://appleid.apple.com/account/manage) → *Anmeldung und Sicherheit***App-spezifisches Passwort** erzeugen.
2. In WannPassts: *Kalender → iCloud / CalDAV*:
- Server: `https://caldav.icloud.com`
- Benutzername: Apple-ID (E-Mail)
- Passwort: das App-spezifische Passwort (nicht das normale!)
3. Der erste Kalender mit Terminen wird automatisch erkannt (Kalender-Pfad ist optional manuell überschreibbar).
### Andere (Fastmail, Nextcloud, GMX, …)
Gleiche CalDAV-Maske mit dem jeweiligen CalDAV-Server des Anbieters, oder beliebige **ICS-Links** abonnieren.
## Datenschutz-Design
- Besucher sehen ausschließlich **Start/Ende von busy-Zeiträumen** die API hat gar keine Felder für Titel & Co.
- Google: FreeBusy-API liefert von sich aus nur Belegungszeiträume.
- CalDAV/ICS: Termine werden serverseitig auf `DTSTART/DTEND` reduziert, abgesagte und „Verfügbar“-Termine (TRANSPARENT) werden ignoriert.
- Tokens/Passwörter der Verbindungen liegen verschlüsselt (AES-GCM aus `ENCRYPTION_KEY`) in der DB.
- Sync erfolgt bedarfsgesteuert (max. alle 5 Min) beim Aufruf der Buchungsseite plus manueller Button.
## Bekannte Grenzen (MVP)
- **Kein E-Mail-Versand** Anfragen/Freigaben erscheinen nur im Dashboard (Mail-Versand via SMTP wäre der nächste Ausbauschritt).
- **ICS-Abos** expandieren Serientermine nicht**; CalDAV und Google tun dies serverseitig korrekt.
- **Ausstehende (pending) Anfragen blockieren noch keine Zeit** erst angenommene. Bei zwei parallelen Anfragen für denselben Slot gewinnt, wer zuerst angenommen wird (Kollision wird beim Annehmen geprüft).
- Ein Google-Konto verbindet aktuell den Primärkalender; weitere Kalender lassen sich als ICS-Abo hinzufügen.
## Projektstruktur
```
backend/
cmd/server/ # Einstieg
internal/config/ # Env-/Secret-Handling
internal/db/ # SQLite-Schema & Queries
internal/crypto/ # AES-GCM für gespeicherte Provider-Tokens
internal/calendar/ # Provider: google.go, caldav.go, ics.go (+ Syncer)
internal/httpapi/ # Router, Middleware, Handler (auth, calendars, bookings, public)
frontend/
src/lib/ # api-Client, Typen, Zeitzonen-Helfer
src/stores/ # Pinia: auth, toast
src/views/ # Landing, Login, Register, Dashboard, Booking (/b/:slug)
src/components/ # GlassCard, ShareLink, Sektionen (Kalender/Anfragen/Einstellungen)
src/styles/main.css # Liquid-Glass-Dark-Designsystem
```
## API-Überblick
| Methode | Pfad | Auth | Zweck |
| ------------------- | --------------------------------- | ---- | ---------------------------------------- |
| POST | `/api/auth/register` \| `/login` | | Konto / JWT |
| GET/PATCH | `/api/me` | ✅ | Profil & Buchungsregeln |
| POST | `/api/me/slug` | ✅ | Neuen Buchungslink erzeugen |
| GET/DELETE | `/api/calendars…` | ✅ | Verbinden (google/caldav/ics), verwalten, sync |
| GET | `/api/public/{slug}` | | Buchungsseite: Regeln + busy-Zeiträume |
| POST | `/api/public/{slug}/bookings` | | Buchungsanfrage stellen |
| GET | `/api/bookings`, `…/accept\|decline` | ✅ | Anfragen verwalten |
## Tests
```bash
cd backend && go test ./... # ICS-Parser, Dauer-Handling, Interval-Clamping
```
PWA-Icons neu erzeugen (benötigt `rsvg-convert`):
```bash
cd frontend/public/icons
rsvg-convert -w 192 -h 192 -o icon-192.png icon.svg
rsvg-convert -w 512 -h 512 -o icon-512.png icon.svg
rsvg-convert -w 512 -h 512 -o icon-maskable-512.png icon-maskable.svg
rsvg-convert -w 180 -h 180 -o apple-touch-icon.png apple-touch-icon.svg
```