Doku-Sync: Auth, Tenant-Dashboard, Middleware, Schema nachgezogen

- SCHEMA.md: users-Tabelle korrigiert, sessions-Tabelle ergänzt
- API-ENDPOINTS.md: Auth-Routen + Tenant-Dashboard-Routen ergänzt
- SERVER-KONZEPT.md: Abschnitte Authentifizierung, Middleware-Kette, Tenant-Dashboard neu
- backend/README.md: komplett neu auf Basis aktueller Implementierung
- DEVELOPMENT.md: veraltete "nicht vorhanden"-Punkte bereinigt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jesko Anschütz 2026-03-23 20:07:12 +01:00
parent 0e66bfdb24
commit 4268da7988
5 changed files with 379 additions and 36 deletions

View file

@ -40,9 +40,9 @@ Bereits vorhanden:
Noch nicht vorhanden: Noch nicht vorhanden:
- admin-seitige Benutzerautentifizierung und Zugriffskontrolle
- Multi-Tenancy-Isolation auf API-Ebene
- produktives SSL/TLS-Handling fuer Deployment - produktives SSL/TLS-Handling fuer Deployment
- Docker-Secret-Integration fuer `MORZ_INFOBOARD_ADMIN_PASSWORD`
- Ansible-Variable `morz_admin_password` als Vault-Variable (Phase 6)
## Voraussetzungen auf dem Entwicklungsrechner ## Voraussetzungen auf dem Entwicklungsrechner
@ -287,11 +287,15 @@ Das Playbook erledigt:
## Empfohlene naechste Implementierungsschritte ## Empfohlene naechste Implementierungsschritte
1. Backend: einheitliches Fehlerformat und Routing-Grundstruktur anlegen Die Punkte 14 der urspruenglichen Liste (Fehlerformat, Routing, Status, MQTT) sind umgesetzt.
2. Backend: Konfigurations- und App-Lifecycle stabilisieren Offene Punkte aus Phase 6 des Tenant-Feature-Plans (`docs/TENANT-FEATURE-PLAN.md`):
3. Agent und Backend: den HTTP-Statuspfad als Grundlage fuer Identitaet, Persistenz und spaetere Admin-Vorschau erweitern
4. Agent: danach MQTT-spezifische Reachability und feinere Connectivity-Schwellenlogik aufsetzen 1. Docker-Secret fuer `MORZ_INFOBOARD_ADMIN_PASSWORD` in `compose/` einrichten
5. Danach die Netzwerk-, Sync- und Kommandopfade schrittweise produktionsnah ausbauen 2. Ansible-Variable `morz_admin_password` als Vault-Variable definieren
3. Code-Review durch Larry (SQL-Injection, Session-Fixation, bcrypt-Cost, Middleware-Reihenfolge)
4. End-to-End-Test-Checkliste in `docs/TEST-CHECKLIST-DEV.md` durchlaufen
5. Deployment: Image bauen, Migration `002_auth.sql` pruefen, Logs kontrollieren
6. Langfristig: Netzwerk-, Sync- und Kommandopfade produktionsnah ausbauen
## End-to-End-Entwicklungstest (Backend + Agent) ## End-to-End-Entwicklungstest (Backend + Agent)

View file

