diff --git a/routers/api/v1/admin/packages.go b/routers/api/v1/admin/packages.go new file mode 100644 index 00000000000..20f87d2d0c5 --- /dev/null +++ b/routers/api/v1/admin/packages.go @@ -0,0 +1,87 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package admin + +import ( + "net/http" + + packages_model "gitea.dev/models/packages" + "gitea.dev/modules/optional" + api "gitea.dev/modules/structs" + "gitea.dev/routers/api/v1/utils" + "gitea.dev/services/context" + "gitea.dev/services/convert" +) + +// ListPackages lists every package version on the instance. +func ListPackages(ctx *context.APIContext) { + // swagger:operation GET /admin/packages admin adminListPackages + // --- + // summary: List all packages + // produces: + // - application/json + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // - name: type + // in: query + // description: package type filter + // type: string + // enum: [alpine, cargo, chef, composer, conan, conda, container, cran, debian, generic, go, helm, maven, npm, nuget, pub, pypi, rpm, rubygems, swift, terraform, vagrant] + // - name: q + // in: query + // description: name filter + // type: string + // responses: + // "200": + // "$ref": "#/responses/PackageList" + // "403": + // "$ref": "#/responses/forbidden" + + listOptions := utils.GetListOptions(ctx) + + apiPackages, count, err := searchPackages(ctx, &packages_model.PackageSearchOptions{ + Type: packages_model.Type(ctx.FormTrim("type")), + Name: packages_model.SearchValue{Value: ctx.FormTrim("q")}, + IsInternal: optional.Some(false), + Paginator: &listOptions, + }) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + ctx.SetLinkHeader(count, listOptions.PageSize) + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, apiPackages) +} + +func searchPackages(ctx *context.APIContext, opts *packages_model.PackageSearchOptions) ([]*api.Package, int64, error) { + pvs, count, err := packages_model.SearchVersions(ctx, opts) + if err != nil { + return nil, 0, err + } + + pds, err := packages_model.GetPackageDescriptors(ctx, pvs) + if err != nil { + return nil, 0, err + } + + apiPackages := make([]*api.Package, 0, len(pds)) + for _, pd := range pds { + apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer) + if err != nil { + return nil, 0, err + } + apiPackages = append(apiPackages, apiPackage) + } + + return apiPackages, count, nil +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index aa58ca954ec..c470f29fc80 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1888,6 +1888,7 @@ func Routes() *web.Router { m.Post("/{task}", admin.PostCronTask) }) m.Get("/orgs", admin.GetAllOrgs) + m.Get("/packages", admin.ListPackages) m.Group("/users", func() { m.Get("", admin.SearchUsers) m.Post("", bind(api.CreateUserOption{}), admin.CreateUser) diff --git a/templates/swagger/v1-openapi3.generated.json b/templates/swagger/v1-openapi3.generated.json index d2fad39293d..d7526f7ad0d 100644 --- a/templates/swagger/v1-openapi3.generated.json +++ b/templates/swagger/v1-openapi3.generated.json @@ -11685,6 +11685,81 @@ ] } }, + "/admin/packages": { + "get": { + "operationId": "adminListPackages", + "parameters": [ + { + "description": "page number of results to return (1-based)", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "page size of results", + "in": "query", + "name": "limit", + "schema": { + "type": "integer" + } + }, + { + "description": "package type filter", + "in": "query", + "name": "type", + "schema": { + "enum": [ + "alpine", + "cargo", + "chef", + "composer", + "conan", + "conda", + "container", + "cran", + "debian", + "generic", + "go", + "helm", + "maven", + "npm", + "nuget", + "pub", + "pypi", + "rpm", + "rubygems", + "swift", + "terraform", + "vagrant" + ], + "type": "string" + } + }, + { + "description": "name filter", + "in": "query", + "name": "q", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/PackageList" + }, + "403": { + "$ref": "#/components/responses/forbidden" + } + }, + "summary": "List all packages", + "tags": [ + "admin" + ] + } + }, "/admin/unadopted": { "get": { "operationId": "adminUnadoptedList", diff --git a/templates/swagger/v1-swagger.generated.json b/templates/swagger/v1-swagger.generated.json index 9d662680fdb..baacc2193cd 100644 --- a/templates/swagger/v1-swagger.generated.json +++ b/templates/swagger/v1-swagger.generated.json @@ -628,6 +628,76 @@ } } }, + "/admin/packages": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List all packages", + "operationId": "adminListPackages", + "parameters": [ + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + }, + { + "enum": [ + "alpine", + "cargo", + "chef", + "composer", + "conan", + "conda", + "container", + "cran", + "debian", + "generic", + "go", + "helm", + "maven", + "npm", + "nuget", + "pub", + "pypi", + "rpm", + "rubygems", + "swift", + "terraform", + "vagrant" + ], + "type": "string", + "description": "package type filter", + "name": "type", + "in": "query" + }, + { + "type": "string", + "description": "name filter", + "name": "q", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/PackageList" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } + } + }, "/admin/unadopted": { "get": { "produces": [ diff --git a/tests/integration/api_admin_packages_test.go b/tests/integration/api_admin_packages_test.go new file mode 100644 index 00000000000..11ef8910502 --- /dev/null +++ b/tests/integration/api_admin_packages_test.go @@ -0,0 +1,44 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "bytes" + "fmt" + "net/http" + "testing" + + auth_model "gitea.dev/models/auth" + "gitea.dev/models/unittest" + user_model "gitea.dev/models/user" + api "gitea.dev/modules/structs" + "gitea.dev/tests" + + "github.com/stretchr/testify/assert" +) + +func TestAPIAdminListPackages(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + packageName := "admin-test-package" + packageVersion := "1.0.0" + + url := fmt.Sprintf("/api/packages/%s/generic/%s/%s/file.bin", user4.Name, packageName, packageVersion) + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader([]byte{})).AddBasicAuth(user4.Name) + MakeRequest(t, req, http.StatusCreated) + + adminToken := getUserToken(t, "user1", auth_model.AccessTokenScopeReadAdmin) + req = NewRequest(t, "GET", "/api/v1/admin/packages").AddTokenAuth(adminToken) + resp := MakeRequest(t, req, http.StatusOK) + + apiPackages := DecodeJSON(t, resp, []*api.Package{}) + actual := map[string]any{} + for _, p := range apiPackages { + actual[p.Name] = map[string]any{"ownerName": p.Owner.UserName, "type": p.Type, "version": p.Version} + } + assert.Equal(t, map[string]any{ + packageName: map[string]any{"ownerName": user4.Name, "type": "generic", "version": packageVersion}, + }, actual) +}