From d1d86126c88e609cee475259992c09a19cc1adcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesko=20Ansch=C3=BCtz?= Date: Mon, 23 Mar 2026 22:06:05 +0100 Subject: [PATCH] Feature: Screen-User-Verwaltung mit rollenbasiertem Zugriff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neue Rolle screen_user: User können sich einloggen und nur ihre zugeordneten Bildschirme verwalten. Admins behalten vollen Zugriff. - Migration 003: users.role-Spalte + user_screen_permissions (M:N) - Store: CreateScreenUser, ListScreenUsers, DeleteUser, GetAccessibleScreens, HasUserScreenAccess, AddUserToScreen, RemoveUserFromScreen, GetScreenUsers - Middleware: RequireScreenAccess enforces screen-level access für alle /manage/{screenSlug}-Routen - 4 neue Admin-Handler: CreateScreenUser, DeleteScreenUser, AddUserToScreen, RemoveUserFromScreen (+4 Routes) - Admin-UI: Tab "Benutzer" (anlegen/löschen) + Screen-User-Modal (User zuordnen/entfernen) direkt in der Bildschirm-Tabelle - Login: screen_user wird nach Login zum ersten zugänglichen Screen weitergeleitet; kein Zugang zu /admin Co-Authored-By: Claude Haiku 4.5 --- .../003_user_screen_permissions.sql | 22 ++ .../backend/internal/httpapi/manage/auth.go | 39 ++- .../internal/httpapi/manage/templates.go | 281 ++++++++++++++++-- server/backend/internal/httpapi/manage/ui.go | 150 +++++++++- server/backend/internal/httpapi/middleware.go | 42 +++ server/backend/internal/httpapi/router.go | 36 ++- server/backend/internal/store/auth.go | 64 ++++ server/backend/internal/store/store.go | 91 ++++++ 8 files changed, 683 insertions(+), 42 deletions(-) create mode 100644 server/backend/internal/db/migrations/003_user_screen_permissions.sql diff --git a/server/backend/internal/db/migrations/003_user_screen_permissions.sql b/server/backend/internal/db/migrations/003_user_screen_permissions.sql new file mode 100644 index 0000000..443e90a --- /dev/null +++ b/server/backend/internal/db/migrations/003_user_screen_permissions.sql @@ -0,0 +1,22 @@ +-- Migration 003: Screen-User-Berechtigungssystem +-- Fügt die Rolle 'screen_user' und die M:N Tabelle user_screen_permissions hinzu. + +-- Neue Spalte 'role' in users (DEFAULT 'screen_user' für zukünftige Nutzer). +ALTER TABLE users ADD COLUMN IF NOT EXISTS role TEXT DEFAULT 'screen_user'; + +-- Bestehende Admins auf 'admin' setzen (alle User im Standard-Tenant morz). +UPDATE users SET role = 'admin' +WHERE tenant_id = (SELECT id FROM tenants WHERE slug = 'morz') + AND role IS DISTINCT FROM 'admin'; + +-- M:N-Tabelle: welche User dürfen welche Screens verwalten. +CREATE TABLE IF NOT EXISTS user_screen_permissions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + screen_id TEXT NOT NULL REFERENCES screens(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(user_id, screen_id) +); + +CREATE INDEX IF NOT EXISTS idx_user_screen_perms_user ON user_screen_permissions(user_id); +CREATE INDEX IF NOT EXISTS idx_user_screen_perms_screen ON user_screen_permissions(screen_id); diff --git a/server/backend/internal/httpapi/manage/auth.go b/server/backend/internal/httpapi/manage/auth.go index 0d4828d..d82af3b 100644 --- a/server/backend/internal/httpapi/manage/auth.go +++ b/server/backend/internal/httpapi/manage/auth.go @@ -13,6 +13,17 @@ import ( "golang.org/x/crypto/bcrypt" ) +// handleScreenUserRedirect looks up accessible screens for a screen_user and +// redirects to the first one. If none exist, it redirects to an error page. +func handleScreenUserRedirect(w http.ResponseWriter, r *http.Request, screenStore *store.ScreenStore, user *store.User) { + screens, err := screenStore.GetAccessibleScreens(r.Context(), user.ID) + if err != nil || len(screens) == 0 { + http.Redirect(w, r, "/login?error=no_screens", http.StatusSeeOther) + return + } + http.Redirect(w, r, "/manage/"+screens[0].Slug, http.StatusSeeOther) +} + const sessionTTL = 8 * time.Hour // sessionCookieName ist ein Alias auf die zentrale Konstante (V5). @@ -26,20 +37,24 @@ type loginData struct { } // HandleLoginUI renders the login form (GET /login). -// If a valid session cookie is already present, the user is redirected to /admin -// (or the tenant dashboard once tenants are wired up in Phase 3). -func HandleLoginUI(authStore *store.AuthStore, cfg config.Config) http.HandlerFunc { +// If a valid session cookie is already present, the user is redirected based on role. +func HandleLoginUI(authStore *store.AuthStore, screenStore *store.ScreenStore, cfg config.Config) http.HandlerFunc { tmpl := template.Must(template.New("login").Parse(loginTmpl)) return func(w http.ResponseWriter, r *http.Request) { // Redirect if already logged in. if cookie, err := r.Cookie(sessionCookieName); err == nil { if u, err := authStore.GetSessionUser(r.Context(), cookie.Value); err == nil { - if u.Role == "admin" { - http.Redirect(w, r, "/admin", http.StatusSeeOther) - } else if u.TenantSlug != "" { - http.Redirect(w, r, "/tenant/"+u.TenantSlug+"/dashboard", http.StatusSeeOther) - } else { + switch u.Role { + case "admin": http.Redirect(w, r, "/admin", http.StatusSeeOther) + case "screen_user": + handleScreenUserRedirect(w, r, screenStore, u) + default: + if u.TenantSlug != "" { + http.Redirect(w, r, "/tenant/"+u.TenantSlug+"/dashboard", http.StatusSeeOther) + } else { + http.Redirect(w, r, "/admin", http.StatusSeeOther) + } } return } @@ -58,7 +73,7 @@ func HandleLoginUI(authStore *store.AuthStore, cfg config.Config) http.HandlerFu // HandleLoginPost handles form submission (POST /login). // It validates credentials, creates a session, sets the session cookie and // redirects the user based on their role or the ?next= parameter. -func HandleLoginPost(authStore *store.AuthStore, cfg config.Config) http.HandlerFunc { +func HandleLoginPost(authStore *store.AuthStore, screenStore *store.ScreenStore, cfg config.Config) http.HandlerFunc { tmpl := template.Must(template.New("login").Parse(loginTmpl)) renderError := func(w http.ResponseWriter, next, msg string) { @@ -122,12 +137,14 @@ func HandleLoginPost(authStore *store.AuthStore, cfg config.Config) http.Handler } switch user.Role { case "admin": - http.Redirect(w, r, "/manage/", http.StatusSeeOther) + http.Redirect(w, r, "/admin", http.StatusSeeOther) + case "screen_user": + handleScreenUserRedirect(w, r, screenStore, user) default: if user.TenantSlug != "" { http.Redirect(w, r, "/tenant/"+user.TenantSlug+"/dashboard", http.StatusSeeOther) } else { - http.Redirect(w, r, "/manage/", http.StatusSeeOther) + http.Redirect(w, r, "/admin", http.StatusSeeOther) } } } diff --git a/server/backend/internal/httpapi/manage/templates.go b/server/backend/internal/httpapi/manage/templates.go index 5cba0b1..ea1c202 100644 --- a/server/backend/internal/httpapi/manage/templates.go +++ b/server/backend/internal/httpapi/manage/templates.go @@ -261,7 +261,7 @@ const adminTmpl = ` - + + + + + + + +
-
+ +
+ +
+ + + +
+

