From 4268da7988fb28ab049eb79b034b37538be4c178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesko=20Ansch=C3=BCtz?= Date: Mon, 23 Mar 2026 20:07:12 +0100 Subject: [PATCH] Doku-Sync: Auth, Tenant-Dashboard, Middleware, Schema nachgezogen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SCHEMA.md: users-Tabelle korrigiert, sessions-Tabelle ergänzt - API-ENDPOINTS.md: Auth-Routen + Tenant-Dashboard-Routen ergänzt - SERVER-KONZEPT.md: Abschnitte Authentifizierung, Middleware-Kette, Tenant-Dashboard neu - backend/README.md: komplett neu auf Basis aktueller Implementierung - DEVELOPMENT.md: veraltete "nicht vorhanden"-Punkte bereinigt Co-Authored-By: Claude Sonnet 4.6 --- DEVELOPMENT.md | 18 +++--- docs/API-ENDPOINTS.md | 121 +++++++++++++++++++++++++++++++++++++++ docs/SCHEMA.md | 67 ++++++++++++++++++---- docs/SERVER-KONZEPT.md | 90 +++++++++++++++++++++++++++++ server/backend/README.md | 119 ++++++++++++++++++++++++++++++++------ 5 files changed, 379 insertions(+), 36 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index e05e96a..0fd555e 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -40,9 +40,9 @@ Bereits vorhanden: Noch nicht vorhanden: -- admin-seitige Benutzerautentifizierung und Zugriffskontrolle -- Multi-Tenancy-Isolation auf API-Ebene - produktives SSL/TLS-Handling fuer Deployment +- Docker-Secret-Integration fuer `MORZ_INFOBOARD_ADMIN_PASSWORD` +- Ansible-Variable `morz_admin_password` als Vault-Variable (Phase 6) ## Voraussetzungen auf dem Entwicklungsrechner @@ -287,11 +287,15 @@ Das Playbook erledigt: ## Empfohlene naechste Implementierungsschritte -1. Backend: einheitliches Fehlerformat und Routing-Grundstruktur anlegen -2. Backend: Konfigurations- und App-Lifecycle stabilisieren -3. Agent und Backend: den HTTP-Statuspfad als Grundlage fuer Identitaet, Persistenz und spaetere Admin-Vorschau erweitern -4. Agent: danach MQTT-spezifische Reachability und feinere Connectivity-Schwellenlogik aufsetzen -5. Danach die Netzwerk-, Sync- und Kommandopfade schrittweise produktionsnah ausbauen +Die Punkte 1–4 der urspruenglichen Liste (Fehlerformat, Routing, Status, MQTT) sind umgesetzt. +Offene Punkte aus Phase 6 des Tenant-Feature-Plans (`docs/TENANT-FEATURE-PLAN.md`): + +1. Docker-Secret fuer `MORZ_INFOBOARD_ADMIN_PASSWORD` in `compose/` einrichten +2. Ansible-Variable `morz_admin_password` als Vault-Variable definieren +3. Code-Review durch Larry (SQL-Injection, Session-Fixation, bcrypt-Cost, Middleware-Reihenfolge) +4. End-to-End-Test-Checkliste in `docs/TEST-CHECKLIST-DEV.md` durchlaufen +5. Deployment: Image bauen, Migration `002_auth.sql` pruefen, Logs kontrollieren +6. Langfristig: Netzwerk-, Sync- und Kommandopfade produktionsnah ausbauen ## End-to-End-Entwicklungstest (Backend + Agent) diff --git a/docs/API-ENDPOINTS.md b/docs/API-ENDPOINTS.md index c0d3ba5..cae626b 100644 --- a/docs/API-ENDPOINTS.md +++ b/docs/API-ENDPOINTS.md @@ -507,6 +507,122 @@ Spezialendpoint zur Auflösung von Nachrichten-Wand-Anfragen (noch in Entwicklun --- +## Authentifizierung (Web-Formulare) + +Alle Auth-Routen erfordern keine vorherige Authentifizierung. + +### GET /login + +Zeigt das Login-Formular. + +- Wenn ein gueltiges `morz_session`-Cookie vorhanden ist, wird direkt zum jeweiligen Dashboard + weitergeleitet (`/admin` fuer Admins, `/tenant/{slug}/dashboard` fuer Tenant-User). + +**Response:** HTML-Seite mit Benutzername/Passwort-Formular und optionaler Flash-Message. + +--- + +### POST /login + +Verarbeitet die Login-Eingabe. + +**Request (Form-Encoded):** +``` +username=admin&password=geheim +``` + +**Verhalten:** +- Passwort wird per `bcrypt.CompareHashAndPassword` geprueft +- Bei Erfolg wird ein `morz_session`-Cookie gesetzt (HttpOnly, Secure, 24h TTL) +- Weiterleitung je nach Rolle: `admin` → `/admin`, `tenant` → `/tenant/{slug}/dashboard` +- Bei Fehler: Rueckkehr zur Login-Seite mit Flash-Message + +**Status:** +- `303 See Other` — Erfolg, Weiterleitung +- `303 See Other` — Fehler, Rueckkehr zur Login-Seite mit `?msg=` + +--- + +### POST /logout + +Meldet den aktuellen Benutzer ab. + +**Verhalten:** +- Session wird in der DB geloescht (`DeleteSession`) +- Cookie wird mit `MaxAge=-1` geloescht +- Weiterleitung zu `/login` + +**Status:** +- `303 See Other` + +--- + +## Tenant Self-Service Dashboard (Web-Formulare) + +Alle Tenant-Routen erfordern `RequireAuth` + `RequireTenantAccess`. +Admins koennen auf jeden Tenant zugreifen; Tenant-User nur auf ihren eigenen. + +### GET /tenant/{tenantSlug}/dashboard + +Zeigt das Tenant-Self-Service-Dashboard. + +**Tabs:** +- Tab A "Meine Monitore" — Screen-Karten mit Live-Status (via JS-Fetch aus `/api/v1/screens/status`) +- Tab B "Mediathek" — Upload-Formular und Dateiliste + +**Query-Parameter:** +- `tab=media` — oeffnet direkt Tab B (z. B. nach Upload-Redirect) +- `flash=uploaded` / `flash=deleted` — zeigt Erfolgs-Flash-Message + +**Response:** HTML-Seite. + +--- + +### POST /tenant/{tenantSlug}/upload + +Laedt ein Medium fuer den Tenant hoch. + +**Request (Multipart Form):** +``` +type: image (oder video, pdf) +title: Mein Bild +file: +``` + +oder fuer eine Web-URL: +``` +type: web +title: Externe Website +url: http://example.com +``` + +**Verhalten:** +- Datei wird in `MORZ_INFOBOARD_UPLOAD_DIR` gespeichert +- MIME-Typ wird aus dem Upload-Header abgeleitet +- Max. Upload-Groesse: 512 MB + +**Status:** +- `303 See Other` → `/tenant/{slug}/dashboard?tab=media&flash=uploaded` +- `400 Bad Request` — fehlender Typ oder Datei +- `404 Not Found` — Tenant nicht vorhanden + +--- + +### POST /tenant/{tenantSlug}/media/{mediaId}/delete + +Loescht ein Medien-Asset des Tenants. + +**Verhalten:** +- Eigentuemer-Pruefung: `asset.TenantID` muss mit dem Tenant uebereinstimmen +- Physische Datei wird geloescht sofern vorhanden + +**Status:** +- `303 See Other` → `/tenant/{slug}/dashboard?tab=media&flash=deleted` +- `403 Forbidden` — Asset gehoert nicht diesem Tenant +- `404 Not Found` — Tenant oder Asset nicht vorhanden + +--- + ## Admin UI (Web-Formulare) ### GET /admin @@ -827,6 +943,11 @@ Typische HTTP-Status: ## Änderungshistorie +- **2026-03-23 (Update):** Auth- und Tenant-Dashboard-Endpoints ergaenzt (Doris / Doku-Review) + - `GET /login`, `POST /login`, `POST /logout` dokumentiert + - `GET /tenant/{tenantSlug}/dashboard` dokumentiert + - `POST /tenant/{tenantSlug}/upload` dokumentiert + - `POST /tenant/{tenantSlug}/media/{mediaId}/delete` dokumentiert - **2026-03-23:** Initiale Dokumentation aller HTTP-Endpoints basierend auf Code-Review - Alle Screen-Management-Endpoints dokumentiert - Alle Playlist-Management-Endpoints dokumentiert diff --git a/docs/SCHEMA.md b/docs/SCHEMA.md index 16655dd..b2c1014 100644 --- a/docs/SCHEMA.md +++ b/docs/SCHEMA.md @@ -48,21 +48,51 @@ Zweck: Spalten: ```sql -id uuid primary key -tenant_id uuid null references tenants(id) on delete set null -username text not null unique -email text not null unique -password_hash text not null -role text not null -active boolean not null default true -last_login_at timestamptz null -created_at timestamptz not null -updated_at timestamptz not null +id text primary key default gen_random_uuid()::text +tenant_id text not null references tenants(id) on delete cascade +username text not null +password_hash text not null +role text not null default 'tenant' +created_at timestamptz not null default now() +unique(tenant_id, username) ``` Regeln: -- `role` in v1: `admin`, `tenant_user` +- `role` in v1: `admin`, `tenant` +- `username` ist nur innerhalb eines Tenants eindeutig (Unique-Constraint auf `(tenant_id, username)`) +- `tenant_id` ist `NOT NULL` — jeder User gehoert genau einem Tenant +- IDs sind `text`, nicht `uuid`, enthalten aber UUID-Werte (via `gen_random_uuid()::text`) +- Felder wie `email`, `active`, `last_login_at` und `updated_at` existieren in v1 nicht + +### `sessions` + +Zweck: + +- Sitzungstokens fuer den Browser-Login + +Spalten: + +```sql +id text primary key default gen_random_uuid()::text +user_id text not null references users(id) on delete cascade +created_at timestamptz not null default now() +expires_at timestamptz not null default (now() + interval '8 hours') +``` + +Indizes: + +```sql +create index idx_sessions_user_id on sessions(user_id); +create index idx_sessions_expires_at on sessions(expires_at); +``` + +Regeln: + +- Session-TTL beim Anlegen betraegt standardmaessig 8 Stunden (Migration-Default); + `AuthStore.CreateSession` uebergibt die tatsaechliche TTL als Parameter (aktuell 24 Stunden) +- Abgelaufene Sessions werden stuendlich per Hintergrund-Ticker bereinigt (`CleanExpiredSessions`) +- Cookie-Name: `morz_session`; `HttpOnly=true`, `Secure=true` (ausser `MORZ_INFOBOARD_DEV_MODE=true`) ### `screen_groups` @@ -494,6 +524,21 @@ last_failed_sync_at timestamptz null last_error_message text null ``` +## Auth-Datenbankschema + +Die Auth-Tabellen werden durch `server/backend/internal/db/migrations/002_auth.sql` angelegt +und sind vollstaendig unter den Abschnitten `users` und `sessions` oben beschrieben. + +Der `AuthStore` (`internal/store/auth.go`) stellt folgende Methoden bereit: + +- `GetUserByUsername(ctx, username)` — Nutzer per Username laden (inkl. `TenantSlug` via LEFT JOIN) +- `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 wenn nicht vorhanden +- `VerifyPassword(ctx, userID, password)` — Passwort gegen bcrypt-Hash pruefen + ## Wichtige Indizes Empfohlen mindestens: diff --git a/docs/SERVER-KONZEPT.md b/docs/SERVER-KONZEPT.md index f0b6378..415252e 100644 --- a/docs/SERVER-KONZEPT.md +++ b/docs/SERVER-KONZEPT.md @@ -247,6 +247,96 @@ Sinnvolle Komponenten in `compose/`: - `mosquitto` - optional `worker` +## Authentifizierung + +Der Server verwendet einen Session-basierten Login-Flow mit `bcrypt`-Passwort-Hashing. + +### Login-Flow + +1. `GET /login` rendert das Login-Formular (Bulma-Card zentriert). +2. `POST /login` prueft Username und Passwort: + - `AuthStore.GetUserByUsername` laedt den User inkl. Tenant-Slug. + - `bcrypt.CompareHashAndPassword` prueft das Passwort (Cost-Faktor 12). + - Bei Erfolg legt `AuthStore.CreateSession` eine Session an (TTL 24 Stunden). + - Das Session-Token wird als `morz_session`-Cookie gesetzt (`HttpOnly=true`, `Secure=true`). + - Im `DevMode` (`MORZ_INFOBOARD_DEV_MODE=true`) wird `Secure=false` gesetzt fuer lokalen HTTP-Betrieb. + - Weiterleitung je nach Rolle: `admin` → `/admin`, `tenant` → `/tenant/{slug}/dashboard`. +3. `POST /logout` loescht die Session in der DB und entfernt den Cookie. + +### Cookie-Lebensdauer + +- Standard-TTL: 24 Stunden +- Der Cookie verfaellt automatisch; die DB wird stuendlich durch `CleanExpiredSessions` bereinigt. + +### Admin-User-Bootstrap + +Beim Server-Start wird `EnsureAdminUser` aufgerufen, wenn `MORZ_INFOBOARD_ADMIN_PASSWORD` gesetzt ist. +Der Admin-User wird dem Tenant mit Slug `MORZ_INFOBOARD_DEFAULT_TENANT` (Standard: `morz`) zugeordnet. +Ist der User bereits vorhanden, passiert nichts. Fehler sind nicht fatal — der Server startet trotzdem. + +--- + +## Middleware-Kette + +Alle geschuetzten Routen werden durch Middleware-Funktionen in `internal/httpapi/middleware.go` abgesichert. + +``` +Eingehende Anfrage + │ + ▼ + RequireAuth Liest morz_session-Cookie, prueft Session via DB, + speichert *store.User im Request-Context. + → Fehler: Redirect zu /login?next= + │ + ├─► RequireAdmin Prueft user.Role == "admin" + │ → Fehler: 403 Forbidden + │ + └─► RequireTenant Prueft user.TenantSlug == {tenantSlug} aus dem URL-Pfad. + Access Admins duerfen immer durch. + → Fehler: 403 Forbidden +``` + +### Route-Gruppen im Router + +| Gruppe | Middleware | Beispielrouten | +|----------------|------------------------------------|---------------------------------------------| +| Oeffentlich | keine | `/healthz`, `/login`, `/api/v1/screens/register` | +| Auth-only | RequireAuth | `/manage/{screenSlug}/...` | +| Admin-only | RequireAuth + RequireAdmin | `/admin`, `/admin/screens/...` | +| Tenant-scoped | RequireAuth + RequireTenantAccess | `/tenant/{tenantSlug}/...`, `/api/v1/tenants/{tenantSlug}/...` | + +Der Hilfsfunktion `chain(middlewares...)` in `router.go` wrappet Handler von aussen nach innen. + +--- + +## Tenant-Dashboard + +Das Tenant-Self-Service-Dashboard ist unter `/tenant/{tenantSlug}/dashboard` erreichbar. + +### URL-Schema + +| Methode | Pfad | Beschreibung | +|---------|---------------------------------------------|---------------------------| +| GET | `/tenant/{tenantSlug}/dashboard` | Dashboard rendern | +| POST | `/tenant/{tenantSlug}/upload` | Medium hochladen | +| POST | `/tenant/{tenantSlug}/media/{mediaId}/delete` | Medium loeschen | + +### Tabs + +- **Tab A – Meine Monitore:** Zeigt Screen-Karten mit Live-Status. Der Status wird per JavaScript + aus `GET /api/v1/screens/status` geladen und alle 30 Sekunden aktualisiert. + Status-Badge: `is-success` (online), `is-danger` (offline), `is-warning` (unbekannt). +- **Tab B – Mediathek:** Upload-Formular (Bild, Video, PDF oder Web-URL) und Dateiliste + mit Loeschen-Button. Nach Upload oder Loeschen Redirect mit `?tab=media&flash=uploaded/deleted`. + +### Eigentuemer-Pruefung beim Loeschen + +`HandleTenantDeleteMedia` prueft, dass `asset.TenantID == tenant.ID`, bevor es loescht. +Damit ist sichergestellt, dass ein Tenant keine Assets anderer Tenants loeschen kann, +selbst wenn er die `mediaId` erraten wuerde. + +--- + ## Sicherheitsgrundsaetze - Root-Bootstrap-Geheimnisse nur kurzlebig oder referenziert speichern diff --git a/server/backend/README.md b/server/backend/README.md index 4ddde7e..c2d2690 100644 --- a/server/backend/README.md +++ b/server/backend/README.md @@ -1,26 +1,109 @@ # Backend -Dieses Verzeichnis enthaelt das erste Geruest fuer das zentrale Backend. +Dieses Verzeichnis enthaelt das zentrale Go-Backend fuer das Info-Board-System. -Ziel fuer die erste Ausbaustufe: +## Aufgaben -- HTTP-API in Go -- Health-Endpunkt -- saubere Projektstruktur fuer spaetere API-, Worker- und Datenbankmodule -- erste serverseitige Aufloesungslogik fuer `message_wall` +- 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 -Geplante Unterstruktur: +## Unterstruktur -- `cmd/api/` fuer den API-Startpunkt -- `internal/app/` fuer App-Initialisierung -- `internal/campaigns/` fuer Kampagnen- und Template-Logik -- `internal/httpapi/` fuer HTTP-Routing und Handler -- `internal/config/` fuer Konfiguration +- `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) +- `internal/httpapi/` — HTTP-Routing, Middleware und Handler +- `internal/httpapi/manage/` — Admin-UI und Playlist-Management-UI +- `internal/httpapi/tenant/` — Tenant-Self-Service-Dashboard +- `internal/mqttnotifier/` — MQTT-Notifizierungen +- `internal/reqcontext/` — Context-Keys fuer authentifizierten User -Aktuell vorhanden: +## Aktuelle Endpunkte -- `GET /healthz` -- `GET /api/v1` -- `GET /api/v1/meta` -- `POST /api/v1/tools/message-wall/resolve` als erste serverseitige Layout-Aufloesung fuer `message_wall` -- einheitliches API-Fehlerformat im HTTP-Layer +### 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 | +| 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/{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) | + +### 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 | + +### 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` | +| `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_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`.