# Tenant-/Firmen-Dashboard – Implementierungs-Checkliste

Stand: 2026-03-23
Verantwortliche: Backy (Auth-Backend), Fred (Tenant-UI), Larry (Code-Review)

Diese Datei ist die einzige verbindliche Quelle fuer die Umsetzung des Tenant-Dashboards.
Alle Schritte sind als Checkboxen formuliert und enthalten Dateipfade und Funktionsnamen,
sodass kein weiterer Kontext benoetigt wird.

---

## Phase 1 – Auth-Fundament

Ziel: Datenbank-Tabellen fuer User/Sessions anlegen, AuthStore mit allen CRUD-Methoden
implementieren, Config erweitern und Admin-User beim Start anlegen.

- [x] **DB-Migration anlegen** – neue Datei
  `server/backend/internal/db/migrations/002_auth.sql` erstellen mit zwei Tabellen:
  `users` (id UUID PK, tenant_id UUID FK tenants.id, username TEXT UNIQUE NOT NULL,
  password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'tenant', created_at TIMESTAMPTZ)
  und `sessions` (id UUID PK DEFAULT gen_random_uuid(), user_id UUID FK users.id ON DELETE CASCADE,
  created_at TIMESTAMPTZ DEFAULT now(), expires_at TIMESTAMPTZ NOT NULL); Index auf
  `sessions(user_id)` und `sessions(expires_at)` anlegen.

- [x] **AuthStore-Datei anlegen** – neue Datei
  `server/backend/internal/store/auth.go` mit Typ `AuthStore struct{ pool *pgxpool.Pool }`
  und Konstruktor `NewAuthStore(pool) *AuthStore`; Paket `store`, gleiche Import-Konventionen
  wie `store.go`.

- [x] **AuthStore.GetUserByUsername implementieren** – Methode
  `(s *AuthStore) GetUserByUsername(ctx, username string) (*User, error)`;
  neuer Domain-Typ `User` (id, tenant_id, username, password_hash, role, created_at) in
  `store/auth.go` definieren; Query: `SELECT ... FROM users WHERE username=$1`.

- [x] **AuthStore Session-Methoden implementieren** –
  `CreateSession(ctx, userID string, ttl time.Duration) (*Session, error)`,
  `GetSessionUser(ctx, sessionID string) (*User, error)` (JOIN users ON users.id = sessions.user_id
  WHERE sessions.id=$1 AND expires_at > now()),
  `DeleteSession(ctx, sessionID string) error`,
  `CleanExpiredSessions(ctx) error` (DELETE FROM sessions WHERE expires_at <= now()).

- [x] **AuthStore.EnsureAdminUser implementieren** – Methode
  `(s *AuthStore) EnsureAdminUser(ctx, tenantSlug, password string) error`;
  prueft ob `username='admin'` existiert, legt ihn mit `bcrypt.GenerateFromPassword` an falls nicht;
  haengt den User an den Tenant mit `tenantSlug` (via Subquery auf `tenants.slug`).

- [x] **Config erweitern** – in `server/backend/internal/config/config.go` drei neue Felder
  ergaenzen: `AdminPassword string` (Env: `MORZ_INFOBOARD_ADMIN_PASSWORD`, kein Fallback –
  leer bedeutet: kein EnsureAdminUser-Lauf),
  `DefaultTenantSlug string` (Env: `MORZ_INFOBOARD_DEFAULT_TENANT`, Fallback `"morz"`),
  `DevMode bool` (Env: `MORZ_INFOBOARD_DEV_MODE`, Fallback `false`; wenn true: Session-Cookie
  wird auch ohne HTTPS gesetzt).

- [x] **main.go / app.go: AuthStore verdrahten** – in `server/backend/internal/app/app.go`
  `store.NewAuthStore(pool.Pool)` instanziieren und an `httpapi.RouterDeps` uebergeben
  (neues Feld `AuthStore *store.AuthStore`).

- [x] **EnsureAdminUser beim Start aufrufen** – in `app.New()` nach der Store-Initialisierung:
  wenn `cfg.AdminPassword != ""`, dann `authStore.EnsureAdminUser(ctx, cfg.DefaultTenantSlug,
  cfg.AdminPassword)` aufrufen; Fehler loggen, aber nicht fatal (Server soll starten).

