diff --git a/docs/API-ENDPOINTS.md b/docs/API-ENDPOINTS.md index dce74ab..ec4017b 100644 --- a/docs/API-ENDPOINTS.md +++ b/docs/API-ENDPOINTS.md @@ -6,6 +6,7 @@ Die Backend-API unterteilt sich in mehrere Bereiche: - **Health & Meta**: System-Status und API-Informationen - **Player Status**: Status-Ingest und Diagnose vom Player +- **Screenshot API**: On-Demand- und periodische Screenshots vom Player - **Screen Management**: CRUD und Registrierung von Screens - **Playlists**: Abruf und Verwaltung von Wiedergabelisten - **Media**: Upload und Verwaltung von Medien-Assets @@ -519,6 +520,19 @@ Spezialendpoint zur Auflösung von Nachrichten-Wand-Anfragen (noch in Entwicklun Alle Auth-Routen erfordern keine vorherige Authentifizierung. +### GET / + +Root-Redirect auf `/login`. + +- Anfragen auf exakt `/` werden per `303 See Other` zu `/login` weitergeleitet. +- Anfragen auf unbekannte Pfade (z. B. `/irgendwas`) geben `404 Not Found` zurück (404-Guard). + +**Status:** +- `303 See Other` — Weiterleitung zu `/login` +- `404 Not Found` — Pfad existiert nicht + +--- + ### GET /login Zeigt das Login-Formular. @@ -536,9 +550,13 @@ Verarbeitet die Login-Eingabe. **Request (Form-Encoded):** ``` -username=admin&password=geheim +username=admin&password=geheim&csrf_token= ``` +Das CSRF-Token muss als verstecktes Formularfeld `csrf_token` mitgesendet werden. +Der Token wird beim `GET /login` als Cookie `morz_csrf` gesetzt und in den Template-Daten +als `{{.CSRFToken}}` bereitgestellt. + **Verhalten:** - Passwort wird per `bcrypt.CompareHashAndPassword` geprueft - Bei Erfolg wird ein `morz_session`-Cookie gesetzt (HttpOnly, Secure, 24h TTL) @@ -555,6 +573,15 @@ username=admin&password=geheim Meldet den aktuellen Benutzer ab. +**Request (Form-Encoded):** +``` +csrf_token= +``` + +Das CSRF-Token muss als verstecktes Formularfeld `csrf_token` mitgesendet werden. +Der aktuelle Token-Wert wird beim GET-Aufruf der aufrufenden Seite als `{{.CSRFToken}}` +in die Template-Daten eingebettet und als Cookie `morz_csrf` gesetzt. + **Verhalten:** - Session wird in der DB geloescht (`DeleteSession`) - Cookie wird mit `MaxAge=-1` geloescht @@ -562,6 +589,7 @@ Meldet den aktuellen Benutzer ab. **Status:** - `303 See Other` +- `403 Forbidden` — CSRF-Token fehlt oder ungueltig --- @@ -787,6 +815,21 @@ Rückleitung zur Admin-Seite oder zum Screen-Detail. ## Playlist Management UI (Web-Formulare) +### GET /manage + +Übersichtsseite für eingeloggte Benutzer. + +**Auth:** `RequireAuth`. + +**Verhalten:** +- Admins und Tenant-User werden direkt zu ihrer Standard-Ansicht weitergeleitet. +- Screen-User mit genau einem zugeordneten Screen werden direkt zu `GET /manage/{screenSlug}` weitergeleitet. +- Screen-User mit mehreren zugeordneten Screens erhalten eine Übersichtsseite mit Links zu den einzelnen Screens. + +**Response:** HTML-Seite oder Redirect (303 See Other). + +--- + ### GET /manage/{screenSlug} Verwaltungs-UI für die Playlist eines Screens. @@ -1032,19 +1075,58 @@ Typische HTTP-Status: --- -## In Vorbereitung (Phase 6 / künftig) +## Screenshot API -Die folgenden Endpoints sind derzeit vorbereitet, aber noch nicht vollständig implementiert: +### POST /api/v1/player/screenshot -- `POST /api/v1/player/screenshot` — Upload von Player-Screenshots an den Backend-Server - - Wird vom Agent unter `player/agent/internal/screenshot/screenshot.go` mit dem Intervall `MORZ_INFOBOARD_SCREENSHOT_EVERY` aufgerufen - - Multipart-Request mit `screen_id`, `screenshot` (Datei), `mime_type` - - Benötigt Backend-Handler für Persistierung und/oder Verarbeitung +Vom Player-Agent aufgerufener Endpoint zum Hochladen eines Screenshots. + +**Auth:** Keine. + +**Request:** `multipart/form-data`, max. 3 MB. + +| Feld | Typ | Pflicht | Beschreibung | +|-------------|--------|---------|------------------------------------------------------| +| `screen_id` | string | ja | Interne Screen-ID (entspricht dem Slug des Players) | +| `screenshot`| Datei | ja | Screenshot-Datei (JPEG oder PNG) | + +Der MIME-Typ wird aus dem `Content-Type`-Header des Datei-Parts übernommen. Fehlt er, wird `image/png` angenommen. + +Der Screenshot wird im In-Memory-`ScreenshotStore` gespeichert (nicht persistiert, kein Filesystem-Zugriff). + +**Response:** +- `200 OK` — Screenshot gespeichert (kein Body) +- `400 Bad Request` — `screen_id` fehlt, `screenshot`-Feld fehlt, oder Multipart-Parsing fehlgeschlagen +- `500 Internal Server Error` — Lesefehler + +--- + +### GET /api/v1/screens/{screenId}/screenshot + +Ruft den zuletzt hochgeladenen Screenshot eines Screens ab. + +**Auth:** `RequireAuth` (eingeloggter Benutzer). + +**Path-Parameter:** +- `screenId` — Screen-ID (wie beim Upload übergeben) + +**Response:** +- `200 OK` — Raw-Image-Daten mit korrektem `Content-Type` (z. B. `image/jpeg`), `Cache-Control: no-store` +- `404 Not Found` — kein Screenshot für diese Screen-ID vorhanden --- ## Änderungshistorie +- **2026-03-24 (Update):** Screenshot-Endpoints implementiert und dokumentiert (Doris / Doku-Review) + - `POST /api/v1/player/screenshot` — war als "In Vorbereitung" markiert, ist jetzt vollständig implementiert; Abschnitt komplett neu verfasst + - `GET /api/v1/screens/{screenId}/screenshot` — neuer Endpoint, `authOnly`, liefert Raw-Image aus In-Memory-Store + - `GET /manage` — neue Übersichtsseite für `screen_user` mit mehreren Screens, `authOnly` +- **2026-03-24 (Update):** CSRF-Pflichtfelder in POST /login und POST /logout dokumentiert (Doris / Doku-Review) + - `POST /login` und `POST /logout` erfordern `csrf_token` als Hidden-Field (Double-Submit-Cookie-Pattern) + - Hinweis auf `morz_csrf`-Cookie und `{{.CSRFToken}}`-Template-Variable ergaenzt +- **2026-03-24 (Update):** Root-Redirect dokumentiert (Doris / Doku-Review) + - `GET /` — Redirect 303 auf `/login`, 404-Guard für unbekannte Pfade - **2026-03-23 (Update):** Screen-User Management Endpoints (Doris / Doku-Review) - `POST /admin/users` — Screen-User anlegen - `POST /admin/users/{userID}/delete` — Screen-User löschen diff --git a/server/backend/README.md b/server/backend/README.md index 71f3927..3262d0a 100644 --- a/server/backend/README.md +++ b/server/backend/README.md @@ -23,10 +23,13 @@ Dieses Verzeichnis enthaelt das zentrale Go-Backend fuer das Info-Board-System. - `internal/httpapi/csrf.go` — Double-Submit-Cookie CSRF-Schutz - `internal/httpapi/ratelimit.go` — Rate-Limiting fuer /login (Brute-Force-Schutz) - `internal/httpapi/uploads.go` — Upload-Handler konsolidiert +- `internal/httpapi/screenshot.go` — Handler fuer Player-Screenshot-Upload und Screenshot-Abruf +- `internal/httpapi/screenshot_store.go` — In-Memory-Store fuer Screenshots (`ScreenshotStore`, thread-safe via `sync.RWMutex`) - `internal/httpapi/manage/` — Admin-UI und Playlist-Management-UI -- `internal/httpapi/manage/csrf_helpers.go` — CSRF-Token Helpers fuer Templates +- `internal/httpapi/manage/csrf_helpers.go` — CSRF-Token Helpers fuer Templates (manage-Package) - `internal/httpapi/tenant/` — Tenant-Self-Service-Dashboard -- `internal/mqttnotifier/` — MQTT-Notifizierungen +- `internal/httpapi/tenant/csrf_helpers.go` — CSRF-Token Helpers fuer Templates (tenant-Package, Import-Cycle-Isolation) +- `internal/mqttnotifier/` — MQTT-Notifizierungen (`NotifyChanged`, `RequestScreenshot`) - `internal/reqcontext/` — Context-Keys fuer authentifizierten User ## Datenbank-Stores @@ -66,6 +69,7 @@ Dieses Verzeichnis enthaelt das zentrale Go-Backend fuer das Info-Board-System. | GET | `/api/v1` | API-Entrypoint | | GET | `/api/v1/meta` | Metainformationen | | POST | `/api/v1/player/status` | Status-Ingest vom Player-Agent | +| POST | `/api/v1/player/screenshot` | Screenshot-Upload 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 | @@ -85,6 +89,7 @@ Dieses Verzeichnis enthaelt das zentrale Go-Backend fuer das Info-Board-System. | Methode | Pfad | Beschreibung | |---------|-------------------------------------------|---------------------------------------| +| GET | `/manage` | Screen-Uebersicht fuer screen_user | | GET | `/manage/{screenSlug}` | Playlist-Management-UI | | POST | `/manage/{screenSlug}/upload` | Medium fuer Screen hochladen | | POST | `/manage/{screenSlug}/items` | Item zur Playlist hinzufuegen | @@ -99,6 +104,7 @@ Dieses Verzeichnis enthaelt das zentrale Go-Backend fuer das Info-Board-System. | 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) | +| GET | `/api/v1/screens/{screenId}/screenshot` | Screenshot eines Screens abrufen | ### Nur Admins (`RequireAuth` + `RequireAdmin`) diff --git a/server/backend/internal/httpapi/manage/auth.go b/server/backend/internal/httpapi/manage/auth.go index a42ffd8..f345ba7 100644 --- a/server/backend/internal/httpapi/manage/auth.go +++ b/server/backend/internal/httpapi/manage/auth.go @@ -21,7 +21,11 @@ func handleScreenUserRedirect(w http.ResponseWriter, r *http.Request, screenStor http.Redirect(w, r, "/login?error=no_screens", http.StatusSeeOther) return } - http.Redirect(w, r, "/manage/"+screens[0].Slug, http.StatusSeeOther) + if len(screens) == 1 { + http.Redirect(w, r, "/manage/"+screens[0].Slug, http.StatusSeeOther) + return + } + http.Redirect(w, r, "/manage", http.StatusSeeOther) } const sessionTTL = 8 * time.Hour @@ -64,7 +68,19 @@ func HandleLoginUI(authStore *store.AuthStore, screenStore *store.ScreenStore, c csrfToken := setCSRFCookie(w, r, cfg.DevMode) next := r.URL.Query().Get("next") - data := loginData{Next: sanitizeNext(next), CSRFToken: csrfToken} + + // K1: ?error= Parameter auswerten und in lesbare Fehlermeldung übersetzen. + var errorMsg string + switch r.URL.Query().Get("error") { + case "no_screens": + errorMsg = "Ihr Konto hat noch keinen Bildschirm zugewiesen. Bitte wenden Sie sich an den Administrator." + case "": + // kein Fehler + default: + errorMsg = "Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut." + } + + data := loginData{Error: errorMsg, Next: sanitizeNext(next), CSRFToken: csrfToken} w.Header().Set("Content-Type", "text/html; charset=utf-8") _ = tmpl.Execute(w, data) } diff --git a/server/backend/internal/httpapi/manage/templates.go b/server/backend/internal/httpapi/manage/templates.go index 2d460a5..f694bc3 100644 --- a/server/backend/internal/httpapi/manage/templates.go +++ b/server/backend/internal/httpapi/manage/templates.go @@ -1,10 +1,11 @@ package manage const loginTmpl = ` - + + Anmelden – morz infoboard @@ -254,7 +286,8 @@ const adminTmpl = ` @@ -271,7 +304,7 @@ const adminTmpl = `