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.
10 KiB
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 (10–60 Min), Dauern, Wochentage, Horizont (1–120 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-actionohne 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)
# 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)
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.
# 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):
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:
-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:
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 chownen, 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 überSTATIC_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 perrsvg-convertzu PNGs konvertiert (Neuerzeugung: siehe Skript unten im Abschnitt „Tests“).
Kalender-Anbindung einrichten
- Google Cloud Console: Projekt anlegen, Google Calendar API aktivieren.
- OAuth-Zustimmungsbildschirm (External) einrichten, Scope
calendar.readonly. - OAuth-Client-ID (Webanwendung) erstellen; als autorisierte Redirect-URI
{APP_URL}/api/calendars/google/callbackeintragen (lokal:http://localhost:8080/api/calendars/google/callback). GOOGLE_CLIENT_ID+GOOGLE_CLIENT_SECRETinbackend/.envsetzen.
Beim Verbinden wird nur der Primärkalender angebunden; gelesen wird ausschließlich Free/Busy.
iCloud
- Auf appleid.apple.com → Anmeldung und Sicherheit → App-spezifisches Passwort erzeugen.
- In WannPassts: Kalender → iCloud / CalDAV:
- Server:
https://caldav.icloud.com - Benutzername: Apple-ID (E-Mail)
- Passwort: das App-spezifische Passwort (nicht das normale!)
- Server:
- 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/DTENDreduziert, 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
cd backend && go test ./... # ICS-Parser, Dauer-Handling, Interval-Clamping
PWA-Icons neu erzeugen (benötigt rsvg-convert):
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