# 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 |