Compare commits

..

No commits in common. "b4f36639bfdcdd89d023f8f2ba04d0e98ed54a35" and "cc9ca2cd81a6baf0986683aca962632d03873258" have entirely different histories.

6 changed files with 31 additions and 37 deletions

View file

@ -15,8 +15,6 @@ UtmpMode=user
Environment=HOME=/home/{{ signage_user }}
ExecStartPre=/bin/sleep 3
ExecStart=/usr/bin/startx /usr/local/bin/morz-kiosk -- :0 vt1 -nocursor
ExecStop=/usr/bin/pkill -u {{ signage_user }} chromium
TimeoutStopSec=10
Restart=on-failure
RestartSec=10

View file

@ -112,7 +112,7 @@ func HandleSetScreenOverride(screens *store.ScreenStore, schedules *store.Screen
http.Error(w, "screen not found", http.StatusNotFound)
return
}
if !requireScreenAccess(w, r, screen, screens) {
if !requireScreenAccess(w, r, screen) {
return
}

View file

@ -18,7 +18,7 @@ func HandleUpdateSchedule(screens *store.ScreenStore, schedules *store.ScreenSch
http.Error(w, "screen not found", http.StatusNotFound)
return
}
if !requireScreenAccess(w, r, screen, screens) {
if !requireScreenAccess(w, r, screen) {
return
}

View file

@ -1100,14 +1100,14 @@ const manageTmpl = `<!DOCTYPE html>
<button id="toggle-restricted-btn" class="button is-small is-light"
onclick="toggleRestrictedMedia(this)"
style="font-size:.75rem">
Alles anzeigen
Restricted-Medien anzeigen
</button>
{{end}}
</div>
{{if .Assets}}
<div class="lib-grid">
{{range .Assets}}
<div class="lib-card" data-owner-restricted="{{if eq $.UserRole "restricted"}}false{{else}}{{.OwnerIsRestricted}}{{end}}">
<div class="lib-card" data-owner-restricted="{{.OwnerIsRestricted}}">
<div class="lib-thumb">
{{if eq .Type "image"}}<img src="{{if .StoragePath}}/uploads/{{.StoragePath}}{{else}}{{.OriginalURL}}{{end}}" style="width:100%;height:80px;object-fit:cover" alt="" loading="lazy" onerror="this.style.display='none';this.parentElement.textContent='🖼'">
{{else if eq .Type "video"}}🎬
@ -1414,18 +1414,6 @@ function startUpload() {
setTimeout(function() { img.src = img.dataset.src + '?t=' + Date.now(); }, 4000);
});
})();
function toggleRestrictedMedia(btn) {
var showing = btn.dataset.showing === '1';
showing = !showing;
btn.dataset.showing = showing ? '1' : '0';
document.querySelectorAll('.lib-card[data-owner-restricted="true"]').forEach(function(el) {
el.style.display = showing ? 'flex' : 'none';
});
btn.textContent = showing ? 'Einschränken' : 'Alles anzeigen';
btn.classList.toggle('is-info', showing);
btn.classList.toggle('is-light', !showing);
}
</script>
</body>
</html>`
@ -1718,6 +1706,14 @@ function clearScreenOverride(slug) {
}).catch(function(){});
}
function toggleRestrictedMedia(btn) {
var lib = document.querySelector('.lib-grid');
if (!lib) return;
var showing = lib.classList.toggle('show-restricted');
btn.textContent = showing ? 'Restricted-Medien ausblenden' : 'Restricted-Medien anzeigen';
btn.classList.toggle('is-info', showing);
btn.classList.toggle('is-light', !showing);
}
</script>
</body>
</html>`

View file

@ -43,10 +43,9 @@ func renderTemplate(w http.ResponseWriter, t *template.Template, data any) {
}
// requireScreenAccess prüft, ob der eingeloggte User Zugriff auf den Screen hat.
// Admins dürfen alles. screen_user dürfen Screens ihres Tenants. restricted User
// benötigen zusätzlich einen Eintrag in user_screen_permissions.
// Admins dürfen alles. Tenant-User dürfen nur Screens ihres eigenen Tenants bearbeiten.
// Gibt true zurück wenn Zugriff erlaubt ist; schreibt 403 und gibt false zurück wenn nicht.
func requireScreenAccess(w http.ResponseWriter, r *http.Request, screen *store.Screen, sc *store.ScreenStore) bool {
func requireScreenAccess(w http.ResponseWriter, r *http.Request, screen *store.Screen) bool {
u := reqcontext.UserFromContext(r.Context())
if u == nil {
http.Error(w, "Forbidden", http.StatusForbidden)
@ -55,18 +54,19 @@ func requireScreenAccess(w http.ResponseWriter, r *http.Request, screen *store.S
if u.Role == "admin" {
return true
}
// Tenant-User: Screen muss zum eigenen Tenant gehören.
// Wir vergleichen über TenantSlug→TenantID, aber der Screen hat TenantID.
// Da uns der Tenant-Slug des Users bekannt ist und wir keinen TenantStore
// hier haben, vergleichen wir TenantID des Screens mit dem user.TenantID-Feld.
// store.User hat TenantSlug aber nicht TenantID — deswegen muss der
// aufrufende Handler nach GetBySlug bereits die TenantID des Screens bekannt haben.
// Wir nutzen u.TenantSlug und vertrauen darauf dass der Screen bereits geladen ist.
// Den eigentlichen Vergleich machen wir via TenantID des Screens vs.
// dem TenantID-Feld im User (das über reqcontext gespeichert ist).
if u.TenantID != "" && u.TenantID != screen.TenantID {
http.Error(w, "Forbidden", http.StatusForbidden)
return false
}
// Restricted User: zusätzlich explizite Screen-Berechtigung prüfen.
if u.Role == "restricted" {
ok, err := sc.HasUserScreenAccess(r.Context(), u.ID, screen.ID)
if err != nil || !ok {
http.Error(w, "Forbidden", http.StatusForbidden)
return false
}
}
return true
}
@ -370,7 +370,7 @@ func HandleManageUI(
notifier.RequestScreenshot(screen.Slug)
// K2: Tenant-Isolation — nur eigener Tenant oder Admin.
if !requireScreenAccess(w, r, screen, screens) {
if !requireScreenAccess(w, r, screen) {
return
}
@ -607,7 +607,7 @@ func HandleUploadMediaUI(media *store.MediaStore, screens *store.ScreenStore, up
}
// K2: Tenant-Isolation.
if !requireScreenAccess(w, r, screen, screens) {
if !requireScreenAccess(w, r, screen) {
return
}
@ -694,7 +694,7 @@ func HandleAddItemUI(playlists *store.PlaylistStore, media *store.MediaStore, sc
}
// K2: Tenant-Isolation.
if !requireScreenAccess(w, r, screen, screens) {
if !requireScreenAccess(w, r, screen) {
return
}
@ -760,7 +760,7 @@ func HandleDeleteItemUI(playlists *store.PlaylistStore, screens *store.ScreenSto
http.Error(w, "screen nicht gefunden", http.StatusNotFound)
return
}
if !requireScreenAccess(w, r, screen, screens) {
if !requireScreenAccess(w, r, screen) {
return
}
@ -783,7 +783,7 @@ func HandleReorderUI(playlists *store.PlaylistStore, screens *store.ScreenStore,
return
}
// K2: Tenant-Isolation.
if !requireScreenAccess(w, r, screen, screens) {
if !requireScreenAccess(w, r, screen) {
return
}
playlist, err := playlists.GetByScreen(r.Context(), screen.ID)
@ -822,7 +822,7 @@ func HandleUpdateItemUI(playlists *store.PlaylistStore, screens *store.ScreenSto
http.Error(w, "screen nicht gefunden", http.StatusNotFound)
return
}
if !requireScreenAccess(w, r, screen, screens) {
if !requireScreenAccess(w, r, screen) {
return
}
@ -864,7 +864,7 @@ func HandleDeleteMediaUI(media *store.MediaStore, screens *store.ScreenStore, up
http.Error(w, "screen nicht gefunden", http.StatusNotFound)
return
}
if !requireScreenAccess(w, r, screen, screens) {
if !requireScreenAccess(w, r, screen) {
return
}

View file

@ -386,7 +386,7 @@ func (s *MediaStore) List(ctx context.Context, tenantID, ownerUserID string) ([]
if ownerUserID != "" {
rows, err = s.pool.Query(ctx, base+` AND m.created_by_user_id=$2 ORDER BY m.created_at DESC`, tenantID, ownerUserID)
} else {
rows, err = s.pool.Query(ctx, base+` ORDER BY u.username NULLS LAST, m.created_at DESC`, tenantID)
rows, err = s.pool.Query(ctx, base+` ORDER BY m.created_at DESC`, tenantID)
}
if err != nil {
return nil, err