- [x] **Migrations-Runner pruefen** – sicherstellen, dass `db.Connect` in
  `server/backend/internal/db/db.go` alle Dateien aus `migrations/` in alphabetischer
  Reihenfolge ausfuehrt und `002_auth.sql` damit automatisch eingespielt wird; ggf. Logzeile
  hinzufuegen.

- [x] **Doku** – Abschnitt "Auth-Datenbankschema" in `docs/SCHEMA.md` ergaenzen:
  Tabellen `users` und `sessions` mit allen Spalten und Constraints beschreiben.

---

## Phase 2 – Login-Flow

Ziel: Login-Seite rendern, POST-Handler mit bcrypt-Pruefung, Session-Cookie setzen,
Logout implementieren, alle Routen eintragen.

- [x] **Login-Template definieren** – in `server/backend/internal/httpapi/manage/templates.go`
  neue Template-Konstante `loginTmpl` anlegen; Bulma-Card zentriert auf der Seite
  (`columns is-centered`, `column is-narrow`), Formular mit Feldern `username` und `password`
  (type=password), Submit-Button `Anmelden`, Flash-Message-Bereich fuer Fehlermeldungen
  (gleiche Klasse wie bestehende Flash-Messages in `manageTmpl`).

- [x] **HandleLoginUI implementieren** – in `server/backend/internal/httpapi/manage/auth.go`
  (neue Datei) Funktion `HandleLoginUI() http.HandlerFunc`; GET-Handler, rendert `loginTmpl`;
  wenn Session-Cookie bereits gueltig ist, redirect zu `/tenant/{slug}/dashboard`.

- [x] **HandleLoginPost implementieren** – in `auth.go` Funktion
  `HandleLoginPost(authStore *store.AuthStore, cfg config.Config) http.HandlerFunc`;
  liest `username`/`password` aus Formular, ruft `authStore.GetUserByUsername` auf,
  prueft Passwort mit `bcrypt.CompareHashAndPassword`, erstellt Session mit
  `authStore.CreateSession(..., 24*time.Hour)`, setzt `HttpOnly`-Cookie `morz_session`
  (Secure=true ausser bei DevMode), redirectet je nach Rolle:
  `admin` zu `/admin`, `tenant` zu `/tenant/{tenantSlug}/dashboard`.

- [x] **HandleLogoutPost implementieren** – in `auth.go` Funktion
  `HandleLogoutPost(authStore *store.AuthStore) http.HandlerFunc`;
  liest Cookie `morz_session`, ruft `authStore.DeleteSession` auf,
  setzt Cookie mit MaxAge=-1 (loeschen), redirectet zu `/login`.

- [x] **Routen eintragen** – in `server/backend/internal/httpapi/router.go` in
  `registerManageRoutes` hinzufuegen:
  `mux.HandleFunc("GET /login", auth.HandleLoginUI())`,
  `mux.HandleFunc("POST /login", auth.HandleLoginPost(d.AuthStore, cfg))`,
  `mux.HandleFunc("POST /logout", auth.HandleLogoutPost(d.AuthStore))`;
  Import `httpapi/manage/auth` ergaenzen (oder Package zusammenfassen – Entscheidung vor Umsetzung
  treffen).

- [x] **Manuell testen** – Server starten, `GET /login` aufrufen, Login mit
  Admin-Credentials pruefen, Cookie in Devtools sichtbar, Logout funktioniert,
  Fehler bei falschem Passwort zeigt Flash-Message.

- [x] **Doku** – `docs/SERVER-KONZEPT.md` Abschnitt "Authentifizierung" um Login-Flow
  und Cookie-Lebensdauer erganzen.

---

## Phase 3 – Middleware + Tenant-Isolation

Ziel: Drei Middleware-Funktionen implementieren, Router umbauen sodass geschuetzte Routen
hinter den Middlewares liegen, hardcoded `"morz"` an allen vier Stellen entfernen.

- [x] **RequireAuth implementieren** – in `server/backend/internal/httpapi/middleware.go`
  (neue Datei) Funktion `RequireAuth(authStore *store.AuthStore) func(http.Handler) http.Handler`;
  liest Cookie `morz_session`, ruft `authStore.GetSessionUser` auf,
  speichert `*store.User` im Context (eigener Key-Typ `contextKey`),
  redirectet bei Fehler zu `/login?next=<aktueller-Pfad>`.

