Compare commits

...

6 commits

Author SHA1 Message Date
Jesko Anschütz
b4f36639bf fix(kiosk): Chromium-Stop beschleunigen (90s → 10s)
Chromium reagiert im Kiosk-Modus nicht auf SIGTERM, sodass systemd
90 Sekunden auf den TimeoutStop wartete und dann SIGKILL senden musste.
ExecStop killt Chromium jetzt explizit per pkill, TimeoutStopSec=10.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 16:06:10 +02:00
Jesko Anschütz
dd96b20263 feat(media): Medien nach Besitzer dann created_at sortieren 2026-03-28 10:49:32 +01:00
Jesko Anschütz
251e1fb15e fix(ui): toggleRestrictedMedia war im falschen Template (screenOverviewTmpl statt manageTmpl) 2026-03-28 10:37:39 +01:00
Jesko Anschütz
e99cac4719 fix(ui): Toggle-Button 'Alles anzeigen' — JS direkt statt CSS-Klasse 2026-03-28 10:24:59 +01:00
Jesko Anschütz
3a0ac13faa fix(auth): restricted User können nur zugewiesene Screens aufrufen
requireScreenAccess prüft jetzt für Rolle 'restricted' zusätzlich
ob ein Eintrag in user_screen_permissions existiert. Tenant-Match
allein reichte bisher nicht — restricted User konnten alle Screens
des Tenants aufrufen.
2026-03-28 10:17:29 +01:00
Jesko Anschütz
3ebeaa70e1 fix(ui): restricted User sehen eigene Medien nicht (CSS-Hidden-Bug)
data-owner-restricted wurde auch für restricted User selbst auf 'true'
gesetzt, wodurch deren eigene Medien durch CSS ausgeblendet wurden.
Fix: Attribut ist für restricted User immer 'false'.
2026-03-28 09:34:13 +01:00
6 changed files with 37 additions and 31 deletions

View file

@ -15,6 +15,8 @@ UtmpMode=user
Environment=HOME=/home/{{ signage_user }} Environment=HOME=/home/{{ signage_user }}
ExecStartPre=/bin/sleep 3 ExecStartPre=/bin/sleep 3
ExecStart=/usr/bin/startx /usr/local/bin/morz-kiosk -- :0 vt1 -nocursor 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 Restart=on-failure
RestartSec=10 RestartSec=10

View file

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

View file

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

View file

