Session-Cleanup: - app.go: stündlicher Ticker für CleanExpiredSessions mit Context-Shutdown Docker/Infra: - compose/.env.example: Vorlage für ADMIN_PASSWORD, DEV_MODE, DEFAULT_TENANT - server-stack.yml: Backend-Service referenziert neue Env-Variablen Security-Review (Larry): - EnsureAdminUser: Admin-Check tenant-scoped statt global - scanUser() (toter Code, falsche Spaltenanzahl) entfernt - RequireTenantAccess: leerer tenantSlug nicht mehr als Bypass nutzbar - Login: Dummy-bcrypt bei unbekanntem User gegen Timing-Leak - Logout-Cookie: Secure-Flag konsistent mit Login gesetzt Doku (Doris): - DEVELOPMENT.md: Abschnitt "Lokale Entwicklung mit Login" - TENANT-FEATURE-PLAN.md: Phase 3-5 Checkboxen abgehakt - TODO.md: erledigte Punkte abgehakt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
16 KiB
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.sqlerstellen 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) undsessions(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 aufsessions(user_id)undsessions(expires_at)anlegen. -
AuthStore-Datei anlegen – neue Datei
server/backend/internal/store/auth.gomit TypAuthStore struct{ pool *pgxpool.Pool }und KonstruktorNewAuthStore(pool) *AuthStore; Paketstore, gleiche Import-Konventionen wiestore.go. -
AuthStore.GetUserByUsername implementieren – Methode
(s *AuthStore) GetUserByUsername(ctx, username string) (*User, error); neuer Domain-TypUser(id, tenant_id, username, password_hash, role, created_at) instore/auth.godefinieren; 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 obusername='admin'existiert, legt ihn mitbcrypt.GenerateFromPasswordan falls nicht; haengt den User an den Tenant mittenantSlug(via Subquery auftenants.slug). -
Config erweitern – in
server/backend/internal/config/config.godrei 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, Fallbackfalse; wenn true: Session-Cookie wird auch ohne HTTPS gesetzt). -
main.go / app.go: AuthStore verdrahten – in
server/backend/internal/app/app.gostore.NewAuthStore(pool.Pool)instanziieren und anhttpapi.RouterDepsuebergeben (neues FeldAuthStore *store.AuthStore). -
EnsureAdminUser beim Start aufrufen – in
app.New()nach der Store-Initialisierung: wenncfg.AdminPassword != "", dannauthStore.EnsureAdminUser(ctx, cfg.DefaultTenantSlug, cfg.AdminPassword)aufrufen; Fehler loggen, aber nicht fatal (Server soll starten). -
Migrations-Runner pruefen – sicherstellen, dass
db.Connectinserver/backend/internal/db/db.goalle Dateien ausmigrations/in alphabetischer Reihenfolge ausfuehrt und002_auth.sqldamit automatisch eingespielt wird; ggf. Logzeile hinzufuegen. -
Doku – Abschnitt "Auth-Datenbankschema" in
docs/SCHEMA.mdergaenzen: Tabellenusersundsessionsmit 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.goneue Template-KonstanteloginTmplanlegen; Bulma-Card zentriert auf der Seite (columns is-centered,column is-narrow), Formular mit Feldernusernameundpassword(type=password), Submit-ButtonAnmelden, Flash-Message-Bereich fuer Fehlermeldungen (gleiche Klasse wie bestehende Flash-Messages inmanageTmpl). -
HandleLoginUI implementieren – in
server/backend/internal/httpapi/manage/auth.go(neue Datei) FunktionHandleLoginUI() http.HandlerFunc; GET-Handler, rendertloginTmpl; wenn Session-Cookie bereits gueltig ist, redirect zu/tenant/{slug}/dashboard. -
HandleLoginPost implementieren – in
auth.goFunktionHandleLoginPost(authStore *store.AuthStore, cfg config.Config) http.HandlerFunc; liestusername/passwordaus Formular, ruftauthStore.GetUserByUsernameauf, prueft Passwort mitbcrypt.CompareHashAndPassword, erstellt Session mitauthStore.CreateSession(..., 24*time.Hour), setztHttpOnly-Cookiemorz_session(Secure=true ausser bei DevMode), redirectet je nach Rolle:adminzu/admin,tenantzu/tenant/{tenantSlug}/dashboard. -
HandleLogoutPost implementieren – in
auth.goFunktionHandleLogoutPost(authStore *store.AuthStore) http.HandlerFunc; liest Cookiemorz_session, ruftauthStore.DeleteSessionauf, setzt Cookie mit MaxAge=-1 (loeschen), redirectet zu/login. -
Routen eintragen – in
server/backend/internal/httpapi/router.goinregisterManageRouteshinzufuegen:mux.HandleFunc("GET /login", auth.HandleLoginUI()),mux.HandleFunc("POST /login", auth.HandleLoginPost(d.AuthStore, cfg)),mux.HandleFunc("POST /logout", auth.HandleLogoutPost(d.AuthStore)); Importhttpapi/manage/authergaenzen (oder Package zusammenfassen – Entscheidung vor Umsetzung treffen). -
Manuell testen – Server starten,
GET /loginaufrufen, Login mit Admin-Credentials pruefen, Cookie in Devtools sichtbar, Logout funktioniert, Fehler bei falschem Passwort zeigt Flash-Message. -
Doku –
docs/SERVER-KONZEPT.mdAbschnitt "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) FunktionRequireAuth(authStore *store.AuthStore) func(http.Handler) http.Handler; liest Cookiemorz_session, ruftauthStore.GetSessionUserauf, speichert*store.Userim Context (eigener Key-TypcontextKey), redirectet bei Fehler zu/login?next=<aktueller-Pfad>. -
RequireAdmin implementieren – in
middleware.goFunktionRequireAdmin(next http.Handler) http.Handler; liest User aus Context, prueftuser.Role == "admin", antwortet sonst mit 403. -
RequireTenantAccess implementieren – in
middleware.goFunktionRequireTenantAccess(next http.Handler) http.Handler; liest User und{tenantSlug}aus Request-Path, erlaubt Zugriff wennuser.Role == "admin"oderuser.TenantSlug == tenantSlug(dazu FeldTenantSlug stringaufstore.Usererganzen, per JOIN inGetSessionUserbefullen), antwortet sonst mit 403. -
Router umbauen – in
router.godie bisherige flache Route-Liste in Gruppen umstrukturieren:/admin-Routen hinterRequireAuth+RequireAdminlegen,/manage/{screenSlug}-Routen und kuenftige/tenant/{tenantSlug}/...-Routen hinterRequireAuth+RequireTenantAccesslegen; Hilfsfunktionchain(...Middleware)nutzen oder inline wrappen. -
Hardcoded "morz" entfernen (Stelle 1) – in
server/backend/internal/httpapi/manage/ui.goZeile 93:tenants.Get(r.Context(), "morz")ersetzen durch Auslesen des authentifizierten Users aus Context;tenant_idaususer.TenantIDverwenden. -
Hardcoded "morz" entfernen (Stelle 2) – in
ui.goZeile 154: gleiche Ersetzung fuerHandleManageUI. -
Hardcoded "morz" entfernen (Stelle 3) – in
ui.goZeile 197: gleiche Ersetzung fuerHandleProvisionUI; 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.goZeile 43:tenants.Get(r.Context(), "morz")durchcfg.DefaultTenantSlugersetzen. -
Doku –
docs/SERVER-KONZEPT.mdum 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 Packagemanage. -
tenantDashTmpl definieren – in
tenant/templates.goBulma-Layout mit: Navbar (Logo links, "Abmelden"-Button rechts als POST /logout), zwei Tabs (<div class="tabs">) mit IDstab-screensundtab-media, Tab A "Meine Monitore", Tab B "Mediathek"; JS-Snippet fuer Tab-Switching inline am Ende des Templates (analog zu bestehenden inline-Scripts inmanage/templates.go). -
Tab A – Screen-Karten implementieren – in
tenantDashTmplTab A mit Bulma-Cards pro Screen: Titel (Screen.Name), Orientierungsicon, Status-Badge (Online/Offline/Unbekannt) per JS-Fetch aus/api/v1/screens/status; JS-FunktionloadScreenStatuses()alle 30 Sekunden aufrufen und Badge-Farbe setzen (is-success / is-danger / is-warning). -
Tab B – Mediathek mit Upload implementieren – in
tenantDashTmplTab B: Upload-Formular (multipart, POST/tenant/{tenantSlug}/upload), Dateiliste als Bulma-Table (Titel, Typ, Groesse, Datum, Loeschen-Button mit Modal-Confirmation analog zumanage/templates.go); Upload-Fortschrittsbalken (bestehende JS-Logik ausmanageTmplwiederverwenden oder extrahieren). -
HandleTenantDashboard implementieren – in
tenant/tenant.goFunktionHandleTenantDashboard(tenantStore *store.TenantStore, screenStore *store.ScreenStore, mediaStore *store.MediaStore, statusStore playerStatusStore) http.HandlerFunc; liest{tenantSlug}aus URL, laedt Screens und Media-Assets, renderttenantDashTmpl. -
HandleTenantUpload implementieren – in
tenant/tenant.goFunktionHandleTenantUpload(tenantStore *store.TenantStore, mediaStore *store.MediaStore, uploadDir string) http.HandlerFunc; identische Upload-Logik wiemanage.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.goinadminTmplundmanageTmpleine minimale Bulma-Navbar mit "Admin" (aktiv) und "Abmelden"-Button erganzen, sodass beide UIs optisch konsistent sind. -
Responsive pruefen –
tenantDashTmplaufis-mobile-Breakpoint testen: Screen-Karten sollen incolumns is-multilinewrappen; Upload-Bereich soll auf schmalen Screens nutzbar bleiben. -
Routen eintragen – in
router.goinnerhalbregisterManageRouteshinterRequireAuth+RequireTenantAccess:mux.HandleFunc("GET /tenant/{tenantSlug}/dashboard", tenant.HandleTenantDashboard(...)),mux.HandleFunc("POST /tenant/{tenantSlug}/upload", tenant.HandleTenantUpload(...)). -
Doku –
docs/SERVER-KONZEPT.mdneuen 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.goStructmanageData(oder gleichwertiges anonymes Struct) um FelderBackLink stringundBackLabel stringerganzen. -
HandleManageUI: BackLink aus Query-Parameter lesen – in
HandleManageUI: wennr.URL.Query().Get("from") == "tenant", dannBackLink = "/tenant/{tenantSlug}/dashboard"undBackLabel = "← Dashboard"; sonstBackLink = "/admin"undBackLabel = "← Admin". -
manageTmpl: statisches "← Admin" ersetzen – in
manage/templates.goden hardcoded Link← Admindurch{{.BackLabel}}mithref="{{.BackLink}}"ersetzen. -
Tenant-Dashboard: Links zu Manage-UI mit ?from=tenant – in
tenant/templates.gojeden "Playlist bearbeiten"-Link als/manage/{screenSlug}?from=tenantformulieren, damit der Ruecklink korrekt gesetzt wird. -
Breadcrumb-Navigation – optional, aber empfohlen: in
manageTmploberhalb des Hauptinhalts eine Bulma-Breadcrumb-Leiste einfuegen: Admin-Pfad:Admin > {ScreenName}, Tenant-Pfad:Dashboard > {ScreenName}; Daten ausBackLabel/BackLinkundScreen.Namezusammensetzen. -
Doku – Kommentar in
manage/ui.gobeiHandleManageUIdokumentiert 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.gonach Server-Start einentime.NewTicker(1 * time.Hour)starten (als Goroutine), derauthStore.CleanExpiredSessionsaufruft; Ticker beim Shutdown stoppen (Context-Abbruch oderdefer ticker.Stop()). -
Docker-Secret fuer AdminPassword – in
compose/(docker-compose.yml oder ein neues Override-File) das EnvMORZ_INFOBOARD_ADMIN_PASSWORDaus einem Docker-Secret oder.env-Datei beziehen;.env.examplemit leerem Wert als Dokumentation erganzen. -
Ansible-Variable erganzen – in
ansible/(Rollensignage_serverodersignage_base) Variablemorz_admin_passwordals 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
/adminzugreifen (403); (4) Upload im Tenant-Dashboard, Datei erscheint in Liste; (5) Playlist-Bearbeitung aus Tenant-Dashboard, Ruecklink zeigt "← Dashboard". -
Deployment – neues Image bauen (
make buildoder CI-Pipeline),docker compose pull && docker compose up -dauf dem Server ausfuehren, Migration 002_auth.sql wird automatisch eingespielt, Logs auf Fehler pruefen. -
TODO.md nachziehen – abgearbeitete Punkte in
TODO.mdabhaken: "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.mdum Abschnitt "Lokale Entwicklung mit Login" erganzen: Env-VariableMORZ_INFOBOARD_ADMIN_PASSWORD=devundMORZ_INFOBOARD_DEV_MODE=truesetzen, 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 |