- [x] **RequireAdmin implementieren** – in `middleware.go` Funktion
  `RequireAdmin(next http.Handler) http.Handler`; liest User aus Context,
  prueft `user.Role == "admin"`, antwortet sonst mit 403.

- [x] **RequireTenantAccess implementieren** – in `middleware.go` Funktion
  `RequireTenantAccess(next http.Handler) http.Handler`; liest User und `{tenantSlug}` aus
  Request-Path, erlaubt Zugriff wenn `user.Role == "admin"` oder `user.TenantSlug == tenantSlug`
  (dazu Feld `TenantSlug string` auf `store.User` erganzen, per JOIN in `GetSessionUser` befullen),
  antwortet sonst mit 403.

- [x] **Router umbauen** – in `router.go` die bisherige flache Route-Liste in Gruppen
  umstrukturieren: `/admin`-Routen hinter `RequireAuth` + `RequireAdmin` legen,
  `/manage/{screenSlug}`-Routen und kuenftige `/tenant/{tenantSlug}/...`-Routen hinter
  `RequireAuth` + `RequireTenantAccess` legen; Hilfsfunktion `chain(...Middleware)` nutzen
  oder inline wrappen.

- [x] **Hardcoded "morz" entfernen (Stelle 1)** – in
  `server/backend/internal/httpapi/manage/ui.go` Zeile 93:
  `tenants.Get(r.Context(), "morz")` ersetzen durch Auslesen des authentifizierten Users aus
  Context; `tenant_id` aus `user.TenantID` verwenden.

- [x] **Hardcoded "morz" entfernen (Stelle 2)** – in `ui.go` Zeile 154:
  gleiche Ersetzung fuer `HandleManageUI`.

- [x] **Hardcoded "morz" entfernen (Stelle 3)** – in `ui.go` Zeile 197:
  gleiche Ersetzung fuer `HandleProvisionUI`; SSH-User `"morz"` (Zeile 191) aus Config
  lesen oder als optionalen Query-Parameter ermoeglichen.

- [x] **Hardcoded "morz" entfernen (Stelle 4)** – in
  `server/backend/internal/httpapi/manage/register.go` Zeile 43:
  `tenants.Get(r.Context(), "morz")` durch `cfg.DefaultTenantSlug` ersetzen.

- [x] **Doku** – `docs/SERVER-KONZEPT.md` um Abschnitt "Middleware-Kette" erganzen:
  Schaubild der Route-Gruppen mit den jeweiligen Middlewares.

---

## Phase 4 – Tenant-Dashboard UI

Ziel: Eigenes Package fuer Tenant-Handler, zweistufige Tab-Ansicht
(Screens mit Live-Status, Mediathek mit Upload), Navbar, Routing.

- [x] **Package-Verzeichnis anlegen** – neues Verzeichnis
  `server/backend/internal/httpapi/tenant/`; Dateien:
  `tenant.go` (Handler), `templates.go` (Template-Strings); gleiche Struktur wie Package `manage`.

- [x] **tenantDashTmpl definieren** – in `tenant/templates.go` Bulma-Layout mit:
  Navbar (Logo links, "Abmelden"-Button rechts als POST /logout),
  zwei Tabs (`<div class="tabs">`) mit IDs `tab-screens` und `tab-media`,
  Tab A "Meine Monitore", Tab B "Mediathek"; JS-Snippet fuer Tab-Switching inline am Ende
  des Templates (analog zu bestehenden inline-Scripts in `manage/templates.go`).

- [x] **Tab A – Screen-Karten implementieren** – in `tenantDashTmpl` Tab A mit Bulma-Cards
  pro Screen: Titel (Screen.Name), Orientierungsicon, Status-Badge
  (Online/Offline/Unbekannt) per JS-Fetch aus `/api/v1/screens/status`;
  JS-Funktion `loadScreenStatuses()` alle 30 Sekunden aufrufen und Badge-Farbe setzen
  (is-success / is-danger / is-warning).

- [x] **Tab B – Mediathek mit Upload implementieren** – in `tenantDashTmpl` Tab B:
  Upload-Formular (multipart, POST `/tenant/{tenantSlug}/upload`), Dateiliste als Bulma-Table
  (Titel, Typ, Groesse, Datum, Loeschen-Button mit Modal-Confirmation analog zu `manage/templates.go`);
  Upload-Fortschrittsbalken (bestehende JS-Logik aus `manageTmpl` wiederverwenden oder extrahieren).