@ -507,6 +507,122 @@ Spezialendpoint zur Auflösung von Nachrichten-Wand-Anfragen (noch in Entwicklun
--- ---
## Authentifizierung (Web-Formulare)
Alle Auth-Routen erfordern keine vorherige Authentifizierung.
### GET /login
Zeigt das Login-Formular.
- Wenn ein gueltiges `morz_session`-Cookie vorhanden ist, wird direkt zum jeweiligen Dashboard
weitergeleitet (`/admin` fuer Admins, `/tenant/{slug}/dashboard` fuer Tenant-User).
**Response:** HTML-Seite mit Benutzername/Passwort-Formular und optionaler Flash-Message.
---
### POST /login
Verarbeitet die Login-Eingabe.
**Request (Form-Encoded):**
```
username=admin&password=geheim
```
**Verhalten:**
- Passwort wird per `bcrypt.CompareHashAndPassword` geprueft
- Bei Erfolg wird ein `morz_session`-Cookie gesetzt (HttpOnly, Secure, 24h TTL)
- Weiterleitung je nach Rolle: `admin``/admin`, `tenant``/tenant/{slug}/dashboard`
- Bei Fehler: Rueckkehr zur Login-Seite mit Flash-Message
**Status:**
- `303 See Other` — Erfolg, Weiterleitung
- `303 See Other` — Fehler, Rueckkehr zur Login-Seite mit `?msg=`
---
### POST /logout
Meldet den aktuellen Benutzer ab.
**Verhalten:**
- Session wird in der DB geloescht (`DeleteSession`)
- Cookie wird mit `MaxAge=-1` geloescht
- Weiterleitung zu `/login`
**Status:**
- `303 See Other`
---
## Tenant Self-Service Dashboard (Web-Formulare)
Alle Tenant-Routen erfordern `RequireAuth` + `RequireTenantAccess`.
Admins koennen auf jeden Tenant zugreifen; Tenant-User nur auf ihren eigenen.
### GET /tenant/{tenantSlug}/dashboard
Zeigt das Tenant-Self-Service-Dashboard.
**Tabs:**
- Tab A "Meine Monitore" — Screen-Karten mit Live-Status (via JS-Fetch aus `/api/v1/screens/status`)
- Tab B "Mediathek" — Upload-Formular und Dateiliste
**Query-Parameter:**
- `tab=media` — oeffnet direkt Tab B (z. B. nach Upload-Redirect)
- `flash=uploaded` / `flash=deleted` — zeigt Erfolgs-Flash-Message
**Response:** HTML-Seite.
---
### POST /tenant/{tenantSlug}/upload
Laedt ein Medium fuer den Tenant hoch.
**Request (Multipart Form):**
```
type: image (oder video, pdf)
title: Mein Bild
file: <binary data>
```
oder fuer eine Web-URL:
```
type: web
title: Externe Website
url: http://example.com
```
**Verhalten:**
- Datei wird in `MORZ_INFOBOARD_UPLOAD_DIR` gespeichert
- MIME-Typ wird aus dem Upload-Header abgeleitet
- Max. Upload-Groesse: 512 MB
**Status:**
- `303 See Other``/tenant/{slug}/dashboard?tab=media&flash=uploaded`
- `400 Bad Request` — fehlender Typ oder Datei
- `404 Not Found` — Tenant nicht vorhanden
---
### POST /tenant/{tenantSlug}/media/{mediaId}/delete
Loescht ein Medien-Asset des Tenants.
**Verhalten:**
- Eigentuemer-Pruefung: `asset.TenantID` muss mit dem Tenant uebereinstimmen
- Physische Datei wird geloescht sofern vorhanden
**Status:**
- `303 See Other``/tenant/{slug}/dashboard?tab=media&flash=deleted`
- `403 Forbidden` — Asset gehoert nicht diesem Tenant
- `404 Not Found` — Tenant oder Asset nicht vorhanden
---
## Admin UI (Web-Formulare) ## Admin UI (Web-Formulare)
### GET /admin ### GET /admin
@ -827,6 +943,11 @@ Typische HTTP-Status:
## Änderungshistorie ## Änderungshistorie
- **2026-03-23 (Update):** Auth- und Tenant-Dashboard-Endpoints ergaenzt (Doris / Doku-Review)
- `GET /login`, `POST /login`, `POST /logout` dokumentiert
- `GET /tenant/{tenantSlug}/dashboard` dokumentiert
- `POST /tenant/{tenantSlug}/upload` dokumentiert
- `POST /tenant/{tenantSlug}/media/{mediaId}/delete` dokumentiert
- **2026-03-23:** Initiale Dokumentation aller HTTP-Endpoints basierend auf Code-Review - **2026-03-23:** Initiale Dokumentation aller HTTP-Endpoints basierend auf Code-Review
- Alle Screen-Management-Endpoints dokumentiert - Alle Screen-Management-Endpoints dokumentiert
- Alle Playlist-Management-Endpoints dokumentiert - Alle Playlist-Management-Endpoints dokumentiert

View file

@ -48,21 +48,51 @@ Zweck:
Spalten: Spalten:
```sql ```sql
id uuid primary key id text primary key default gen_random_uuid()::text
tenant_id uuid null references tenants(id) on delete set null tenant_id text not null references tenants(id) on delete cascade
username text not null unique username text not null
email text not null unique password_hash text not null
password_hash text not null role text not null default 'tenant'
role text not null created_at timestamptz not null default now()
active boolean not null default true unique(tenant_id, username)
last_login_at timestamptz null
created_at timestamptz not null
updated_at timestamptz not null
``` ```
Regeln: Regeln:
- `role` in v1: `admin`, `tenant_user` - `role` in v1: `admin`, `tenant`
- `username` ist nur innerhalb eines Tenants eindeutig (Unique-Constraint auf `(tenant_id, username)`)
- `tenant_id` ist `NOT NULL` — jeder User gehoert genau einem Tenant
- IDs sind `text`, nicht `uuid`, enthalten aber UUID-Werte (via `gen_random_uuid()::text`)
- Felder wie `email`, `active`, `last_login_at` und `updated_at` existieren in v1 nicht
### `sessions`
Zweck:
- Sitzungstokens fuer den Browser-Login
Spalten:
```sql
id text primary key default gen_random_uuid()::text
user_id text not null references users(id) on delete cascade
created_at timestamptz not null default now()
expires_at timestamptz not null default (now() + interval '8 hours')
```
Indizes:
```sql
create index idx_sessions_user_id on sessions(user_id);
create index idx_sessions_expires_at on sessions(expires_at);
```
Regeln:
- Session-TTL beim Anlegen betraegt standardmaessig 8 Stunden (Migration-Default);
`AuthStore.CreateSession` uebergibt die tatsaechliche TTL als Parameter (aktuell 24 Stunden)
- Abgelaufene Sessions werden stuendlich per Hintergrund-Ticker bereinigt (`CleanExpiredSessions`)
- Cookie-Name: `morz_session`; `HttpOnly=true`, `Secure=true` (ausser `MORZ_INFOBOARD_DEV_MODE=true`)
### `screen_groups` ### `screen_groups`
@ -494,6 +524,21 @@ last_failed_sync_at timestamptz null
last_error_message text null last_error_message text null
``` ```
## Auth-Datenbankschema
Die Auth-Tabellen werden durch `server/backend/internal/db/migrations/002_auth.sql` angelegt
und sind vollstaendig unter den Abschnitten `users` und `sessions` oben beschrieben.
Der `AuthStore` (`internal/store/auth.go`) stellt folgende Methoden bereit:
- `GetUserByUsername(ctx, username)` — Nutzer per Username laden (inkl. `TenantSlug` via LEFT JOIN)
- `CreateSession(ctx, userID, ttl)` — neue Session anlegen
- `GetSessionUser(ctx, sessionID)` — User zu gueltigem Session-Token laden
- `DeleteSession(ctx, sessionID)` — Session loeschen (Logout)
- `CleanExpiredSessions(ctx)` — abgelaufene Sessions bereinigen
- `EnsureAdminUser(ctx, tenantSlug, password)` — Admin-User beim Start anlegen wenn nicht vorhanden
- `VerifyPassword(ctx, userID, password)` — Passwort gegen bcrypt-Hash pruefen
## Wichtige Indizes ## Wichtige Indizes
Empfohlen mindestens: Empfohlen mindestens:

View file

@ -247,6 +247,96 @@ Sinnvolle Komponenten in `compose/`:
- `mosquitto` - `mosquitto`
- optional `worker` - optional `worker`
## Authentifizierung
Der Server verwendet einen Session-basierten Login-Flow mit `bcrypt`-Passwort-Hashing.
### Login-Flow
1. `GET /login` rendert das Login-Formular (Bulma-Card zentriert).
2. `POST /login` prueft Username und Passwort:
- `AuthStore.GetUserByUsername` laedt den User inkl. Tenant-Slug.
- `bcrypt.CompareHashAndPassword` prueft das Passwort (Cost-Faktor 12).
- Bei Erfolg legt `AuthStore.CreateSession` eine Session an (TTL 24 Stunden).
- Das Session-Token wird als `morz_session`-Cookie gesetzt (`HttpOnly=true`, `Secure=true`).
- Im `DevMode` (`MORZ_INFOBOARD_DEV_MODE=true`) wird `Secure=false` gesetzt fuer lokalen HTTP-Betrieb.
- Weiterleitung je nach Rolle: `admin``/admin`, `tenant``/tenant/{slug}/dashboard`.
3. `POST /logout` loescht die Session in der DB und entfernt den Cookie.
### Cookie-Lebensdauer
- Standard-TTL: 24 Stunden
- Der Cookie verfaellt automatisch; die DB wird stuendlich durch `CleanExpiredSessions` bereinigt.
### Admin-User-Bootstrap
Beim Server-Start wird `EnsureAdminUser` aufgerufen, wenn `MORZ_INFOBOARD_ADMIN_PASSWORD` gesetzt ist.
Der Admin-User wird dem Tenant mit Slug `MORZ_INFOBOARD_DEFAULT_TENANT` (Standard: `morz`) zugeordnet.
Ist der User bereits vorhanden, passiert nichts. Fehler sind nicht fatal — der Server startet trotzdem.
---
## Middleware-Kette
Alle geschuetzten Routen werden durch Middleware-Funktionen in `internal/httpapi/middleware.go` abgesichert.
```
Eingehende Anfrage
RequireAuth Liest morz_session-Cookie, prueft Session via DB,
speichert *store.User im Request-Context.
→ Fehler: Redirect zu /login?next=<Pfad>
├─► RequireAdmin Prueft user.Role == "admin"
│ → Fehler: 403 Forbidden
└─► RequireTenant Prueft user.TenantSlug == {tenantSlug} aus dem URL-Pfad.
Access Admins duerfen immer durch.
→ Fehler: 403 Forbidden
```
### Route-Gruppen im Router
| Gruppe | Middleware | Beispielrouten |
|----------------|------------------------------------|---------------------------------------------|
| Oeffentlich | keine | `/healthz`, `/login`, `/api/v1/screens/register` |
| Auth-only | RequireAuth | `/manage/{screenSlug}/...` |
| Admin-only | RequireAuth + RequireAdmin | `/admin`, `/admin/screens/...` |
| Tenant-scoped | RequireAuth + RequireTenantAccess | `/tenant/{tenantSlug}/...`, `/api/v1/tenants/{tenantSlug}/...` |
Der Hilfsfunktion `chain(middlewares...)` in `router.go` wrappet Handler von aussen nach innen.
---
## Tenant-Dashboard
Das Tenant-Self-Service-Dashboard ist unter `/tenant/{tenantSlug}/dashboard` erreichbar.
### URL-Schema
| Methode | Pfad | Beschreibung |
|---------|---------------------------------------------|---------------------------|
| GET | `/tenant/{tenantSlug}/dashboard` | Dashboard rendern |
| POST | `/tenant/{tenantSlug}/upload` | Medium hochladen |
| POST | `/tenant/{tenantSlug}/media/{mediaId}/delete` | Medium loeschen |
### Tabs
- **Tab A Meine Monitore:** Zeigt Screen-Karten mit Live-Status. Der Status wird per JavaScript
aus `GET /api/v1/screens/status` geladen und alle 30 Sekunden aktualisiert.
Status-Badge: `is-success` (online), `is-danger` (offline), `is-warning` (unbekannt).
- **Tab B Mediathek:** Upload-Formular (Bild, Video, PDF oder Web-URL) und Dateiliste
mit Loeschen-Button. Nach Upload oder Loeschen Redirect mit `?tab=media&flash=uploaded/deleted`.
### Eigentuemer-Pruefung beim Loeschen
`HandleTenantDeleteMedia` prueft, dass `asset.TenantID == tenant.ID`, bevor es loescht.
Damit ist sichergestellt, dass ein Tenant keine Assets anderer Tenants loeschen kann,
selbst wenn er die `mediaId` erraten wuerde.
---
## Sicherheitsgrundsaetze ## Sicherheitsgrundsaetze
- Root-Bootstrap-Geheimnisse nur kurzlebig oder referenziert speichern - Root-Bootstrap-Geheimnisse nur kurzlebig oder referenziert speichern

View file

@ -1,26 +1,109 @@
# Backend # Backend
Dieses Verzeichnis enthaelt das erste Geruest fuer das zentrale Backend. Dieses Verzeichnis enthaelt das zentrale Go-Backend fuer das Info-Board-System.
Ziel fuer die erste Ausbaustufe: ## Aufgaben
- HTTP-API in Go - HTTP-API und serverseitige HTML-UI (Bulma)
- Health-Endpunkt - PostgreSQL-Anbindung mit automatischen Migrationen
- saubere Projektstruktur fuer spaetere API-, Worker- und Datenbankmodule - Session-basierte Authentifizierung und rollenbasierte Zugriffskontrolle
- erste serverseitige Aufloesungslogik fuer `message_wall` - Medienverwaltung und Playlist-Management
- Player-Status-Ingest und Diagnose
- MQTT-Notifizierungen bei Playlist-Aenderungen
Geplante Unterstruktur: ## Unterstruktur
- `cmd/api/` fuer den API-Startpunkt - `cmd/api/` — Startpunkt des Backends
- `internal/app/` fuer App-Initialisierung - `internal/app/` — App-Initialisierung und Lifecycle
- `internal/campaigns/` fuer Kampagnen- und Template-Logik - `internal/config/` — Konfiguration via Umgebungsvariablen
- `internal/httpapi/` fuer HTTP-Routing und Handler - `internal/db/` — PostgreSQL-Anbindung und Migrations-Runner
- `internal/config/` fuer Konfiguration - `internal/store/` — Datenbankzugriff (TenantStore, ScreenStore, MediaStore, PlaylistStore, AuthStore)
- `internal/httpapi/` — HTTP-Routing, Middleware und Handler
- `internal/httpapi/manage/` — Admin-UI und Playlist-Management-UI
- `internal/httpapi/tenant/` — Tenant-Self-Service-Dashboard
- `internal/mqttnotifier/` — MQTT-Notifizierungen
- `internal/reqcontext/` — Context-Keys fuer authentifizierten User
Aktuell vorhanden: ## Aktuelle Endpunkte
- `GET /healthz` ### Oeffentlich (keine Auth)
- `GET /api/v1`
- `GET /api/v1/meta` | Methode | Pfad | Beschreibung |
- `POST /api/v1/tools/message-wall/resolve` als erste serverseitige Layout-Aufloesung fuer `message_wall` |---------|-------------------------------------|---------------------------------------|
- einheitliches API-Fehlerformat im HTTP-Layer | GET | `/healthz` | Health-Check |
| GET | `/api/v1` | API-Entrypoint |
| GET | `/api/v1/meta` | Metainformationen |
| POST | `/api/v1/player/status` | Status-Ingest vom Player-Agent |
| GET | `/api/v1/screens/status` | Uebersicht aller Screen-Status |
| GET | `/api/v1/screens/{screenId}/status` | Einzelner Screen-Status |
| DELETE | `/api/v1/screens/{screenId}/status` | Screen-Status loeschen |
| GET | `/api/v1/screens/{screenId}/playlist` | Playlist fuer Player (kein Auth) |
| POST | `/api/v1/screens/register` | Agent-Selbstregistrierung |
| POST | `/api/v1/tools/message-wall/resolve`| Message-Wall-Aufloesungsendpunkt |
| GET | `/status` | HTML-Diagnoseseite |
| GET | `/status/{screenId}` | HTML-Detailseite Einzelscreen |
| GET | `/uploads/{filename}` | Hochgeladene Dateien abrufen |
| GET | `/static/bulma.min.css` | Statisches CSS |
| GET | `/static/Sortable.min.js` | Statisches JS |
| GET | `/login` | Login-Formular |
| POST | `/login` | Login verarbeiten |
| POST | `/logout` | Session beenden |
### Nur eingeloggte Benutzer (`RequireAuth`)
| Methode | Pfad | Beschreibung |
|---------|-------------------------------------------|---------------------------------------|
| GET | `/manage/{screenSlug}` | Playlist-Management-UI |
| POST | `/manage/{screenSlug}/upload` | Medium fuer Screen hochladen |
| POST | `/manage/{screenSlug}/items` | Item zur Playlist hinzufuegen |
| POST | `/manage/{screenSlug}/items/{itemId}` | Item aktualisieren |
| POST | `/manage/{screenSlug}/items/{itemId}/delete` | Item loeschen |
| POST | `/manage/{screenSlug}/reorder` | Items reordnen |
| POST | `/manage/{screenSlug}/media/{mediaId}/delete` | Medium loeschen |
| GET | `/api/v1/playlists/{screenId}` | Playlist mit Metadaten abrufen |
| POST | `/api/v1/playlists/{playlistId}/items` | Item zur Playlist hinzufuegen (API) |
| PATCH | `/api/v1/items/{itemId}` | Item aktualisieren (API) |
| DELETE | `/api/v1/items/{itemId}` | Item loeschen (API) |
| PUT | `/api/v1/playlists/{playlistId}/order` | Items reordnen (API) |
| PATCH | `/api/v1/playlists/{playlistId}/duration` | Standard-Dauer setzen (API) |
| DELETE | `/api/v1/media/{id}` | Medium loeschen (API) |
### Nur Admins (`RequireAuth` + `RequireAdmin`)
| Methode | Pfad | Beschreibung |
|---------|-------------------------------------|---------------------------------------|
| GET | `/admin` | Admin-Uebersicht |
| POST | `/admin/screens/provision` | Provisionierungs-Job starten |
| POST | `/admin/screens` | Neuen Screen anlegen |
| POST | `/admin/screens/{screenId}/delete` | Screen loeschen |
### Tenant-scoped (`RequireAuth` + `RequireTenantAccess`)
| Methode | Pfad | Beschreibung |
|---------|---------------------------------------------------|---------------------------------|
| GET | `/tenant/{tenantSlug}/dashboard` | Tenant-Self-Service-Dashboard |
| POST | `/tenant/{tenantSlug}/upload` | Medium hochladen |
| POST | `/tenant/{tenantSlug}/media/{mediaId}/delete` | Medium loeschen |
| GET | `/api/v1/tenants/{tenantSlug}/screens` | Screens eines Tenants auflisten |
| POST | `/api/v1/tenants/{tenantSlug}/screens` | Screen anlegen |
| GET | `/api/v1/tenants/{tenantSlug}/media` | Medien eines Tenants auflisten |
| POST | `/api/v1/tenants/{tenantSlug}/media` | Medium hochladen (API) |
## Konfiguration
Alle Werte per Umgebungsvariable:
| Variable | Bedeutung | Standard |
|-----------------------------------|----------------------------------------------------------|---------------|
| `MORZ_INFOBOARD_HTTP_ADDR` | HTTP-Listen-Adresse | `:8080` |
| `DATABASE_URL` | PostgreSQL-Connection-String | — |
| `MORZ_INFOBOARD_UPLOAD_DIR` | Verzeichnis fuer hochgeladene Medien | `/tmp/morz-uploads` |
| `MORZ_INFOBOARD_STATUS_STORE_PATH`| Pfad zur JSON-Persistenz-Datei fuer Status-Store | leer (in-memory) |
| `MORZ_INFOBOARD_ADMIN_PASSWORD` | Passwort des initialen Admin-Users (leer = kein Anlegen) | leer |
| `MORZ_INFOBOARD_DEFAULT_TENANT` | Slug des Tenants, dem der Admin zugeordnet wird | `morz` |
| `MORZ_INFOBOARD_DEV_MODE` | `true` = Session-Cookie ohne Secure-Flag (nur lokal) | `false` |
| `MORZ_INFOBOARD_MQTT_BROKER` | MQTT-Broker-URL (leer = kein MQTT) | leer |
| `MORZ_INFOBOARD_MQTT_USERNAME` | MQTT-Benutzername | leer |
| `MORZ_INFOBOARD_MQTT_PASSWORD` | MQTT-Passwort | leer |
Detailliertere Beschreibung und lokale Startbeispiele: `DEVELOPMENT.md`.