package auth import ( "context" "fmt" "net/http" "github.com/google/uuid" ) // TenantLookup resolves the default tenant for a user. // Defined as an interface to avoid circular dependency with services. type TenantLookup interface { FirstTenantForUser(ctx context.Context, userID uuid.UUID) (*uuid.UUID, error) } // TenantResolver is middleware that resolves the tenant from X-Tenant-ID header // or defaults to the user's first tenant. type TenantResolver struct { lookup TenantLookup } func NewTenantResolver(lookup TenantLookup) *TenantResolver { return &TenantResolver{lookup: lookup} } func (tr *TenantResolver) Resolve(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userID, ok := UserFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } var tenantID uuid.UUID if header := r.Header.Get("X-Tenant-ID"); header != "" { parsed, err := uuid.Parse(header) if err != nil { http.Error(w, fmt.Sprintf("invalid X-Tenant-ID: %v", err), http.StatusBadRequest) return } tenantID = parsed } else { // Default to user's first tenant first, err := tr.lookup.FirstTenantForUser(r.Context(), userID) if err != nil { http.Error(w, fmt.Sprintf("resolving tenant: %v", err), http.StatusInternalServerError) return } if first == nil { http.Error(w, "no tenant found for user", http.StatusBadRequest) return } tenantID = *first } ctx := ContextWithTenantID(r.Context(), tenantID) next.ServeHTTP(w, r.WithContext(ctx)) }) }