# Backend

Dieses Verzeichnis enthaelt das zentrale Go-Backend fuer das Info-Board-System.

## Aufgaben

- HTTP-API und serverseitige HTML-UI (Bulma)
- PostgreSQL-Anbindung mit automatischen Migrationen
- Session-basierte Authentifizierung und rollenbasierte Zugriffskontrolle
- Medienverwaltung und Playlist-Management
- Player-Status-Ingest und Diagnose
- MQTT-Notifizierungen bei Playlist-Aenderungen

## Benutzerrollen

- `admin_user` — Volller Zugriff auf alle Funktionen
- `screen_user` — Darf Medien hochladen und Playlist bearbeiten. Display-Steuerung (An/Aus, Zeitplan, Override)
- `restricted` — Darf Medien hochladen und Playlist bearbeiten. Keine Display-Steuerung.
- `tenant_user` — Tenant-Operator für Self-Service-Dashboard

## Unterstruktur

- `cmd/api/` — Startpunkt des Backends
- `internal/app/` — App-Initialisierung und Lifecycle
- `internal/config/` — Konfiguration via Umgebungsvariablen
- `internal/db/` — PostgreSQL-Anbindung und Migrations-Runner
- `internal/store/` — Datenbankzugriff (TenantStore, ScreenStore, MediaStore, PlaylistStore, AuthStore, ScreenScheduleStore, GlobalOverrideStore)
- `internal/fileutil/` — Upload-Hilfsfunktionen (SaveUploadedFile mit Tenant-Isolation)
- `internal/httpapi/` — HTTP-Routing, Middleware und Handler
- `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 (manage-Package)
- `internal/httpapi/tenant/` — Tenant-Self-Service-Dashboard
- `internal/httpapi/tenant/csrf_helpers.go` — CSRF-Token Helpers fuer Templates (tenant-Package, Import-Cycle-Isolation)
- `internal/mqttnotifier/` — MQTT-Notifizierungen (`NotifyChanged`, `RequestScreenshot`, `SendDisplayCommand`)
- `internal/scheduler/` — Display-Zeitplan-Scheduler (prueft jede Minute aktive Zeitplaene und sendet MQTT-Befehle)
- `internal/reqcontext/` — Context-Keys fuer authentifizierten User

### scheduler (`internal/scheduler/scheduler.go`)

Startet eine Goroutine (`scheduler.Run`), die jede Minute alle aktiven Zeitplaene aus
`screen_schedules` laedt und — sofern `power_on_time` oder `power_off_time` mit der aktuellen
Uhrzeit übereinstimmt — per MQTT den Befehl `display_on` bzw. `display_off` sendet.

Der Scheduler wird in `internal/app/app.go` als Goroutine gestartet und laeuft bis zum
Kontext-Abbruch beim Server-Shutdown.

**Wochenend-Sperre:** An Samstagen und Sonntagen werden Zeitplaene ignoriert — der Reconciler
sendet dann keine automatischen Ein-/Ausschalt-Kommandos. Manuelle Overrides (global oder
per-Screen) wirken jedoch auch am Wochenende.

## Datenbank-Stores

### AuthStore (`internal/store/auth.go`)

**Screen-User Management:**
- `CreateScreenUser(ctx, tenantID, username, passwordHash)` — neuen Screen-User anlegen
- `ListScreenUsers(ctx, tenantID)` — alle Screen-User eines Tenants auflisten
- `DeleteUser(ctx, userID)` — User und alle zugeordneten Permissions loeschen

**Authentifizierung:**
- `GetUserByUsername(ctx, username)` — Nutzer per Username laden
- `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
- `VerifyPassword(ctx, userID, password)` — Passwort gegen bcrypt-Hash pruefen

### GlobalOverrideStore (`internal/store/store.go`)

Verwaltet einen systemweiten Display-Override (max. 1 Zeile in `global_override`):

