morz-infoboard/docs/superpowers/specs/2026-03-28-user-spezifische-medien-design.md

4.8 KiB

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:

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:

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

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():

  • restrictedownerUserID = 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):

<span class="tag is-warning is-light">Kein Besitzer</span>

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):

<span class="tag is-info is-light">{{ .OwnerUsername }}</span>

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