Bildschirme

{{if .Screens}}
@@ -348,16 +447,27 @@ document.addEventListener('keydown', function(e) { Slug Format Status + Benutzer Aktionen {{range .Screens}} + {{$users := index $.ScreenUserMap .ID}} {{.Name}} {{.Slug}} {{orientationLabel .Orientation}} + + {{$screenID := .ID}} + {{$screenName := .Name}} + + Playlist verwalten   @@ -374,9 +484,9 @@ document.addEventListener('keydown', function(e) { {{else}}

Noch keine Bildschirme angelegt.

{{end}} -
-
+
+

Neuen Bildschirm einrichten

Fülle die Angaben aus. Der Bildschirm wird im Backend angelegt und du erhältst @@ -435,12 +545,9 @@ document.addEventListener('keydown', function(e) {

-
-
-

Bestehenden Screen manuell anlegen

-
- Nur DB-Eintrag, kein Deployment (aufklappen) +
+ Bestehenden Screen manuell anlegen (nur DB-Eintrag, kein Deployment)
@@ -482,11 +589,151 @@ document.addEventListener('keydown', function(e) {
-
+ +
+ + +
+ +

Screen-Benutzer

+

Screen-Benutzer können sich einloggen und nur ihre zugeordneten Bildschirme verwalten.

+ + {{if .ScreenUsers}} + + + + + + + + + + {{range .ScreenUsers}} + + + + + + {{end}} + +
BenutzernameErstelltAktionen
{{.Username}}{{.CreatedAt.Format "02.01.2006 15:04"}} + +
+ {{else}} +

Noch keine Screen-Benutzer angelegt.

+ {{end}} + +
+

Neuen Benutzer anlegen

+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ + +
+
+
+
+ +
+ + +