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

3.6 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

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 ein neues Feld:

CreatedByUserID string // leer = kein Besitzer (legacy)

Store-Layer

List

func (s *MediaStore) List(ctx context.Context, tenantID, ownerUserID string) ([]MediaAsset, error)
  • ownerUserID == "" → keine Einschränkung, alle Tenant-Medien (Admin, screen_user)
  • ownerUserID != ""AND created_by_user_id = $2 (Restricted User)

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-Ansicht: Badge für Medien ohne Besitzer

In der Medienliste zeigt der Admin bei Einträgen mit leerem CreatedByUserID ein Badge:

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

Medien mit Besitzer erhalten kein Badge — der Benutzername wird nicht angezeigt.

Restricted-User-Ansicht

Keine strukturellen Änderungen. Die Liste ist serverseitig gefiltert — der User sieht einfach weniger Einträge.

Nicht im Scope

  • Anzeige des Besitzernamens bei Medien
  • Übertragung von Medien zwischen Usern
  • Sichtbarkeit von Restricted-User-Medien für screen_users (sie sehen alles im Tenant)

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
docs/SCHEMA.md neue Spalte dokumentieren
docs/API-ENDPOINTS.md ggf. List-Endpoint-Verhalten dokumentieren