- [x] **HandleTenantDashboard implementieren** – in `tenant/tenant.go` Funktion
  `HandleTenantDashboard(tenantStore *store.TenantStore, screenStore *store.ScreenStore,
  mediaStore *store.MediaStore, statusStore playerStatusStore) http.HandlerFunc`;
  liest `{tenantSlug}` aus URL, laedt Screens und Media-Assets, rendert `tenantDashTmpl`.

- [x] **HandleTenantUpload implementieren** – in `tenant/tenant.go` Funktion
  `HandleTenantUpload(tenantStore *store.TenantStore, mediaStore *store.MediaStore,
  uploadDir string) http.HandlerFunc`; identische Upload-Logik wie `manage.HandleUploadMediaUI`,
  aber ohne Screen-Kontext (Media gehoert direkt dem Tenant);
  nach Erfolg Redirect zu `/tenant/{tenantSlug}/dashboard?tab=media&flash=uploaded`.

- [x] **Navbar in Admin-UI erganzen** – in `manage/templates.go` in `adminTmpl` und
  `manageTmpl` eine minimale Bulma-Navbar mit "Admin" (aktiv) und "Abmelden"-Button erganzen,
  sodass beide UIs optisch konsistent sind.

- [x] **Responsive pruefen** – `tenantDashTmpl` auf `is-mobile`-Breakpoint testen:
  Screen-Karten sollen in `columns is-multiline` wrappen; Upload-Bereich soll auf schmalen
  Screens nutzbar bleiben.

- [x] **Routen eintragen** – in `router.go` innerhalb `registerManageRoutes` hinter
  `RequireAuth` + `RequireTenantAccess`:
  `mux.HandleFunc("GET /tenant/{tenantSlug}/dashboard", tenant.HandleTenantDashboard(...))`,
  `mux.HandleFunc("POST /tenant/{tenantSlug}/upload", tenant.HandleTenantUpload(...))`.

- [x] **Doku** – `docs/SERVER-KONZEPT.md` neuen Abschnitt "Tenant-Dashboard" mit
  URL-Schema, Tab-Beschreibung und Status-Polling-Intervall erganzen.

---

## Phase 5 – Manage-UI Anpassung

Ziel: Der "Zurueck"-Link in der Manage-UI soll kontextsensitiv sein –
aus dem Admin-Bereich kommend zeigt er zur Admin-Uebersicht,
aus dem Tenant-Dashboard kommend zurueck zum Dashboard.

- [x] **TemplateData um BackLink/BackLabel erweitern** – in `manage/ui.go`
  Struct `manageData` (oder gleichwertiges anonymes Struct) um Felder
  `BackLink string` und `BackLabel string` erganzen.

- [x] **HandleManageUI: BackLink aus Query-Parameter lesen** – in `HandleManageUI`:
  wenn `r.URL.Query().Get("from") == "tenant"`, dann
  `BackLink = "/tenant/{tenantSlug}/dashboard"` und `BackLabel = "← Dashboard"`;
  sonst `BackLink = "/admin"` und `BackLabel = "← Admin"`.

- [x] **manageTmpl: statisches "← Admin" ersetzen** – in `manage/templates.go`
  den hardcoded Link `← Admin` durch `{{.BackLabel}}` mit `href="{{.BackLink}}"` ersetzen.

- [x] **Tenant-Dashboard: Links zu Manage-UI mit ?from=tenant** – in `tenant/templates.go`
  jeden "Playlist bearbeiten"-Link als `/manage/{screenSlug}?from=tenant` formulieren,
  damit der Ruecklink korrekt gesetzt wird.

- [x] **Breadcrumb-Navigation** – optional, aber empfohlen: in `manageTmpl` oberhalb des
  Hauptinhalts eine Bulma-Breadcrumb-Leiste einfuegen:
  Admin-Pfad: `Admin > {ScreenName}`, Tenant-Pfad: `Dashboard > {ScreenName}`;
  Daten aus `BackLabel`/`BackLink` und `Screen.Name` zusammensetzen.

- [x] **Doku** – Kommentar in `manage/ui.go` bei `HandleManageUI` dokumentiert
  den `?from=tenant`-Parameter und das BackLink-Verhalten.

---

