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

117 lines
3.6 KiB
Markdown

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