@ -1100,14 +1100,14 @@ const manageTmpl = `<!DOCTYPE html>
<button id="toggle-restricted-btn" class="button is-small is-light" <button id="toggle-restricted-btn" class="button is-small is-light"
onclick="toggleRestrictedMedia(this)" onclick="toggleRestrictedMedia(this)"
style="font-size:.75rem"> style="font-size:.75rem">
Restricted-Medien anzeigen Alles anzeigen
</button> </button>
{{end}} {{end}}
</div> </div>
{{if .Assets}} {{if .Assets}}
<div class="lib-grid"> <div class="lib-grid">
{{range .Assets}} {{range .Assets}}
<div class="lib-card" data-owner-restricted="{{.OwnerIsRestricted}}"> <div class="lib-card" data-owner-restricted="{{if eq $.UserRole "restricted"}}false{{else}}{{.OwnerIsRestricted}}{{end}}">
<div class="lib-thumb"> <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='🖼'"> {{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"}}🎬 {{else if eq .Type "video"}}🎬
@ -1414,6 +1414,18 @@ function startUpload() {
setTimeout(function() { img.src = img.dataset.src + '?t=' + Date.now(); }, 4000); 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> </script>
</body> </body>
</html>` </html>`
@ -1706,14 +1718,6 @@ function clearScreenOverride(slug) {
}).catch(function(){}); }).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> </script>
</body> </body>
</html>` </html>`

View file

@ -43,9 +43,10 @@ func renderTemplate(w http.ResponseWriter, t *template.Template, data any) {
} }
// requireScreenAccess prüft, ob der eingeloggte User Zugriff auf den Screen hat. // requireScreenAccess prüft, ob der eingeloggte User Zugriff auf den Screen hat.
// Admins dürfen alles. Tenant-User dürfen nur Screens ihres eigenen Tenants bearbeiten. // Admins dürfen alles. screen_user dürfen Screens ihres Tenants. restricted User
// benötigen zusätzlich einen Eintrag in user_screen_permissions.
// Gibt true zurück wenn Zugriff erlaubt ist; schreibt 403 und gibt false zurück wenn nicht. // 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) bool { func requireScreenAccess(w http.ResponseWriter, r *http.Request, screen *store.Screen, sc *store.ScreenStore) bool {
u := reqcontext.UserFromContext(r.Context()) u := reqcontext.UserFromContext(r.Context())
if u == nil { if u == nil {
http.Error(w, "Forbidden", http.StatusForbidden) http.Error(w, "Forbidden", http.StatusForbidden)
@ -54,19 +55,18 @@ func requireScreenAccess(w http.ResponseWriter, r *http.Request, screen *store.S
if u.Role == "admin" { if u.Role == "admin" {
return true 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 { if u.TenantID != "" && u.TenantID != screen.TenantID {
http.Error(w, "Forbidden", http.StatusForbidden) http.Error(w, "Forbidden", http.StatusForbidden)
return false 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 return true
} }
@ -370,7 +370,7 @@ func HandleManageUI(
notifier.RequestScreenshot(screen.Slug) notifier.RequestScreenshot(screen.Slug)
// K2: Tenant-Isolation — nur eigener Tenant oder Admin. // K2: Tenant-Isolation — nur eigener Tenant oder Admin.
if !requireScreenAccess(w, r, screen) { if !requireScreenAccess(w, r, screen, screens) {
return return
} }
@ -607,7 +607,7 @@ func HandleUploadMediaUI(media *store.MediaStore, screens *store.ScreenStore, up
} }
// K2: Tenant-Isolation. // K2: Tenant-Isolation.
if !requireScreenAccess(w, r, screen) { if !requireScreenAccess(w, r, screen, screens) {
return return
} }
@ -694,7 +694,7 @@ func HandleAddItemUI(playlists *store.PlaylistStore, media *store.MediaStore, sc
} }
// K2: Tenant-Isolation. // K2: Tenant-Isolation.
if !requireScreenAccess(w, r, screen) { if !requireScreenAccess(w, r, screen, screens) {
return return
} }
@ -760,7 +760,7 @@ func HandleDeleteItemUI(playlists *store.PlaylistStore, screens *store.ScreenSto
http.Error(w, "screen nicht gefunden", http.StatusNotFound) http.Error(w, "screen nicht gefunden", http.StatusNotFound)
return return
} }
if !requireScreenAccess(w, r, screen) { if !requireScreenAccess(w, r, screen, screens) {
return return
} }
@ -783,7 +783,7 @@ func HandleReorderUI(playlists *store.PlaylistStore, screens *store.ScreenStore,
return return
} }
// K2: Tenant-Isolation. // K2: Tenant-Isolation.
if !requireScreenAccess(w, r, screen) { if !requireScreenAccess(w, r, screen, screens) {
return return
} }
playlist, err := playlists.GetByScreen(r.Context(), screen.ID) 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) http.Error(w, "screen nicht gefunden", http.StatusNotFound)
return return
} }
if !requireScreenAccess(w, r, screen) { if !requireScreenAccess(w, r, screen, screens) {
return return
} }
@ -864,7 +864,7 @@ func HandleDeleteMediaUI(media *store.MediaStore, screens *store.ScreenStore, up
http.Error(w, "screen nicht gefunden", http.StatusNotFound) http.Error(w, "screen nicht gefunden", http.StatusNotFound)
return return
} }
if !requireScreenAccess(w, r, screen) { if !requireScreenAccess(w, r, screen, screens) {
return return
} }

View file

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