## Phase 6 – Abschluss + Deployment

Ziel: Session-Cleanup als Hintergrundprozess, Secrets in Docker/Ansible,
Code-Review durch Larry, End-to-End-Test, Deployment, Nachziehen der Kerndokumentation.

- [x] **Session-Cleanup-Ticker implementieren** – in `app.go` nach Server-Start einen
  `time.NewTicker(1 * time.Hour)` starten (als Goroutine), der `authStore.CleanExpiredSessions`
  aufruft; Ticker beim Shutdown stoppen (Context-Abbruch oder `defer ticker.Stop()`).

- [ ] **Docker-Secret fuer AdminPassword** – in `compose/` (docker-compose.yml oder
  ein neues Override-File) das Env `MORZ_INFOBOARD_ADMIN_PASSWORD` aus einem Docker-Secret
  oder `.env`-Datei beziehen; `.env.example` mit leerem Wert als Dokumentation erganzen.

- [ ] **Ansible-Variable erganzen** – in `ansible/` (Rollen `signage_server` oder
  `signage_base`) Variable `morz_admin_password` als Vault-Variable definieren;
  in das entsprechende Docker-Compose-Template eintragen.

- [ ] **Code-Review durch Larry** – Pull-Request / Branch auf Gitea erstellen,
  Larry als Reviewer eintragen; Review-Fokus: SQL-Injection, Session-Fixation,
  bcrypt-Cost-Faktor (mind. 12), Middleware-Reihenfolge, fehlende Fehlerbehandlung.

- [ ] **End-to-End-Test** – manuelle Checkliste durchlaufen:
  (1) Admin-Login, Admin-UI sichtbar, Logout funktioniert;
  (2) Tenant-Login, nur eigene Screens und Medien sichtbar;
  (3) Tenant kann nicht auf `/admin` zugreifen (403);
  (4) Upload im Tenant-Dashboard, Datei erscheint in Liste;
  (5) Playlist-Bearbeitung aus Tenant-Dashboard, Ruecklink zeigt "← Dashboard".

- [ ] **Deployment** – neues Image bauen (`make build` oder CI-Pipeline),
  `docker compose pull && docker compose up -d` auf dem Server ausfuehren,
  Migration 002_auth.sql wird automatisch eingespielt, Logs auf Fehler pruefen.

- [x] **TODO.md nachziehen** – abgearbeitete Punkte in `TODO.md` abhaken:
  "Firmen-/Monitor-Oberflaeche in Hauptbereiche aufteilen" (Phase 4),
  "Authentifizierungskonzept festlegen" (falls noch offen),
  "Mandantentrennung in den APIs absichern" (falls noch offen).

- [x] **README / DEVELOPMENT nachziehen** – `DEVELOPMENT.md` um Abschnitt
  "Lokale Entwicklung mit Login" erganzen: Env-Variable `MORZ_INFOBOARD_ADMIN_PASSWORD=dev`
  und `MORZ_INFOBOARD_DEV_MODE=true` setzen, um ohne HTTPS-Cookie arbeiten zu koennen.

---

## Offene Entscheidungen

Diese Punkte sind bewusst noch nicht spezifiziert. Sie muessen vor oder waehrend der
Umsetzung entschieden und dann in dieser Datei oder `docs/OFFENE-ARCHITEKTURFRAGEN.md`
festgeschrieben werden.

| Thema | Frage | Wer entscheidet |
|---|---|---|
| Passwort-Reset | Soll es eine "Passwort vergessen"-Funktion geben? Falls ja: E-Mail-Versand oder Admin-seitig? | Jesko |
| CSRF-Schutz | Brauchen die POST-Formulare CSRF-Tokens? Aktuell kein Schutz implementiert. Empfehlung: `gorilla/csrf` oder eigenes Double-Submit-Cookie. | Backy + Larry |
| Rate-Limiting | Soll `/login` gegen Brute-Force abgesichert werden (z.B. 10 Versuche / 15 Min per IP)? | Backy |
| Session-TTL | 24 Stunden ist ein erster Wert. Soll es eine "Eingeloggt bleiben"-Option geben? | Jesko |
| Tenant-User-Verwaltung | Wer legt Tenant-User an? Aktuell nur der Admin via DB oder EnsureAdminUser. Braucht es eine UI fuer neue Firmen-User? | Jesko |
