# User-spezifische Medien für Restricted Users — Design **Datum:** 2026-03-28 ## Problem Restricted Users können aktuell alle Medien des Tenants sehen und löschen. Sie sollen nur ihre eigenen Medien sehen und verwalten dürfen. ## Anforderungen - **Restricted Users** sehen ausschließlich Medien, die sie selbst hochgeladen haben - **Restricted Users** dürfen nur ihre eigenen Medien löschen - **Admins** sehen alle Medien des Tenants — inkl. solcher ohne Besitzer (Legacy-Medien) - **screen_users** sehen alle Medien des Tenants (unverändert) - Bestehende Medien (ohne Besitzer) bleiben erhalten und funktionieren weiter - **Admins und screen_users** können Medien von Restricted Users ein-/ausblenden (Toggle). Default: **ausgeblendet** ## Ansatz: Filter im Store-Layer Ownership wird als Datenbankfeld gespeichert. Die Filter-Logik liegt im Store-Layer, Handlers delegieren nach Rolle. Entspricht dem bestehenden pgx/raw-SQL-Muster (analog K4-Checks in playlist.go). ## Datenbank ### Migration Neue Spalte in `media_assets`: ```sql ALTER TABLE media_assets ADD COLUMN created_by_user_id text NULL REFERENCES users(id) ON DELETE SET NULL; ``` - **Nullable**: bestehende Medien behalten `NULL` als Besitzer - **ON DELETE SET NULL**: wird ein User gelöscht, bleibt das Medium erhalten, verliert aber den Besitzer - Kein Backfill nötig — `NULL` = kein Besitzer (Legacy oder Admin-Upload) ## Go-Datenmodell `MediaAsset`-Struct bekommt drei neue Felder: ```go CreatedByUserID string // leer = kein Besitzer (legacy) OwnerIsRestricted bool // true wenn Uploader Rolle "restricted" hat OwnerUsername string // Benutzername des Uploaders; leer wenn kein Besitzer ``` `OwnerIsRestricted` und `OwnerUsername` werden per `LEFT JOIN users` in der List-Query befüllt — kein separater Lookup nötig. ## Store-Layer ### List ```go func (s *MediaStore) List(ctx context.Context, tenantID, ownerUserID string) ([]MediaAsset, error) ``` - `ownerUserID == ""` → alle Tenant-Medien (Admin, screen_user); `OwnerIsRestricted` per LEFT JOIN befüllt - `ownerUserID != ""` → `AND m.created_by_user_id = $2` (Restricted User); kein JOIN nötig ### Create Kein Signatur-Wandel. `MediaAsset.CreatedByUserID` wird vom Handler befüllt und per Struct übergeben. Bestehende Create-Logik bleibt unverändert. ## Handler-Layer ### Upload (API + Manage-Endpoint) Beide Upload-Handler befüllen `CreatedByUserID` mit `user.ID`: - `POST /api/v1/tenants/{tenantSlug}/media` - `POST /manage/{screenSlug}/upload` ### List Handler übergibt an `Store.List()`: - `restricted` → `ownerUserID = u.ID` - alle anderen → `ownerUserID = ""` ### Delete (K3-Check) Aktuell: `u.TenantID == asset.TenantID || u.Role == "admin"` Neu: | Rolle | Bedingung | |---|---| | `admin` | immer erlaubt | | `screen_user` | Tenant-Match reicht (wie bisher) | | `restricted` | Tenant-Match **und** `asset.CreatedByUserID == u.ID` | ## UI ### Admin- und screen_user-Ansicht **Badge für Medien ohne Besitzer** (nur für Admins, da screen_users keine Legacy-Medien hochladen können): ```html Kein Besitzer ``` Wird angezeigt wenn `CreatedByUserID == ""`. Kein Benutzername wird angezeigt. **Toggle: Restricted-Medien ein-/ausblenden** (Admin + screen_user): - Button in der Medienliste: `Restricted-Medien anzeigen` (Bulma `button is-small`) - Jedes Medium von einem Restricted-User erhält `data-owner-restricted="true"` im HTML - Default: diese Elemente sind per CSS ausgeblendet (`display: none`) - Vanilla JS: Toggle-Button wechselt eine CSS-Klasse auf dem Container; Items mit `data-owner-restricted="true"` werden sichtbar/unsichtbar - Kein Page-Reload, kein Server-Request — rein clientseitig **Besitzer-Kennzeichnung** bei Restricted-Medien (Admin + screen_user, wenn sichtbar): ```html {{ .OwnerUsername }} ``` Wird neben dem Medientitel angezeigt, wenn `OwnerIsRestricted == true`. ### Restricted-User-Ansicht Keine strukturellen Änderungen. Die Liste ist serverseitig gefiltert — der User sieht einfach nur eigene Einträge. Kein Toggle-Button sichtbar. ## Nicht im Scope - Übertragung von Medien zwischen Usern - Persistierung der Toggle-Einstellung (wird nicht gespeichert, reset bei Seitenladen) ## Betroffene Dateien | Datei | Änderung | |---|---| | `server/backend/db/migrations/00X_media_owner.sql` | neue Spalte `created_by_user_id` | | `server/backend/internal/store/store.go` | `MediaAsset`-Struct, `List()`-Signatur, `Create()` | | `server/backend/internal/httpapi/manage/media.go` | Upload + Delete + List Handler | | `manage/templates.go` | Badge für Medien ohne Besitzer; Toggle-Button + JS; `data-owner-restricted` Attribut | | `docs/SCHEMA.md` | neue Spalte dokumentieren | | `docs/API-ENDPOINTS.md` | ggf. List-Endpoint-Verhalten dokumentieren |