package httpapi import ( "context" "net/http" "net/url" "git.az-it.net/az/morz-infoboard/server/backend/internal/reqcontext" "git.az-it.net/az/morz-infoboard/server/backend/internal/store" ) // UserFromContext returns the authenticated *store.User stored in ctx, // or nil if none is present. // It delegates to reqcontext so that sub-packages (e.g. manage) can share // the same context key without creating an import cycle. func UserFromContext(ctx context.Context) *store.User { return reqcontext.UserFromContext(ctx) } // RequireAuth returns middleware that validates the morz_session cookie. // On success it stores the *store.User in the request context and calls next. // On failure it redirects to /login?next=. func RequireAuth(authStore *store.AuthStore) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie(reqcontext.SessionCookieName) if err != nil { redirectToLogin(w, r) return } user, err := authStore.GetSessionUser(r.Context(), cookie.Value) if err != nil { redirectToLogin(w, r) return } ctx := reqcontext.WithUser(r.Context(), user) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // RequireAdmin is middleware that allows only users with role "admin". // It must be chained after RequireAuth (so a user is present in context). // On failure it responds with 403 Forbidden. func RequireAdmin(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := UserFromContext(r.Context()) if user == nil || user.Role != "admin" { http.Error(w, "Forbidden", http.StatusForbidden) return } next.ServeHTTP(w, r) }) } // RequireNotRestricted is middleware that blocks users with role "restricted". // It must be chained after RequireAuth (so a user is present in context). // On failure it responds with 403 Forbidden. func RequireNotRestricted(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := UserFromContext(r.Context()) if user != nil && user.Role == "restricted" { http.Error(w, "Forbidden", http.StatusForbidden) return } next.ServeHTTP(w, r) }) } // RequireTenantAccess is middleware that allows access only when the // authenticated user belongs to the tenant identified by the {tenantSlug} // path value, or when the user has role "admin" (admins can access everything). // It must be chained after RequireAuth. func RequireTenantAccess(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := UserFromContext(r.Context()) if user == nil { http.Error(w, "Forbidden", http.StatusForbidden) return } // Admins bypass tenant isolation. if user.Role == "admin" { next.ServeHTTP(w, r) return } tenantSlug := r.PathValue("tenantSlug") // An empty tenantSlug means the route was registered without a // {tenantSlug} parameter — that is a configuration error. Deny // access rather than silently granting it to every logged-in user. if tenantSlug == "" || user.TenantSlug != tenantSlug { http.Error(w, "Forbidden", http.StatusForbidden) return } next.ServeHTTP(w, r) }) } // RequireScreenAccess returns middleware that enforces per-screen access control. // Admins bypass the check. Screen-Users must have an explicit entry in // user_screen_permissions for the screen identified by the {screenSlug} path // value. The screenStore is used to look up the screen and check permissions. // Must be chained after RequireAuth. func RequireScreenAccess(screenStore *store.ScreenStore) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := UserFromContext(r.Context()) if user == nil { http.Error(w, "Forbidden", http.StatusForbidden) return } // Admins always have access. if user.Role == "admin" { next.ServeHTTP(w, r) return } screenSlug := r.PathValue("screenSlug") if screenSlug == "" { http.Error(w, "Forbidden", http.StatusForbidden) return } screen, err := screenStore.GetBySlug(r.Context(), screenSlug) if err != nil { http.Error(w, "Screen nicht gefunden", http.StatusNotFound) return } ok, err := screenStore.HasUserScreenAccess(r.Context(), user.ID, screen.ID) if err != nil || !ok { http.Error(w, "Forbidden", http.StatusForbidden) return } next.ServeHTTP(w, r) }) } } // chain applies a list of middleware to a handler, wrapping outermost first. // chain(m1, m2, m3)(h) == m1(m2(m3(h))) func chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler { for i := len(middlewares) - 1; i >= 0; i-- { h = middlewares[i](h) } return h } // redirectToLogin issues a 303 redirect to /login with the current path as ?next=. func redirectToLogin(w http.ResponseWriter, r *http.Request) { target := "/login?next=" + url.QueryEscape(r.URL.RequestURI()) http.Redirect(w, r, target, http.StatusSeeOther) }