package membership import ( "context" "encoding/json" "log/slog" "net/http" "proxui/backend/internal/auth" ) type Resolver interface { ResolveTenantMembership(ctx context.Context, profileID string, tenantID string) (Membership, bool, error) } type TenantIDExtractor func(*http.Request) (string, bool) type Middleware struct { resolver Resolver logger *slog.Logger } func NewMiddleware(resolver Resolver, logger *slog.Logger) Middleware { return Middleware{ resolver: resolver, logger: logger, } } func (m Middleware) RequireTenantMembership(extractTenantID TenantIDExtractor, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := auth.PrincipalFromRequest(r) if !ok { writeError(w, http.StatusUnauthorized, "unauthorized") return } tenantID, ok := extractTenantID(r) if !ok || tenantID == "" { writeError(w, http.StatusBadRequest, "tenant_id_required") return } membership, found, err := m.resolver.ResolveTenantMembership(r.Context(), principal.Subject, tenantID) if err != nil { m.logger.Error("membership resolve failed", "error", err) writeError(w, http.StatusInternalServerError, "membership_resolve_failed") return } if !found { writeError(w, http.StatusForbidden, "tenant_forbidden") return } next.ServeHTTP(w, r.WithContext(ContextWithMembership(r.Context(), membership))) }) } func writeError(w http.ResponseWriter, status int, message string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(map[string]string{"error": message}) }