diff --git a/docs/superpowers/specs/2026-03-28-user-spezifische-medien-design.md b/docs/superpowers/specs/2026-03-28-user-spezifische-medien-design.md new file mode 100644 index 0000000..a8d4332 --- /dev/null +++ b/docs/superpowers/specs/2026-03-28-user-spezifische-medien-design.md @@ -0,0 +1,117 @@ +# 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`: + +```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 ein neues Feld: + +```go +CreatedByUserID string // leer = kein Besitzer (legacy) +``` + +## Store-Layer + +### List + +```go +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()`: +- `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-Ansicht: Badge für Medien ohne Besitzer + +In der Medienliste zeigt der Admin bei Einträgen mit leerem `CreatedByUserID` ein Badge: + +```html +Kein Besitzer +``` + +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 |