morz-infoboard/docs/TENANT-FEATURE-PLAN.md
Jesko Anschütz 7e7a692521 Tenant-Feature Phase 1+2: Auth-Fundament + Login-Flow + UX-Textverbesserung
- DB-Migration 002_auth.sql (users + sessions Tabellen)
- AuthStore mit Session-Management, bcrypt, EnsureAdminUser
- Login/Logout Handler mit Cookie-Session (HttpOnly, SameSite=Lax)
- Login-Template (Bulma-Card, deutsche Labels)
- Config: AdminPassword, DefaultTenantSlug, DevMode
- Fallback-Texte: "Netzwerk offline" → "Server nicht erreichbar"
- TENANT-FEATURE-PLAN.md mit 46 Checkboxen als Steuerungsdatei

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 15:46:14 +01:00

16 KiB
Raw Blame History

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.

  • 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.

  • 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.

  • 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.

  • 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()).

  • 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).

  • 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).

  • 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).

  • 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).

  • 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.

  • 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.

  • 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).

  • 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.

  • 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.

  • 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.

  • 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).

  • 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.

  • 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.

  • 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>.

  • RequireAdmin implementieren in middleware.go Funktion RequireAdmin(next http.Handler) http.Handler; liest User aus Context, prueft user.Role == "admin", antwortet sonst mit 403.

  • 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.

  • 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.

  • 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.

  • Hardcoded "morz" entfernen (Stelle 2) in ui.go Zeile 154: gleiche Ersetzung fuer HandleManageUI.

  • 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.

  • Hardcoded "morz" entfernen (Stelle 4) in server/backend/internal/httpapi/manage/register.go Zeile 43: tenants.Get(r.Context(), "morz") durch cfg.DefaultTenantSlug ersetzen.

  • 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.

  • Package-Verzeichnis anlegen neues Verzeichnis server/backend/internal/httpapi/tenant/; Dateien: tenant.go (Handler), templates.go (Template-Strings); gleiche Struktur wie Package manage.

  • 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).

  • 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).

  • 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).

  • 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.

  • 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.

  • 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.

  • Responsive pruefen tenantDashTmpl auf is-mobile-Breakpoint testen: Screen-Karten sollen in columns is-multiline wrappen; Upload-Bereich soll auf schmalen Screens nutzbar bleiben.

  • 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(...)).

  • 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.

  • TemplateData um BackLink/BackLabel erweitern in manage/ui.go Struct manageData (oder gleichwertiges anonymes Struct) um Felder BackLink string und BackLabel string erganzen.

  • 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".

  • manageTmpl: statisches "← Admin" ersetzen in manage/templates.go den hardcoded Link ← Admin durch {{.BackLabel}} mit href="{{.BackLink}}" ersetzen.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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).

  • 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