diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index 55ea6e2e70..ba079a534b 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -1350,6 +1350,56 @@ const docTemplate = `{ } }, "/repos": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Repositories" + ], + "summary": "List all repositories on the server. Requires admin rights.", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cpersonal access token\u003e", + "description": "Insert your personal access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "boolean", + "description": "only list active repos", + "name": "active", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "for response pagination, page offset number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 50, + "description": "for response pagination, max items per page", + "name": "perPage", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Repo" + } + } + } + } + }, "post": { "produces": [ "application/json" diff --git a/server/api/repo.go b/server/api/repo.go index 2e269d8cbc..fc92d17ccc 100644 --- a/server/api/repo.go +++ b/server/api/repo.go @@ -550,3 +550,28 @@ func MoveRepo(c *gin.Context) { } c.Status(http.StatusOK) } + +// GetAllRepos +// +// @Summary List all repositories on the server. Requires admin rights. +// @Router /repos [get] +// @Produce json +// @Success 200 {array} Repo +// @Tags Repositories +// @Param Authorization header string true "Insert your personal access token" default(Bearer ) +// @Param active query bool false "only list active repos" +// @Param page query int false "for response pagination, page offset number" default(1) +// @Param perPage query int false "for response pagination, max items per page" default(50) +func GetAllRepos(c *gin.Context) { + _store := store.FromContext(c) + + active, _ := strconv.ParseBool(c.Query("active")) + + repos, err := _store.RepoListAll(active, session.Pagination(c)) + if err != nil { + c.String(http.StatusInternalServerError, "Error fetching repository list. %s", err) + return + } + + c.JSON(http.StatusOK, repos) +} diff --git a/server/router/api.go b/server/router/api.go index 43ac2243fe..dbec95cce4 100644 --- a/server/router/api.go +++ b/server/router/api.go @@ -68,68 +68,72 @@ func apiRoutes(e *gin.RouterGroup) { } } - apiBase.GET("/repos/lookup/*repo_full_name", session.SetRepo(), session.SetPerm(), session.MustPull, api.LookupRepo) - apiBase.POST("/repos", session.MustUser(), api.PostRepo) - repoBase := apiBase.Group("/repos/:repo_id") + repo := apiBase.Group("/repos") { - repoBase.Use(session.SetRepo()) - repoBase.Use(session.SetPerm()) + repo.GET("/lookup/*repo_full_name", session.SetRepo(), session.SetPerm(), session.MustPull, api.LookupRepo) + repo.POST("", session.MustUser(), api.PostRepo) + repo.GET("", session.MustAdmin(), api.GetAllRepos) + repoBase := repo.Group("/:repo_id") + { + repoBase.Use(session.SetRepo()) + repoBase.Use(session.SetPerm()) - repoBase.GET("/permissions", api.GetRepoPermissions) + repoBase.GET("/permissions", api.GetRepoPermissions) - repo := repoBase.Group("") - { - repo.Use(session.MustPull) - - repo.GET("", api.GetRepo) - - repo.GET("/branches", api.GetRepoBranches) - repo.GET("/pull_requests", api.GetRepoPullRequests) - - repo.GET("/pipelines", api.GetPipelines) - repo.POST("/pipelines", session.MustPush, api.CreatePipeline) - repo.GET("/pipelines/:number", api.GetPipeline) - repo.GET("/pipelines/:number/config", api.GetPipelineConfig) - - // requires push permissions - repo.POST("/pipelines/:number", session.MustPush, api.PostPipeline) - repo.POST("/pipelines/:number/cancel", session.MustPush, api.CancelPipeline) - repo.POST("/pipelines/:number/approve", session.MustPush, api.PostApproval) - repo.POST("/pipelines/:number/decline", session.MustPush, api.PostDecline) - - repo.GET("/logs/:number/:stepId", api.GetStepLogs) - - // requires push permissions - repo.DELETE("/logs/:number", session.MustPush, api.DeletePipelineLogs) - - // requires push permissions - repo.GET("/secrets", session.MustPush, api.GetSecretList) - repo.POST("/secrets", session.MustPush, api.PostSecret) - repo.GET("/secrets/:secret", session.MustPush, api.GetSecret) - repo.PATCH("/secrets/:secret", session.MustPush, api.PatchSecret) - repo.DELETE("/secrets/:secret", session.MustPush, api.DeleteSecret) - - // requires push permissions - repo.GET("/registry", session.MustPush, api.GetRegistryList) - repo.POST("/registry", session.MustPush, api.PostRegistry) - repo.GET("/registry/:registry", session.MustPush, api.GetRegistry) - repo.PATCH("/registry/:registry", session.MustPush, api.PatchRegistry) - repo.DELETE("/registry/:registry", session.MustPush, api.DeleteRegistry) - - // requires push permissions - repo.GET("/cron", session.MustPush, api.GetCronList) - repo.POST("/cron", session.MustPush, api.PostCron) - repo.GET("/cron/:cron", session.MustPush, api.GetCron) - repo.POST("/cron/:cron", session.MustPush, api.RunCron) - repo.PATCH("/cron/:cron", session.MustPush, api.PatchCron) - repo.DELETE("/cron/:cron", session.MustPush, api.DeleteCron) - - // requires admin permissions - repo.PATCH("", session.MustRepoAdmin(), api.PatchRepo) - repo.DELETE("", session.MustRepoAdmin(), api.DeleteRepo) - repo.POST("/chown", session.MustRepoAdmin(), api.ChownRepo) - repo.POST("/repair", session.MustRepoAdmin(), api.RepairRepo) - repo.POST("/move", session.MustRepoAdmin(), api.MoveRepo) + repo := repoBase.Group("") + { + repo.Use(session.MustPull) + + repo.GET("", api.GetRepo) + + repo.GET("/branches", api.GetRepoBranches) + repo.GET("/pull_requests", api.GetRepoPullRequests) + + repo.GET("/pipelines", api.GetPipelines) + repo.POST("/pipelines", session.MustPush, api.CreatePipeline) + repo.GET("/pipelines/:number", api.GetPipeline) + repo.GET("/pipelines/:number/config", api.GetPipelineConfig) + + // requires push permissions + repo.POST("/pipelines/:number", session.MustPush, api.PostPipeline) + repo.POST("/pipelines/:number/cancel", session.MustPush, api.CancelPipeline) + repo.POST("/pipelines/:number/approve", session.MustPush, api.PostApproval) + repo.POST("/pipelines/:number/decline", session.MustPush, api.PostDecline) + + repo.GET("/logs/:number/:stepId", api.GetStepLogs) + + // requires push permissions + repo.DELETE("/logs/:number", session.MustPush, api.DeletePipelineLogs) + + // requires push permissions + repo.GET("/secrets", session.MustPush, api.GetSecretList) + repo.POST("/secrets", session.MustPush, api.PostSecret) + repo.GET("/secrets/:secret", session.MustPush, api.GetSecret) + repo.PATCH("/secrets/:secret", session.MustPush, api.PatchSecret) + repo.DELETE("/secrets/:secret", session.MustPush, api.DeleteSecret) + + // requires push permissions + repo.GET("/registry", session.MustPush, api.GetRegistryList) + repo.POST("/registry", session.MustPush, api.PostRegistry) + repo.GET("/registry/:registry", session.MustPush, api.GetRegistry) + repo.PATCH("/registry/:registry", session.MustPush, api.PatchRegistry) + repo.DELETE("/registry/:registry", session.MustPush, api.DeleteRegistry) + + // requires push permissions + repo.GET("/cron", session.MustPush, api.GetCronList) + repo.POST("/cron", session.MustPush, api.PostCron) + repo.GET("/cron/:cron", session.MustPush, api.GetCron) + repo.POST("/cron/:cron", session.MustPush, api.RunCron) + repo.PATCH("/cron/:cron", session.MustPush, api.PatchCron) + repo.DELETE("/cron/:cron", session.MustPush, api.DeleteCron) + + // requires admin permissions + repo.PATCH("", session.MustRepoAdmin(), api.PatchRepo) + repo.DELETE("", session.MustRepoAdmin(), api.DeleteRepo) + repo.POST("/chown", session.MustRepoAdmin(), api.ChownRepo) + repo.POST("/repair", session.MustRepoAdmin(), api.RepairRepo) + repo.POST("/move", session.MustRepoAdmin(), api.MoveRepo) + } } } diff --git a/server/store/datastore/repo.go b/server/store/datastore/repo.go index 89ff7a37ca..8bd9d6eb89 100644 --- a/server/store/datastore/repo.go +++ b/server/store/datastore/repo.go @@ -149,3 +149,15 @@ func (s storage) RepoList(user *model.User, owned, active bool) ([]*model.Repo, Asc("repo_full_name"). Find(&repos) } + +// RepoListAll list all repos +func (s storage) RepoListAll(active bool, p *model.ListOptions) ([]*model.Repo, error) { + repos := make([]*model.Repo, 0) + sess := s.paginate(p).Table("repos") + if active { + sess = sess.And(builder.Eq{"repos.repo_active": true}) + } + return repos, sess. + Asc("repo_full_name"). + Find(&repos) +} diff --git a/server/store/mocks/store.go b/server/store/mocks/store.go index 6b7e5ceb56..5d3c58ffc7 100644 --- a/server/store/mocks/store.go +++ b/server/store/mocks/store.go @@ -1583,6 +1583,32 @@ func (_m *Store) RepoList(user *model.User, owned bool, active bool) ([]*model.R return r0, r1 } +// RepoListAll provides a mock function with given fields: active, p +func (_m *Store) RepoListAll(active bool, p *model.ListOptions) ([]*model.Repo, error) { + ret := _m.Called(active, p) + + var r0 []*model.Repo + var r1 error + if rf, ok := ret.Get(0).(func(bool, *model.ListOptions) ([]*model.Repo, error)); ok { + return rf(active, p) + } + if rf, ok := ret.Get(0).(func(bool, *model.ListOptions) []*model.Repo); ok { + r0 = rf(active, p) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Repo) + } + } + + if rf, ok := ret.Get(1).(func(bool, *model.ListOptions) error); ok { + r1 = rf(active, p) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // RepoListLatest provides a mock function with given fields: _a0 func (_m *Store) RepoListLatest(_a0 *model.User) ([]*model.Feed, error) { ret := _m.Called(_a0) diff --git a/server/store/store.go b/server/store/store.go index 1774aa1c3f..42aa951293 100644 --- a/server/store/store.go +++ b/server/store/store.go @@ -100,6 +100,7 @@ type Store interface { // Repositories RepoList(user *model.User, owned, active bool) ([]*model.Repo, error) RepoListLatest(*model.User) ([]*model.Feed, error) + RepoListAll(active bool, p *model.ListOptions) ([]*model.Repo, error) // Permissions PermFind(user *model.User, repo *model.Repo) (*model.Perm, error) diff --git a/web/components.d.ts b/web/components.d.ts index 1d54a87d56..55c5721b63 100644 --- a/web/components.d.ts +++ b/web/components.d.ts @@ -15,6 +15,7 @@ declare module '@vue/runtime-core' { AdminOrgsTab: typeof import('./src/components/admin/settings/AdminOrgsTab.vue')['default'] AdminQueueStats: typeof import('./src/components/admin/settings/queue/AdminQueueStats.vue')['default'] AdminQueueTab: typeof import('./src/components/admin/settings/AdminQueueTab.vue')['default'] + AdminReposTab: typeof import('./src/components/admin/settings/AdminReposTab.vue')['default'] AdminSecretsTab: typeof import('./src/components/admin/settings/AdminSecretsTab.vue')['default'] AdminUsersTab: typeof import('./src/components/admin/settings/AdminUsersTab.vue')['default'] Badge: typeof import('./src/components/atomic/Badge.vue')['default'] diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index b13ef77909..2aa2bb6309 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -432,6 +432,14 @@ "deleted": "Organization deleted", "delete_confirm": "Do you really want to delete this organization? This will also delete all repositories owned by this organization.", "view": "View organization" + }, + "repos": { + "repos": "Repositories", + "desc": "Repositories that are or were enabled on this server", + "none": "There are no repositories yet.", + "view": "View repository", + "settings": "Repository settings", + "disabled": "Disabled" } } }, diff --git a/web/src/components/admin/settings/AdminReposTab.vue b/web/src/components/admin/settings/AdminReposTab.vue new file mode 100644 index 0000000000..757e7ffd39 --- /dev/null +++ b/web/src/components/admin/settings/AdminReposTab.vue @@ -0,0 +1,48 @@ + + + diff --git a/web/src/lib/api/index.ts b/web/src/lib/api/index.ts index a95faad57a..13bce15aee 100644 --- a/web/src/lib/api/index.ts +++ b/web/src/lib/api/index.ts @@ -311,6 +311,10 @@ export default class WoodpeckerClient extends ApiClient { return this._delete(`/api/orgs/${org.id}`); } + getAllRepos(page: number): Promise { + return this._get(`/api/repos?page=${page}`) as Promise; + } + // eslint-disable-next-line promise/prefer-await-to-callbacks on(callback: (data: { pipeline?: Pipeline; repo?: Repo; step?: PipelineWorkflow }) => void): EventSource { return this._subscribe('/api/stream/events', callback, { diff --git a/web/src/views/admin/AdminSettings.vue b/web/src/views/admin/AdminSettings.vue index 84f6ec92d2..fc15dd377d 100644 --- a/web/src/views/admin/AdminSettings.vue +++ b/web/src/views/admin/AdminSettings.vue @@ -6,6 +6,9 @@ + + + @@ -29,6 +32,7 @@ import { useRouter } from 'vue-router'; import AdminAgentsTab from '~/components/admin/settings/AdminAgentsTab.vue'; import AdminOrgsTab from '~/components/admin/settings/AdminOrgsTab.vue'; import AdminQueueTab from '~/components/admin/settings/AdminQueueTab.vue'; +import AdminReposTab from '~/components/admin/settings/AdminReposTab.vue'; import AdminSecretsTab from '~/components/admin/settings/AdminSecretsTab.vue'; import AdminUsersTab from '~/components/admin/settings/AdminUsersTab.vue'; import Scaffold from '~/components/layout/scaffold/Scaffold.vue';