- `Get(ctx)` — aktuellen globalen Override laden (nil wenn keiner gesetzt)
- `Upsert(ctx, type, until)` — Override setzen oder ueberschreiben (`type`: `"on"` | `"off"`)
- `Delete(ctx)` — Override entfernen

Der Reconciler im Scheduler wertet den globalen Override aus und wendet ihn auf alle Screens an.

### ScreenStore (`internal/store/screen.go`)

**Screen-User Zugriffskontrolle:**
- `GetAccessibleScreens(ctx, userID)` — alle Screens, auf die der User Zugriff hat
- `HasUserScreenAccess(ctx, userID, screenID)` — prueft ob User auf Screen zugreifen darf (boolean)
- `AddUserToScreen(ctx, userID, screenID)` — User zu Screen hinzufuegen (Eintrag in `user_screen_permissions`)
- `RemoveUserFromScreen(ctx, userID, screenID)` — User von Screen entfernen
- `GetScreenUsers(ctx, screenID)` — alle User, die auf Screen Zugriff haben

## Aktuelle Endpunkte

### Oeffentlich (keine Auth)

| Methode | Pfad                                | Beschreibung                          |
|---------|-------------------------------------|---------------------------------------|
| GET     | `/healthz`                          | Health-Check                          |
| 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                |
| 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`                                 | 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         |
| 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)                 |
| GET     | `/api/v1/screens/{screenId}/screenshot`   | Screenshot eines Screens abrufen      |
| POST    | `/api/v1/screens/{screenSlug}/display`    | Display ein-/ausschalten (MQTT)       |
| POST    | `/api/v1/screens/{screenSlug}/schedule`   | Display-Zeitplan speichern            |
| GET     | `/api/v1/global-override`                 | Globalen Override abrufen (204 = kein aktiver Override) |
| POST    | `/api/v1/global-override`                 | Globalen Override setzen (type + until); sendet sofort MQTT |
| DELETE  | `/api/v1/global-override`                 | Globalen Override loeschen            |
| POST    | `/api/v1/screens/{screenSlug}/override`   | Per-Screen-Override setzen oder loeschen (on_until: null = loeschen) |

### 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                       |
| POST    | `/admin/users`                            | Screen-User anlegen                   |
| POST    | `/admin/users/{userID}/delete`            | Screen-User loeschen                  |
| POST    | `/admin/screens/{screenID}/users`         | User zu Screen hinzufuegen            |
| POST    | `/admin/screens/{screenID}/users/{userID}/remove` | User von Screen entfernen      |

### 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`       |
| `MORZ_INFOBOARD_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_REGISTER_SECRET`  | Pre-Shared-Secret fuer POST /api/v1/screens/register      | leer          |
| `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`.

## Middleware

### `RequireScreenAccess`

Middleware zur rollenbasierten Zugriffskontrolle auf Screen-Ressourcen.

**Verhalten:**
- Admins duerfen auf alle Screens zugreifen
- Screen-User duerfen nur auf Screens zugreifen, fuer die sie in `user_screen_permissions` eingetragen sind
- Tenant-User duerfen auf alle Screens ihres Tenants zugreifen
- Response: `403 Forbidden` wenn keine Berechtigung

**Verwendung:**
- `GET /api/v1/screens/{screenId}/playlist`
- `POST /manage/{screenSlug}/...`
- Alle privaten Screen-Endpunkte

## Migrationen

- `001_initial.sql` — initiales Schema (Tenants, Screens, Playlists, Media, etc.)
- `002_auth.sql` — Auth-Tabellen (`users`, `sessions`)
- `003_user_screen_permissions.sql` — Screen-User Management (`user_screen_permissions`)
- `004_screen_status.sql` — Display-Zustand pro Screen (`screen_status`: screen_id, display_state, reported_at)
- `005_screen_schedules.sql` — Zeitplan pro Screen (`screen_schedules`: screen_id, schedule_enabled, power_on_time, power_off_time)
- `006_override.sql` — Spalte `override_on_until` in `screen_schedules` (per-Screen-Override) und Tabelle `global_override` (systemweiter Display-Override)
