package repo import ( "encoding/json" "fmt" "net/http" "net/url" "strconv" "strings" "time" "github.com/artifacthub/hub/internal/handlers/helpers" "github.com/artifacthub/hub/internal/hub" "github.com/go-chi/chi/v5" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/viper" ) const ( logoSVG = `` searchDefaultLimit = 20 searchMaxLimit = 60 ) // Handlers represents a group of http handlers in charge of handling // repositories operations. type Handlers struct { cfg *viper.Viper repoManager hub.RepositoryManager logger zerolog.Logger } // NewHandlers creates a new Handlers instance. func NewHandlers(cfg *viper.Viper, repoManager hub.RepositoryManager) *Handlers { return &Handlers{ cfg: cfg, repoManager: repoManager, logger: log.With().Str("handlers", "repo").Logger(), } } // Add is an http handler that adds the provided repository to the database. func (h *Handlers) Add(w http.ResponseWriter, r *http.Request) { orgName := chi.URLParam(r, "orgName") repo := &hub.Repository{} if err := json.NewDecoder(r.Body).Decode(&repo); err != nil { h.logger.Error().Err(err).Str("method", "Add").Msg(hub.ErrInvalidInput.Error()) helpers.RenderErrorJSON(w, hub.ErrInvalidInput) return } if err := h.repoManager.Add(r.Context(), orgName, repo); err != nil { h.logger.Error().Err(err).Str("method", "Add").Send() helpers.RenderErrorJSON(w, err) return } w.WriteHeader(http.StatusCreated) } // Badge is an http handler that returns the information needed to render the // repository badge. func (h *Handlers) Badge(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "color": strings.TrimPrefix(h.cfg.GetString("theme.colors.secondary"), "#"), "label": h.cfg.GetString("theme.siteName"), "labelColor": strings.TrimPrefix(h.cfg.GetString("theme.colors.primary"), "#"), "logoSvg": logoSVG, "logoWidth": 18, "message": chi.URLParam(r, "repoName"), "schemaVersion": 1, "style": "flat", } dataJSON, err := json.Marshal(data) if err != nil { h.logger.Error().Err(err).Str("method", "Badge").Send() helpers.RenderErrorJSON(w, err) return } helpers.RenderJSON(w, dataJSON, helpers.DefaultAPICacheMaxAge, http.StatusOK) } // CheckAvailability is an http handler that checks the availability of a given // value for the provided resource kind. func (h *Handlers) CheckAvailability(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", helpers.BuildCacheControlHeader(0)) resourceKind := chi.URLParam(r, "resourceKind") value := r.FormValue("v") available, err := h.repoManager.CheckAvailability(r.Context(), resourceKind, value) if err != nil { h.logger.Error().Err(err).Str("method", "CheckAvailability").Send() helpers.RenderErrorJSON(w, err) return } if available { helpers.RenderErrorWithCodeJSON(w, nil, http.StatusNotFound) return } w.WriteHeader(http.StatusNoContent) } // ClaimOwnership is an http handler used to claim the ownership of a given // repository, transferring it to the selected entity if the requesting user // has permissions to do so. func (h *Handlers) ClaimOwnership(w http.ResponseWriter, r *http.Request) { repoName := chi.URLParam(r, "repoName") orgName := r.FormValue("org") if err := h.repoManager.ClaimOwnership(r.Context(), repoName, orgName); err != nil { h.logger.Error().Err(err).Str("method", "ClaimOwnership").Send() helpers.RenderErrorJSON(w, err) return } w.WriteHeader(http.StatusNoContent) } // Delete is an http handler that deletes the provided repository from the // database. func (h *Handlers) Delete(w http.ResponseWriter, r *http.Request) { repoName := chi.URLParam(r, "repoName") if err := h.repoManager.Delete(r.Context(), repoName); err != nil { h.logger.Error().Err(err).Str("method", "Delete").Send() helpers.RenderErrorJSON(w, err) return } w.WriteHeader(http.StatusNoContent) } // Search is an http handler used to search for repositories in the hub // database. func (h *Handlers) Search(w http.ResponseWriter, r *http.Request) { input, err := buildSearchInput(r.URL.Query()) if err != nil { err = fmt.Errorf("%w: %s", hub.ErrInvalidInput, err.Error()) h.logger.Error().Err(err).Str("query", r.URL.RawQuery).Str("method", "Search").Msg("invalid query") helpers.RenderErrorJSON(w, err) return } result, err := h.repoManager.SearchJSON(r.Context(), input) if err != nil { h.logger.Error().Err(err).Str("query", r.URL.RawQuery).Str("method", "Search").Send() helpers.RenderErrorJSON(w, err) return } w.Header().Set(helpers.PaginationTotalCount, strconv.Itoa(result.TotalCount)) cacheMaxAge := 1 * time.Hour if r.Context().Value(hub.UserIDKey) != nil { cacheMaxAge = 0 } helpers.RenderJSON(w, result.Data, cacheMaxAge, http.StatusOK) } // Transfer is an http handler that transfers the provided repository to a // different owner. func (h *Handlers) Transfer(w http.ResponseWriter, r *http.Request) { repoName := chi.URLParam(r, "repoName") orgName := r.FormValue("org") if err := h.repoManager.Transfer(r.Context(), repoName, orgName, false); err != nil { h.logger.Error().Err(err).Str("method", "Transfer").Send() helpers.RenderErrorJSON(w, err) return } w.WriteHeader(http.StatusNoContent) } // Update is an http handler that updates the provided repository in the // database. func (h *Handlers) Update(w http.ResponseWriter, r *http.Request) { repo := &hub.Repository{} if err := json.NewDecoder(r.Body).Decode(&repo); err != nil { h.logger.Error().Err(err).Str("method", "Update").Msg("invalid repository") helpers.RenderErrorJSON(w, hub.ErrInvalidInput) return } repo.Name = chi.URLParam(r, "repoName") if err := h.repoManager.Update(r.Context(), repo); err != nil { h.logger.Error().Err(err).Str("method", "Update").Send() helpers.RenderErrorJSON(w, err) return } w.WriteHeader(http.StatusNoContent) } // buildSearchInput builds a packages search query from a map of query string // values, validating them as they are extracted. func buildSearchInput(qs url.Values) (*hub.SearchRepositoryInput, error) { // Kinds kinds := make([]hub.RepositoryKind, 0, len(qs["kind"])) for _, kindStr := range qs["kind"] { kind, err := strconv.Atoi(kindStr) if err != nil { return nil, fmt.Errorf("invalid kind: %s", kindStr) } kinds = append(kinds, hub.RepositoryKind(kind)) } // Limit var limit int if qs.Get("limit") != "" { var err error limit, err = strconv.Atoi(qs.Get("limit")) if err != nil { return nil, fmt.Errorf("invalid limit: %s", qs.Get("limit")) } if limit > searchMaxLimit { return nil, fmt.Errorf("invalid limit: %s (max: %d)", qs.Get("limit"), searchMaxLimit) } } else { limit = searchDefaultLimit } // Offset var offset int if qs.Get("offset") != "" { var err error offset, err = strconv.Atoi(qs.Get("offset")) if err != nil { return nil, fmt.Errorf("invalid offset: %s", qs.Get("offset")) } } return &hub.SearchRepositoryInput{ Name: qs.Get("name"), URL: qs.Get("url"), Kinds: kinds, Orgs: qs["org"], Users: qs["user"], IncludeCredentials: false, Limit: limit, Offset: offset, }, nil }