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("morz_session") 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) }) } // 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) }) } // 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) }