diff --git a/Makefile b/Makefile index fead562a..67811ca6 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ certs: openssl req -x509 -newkey rsa:4096 -keyout .certs/registry.local -out .certs/registry.local.crt -sha256 -days 365 \ -subj "/C=US/ST=Oregon/L=Portland/O=Company Name/OU=Org/CN=registry.local" -nodes -load_dummy_users: +dummy_users: sh ./scripts/load_dummy_users.sh reset: diff --git a/auth/reset_password.go b/auth/reset_password.go index c194269a..2a717fa8 100644 --- a/auth/reset_password.go +++ b/auth/reset_password.go @@ -234,9 +234,13 @@ func (a *auth) ForgotPassword(ctx echo.Context) error { } if !user.IsActive { - return ctx.JSON(http.StatusUnauthorized, echo.Map{ - "message": "account is inactive, please check your email and verify your account", + err = fmt.Errorf("account is inactive, please check your email and verify your account") + echoErr := ctx.JSON(http.StatusUnauthorized, echo.Map{ + "message": err.Error(), }) + + a.logger.Log(ctx, err).Send() + return echoErr } opts := &WebLoginJWTOptions{ diff --git a/go.mod b/go.mod index 22e674ed..2ef42333 100644 --- a/go.mod +++ b/go.mod @@ -30,8 +30,10 @@ require ( github.com/labstack/echo-jwt/v4 v4.2.0 github.com/labstack/echo/v4 v4.11.4 github.com/multiformats/go-multiaddr v0.12.0 + github.com/oklog/ulid/v2 v2.1.0 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0-rc5 + github.com/rs/cors v1.10.1 github.com/rs/zerolog v1.31.0 github.com/sendgrid/sendgrid-go v3.14.0+incompatible github.com/spf13/afero v1.11.0 @@ -158,7 +160,6 @@ require ( github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-multistream v0.5.0 // indirect github.com/multiformats/go-varint v0.0.7 // indirect - github.com/oklog/ulid/v2 v2.1.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect @@ -170,7 +171,6 @@ require ( github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/rs/cors v1.10.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect diff --git a/go.sum b/go.sum index dbf95d9a..e2919054 100644 --- a/go.sum +++ b/go.sum @@ -663,8 +663,6 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= -github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= diff --git a/registry/v2/extensions/analytics.go b/registry/v2/extensions/analytics.go index 52ddf5dc..93b76f7f 100644 --- a/registry/v2/extensions/analytics.go +++ b/registry/v2/extensions/analytics.go @@ -86,3 +86,27 @@ func (ext *extension) RemoveRepositoryFromFavorites(ctx echo.Context) error { ext.logger.Log(ctx, nil).Send() return echoErr } + +func (ext *extension) ListFavoriteRepositories(ctx echo.Context) error { + ctx.Set(types.HandlerStartTime, time.Now()) + + user, ok := ctx.Get(string(types.UserContextKey)).(*types.User) + if !ok { + err := fmt.Errorf("missing authentication credentials") + echoErr := ctx.JSON(http.StatusForbidden, echo.Map{ + "error": err.Error(), + }) + ext.logger.Log(ctx, err).Send() + return echoErr + } + + repos, err := ext.store.ListFavoriteRepositories(ctx.Request().Context(), user.ID) + if err != nil { + repos = make([]*types.ContainerImageRepository, 0) + } + + echoErr := ctx.JSON(http.StatusOK, repos) + + ext.logger.Log(ctx, nil).Send() + return echoErr +} diff --git a/registry/v2/extensions/catalog_detail.go b/registry/v2/extensions/catalog_detail.go index b2f4d985..8db429d3 100644 --- a/registry/v2/extensions/catalog_detail.go +++ b/registry/v2/extensions/catalog_detail.go @@ -20,6 +20,7 @@ type Extenion interface { GetUserCatalog(ctx echo.Context) error AddRepositoryToFavorites(ctx echo.Context) error RemoveRepositoryFromFavorites(ctx echo.Context) error + ListFavoriteRepositories(ctx echo.Context) error } type extension struct { @@ -198,15 +199,21 @@ func (ext *extension) PublicCatalog(ctx echo.Context) error { repositories, total, err := ext.store.GetPublicRepositories(ctx.Request().Context(), pageSize, offset) if err != nil { - return ctx.JSON(http.StatusInternalServerError, echo.Map{ + echoErr := ctx.JSON(http.StatusInternalServerError, echo.Map{ "error": err.Error(), }) + + ext.logger.Log(ctx, err).Send() + return echoErr } - return ctx.JSON(http.StatusOK, echo.Map{ + echoErr := ctx.JSON(http.StatusOK, echo.Map{ "repositories": repositories, "total": total, }) + + ext.logger.Log(ctx, nil).Send() + return echoErr } func (ext *extension) GetUserCatalog(ctx echo.Context) error { @@ -267,13 +274,19 @@ func (ext *extension) GetUserCatalog(ctx echo.Context) error { offset, ) if err != nil { - return ctx.JSON(http.StatusInternalServerError, echo.Map{ + echoErr := ctx.JSON(http.StatusInternalServerError, echo.Map{ "error": err.Error(), }) + + ext.logger.Log(ctx, err).Send() + return echoErr } - return ctx.JSON(http.StatusOK, echo.Map{ + echoErr := ctx.JSON(http.StatusOK, echo.Map{ "repositories": repositories, "total": total, }) + + ext.logger.Log(ctx, nil).Send() + return echoErr } diff --git a/registry/v2/registry.go b/registry/v2/registry.go index 98c6a505..0a8c15c8 100644 --- a/registry/v2/registry.go +++ b/registry/v2/registry.go @@ -86,8 +86,9 @@ func (r *registry) ManifestExists(ctx echo.Context) error { } errMsg := common.RegistryErrorResponse(RegistryErrorCodeManifestBlobUnknown, err.Error(), details) - r.logger.Log(ctx, fmt.Errorf("%s", errMsg)).Send() - return ctx.NoContent(http.StatusNotFound) + echoErr := ctx.JSONBlob(http.StatusNotFound, errMsg.Bytes()) + r.logger.Log(ctx, errMsg).Send() + return echoErr } ctx.Response().Header().Set("Content-Length", fmt.Sprintf("%d", manifest.Size)) @@ -792,10 +793,13 @@ func (r *registry) PushManifest(ctx echo.Context) error { buf := &bytes.Buffer{} _, err = io.Copy(buf, ctx.Request().Body) if err != nil { - return ctx.JSON(http.StatusBadRequest, echo.Map{ + echoErr := ctx.JSON(http.StatusBadRequest, echo.Map{ "error": err.Error(), "message": "failed in push manifest while io Copy", }) + + r.logger.Log(ctx, nil).Send() + return echoErr } defer ctx.Request().Body.Close() diff --git a/registry/v2/repository.go b/registry/v2/repository.go index b2bfcbd8..3c8ff7b1 100644 --- a/registry/v2/repository.go +++ b/registry/v2/repository.go @@ -31,18 +31,24 @@ func (r *registry) CreateRepository(ctx echo.Context) error { var body CreateRepositoryRequest err := json.NewDecoder(ctx.Request().Body).Decode(&body) if err != nil { - return ctx.JSON(http.StatusBadRequest, echo.Map{ + echoErr := ctx.JSON(http.StatusBadRequest, echo.Map{ "error": err.Error(), "message": "error parsing request input", }) + + r.logger.Log(ctx, err).Send() + return echoErr } defer ctx.Request().Body.Close() if err = body.Validate(); err != nil { - return ctx.JSON(http.StatusBadRequest, echo.Map{ + echoErr := ctx.JSON(http.StatusBadRequest, echo.Map{ "error": err.Error(), "message": "invalid request body", }) + + r.logger.Log(ctx, err).Send() + return echoErr } user := ctx.Get(string(types.UserContextKey)).(*types.User) @@ -55,13 +61,19 @@ func (r *registry) CreateRepository(ctx echo.Context) error { OwnerID: user.ID, } if err := r.store.CreateRepository(ctx.Request().Context(), repository); err != nil { - return ctx.JSON(http.StatusBadGateway, echo.Map{ + echoErr := ctx.JSON(http.StatusBadGateway, echo.Map{ "error": err.Error(), "message": "error creating repository", }) + + r.logger.Log(ctx, err).Send() + return echoErr } - return ctx.JSON(http.StatusCreated, echo.Map{ + echoErr := ctx.JSON(http.StatusCreated, echo.Map{ "message": "repository created successfully", }) + + r.logger.Log(ctx, nil).Send() + return echoErr } diff --git a/router/registry.go b/router/registry.go index b599632a..41d84df5 100644 --- a/router/registry.go +++ b/router/registry.go @@ -105,5 +105,6 @@ func RegisterExtensionsRoutes( group.Add(http.MethodPost, ChangeRepositoryVisibility, ext.ChangeContainerImageVisibility, middlewares...) group.Add(http.MethodPost, CreateRepository, reg.CreateRepository, middlewares...) group.Add(http.MethodPost, RepositoryFavorites, ext.AddRepositoryToFavorites, middlewares...) + group.Add(http.MethodGet, RepositoryFavorites, ext.ListFavoriteRepositories, middlewares...) group.Add(http.MethodDelete, RemoveRepositoryFavorites, ext.RemoveRepositoryFromFavorites, middlewares...) } diff --git a/router/router.go b/router/router.go index eeb16367..1064e75b 100644 --- a/router/router.go +++ b/router/router.go @@ -88,7 +88,7 @@ func Register( } //catch-all will redirect user back to the web interface - e.Add(http.MethodGet, "/", func(ctx echo.Context) error { + e.Add(http.MethodGet, "", func(ctx echo.Context) error { webAppURL := "" for _, url := range cfg.WebAppConfig.AllowedEndpoints { if url == ctx.Request().Header.Get("Origin") { @@ -101,7 +101,18 @@ func Register( webAppURL = ctx.Request().Header.Get("Origin") } - return ctx.Redirect(http.StatusTemporaryRedirect, webAppURL) + if webAppURL != "" { + echoErr := ctx.Redirect(http.StatusTemporaryRedirect, webAppURL) + logger.Log(ctx, nil).Send() + return echoErr + } + + echoErr := ctx.JSON(http.StatusOK, echo.Map{ + "API": "running", + }) + + logger.Log(ctx, nil).Send() + return echoErr }) return e diff --git a/store/v1/registry/registry_impl.go b/store/v1/registry/registry_impl.go index 571dfeb4..8ac05bf8 100644 --- a/store/v1/registry/registry_impl.go +++ b/store/v1/registry/registry_impl.go @@ -854,3 +854,42 @@ func (s *registryStore) GetLayersLinksForManifest( logEvent.Bool("success", true).Send() return layers, nil } +func (s *registryStore) ListFavoriteRepositories( + ctx context.Context, + userID uuid.UUID, +) ([]*types.ContainerImageRepository, error) { + logEvent := s.logger.Debug().Str("method", "ListFavoriteRepositories") + + repositories := []*types.ContainerImageRepository{} + user := &types.User{ID: userID} + err := s. + db. + NewSelect(). + Model(user). + WherePK(). + Scan(ctx) + + if err != nil { + return nil, err + } + + if len(user.FavoriteRepositories) == 0 { + return repositories, nil + } + + q := s. + db. + NewSelect(). + Model(&repositories). + Where(`"r"."id" in (?)`, bun.In(user.FavoriteRepositories)). + Relation("User", func(sq *bun.SelectQuery) *bun.SelectQuery { + return sq.ExcludeColumn("password").ExcludeColumn("github_connected").ExcludeColumn("webauthn_connected") + }) + + if err := q.Scan(ctx); err != nil { + logEvent.Err(err).Send() + return nil, v1.WrapDatabaseError(err, v1.DatabaseOperationRead) + } + + return repositories, nil +} diff --git a/store/v1/registry/store.go b/store/v1/registry/store.go index 61b40674..0d16e44a 100644 --- a/store/v1/registry/store.go +++ b/store/v1/registry/store.go @@ -92,4 +92,5 @@ type RegistryStore interface { AddRepositoryToFavorites(ctx context.Context, repoID uuid.UUID, userID uuid.UUID) error RemoveRepositoryFromFavorites(ctx context.Context, repoID uuid.UUID, userID uuid.UUID) error GetLayersLinksForManifest(ctx context.Context, manifestDigest string) ([]*types.ContainerImageLayer, error) + ListFavoriteRepositories(ctx context.Context, userID uuid.UUID) ([]*types.ContainerImageRepository, error) } diff --git a/store/v1/types/users.go b/store/v1/types/users.go index ed345b2e..825ce5bd 100644 --- a/store/v1/types/users.go +++ b/store/v1/types/users.go @@ -60,7 +60,7 @@ type ( Projects []*RepositoryBuildProject `bun:"rel:has-many,join:id=repository_owner_id" json:"-"` AuthTokens []*AuthTokens `bun:"rel:has-many,join:id=owner_id" json:"-"` // nolint:lll - FavoriteRepositories []uuid.UUID `bun:"favorite_repositories,type:uuid[],default:'{}'" json:"favorite_repositories"` + FavoriteRepositories []uuid.UUID `bun:"favorite_repositories,nullzero,type:uuid[],default:'{}'" json:"favorite_repositories"` ID uuid.UUID `bun:"id,type:uuid,pk" json:"id,omitempty" validate:"-"` IsActive bool `bun:"is_active" json:"is_active,omitempty" validate:"-"` WebauthnConnected bool `bun:"webauthn_connected" json:"webauthn_connected"` @@ -71,11 +71,11 @@ type ( AuthTokens struct { bun.BaseModel `bun:"table:auth_tokens,alias:s" json:"-"` - Name string `bun:"name" json:"name"` CreatedAt time.Time `bun:"created_at" json:"created_at,omitempty" validate:"-"` ExpiresAt time.Time `bun:"expires_at" json:"expires_at,omitempty" validate:"-"` - OwnerID uuid.UUID `bun:"owner_id,type:uuid" json:"-"` + Name string `bun:"name" json:"name"` AuthToken string `bun:"auth_token,type:text,pk" json:"-"` + OwnerID uuid.UUID `bun:"owner_id,type:uuid" json:"-"` } // type here is string so that we can use it with echo.Context & std context.Context