From ab1a36dee131b798b0f7c3d6eb12b2ad7ebdc618 Mon Sep 17 00:00:00 2001 From: Erik Thiele Date: Sat, 20 Jun 2026 15:05:43 +0200 Subject: [PATCH] =?UTF-8?q?Doku=20erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 62 ++++++++++++++++++++++++++++++++++++++++++++---------- README.md | 47 +++++++++++++++++++++++++++++++++++++++-- users.json | 8 +------ 3 files changed, 97 insertions(+), 20 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9b253cc..4f339d4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,18 +14,19 @@ docker compose up -d - Single Flask app in `app.py`; there is no database. - Persistent state is `config.json` plus files under `media///`. +- User data in `users.json` (hashed passwords via werkzeug.security scrypt). - Port 5005; `README.md` hat den korrekten Port. - Existing repo instructions in this file are the main local guidance; there is no `opencode.json` or workflow config in this repo. -## Multi-Standort-URL-Struktur (seit v4.2.0) +## Multi-Standort-URL-Struktur Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens. | Route | Beschreibung | |-------|-------------| | `GET /player//` | Player für Screen an einem Standort | -| `GET /admin` | Redirect zum ersten Standort | -| `GET /admin/` | Admin-Dashboard für einen Standort | +| `GET /admin` | Redirect zum ersten zugänglichen Standort | +| `GET /admin/` | Admin-Dashboard für einen Standort (`@site_access_required`) | | `GET /admin//priority` | Priority-Playlist als separate Seite | | `GET /media///` | Medien-Datei ausliefern | | `GET /media/priority/` | Priority-Medien (global) | @@ -37,22 +38,57 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens. | `POST /api/customer` | API-Endpunkt (JSON mit "site"-Feld) | | `GET /admin//add-screen?name=` | Neuen Screen anlegen | | `POST /admin//delete-screen/` | Screen + Medien löschen | -| `GET /add-site?name=` | Neuen Standort anlegen | +| `GET /add-site?name=` | Neuen Standort anlegen (nur Admin) | | `POST /admin//update-actions/` | Aktionen-Einstellungen speichern (Custom-URL + Position) | | `POST /admin//update-voice/` | Voice-Agent-Einstellungen speichern (Enabled, Label, Target, Position) | -| `POST /admin//delete-site` | Standort + alle Screens/Medien löschen | +| `POST /admin//delete-site` | Standort + alle Screens/Medien löschen (nur Admin) | +| `GET /change-password` | Passwort ändern-Seite | +| `GET /admin/users` | User-Liste (nur Admin) | +| `GET /admin/users/create` | User anlegen (nur Admin) | +| `GET /admin/users/edit/` | User bearbeiten (nur Admin) | + +## User-Verwaltung (seit v5.6.0) + +- Login via E-Mail + Passwort (scrypt-gehasht in `users.json`) +- Drei Rollen: + - **Admin**: Zugriff auf alle Standorte + Userverwaltung (anlegen/bearbeiten/löschen/Reset) + - **Superuser**: Zugriff auf alle Standorte, keine Userverwaltung + - **User**: Zugriff nur auf zugewiesene Standorte +- Erster Login erfordert Passwort-Änderung (`must_change_password=True`) +- Admin-Reset generiert temporäres Passwort (wird im UI angezeigt) +- Fehlender/leerer `password_hash` → Login ohne Prüfung, direkt zu `/change-password` +- Letzter Admin kann nicht gelöscht werden +- `users.json` wird beim ersten Start aus `config.json.admin` befüllt +- Notfall: `users.json` löschen → Server-Neustart erzeugt neuen Admin + +### User-Routen + +| Route | Beschreibung | +|-------|-------------| +| `POST /admin/users/create` | User anlegen | +| `POST /admin/users/delete/` | User löschen (letzter Admin geschützt) | +| `POST /admin/users/reset-password/` | Passwort-Reset (temporäres Passwort) | +| `POST /admin/users/edit/` | User bearbeiten speichern | + +### Zugriffs-Dekoratoren + +| Dekorator | Wirkung | +|-----------|---------| +| `@login_required` | User muss eingeloggt sein | +| `@admin_required` | Nur Admins | +| `@site_access_required` | Admin/Superuser → alle Sites, User → nur freigegebene | ## Behavior To Preserve - `GET /player//` renders the playlist and auto-reloads from `/playlist///hash`. -- `GET /admin` requires login; `config.json.admin` holds the credentials. +- `GET /admin` requires login; redirects to first accessible site for the user. - URL playlist items are stored as dicts like `{"url": "https://...", "zoom": 0.8}` and the zoom value must survive save/reorder flows. - `.html` items in `media/` are rendered inline as content, not in an iframe. - `config.priority.enabled` makes the priority playlist show on every screen. - `POST /api/customer` generates `welcome.html` and inserts it at the front of the lobby playlist for the specified site. - New standorte can be added by creating `media///` directories and optionally adding config to `config.json["sites"][]`. - Priority playlist (`config.priority`) is global and affects all sites/screens. -- Willkommensseite (`customer.html`) accepts up to 3 customer names; logos are fetched via OpenAI→Brandfetch and displayed in a flex row. +- Willkommensseite (`customer.html`) accepts up to 3 customer names; logos are fetched via OpenAI→Brandfetch and displayed in a flex row (portrait: column). - `generate_welcome_html(customer_names, logo_urls)` takes lists for up to 3 customers; logos have equal width (280px) with `max-height: 180px`. - `customer_names` are preserved in form fields after POST (via `value`-Attribute). - Admin-UI nutzt keyadmin-Design: `brand-surface` (#2b2f36), `nav-surface` (rot #DA002D), Dark Mode per `localStorage("signage-theme")`. @@ -67,20 +103,25 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens. - `voice_agent_show_image` (per Screen): optionales Bild (`static/dva.png`) über dem Button, per Admin-Toggle schaltbar (Beta). - Action-Button (custom_url) und Voice-Agent-Button haben einheitliches Styling: weißer Hintergrund, schwarzer Text, 20px Border-Radius, fette Schrift. -- Neue Standorte können über den `+`-Button im Header oder über `GET /add-site?name=` angelegt werden. -- `delete_site` entfernt den Standort aus Config und löscht das Medienverzeichnis rekursiv. +- Neue Standorte können über den `+`-Button im Header oder über `GET /add-site?name=` angelegt werden (nur Admins). +- `delete_site` entfernt den Standort aus Config und löscht das Medienverzeichnis rekursiv (nur Admins). - `config.json["server_url"]` (z. B. `http://signage.ccmake.de`) wird in der Admin-Ansicht für die Player-URLs verwendet. - Screen-Card-Body hat Tabler-Tabs: **Playlist** (1, aktiv), **Einstellungen** (2), **Aktionen** (3), **Digital Voice Agent** (4), **Medien** (5); Priority-Seite ebenfalls Tabs **Playlist** und **Medien**. - `stay_on_first`: Wenn aktiviert bleibt der Player auf dem ersten Playlist-Element stehen (kein Durchlauf). - Tab-Reihenfolge in Screen-Cards: Playlist → Einstellungen → Aktionen → Digital Voice Agent → Medien. - Player-URL im Screen-Header ist ein klickbarer Link in grauer Farbe. +- `add_customer` und `/customer` erfordern jetzt Login (`@login_required`) mit Site-Zugriffsprüfung. +- `/admin` redirectet zum ersten für den User zugänglichen Standort (nicht mehr global ersten). +- User-Dropdown im Header: zeigt E-Mail + Role-Badge, Menü mit Passwort ändern, Userverwaltung (Admin), Abmelden. ## Repo Quirks - `media/` and media file extensions are gitignored. +- `users.json` is NOT gitignored (tracked for initial admin setup, contains no secrets by default). - The app has no configured tests, lint, typecheck, formatter, or CI. - Hardcoded secrets exist in tracked files; do not commit new secrets or reshuffle them casually. -- Wichtige Helper-Funktionen in `app.py`: `load_config()`, `save_config()`, `get_site_list()`, `get_screen_config()`, `is_url()`, `normalize_url()`, `playlist_item_name()`, `playlist_item_enabled()`, `load_priority_files()`, `prio_redirect()`, `get_background_url()`. +- Wichtige Helper-Funktionen in `app.py`: `load_config()`, `save_config()`, `get_site_list()`, `get_screen_config()`, `is_url()`, `normalize_url()`, `playlist_item_name()`, `playlist_item_enabled()`, `load_priority_files()`, `prio_redirect()`, `get_background_url()`, `load_users()`, `save_users()`, `get_user()`, `init_user_db()`, `get_accessible_sites()`. +- Zugriffs-Dekoratoren in `app.py`: `admin_required`, `site_access_required`. - Hintergrundbild der Willkommensseite wird pro Standort unter `media//background.*` gespeichert; Fallback auf `static/wallpaper.png` wenn keine Datei existiert. - `get_background_url(site)` prüft auf benutzerdefiniertes Hintergrundbild für einen Standort. - `generate_welcome_html(customer_names, logo_urls, background_url=None)` akzeptiert optionalen `background_url`-Parameter. @@ -88,4 +129,3 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens. - `search_customer_logo` in `generate_welcome_page.py` nutzt OpenAI GPT-4 + Brandfetch CDN. - `admin_priority` rendert `priority.html` mit `site_list`, `current_site`, `priority_files`, `server_url`. - `static/dva.png`: Bild für den Voice-Agent-Button (optional, per Admin-Toggle ein-/ausblendbar). - diff --git a/README.md b/README.md index a05d18d..83d6796 100755 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ Browserbasiertes Digital-Signage-System für interne Info-Screens. - **Custom-URL-Button**: pro Screen konfigurierbarer Aktions-Button mit frei wählbarer Position (9 Positionen) im Player (öffnet URL in iframe-Overlay mit Zurück-Button oder per Direkt-Weiterleitung) - **Digital Voice Agent**: global konfigurierbarer Voice-Agent-Button pro Screen (9 Positionen, iframe-Overlay mit positionsgetreuem Zurück-Button, Typewriter-Tagline mit mehrsprachigen Wechseltexten, optionales Bild über dem Button) - **Stay-on-First**: Screen kann auf erstem Playlist-Element stehen bleiben (kein automatischer Refresh) +- **User-Verwaltung**: Mehrere User mit Rollen (Admin / Superuser / User), E-Mail als Login, Passwort-Hashing (scrypt), Berechtigungen pro Standort +- **Passwort-Workflow**: First-Login-Änderung, Admin-Reset mit temporärem Passwort, Notfall-Login bei fehlendem Hash - Tab-basierte Admin-UI pro Screen: Playlist, Einstellungen, Aktionen, Digital Voice Agent, Medien (Tabler Tabs) - Priority-Seite ebenfalls mit Tabs: Playlist und Medien - Dark Mode (localStorage-persistiert) @@ -35,6 +37,7 @@ Flask App (Server) ├── Player UI /player// ├── Priority-Seite /admin//priority ├── config.json +├── users.json ├── media/ │ ├── / │ │ ├── / @@ -59,6 +62,7 @@ signage/ ├── app.py # Flask-App (alle Routen) ├── generate_welcome_page.py # Logo-Suche + Willkommensseite-Generierung ├── config.json # Persistente Konfiguration +├── users.json # User-Datenbank (gehashte Passwörter) ├── media/ │ ├── / │ │ ├── lobby/ @@ -71,6 +75,10 @@ signage/ │ ├── customer.html # Willkommensseite-Formular │ ├── player.html # Player-Ansicht │ ├── login.html +│ ├── change_password.html # Passwort ändern (First-Login / Reset) +│ ├── user_list.html # User-Liste (Admin) +│ ├── user_create.html # User anlegen (Admin) +│ ├── user_edit.html # User bearbeiten (Admin) │ ├── _header.html # Gemeinsamer Header │ ├── _footer.html # Gemeinsamer Footer │ └── _styles.html # Zentrale CSS (Variablen, Dark Mode) @@ -140,6 +148,14 @@ docker compose up -d | `GET /playlist///hash` | Playlist-Checksumme (für Auto-Reload) | | `GET /willkommen?site=` | Willkommensseite-Formular (GET + POST) | | `POST /api/customer` | API-Endpunkt für Willkommensseite (JSON) | +| `GET /change-password` | Passwort ändern (First-Login / nach Admin-Reset) | +| `GET /admin/users` | User-Liste (Admin) | +| `GET /admin/users/create` | User anlegen (Admin) | +| `GET /admin/users/edit/` | User bearbeiten (Admin) | +| `POST /admin/users/create` | User anlegen (Admin) | +| `POST /admin/users/delete/` | User löschen (Admin, letzter Admin geschützt) | +| `POST /admin/users/reset-password/` | Passwort-Reset mit temporärem Passwort (Admin) | +| `POST /admin/users/edit/` | User bearbeiten speichern (Admin) | | `GET /media///` | Medien-Datei ausliefern | | `GET /media/priority/` | Priority-Medien (global) | | `GET /media//background/` | Hintergrundbild der Willkommensseite | @@ -159,6 +175,7 @@ http://localhost:5005/admin/ - Digital Voice Agent pro Screen konfigurieren (Position, Bild ein/aus, Typewriter-Tagline) - Stay-on-First-Modus pro Screen (kein automatischer Refresh) - Standorte anlegen & löschen +- **Userverwaltung**: User anlegen, bearbeiten, löschen, Passwort-Reset (temporäres Passwort wird angezeigt) ### Player-URL @@ -181,7 +198,25 @@ Maximal 3 Kunden eingeben → Logos werden via OpenAI + Brandfetch gesucht → ` --- -### Globale Konfiguration +### Globale Konfiguration (config.json) + +| Feld | Typ | Beschreibung | +|------|-----|-------------| +| `server_url` | string | Öffentliche Server-URL für Player-Links | +| `voice_agent_url` | string | URL des Digital Voice Agents (global, read-only im Admin) | + +### User-Rollen (users.json) + +| Rolle | Zugriff | Userverwaltung | +|-------|---------|----------------| +| **Admin** | Alle Standorte | Ja (anlegen/bearbeiten/löschen/Reset) | +| **Superuser** | Alle Standorte | Nein | +| **User** | Nur freigegebene Standorte | Nein | + +- Usernamen sind E-Mail-Adressen +- Passwörter werden als scrypt-Hash gespeichert +- `users.json` wird beim ersten Start aus `config.json.admin` befüllt +- **Notfall**: `users.json` löschen → Server-Neustart erzeugt neuen Admin aus `config.json` | Feld | Typ | Beschreibung | |------|-----|-------------| @@ -269,6 +304,10 @@ Maximal 3 Kunden eingeben → Logos werden via OpenAI + Brandfetch gesucht → ` - `load_priority_files()` – Priority-Playlist + Dateien laden - `prio_redirect(site)` – Redirect-Pfad zur Priority-Seite - `get_background_url(site)` – URL zum benutzerdefinierten Hintergrundbild oder `None` +- `load_users()` / `save_users()` / `get_user()` – User aus `users.json` +- `init_user_db()` – initialen Admin aus `config.json` anlegen +- `get_accessible_sites(cfg, user)` – Standorte nach User-Berechtigung filtern +- `admin_required` / `site_access_required` – Zugriffs-Dekoratoren --- @@ -280,7 +319,11 @@ Wird über `localStorage("signage-theme")` persistiert. Umschalt-Button im Heade ## Sicherheit -- Admin-Bereich per Flask-Login geschützt +- Admin-Bereich per Flask-Login geschützt (E-Mail + Passwort) +- Passwörter gehasht (scrypt) in `users.json` +- Drei Rollen: Admin (alle Standorte + Userverwaltung), Superuser (alle Standorte), User (nur zugewiesene Standorte) +- Zugriffskontrolle per Decorator (`@admin_required`, `@site_access_required`) +- Letzter Admin kann nicht gelöscht werden - Player-Seiten Read-Only - Externe Nutzung via Reverse Proxy + TLS empfohlen diff --git a/users.json b/users.json index 865170d..113b8ec 100644 --- a/users.json +++ b/users.json @@ -1,12 +1,6 @@ { "admin": { - "password_hash": "scrypt:32768:8:1$2IMM3RQ2eqdrtYj1$960cf075435d7c7584ecf240a3b1858551727e875c9ff61874d4269f412ec6eb8cdef732e509408968460fe62534b193f2a691cf3ef329a45a61c20ec3d12c8a", - "role": "admin", - "sites": [], - "must_change_password": false - }, - "erik.thiele@cancom.de": { - "password_hash": "scrypt:32768:8:1$Cy3cP6oIQuIsYSad$f5feb9e476e79cd917f12ca62a6813104679cd96cb76647cb177e640946054ab8a3f8748020f0782a07e4d47d970ea670b5910b05a685939c2161115eadbc440", + "password_hash": "scrypt:32768:8:1$JNEBAJBSSigcWCxK$4809dcc38b2abcaa3f6397d052560f9f59dedf7c95cf99b61bac0df08a65b64287907522841cbb23e06b36760814007d5757542db1c968a8536be7fd6604a382", "role": "admin", "sites": [], "must_change_password": false