mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-09-14 05:23:23 +00:00
Add org list (#2338)
 Closes #2307
This commit is contained in:
@@ -819,6 +819,90 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/orgs": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns all registered orgs in the system. Requires admin rights.",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Orgs"
|
||||||
|
],
|
||||||
|
"summary": "Get all orgs",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"default": "Bearer \u003cpersonal access token\u003e",
|
||||||
|
"description": "Insert your personal access token",
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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/Org"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/orgs/{id}": {
|
||||||
|
"delete": {
|
||||||
|
"description": "Deletes the given org. Requires admin rights.",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Orgs"
|
||||||
|
],
|
||||||
|
"summary": "Delete an org",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"default": "Bearer \u003cpersonal access token\u003e",
|
||||||
|
"description": "Insert your personal access token",
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "the org's id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Org"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/orgs/{org_id}": {
|
"/orgs/{org_id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
72
server/api/orgs.go
Normal file
72
server/api/orgs.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// Copyright 2023 Woodpecker Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/woodpecker-ci/woodpecker/server/router/middleware/session"
|
||||||
|
"github.com/woodpecker-ci/woodpecker/server/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetOrgs
|
||||||
|
//
|
||||||
|
// @Summary Get all orgs
|
||||||
|
// @Description Returns all registered orgs in the system. Requires admin rights.
|
||||||
|
// @Router /orgs [get]
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} Org
|
||||||
|
// @Tags Orgs
|
||||||
|
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
|
||||||
|
// @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 GetOrgs(c *gin.Context) {
|
||||||
|
orgs, err := store.FromContext(c).OrgList(session.Pagination(c))
|
||||||
|
if err != nil {
|
||||||
|
c.String(500, "Error getting user list. %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, orgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteOrg
|
||||||
|
//
|
||||||
|
// @Summary Delete an org
|
||||||
|
// @Description Deletes the given org. Requires admin rights.
|
||||||
|
// @Router /orgs/{id} [delete]
|
||||||
|
// @Produce json
|
||||||
|
// @Success 204 {object} Org
|
||||||
|
// @Tags Orgs
|
||||||
|
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
|
||||||
|
// @Param id path string true "the org's id"
|
||||||
|
func DeleteOrg(c *gin.Context) {
|
||||||
|
_store := store.FromContext(c)
|
||||||
|
|
||||||
|
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = _store.OrgDelete(orgID)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusInternalServerError, "Error deleting org %d. %s", orgID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.String(http.StatusNoContent, "")
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
// Code generated by mockery v2.32.3. DO NOT EDIT.
|
// Code generated by mockery v2.33.0. DO NOT EDIT.
|
||||||
|
|
||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
|
@@ -46,20 +46,25 @@ func apiRoutes(e *gin.RouterGroup) {
|
|||||||
users.DELETE("/:login", api.DeleteUser)
|
users.DELETE("/:login", api.DeleteUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
apiBase.GET("/orgs/lookup/*org_full_name", api.LookupOrg)
|
orgs := apiBase.Group("/orgs")
|
||||||
orgBase := apiBase.Group("/orgs/:org_id")
|
|
||||||
{
|
{
|
||||||
orgBase.GET("/permissions", api.GetOrgPermissions)
|
orgs.GET("", session.MustAdmin(), api.GetOrgs)
|
||||||
|
orgs.GET("/lookup/*org_full_name", api.LookupOrg)
|
||||||
org := orgBase.Group("")
|
orgBase := orgs.Group("/:org_id")
|
||||||
{
|
{
|
||||||
org.Use(session.MustOrgMember(true))
|
orgBase.GET("/permissions", api.GetOrgPermissions)
|
||||||
org.GET("", api.GetOrg)
|
|
||||||
org.GET("/secrets", api.GetOrgSecretList)
|
org := orgBase.Group("")
|
||||||
org.POST("/secrets", api.PostOrgSecret)
|
{
|
||||||
org.GET("/secrets/:secret", api.GetOrgSecret)
|
org.Use(session.MustOrgMember(true))
|
||||||
org.PATCH("/secrets/:secret", api.PatchOrgSecret)
|
org.DELETE("", session.MustAdmin(), api.DeleteOrg)
|
||||||
org.DELETE("/secrets/:secret", api.DeleteOrgSecret)
|
org.GET("", api.GetOrg)
|
||||||
|
org.GET("/secrets", api.GetOrgSecretList)
|
||||||
|
org.POST("/secrets", api.PostOrgSecret)
|
||||||
|
org.GET("/secrets/:secret", api.GetOrgSecret)
|
||||||
|
org.PATCH("/secrets/:secret", api.PatchOrgSecret)
|
||||||
|
org.DELETE("/secrets/:secret", api.DeleteOrgSecret)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -17,8 +17,9 @@ package datastore
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/woodpecker-ci/woodpecker/server/model"
|
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
|
|
||||||
|
"github.com/woodpecker-ci/woodpecker/server/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s storage) OrgCreate(org *model.Org) error {
|
func (s storage) OrgCreate(org *model.Org) error {
|
||||||
@@ -62,3 +63,8 @@ func (s storage) OrgRepoList(org *model.Org, p *model.ListOptions) ([]*model.Rep
|
|||||||
var repos []*model.Repo
|
var repos []*model.Repo
|
||||||
return repos, s.paginate(p).OrderBy("repo_id").Where("repo_org_id = ?", org.ID).Find(&repos)
|
return repos, s.paginate(p).OrderBy("repo_id").Where("repo_org_id = ?", org.ID).Find(&repos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s storage) OrgList(p *model.ListOptions) ([]*model.Org, error) {
|
||||||
|
var orgs []*model.Org
|
||||||
|
return orgs, s.paginate(p).Where("is_user = ?", false).OrderBy("id").Find(&orgs)
|
||||||
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
// Code generated by mockery v2.32.3. DO NOT EDIT.
|
// Code generated by mockery v2.33.0. DO NOT EDIT.
|
||||||
|
|
||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
@@ -1249,6 +1249,32 @@ func (_m *Store) OrgGet(_a0 int64) (*model.Org, error) {
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OrgList provides a mock function with given fields: _a0
|
||||||
|
func (_m *Store) OrgList(_a0 *model.ListOptions) ([]*model.Org, error) {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 []*model.Org
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*model.ListOptions) ([]*model.Org, error)); ok {
|
||||||
|
return rf(_a0)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(*model.ListOptions) []*model.Org); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*model.Org)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(*model.ListOptions) error); ok {
|
||||||
|
r1 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// OrgRepoList provides a mock function with given fields: _a0, _a1
|
// OrgRepoList provides a mock function with given fields: _a0, _a1
|
||||||
func (_m *Store) OrgRepoList(_a0 *model.Org, _a1 *model.ListOptions) ([]*model.Repo, error) {
|
func (_m *Store) OrgRepoList(_a0 *model.Org, _a1 *model.ListOptions) ([]*model.Repo, error) {
|
||||||
ret := _m.Called(_a0, _a1)
|
ret := _m.Called(_a0, _a1)
|
||||||
|
@@ -189,6 +189,7 @@ type Store interface {
|
|||||||
OrgFindByName(string) (*model.Org, error)
|
OrgFindByName(string) (*model.Org, error)
|
||||||
OrgUpdate(*model.Org) error
|
OrgUpdate(*model.Org) error
|
||||||
OrgDelete(int64) error
|
OrgDelete(int64) error
|
||||||
|
OrgList(*model.ListOptions) ([]*model.Org, error)
|
||||||
|
|
||||||
// Org repos
|
// Org repos
|
||||||
OrgRepoList(*model.Org, *model.ListOptions) ([]*model.Repo, error)
|
OrgRepoList(*model.Org, *model.ListOptions) ([]*model.Repo, error)
|
||||||
|
1
web/components.d.ts
vendored
1
web/components.d.ts
vendored
@@ -12,6 +12,7 @@ declare module '@vue/runtime-core' {
|
|||||||
ActionsTab: typeof import('./src/components/repo/settings/ActionsTab.vue')['default']
|
ActionsTab: typeof import('./src/components/repo/settings/ActionsTab.vue')['default']
|
||||||
ActivePipelines: typeof import('./src/components/layout/header/ActivePipelines.vue')['default']
|
ActivePipelines: typeof import('./src/components/layout/header/ActivePipelines.vue')['default']
|
||||||
AdminAgentsTab: typeof import('./src/components/admin/settings/AdminAgentsTab.vue')['default']
|
AdminAgentsTab: typeof import('./src/components/admin/settings/AdminAgentsTab.vue')['default']
|
||||||
|
AdminOrgsTab: typeof import('./src/components/admin/settings/AdminOrgsTab.vue')['default']
|
||||||
AdminQueueStats: typeof import('./src/components/admin/settings/queue/AdminQueueStats.vue')['default']
|
AdminQueueStats: typeof import('./src/components/admin/settings/queue/AdminQueueStats.vue')['default']
|
||||||
AdminQueueTab: typeof import('./src/components/admin/settings/AdminQueueTab.vue')['default']
|
AdminQueueTab: typeof import('./src/components/admin/settings/AdminQueueTab.vue')['default']
|
||||||
AdminSecretsTab: typeof import('./src/components/admin/settings/AdminSecretsTab.vue')['default']
|
AdminSecretsTab: typeof import('./src/components/admin/settings/AdminSecretsTab.vue')['default']
|
||||||
|
@@ -421,6 +421,16 @@
|
|||||||
},
|
},
|
||||||
"delete_user": "Delete user",
|
"delete_user": "Delete user",
|
||||||
"edit_user": "Edit user"
|
"edit_user": "Edit user"
|
||||||
|
},
|
||||||
|
"orgs": {
|
||||||
|
"orgs": "Organizations",
|
||||||
|
"desc": "Organizations owning repositories on this server",
|
||||||
|
"none": "There are no organizations yet.",
|
||||||
|
"org_settings": "Organization settings",
|
||||||
|
"delete_org": "Delete organization",
|
||||||
|
"deleted": "Organization deleted",
|
||||||
|
"delete_confirm": "Do you really want to delete this organization?",
|
||||||
|
"view": "View organization"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
68
web/src/components/admin/settings/AdminOrgsTab.vue
Normal file
68
web/src/components/admin/settings/AdminOrgsTab.vue
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<Settings :title="$t('admin.settings.orgs.orgs')" :desc="$t('admin.settings.orgs.desc')">
|
||||||
|
<div class="space-y-4 text-wp-text-100">
|
||||||
|
<ListItem
|
||||||
|
v-for="org in orgs"
|
||||||
|
:key="org.id"
|
||||||
|
class="items-center gap-2 !bg-wp-background-200 !dark:bg-wp-background-100"
|
||||||
|
>
|
||||||
|
<span>{{ org.name }}</span>
|
||||||
|
<IconButton
|
||||||
|
icon="chevron-right"
|
||||||
|
:title="$t('admin.settings.orgs.view')"
|
||||||
|
class="ml-auto w-8 h-8"
|
||||||
|
:to="{ name: 'org', params: { orgId: org.id } }"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon="settings"
|
||||||
|
:title="$t('admin.settings.orgs.org_settings')"
|
||||||
|
class="w-8 h-8"
|
||||||
|
:to="{ name: 'org-settings', params: { orgId: org.id } }"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon="trash"
|
||||||
|
:title="$t('admin.settings.orgs.delete_org')"
|
||||||
|
class="ml-2 w-8 h-8 hover:text-wp-control-error-100"
|
||||||
|
:is-loading="isDeleting"
|
||||||
|
@click="deleteOrg(org)"
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<div v-if="orgs?.length === 0" class="ml-2">{{ $t('admin.settings.orgs.none') }}</div>
|
||||||
|
</div>
|
||||||
|
</Settings>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import IconButton from '~/components/atomic/IconButton.vue';
|
||||||
|
import ListItem from '~/components/atomic/ListItem.vue';
|
||||||
|
import Settings from '~/components/layout/Settings.vue';
|
||||||
|
import useApiClient from '~/compositions/useApiClient';
|
||||||
|
import { useAsyncAction } from '~/compositions/useAsyncAction';
|
||||||
|
import useNotifications from '~/compositions/useNotifications';
|
||||||
|
import { usePagination } from '~/compositions/usePaginate';
|
||||||
|
import { Org } from '~/lib/api/types';
|
||||||
|
|
||||||
|
const apiClient = useApiClient();
|
||||||
|
const notifications = useNotifications();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
async function loadOrgs(page: number): Promise<Org[] | null> {
|
||||||
|
return apiClient.getOrgs(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { resetPage, data: orgs } = usePagination(loadOrgs);
|
||||||
|
|
||||||
|
const { doSubmit: deleteOrg, isLoading: isDeleting } = useAsyncAction(async (_org: Org) => {
|
||||||
|
// eslint-disable-next-line no-restricted-globals, no-alert
|
||||||
|
if (!confirm(t('admin.settings.orgs.delete_confirm'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await apiClient.deleteOrg(_org);
|
||||||
|
notifications.notify({ title: t('admin.settings.orgs.deleted'), type: 'success' });
|
||||||
|
resetPage();
|
||||||
|
});
|
||||||
|
</script>
|
@@ -303,6 +303,14 @@ export default class WoodpeckerClient extends ApiClient {
|
|||||||
return this._delete('/api/user/token') as Promise<string>;
|
return this._delete('/api/user/token') as Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOrgs(page: number): Promise<Org[] | null> {
|
||||||
|
return this._get(`/api/orgs?page=${page}`) as Promise<Org[] | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteOrg(org: Org): Promise<unknown> {
|
||||||
|
return this._delete(`/api/orgs/${org.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line promise/prefer-await-to-callbacks
|
// eslint-disable-next-line promise/prefer-await-to-callbacks
|
||||||
on(callback: (data: { pipeline?: Pipeline; repo?: Repo; step?: PipelineWorkflow }) => void): EventSource {
|
on(callback: (data: { pipeline?: Pipeline; repo?: Repo; step?: PipelineWorkflow }) => void): EventSource {
|
||||||
return this._subscribe('/api/stream/events', callback, {
|
return this._subscribe('/api/stream/events', callback, {
|
||||||
|
@@ -9,6 +9,9 @@
|
|||||||
<Tab id="users" :title="$t('admin.settings.users.users')">
|
<Tab id="users" :title="$t('admin.settings.users.users')">
|
||||||
<AdminUsersTab />
|
<AdminUsersTab />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab id="orgs" :title="$t('admin.settings.orgs.orgs')">
|
||||||
|
<AdminOrgsTab />
|
||||||
|
</Tab>
|
||||||
<Tab id="agents" :title="$t('admin.settings.agents.agents')">
|
<Tab id="agents" :title="$t('admin.settings.agents.agents')">
|
||||||
<AdminAgentsTab />
|
<AdminAgentsTab />
|
||||||
</Tab>
|
</Tab>
|
||||||
@@ -24,6 +27,7 @@ import { useI18n } from 'vue-i18n';
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import AdminAgentsTab from '~/components/admin/settings/AdminAgentsTab.vue';
|
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 AdminQueueTab from '~/components/admin/settings/AdminQueueTab.vue';
|
||||||
import AdminSecretsTab from '~/components/admin/settings/AdminSecretsTab.vue';
|
import AdminSecretsTab from '~/components/admin/settings/AdminSecretsTab.vue';
|
||||||
import AdminUsersTab from '~/components/admin/settings/AdminUsersTab.vue';
|
import AdminUsersTab from '~/components/admin/settings/AdminUsersTab.vue';
|
||||||
|
Reference in New Issue
Block a user