mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-30 12:20:33 +00:00
Support user secrets (#2126)
This commit is contained in:
parent
09624aa286
commit
81ead7cbf2
18 changed files with 251 additions and 22 deletions
2
Makefile
2
Makefile
|
@ -139,7 +139,7 @@ ui-dependencies: ## Install UI dependencies
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint: install-tools ## Lint code
|
lint: install-tools ## Lint code
|
||||||
@echo "Running golangci-lint"
|
@echo "Running golangci-lint"
|
||||||
golangci-lint run --timeout 10m
|
golangci-lint run --timeout 15m
|
||||||
@echo "Running zerolog linter"
|
@echo "Running zerolog linter"
|
||||||
lint github.com/woodpecker-ci/woodpecker/cmd/agent
|
lint github.com/woodpecker-ci/woodpecker/cmd/agent
|
||||||
lint github.com/woodpecker-ci/woodpecker/cmd/cli
|
lint github.com/woodpecker-ci/woodpecker/cmd/cli
|
||||||
|
|
|
@ -4197,6 +4197,10 @@ const docTemplate = `{
|
||||||
"login": {
|
"login": {
|
||||||
"description": "Login is the username for this user.\n\nrequired: true",
|
"description": "Login is the username for this user.\n\nrequired: true",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"org_id": {
|
||||||
|
"description": "OrgID is the of the user as model.Org.",
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -91,12 +91,15 @@ func GetOrgPermissions(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (org.IsUser && org.Name == user.Login) || user.Admin {
|
if (org.IsUser && org.Name == user.Login) || (user.Admin && !org.IsUser) {
|
||||||
c.JSON(http.StatusOK, &model.OrgPerm{
|
c.JSON(http.StatusOK, &model.OrgPerm{
|
||||||
Member: true,
|
Member: true,
|
||||||
Admin: true,
|
Admin: true,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
} else if org.IsUser {
|
||||||
|
c.JSON(http.StatusOK, &model.OrgPerm{})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
perm, err := server.Config.Services.Membership.Get(c, user, org.Name)
|
perm, err := server.Config.Services.Membership.Get(c, user, org.Name)
|
||||||
|
|
|
@ -64,6 +64,9 @@ type User struct {
|
||||||
|
|
||||||
// Hash is a unique token used to sign tokens.
|
// Hash is a unique token used to sign tokens.
|
||||||
Hash string `json:"-" xorm:"UNIQUE varchar(500) 'user_hash'"`
|
Hash string `json:"-" xorm:"UNIQUE varchar(500) 'user_hash'"`
|
||||||
|
|
||||||
|
// OrgID is the of the user as model.Org.
|
||||||
|
OrgID int64 `json:"org_id" xorm:"user_org_id"`
|
||||||
} // @name User
|
} // @name User
|
||||||
|
|
||||||
// TableName return database table name for xorm
|
// TableName return database table name for xorm
|
||||||
|
|
|
@ -23,7 +23,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetPipelineQueue(t *testing.T) {
|
func TestGetPipelineQueue(t *testing.T) {
|
||||||
store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm), new(model.Pipeline))
|
store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm), new(model.Pipeline), new(model.Org))
|
||||||
defer closer()
|
defer closer()
|
||||||
|
|
||||||
user := &model.User{
|
user := &model.User{
|
||||||
|
@ -64,7 +64,7 @@ func TestGetPipelineQueue(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUserFeed(t *testing.T) {
|
func TestUserFeed(t *testing.T) {
|
||||||
store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm), new(model.Pipeline))
|
store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm), new(model.Pipeline), new(model.Org))
|
||||||
defer closer()
|
defer closer()
|
||||||
|
|
||||||
user := &model.User{
|
user := &model.User{
|
||||||
|
@ -115,7 +115,7 @@ func TestUserFeed(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRepoListLatest(t *testing.T) {
|
func TestRepoListLatest(t *testing.T) {
|
||||||
store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm), new(model.Pipeline))
|
store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm), new(model.Pipeline), new(model.Org))
|
||||||
defer closer()
|
defer closer()
|
||||||
|
|
||||||
user := &model.User{
|
user := &model.User{
|
||||||
|
|
61
server/store/datastore/migration/022_add_org_id.go
Normal file
61
server/store/datastore/migration/022_add_org_id.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
// Copyright 2022 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 migration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"xorm.io/xorm"
|
||||||
|
|
||||||
|
"github.com/woodpecker-ci/woodpecker/server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
var addOrgID = task{
|
||||||
|
name: "add-org-id",
|
||||||
|
required: true,
|
||||||
|
fn: func(sess *xorm.Session) error {
|
||||||
|
if err := sess.Sync(new(model.User)); err != nil {
|
||||||
|
return fmt.Errorf("sync new models failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all users
|
||||||
|
var users []*model.User
|
||||||
|
if err := sess.Find(&users); err != nil {
|
||||||
|
return fmt.Errorf("find all repos failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
org := &model.Org{}
|
||||||
|
has, err := sess.Where("name = ?", user.Login).Get(org)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting org failed: %w", err)
|
||||||
|
} else if !has {
|
||||||
|
org = &model.Org{
|
||||||
|
Name: user.Login,
|
||||||
|
IsUser: true,
|
||||||
|
}
|
||||||
|
if _, err := sess.Insert(org); err != nil {
|
||||||
|
return fmt.Errorf("inserting org failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user.OrgID = org.ID
|
||||||
|
if _, err := sess.Cols("user_org_id").Update(user); err != nil {
|
||||||
|
return fmt.Errorf("updating user failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dropTableColumns(sess, "secrets", "secret_owner")
|
||||||
|
},
|
||||||
|
}
|
|
@ -54,6 +54,7 @@ var migrationTasks = []*task{
|
||||||
&migrateLogs2LogEntries,
|
&migrateLogs2LogEntries,
|
||||||
&parentStepsToWorkflows,
|
&parentStepsToWorkflows,
|
||||||
&addOrgs,
|
&addOrgs,
|
||||||
|
&addOrgID,
|
||||||
}
|
}
|
||||||
|
|
||||||
var allBeans = []interface{}{
|
var allBeans = []interface{}{
|
||||||
|
|
|
@ -18,13 +18,18 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/woodpecker-ci/woodpecker/server/model"
|
"github.com/woodpecker-ci/woodpecker/server/model"
|
||||||
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s storage) OrgCreate(org *model.Org) error {
|
func (s storage) OrgCreate(org *model.Org) error {
|
||||||
|
return s.orgCreate(org, s.engine.NewSession())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s storage) orgCreate(org *model.Org, sess *xorm.Session) error {
|
||||||
// sanitize
|
// sanitize
|
||||||
org.Name = strings.ToLower(org.Name)
|
org.Name = strings.ToLower(org.Name)
|
||||||
// insert
|
// insert
|
||||||
_, err := s.engine.Insert(org)
|
_, err := sess.Insert(org)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -140,7 +140,7 @@ func TestRepos(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRepoList(t *testing.T) {
|
func TestRepoList(t *testing.T) {
|
||||||
store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm))
|
store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm), new(model.Org))
|
||||||
defer closer()
|
defer closer()
|
||||||
|
|
||||||
user := &model.User{
|
user := &model.User{
|
||||||
|
@ -196,7 +196,7 @@ func TestRepoList(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOwnedRepoList(t *testing.T) {
|
func TestOwnedRepoList(t *testing.T) {
|
||||||
store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm))
|
store, closer := newTestStore(t, new(model.Repo), new(model.User), new(model.Perm), new(model.Org))
|
||||||
defer closer()
|
defer closer()
|
||||||
|
|
||||||
user := &model.User{
|
user := &model.User{
|
||||||
|
|
|
@ -53,8 +53,18 @@ func (s storage) GetUserCount() (int64, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s storage) CreateUser(user *model.User) error {
|
func (s storage) CreateUser(user *model.User) error {
|
||||||
|
sess := s.engine.NewSession()
|
||||||
|
org := &model.Org{
|
||||||
|
Name: user.Login,
|
||||||
|
IsUser: true,
|
||||||
|
}
|
||||||
|
err := s.orgCreate(org, sess)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user.OrgID = org.ID
|
||||||
// only Insert set auto created ID back to object
|
// only Insert set auto created ID back to object
|
||||||
_, err := s.engine.Insert(user)
|
_, err = sess.Insert(user)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUsers(t *testing.T) {
|
func TestUsers(t *testing.T) {
|
||||||
store, closer := newTestStore(t, new(model.User), new(model.Repo), new(model.Pipeline), new(model.Step), new(model.Perm))
|
store, closer := newTestStore(t, new(model.User), new(model.Repo), new(model.Pipeline), new(model.Step), new(model.Perm), new(model.Org))
|
||||||
defer closer()
|
defer closer()
|
||||||
|
|
||||||
g := goblin.Goblin(t)
|
g := goblin.Goblin(t)
|
||||||
|
@ -40,6 +40,8 @@ func TestUsers(t *testing.T) {
|
||||||
g.Assert(err).IsNil()
|
g.Assert(err).IsNil()
|
||||||
_, err = store.engine.Exec("DELETE FROM steps")
|
_, err = store.engine.Exec("DELETE FROM steps")
|
||||||
g.Assert(err).IsNil()
|
g.Assert(err).IsNil()
|
||||||
|
_, err = store.engine.Exec("DELETE FROM orgs")
|
||||||
|
g.Assert(err).IsNil()
|
||||||
})
|
})
|
||||||
|
|
||||||
g.It("Should Update a User", func() {
|
g.It("Should Update a User", func() {
|
||||||
|
|
1
web/components.d.ts
vendored
1
web/components.d.ts
vendored
|
@ -104,6 +104,7 @@ declare module '@vue/runtime-core' {
|
||||||
TextField: typeof import('./src/components/form/TextField.vue')['default']
|
TextField: typeof import('./src/components/form/TextField.vue')['default']
|
||||||
UserAPITab: typeof import('./src/components/user/UserAPITab.vue')['default']
|
UserAPITab: typeof import('./src/components/user/UserAPITab.vue')['default']
|
||||||
UserGeneralTab: typeof import('./src/components/user/UserGeneralTab.vue')['default']
|
UserGeneralTab: typeof import('./src/components/user/UserGeneralTab.vue')['default']
|
||||||
|
UserSecretsTab: typeof import('./src/components/user/UserSecretsTab.vue')['default']
|
||||||
Warning: typeof import('./src/components/atomic/Warning.vue')['default']
|
Warning: typeof import('./src/components/atomic/Warning.vue')['default']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -431,6 +431,28 @@
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"language": "Language"
|
"language": "Language"
|
||||||
},
|
},
|
||||||
|
"secrets": {
|
||||||
|
"secrets": "Secrets",
|
||||||
|
"desc": "User secrets can be passed to all user's repository individual pipeline steps at runtime as environmental variables.",
|
||||||
|
"none": "There are no user secrets yet.",
|
||||||
|
"add": "Add secret",
|
||||||
|
"save": "Save secret",
|
||||||
|
"show": "Show secrets",
|
||||||
|
"name": "Name",
|
||||||
|
"value": "Value",
|
||||||
|
"deleted": "User secret deleted",
|
||||||
|
"created": "User secret created",
|
||||||
|
"saved": "User secret saved",
|
||||||
|
"images": {
|
||||||
|
"images": "Available for following images",
|
||||||
|
"desc": "Comma separated list of images where this secret is available, leave empty to allow all images"
|
||||||
|
},
|
||||||
|
"plugins_only": "Only available for plugins",
|
||||||
|
"events": {
|
||||||
|
"events": "Available at following events",
|
||||||
|
"pr_warning": "Please be careful with this option as a bad actor can submit a malicious pull request that exposes your secrets."
|
||||||
|
}
|
||||||
|
},
|
||||||
"api": {
|
"api": {
|
||||||
"api": "API",
|
"api": "API",
|
||||||
"desc": "Personal Access Token and API usage",
|
"desc": "Personal Access Token and API usage",
|
||||||
|
|
117
web/src/components/user/UserSecretsTab.vue
Normal file
117
web/src/components/user/UserSecretsTab.vue
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
<template>
|
||||||
|
<Panel>
|
||||||
|
<div class="flex flex-row border-b mb-4 pb-4 items-center dark:border-wp-background-100">
|
||||||
|
<div class="ml-2">
|
||||||
|
<h1 class="text-xl text-wp-text-100">{{ $t('user.settings.secrets.secrets') }}</h1>
|
||||||
|
<p class="text-sm text-wp-text-alt-100">
|
||||||
|
{{ $t('user.settings.secrets.desc') }}
|
||||||
|
<DocsLink :topic="$t('user.settings.secrets.secrets')" url="docs/usage/secrets" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
v-if="selectedSecret"
|
||||||
|
class="ml-auto"
|
||||||
|
:text="$t('user.settings.secrets.show')"
|
||||||
|
start-icon="back"
|
||||||
|
@click="selectedSecret = undefined"
|
||||||
|
/>
|
||||||
|
<Button v-else class="ml-auto" :text="$t('user.settings.secrets.add')" start-icon="plus" @click="showAddSecret" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SecretList
|
||||||
|
v-if="!selectedSecret"
|
||||||
|
v-model="secrets"
|
||||||
|
i18n-prefix="user.settings.secrets."
|
||||||
|
:is-deleting="isDeleting"
|
||||||
|
@edit="editSecret"
|
||||||
|
@delete="deleteSecret"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SecretEdit
|
||||||
|
v-else
|
||||||
|
v-model="selectedSecret"
|
||||||
|
i18n-prefix="user.settings.secrets."
|
||||||
|
:is-saving="isSaving"
|
||||||
|
@save="createSecret"
|
||||||
|
@cancel="selectedSecret = undefined"
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import Button from '~/components/atomic/Button.vue';
|
||||||
|
import DocsLink from '~/components/atomic/DocsLink.vue';
|
||||||
|
import Panel from '~/components/layout/Panel.vue';
|
||||||
|
import SecretEdit from '~/components/secrets/SecretEdit.vue';
|
||||||
|
import SecretList from '~/components/secrets/SecretList.vue';
|
||||||
|
import useApiClient from '~/compositions/useApiClient';
|
||||||
|
import { useAsyncAction } from '~/compositions/useAsyncAction';
|
||||||
|
import useAuthentication from '~/compositions/useAuthentication';
|
||||||
|
import useNotifications from '~/compositions/useNotifications';
|
||||||
|
import { usePagination } from '~/compositions/usePaginate';
|
||||||
|
import { Secret, WebhookEvents } from '~/lib/api/types';
|
||||||
|
|
||||||
|
const emptySecret = {
|
||||||
|
name: '',
|
||||||
|
value: '',
|
||||||
|
image: [],
|
||||||
|
event: [WebhookEvents.Push],
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiClient = useApiClient();
|
||||||
|
const notifications = useNotifications();
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const { user } = useAuthentication();
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('Unexpected: Unauthenticated');
|
||||||
|
}
|
||||||
|
const selectedSecret = ref<Partial<Secret>>();
|
||||||
|
const isEditingSecret = computed(() => !!selectedSecret.value?.id);
|
||||||
|
|
||||||
|
async function loadSecrets(page: number): Promise<Secret[] | null> {
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('Unexpected: Unauthenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiClient.getOrgSecretList(user.org_id, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { resetPage, data: secrets } = usePagination(loadSecrets, () => !selectedSecret.value);
|
||||||
|
|
||||||
|
const { doSubmit: createSecret, isLoading: isSaving } = useAsyncAction(async () => {
|
||||||
|
if (!selectedSecret.value) {
|
||||||
|
throw new Error("Unexpected: Can't get secret");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditingSecret.value) {
|
||||||
|
await apiClient.updateOrgSecret(user.org_id, selectedSecret.value);
|
||||||
|
} else {
|
||||||
|
await apiClient.createOrgSecret(user.org_id, selectedSecret.value);
|
||||||
|
}
|
||||||
|
notifications.notify({
|
||||||
|
title: i18n.t(isEditingSecret.value ? 'user.settings.secrets.saved' : 'user.settings.secrets.created'),
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
selectedSecret.value = undefined;
|
||||||
|
resetPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
const { doSubmit: deleteSecret, isLoading: isDeleting } = useAsyncAction(async (_secret: Secret) => {
|
||||||
|
await apiClient.deleteOrgSecret(user.org_id, _secret.name);
|
||||||
|
notifications.notify({ title: i18n.t('user.settings.secrets.deleted'), type: 'success' });
|
||||||
|
resetPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
function editSecret(secret: Secret) {
|
||||||
|
selectedSecret.value = cloneDeep(secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAddSecret() {
|
||||||
|
selectedSecret.value = cloneDeep(emptySecret);
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -17,4 +17,7 @@ export type User = {
|
||||||
|
|
||||||
active: boolean;
|
active: boolean;
|
||||||
// Whether the account is currently active.
|
// Whether the account is currently active.
|
||||||
|
|
||||||
|
org_id: number;
|
||||||
|
// The ID of the org assigned to the user.
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
<Tab id="general" :title="$t('user.settings.general.general')">
|
<Tab id="general" :title="$t('user.settings.general.general')">
|
||||||
<UserGeneralTab />
|
<UserGeneralTab />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab id="secrets" :title="$t('user.settings.secrets.secrets')">
|
||||||
|
<UserSecretsTab />
|
||||||
|
</Tab>
|
||||||
<Tab id="api" :title="$t('user.settings.api.api')">
|
<Tab id="api" :title="$t('user.settings.api.api')">
|
||||||
<UserAPITab />
|
<UserAPITab />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
@ -16,6 +19,7 @@ import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
|
||||||
import Tab from '~/components/layout/scaffold/Tab.vue';
|
import Tab from '~/components/layout/scaffold/Tab.vue';
|
||||||
import UserAPITab from '~/components/user/UserAPITab.vue';
|
import UserAPITab from '~/components/user/UserAPITab.vue';
|
||||||
import UserGeneralTab from '~/components/user/UserGeneralTab.vue';
|
import UserGeneralTab from '~/components/user/UserGeneralTab.vue';
|
||||||
|
import UserSecretsTab from '~/components/user/UserSecretsTab.vue';
|
||||||
import useConfig from '~/compositions/useConfig';
|
import useConfig from '~/compositions/useConfig';
|
||||||
|
|
||||||
const address = `${window.location.protocol}//${window.location.host}${useConfig().rootPath}`; // port is included in location.host
|
const address = `${window.location.protocol}//${window.location.host}${useConfig().rootPath}`; // port is included in location.host
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
|
|
||||||
<template #titleActions>
|
<template #titleActions>
|
||||||
<IconButton
|
<IconButton
|
||||||
v-if="!org.is_user && orgPermissions.admin"
|
v-if="orgPermissions.admin"
|
||||||
icon="settings"
|
icon="settings"
|
||||||
:to="{ name: 'org-settings' }"
|
:to="{ name: org.is_user ? 'user' : 'org-settings' }"
|
||||||
:title="$t('org.settings.settings')"
|
:title="$t('org.settings.settings')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
|
|
||||||
<template #titleActions>
|
<template #titleActions>
|
||||||
<IconButton
|
<IconButton
|
||||||
v-if="!org.is_user && orgPermissions.admin"
|
v-if="orgPermissions.admin"
|
||||||
:to="{ name: 'repo-settings' }"
|
:to="{ name: org.is_user ? 'user' : 'repo-settings' }"
|
||||||
:title="$t('org.settings.settings')"
|
:title="$t('org.settings.settings')"
|
||||||
icon="settings"
|
icon="settings"
|
||||||
/>
|
/>
|
||||||
|
@ -41,15 +41,8 @@ provide('org-permissions', orgPermissions);
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
org.value = await apiClient.getOrg(orgId.value);
|
org.value = await apiClient.getOrg(orgId.value);
|
||||||
if (org.value.is_user) {
|
|
||||||
orgPermissions.value = {
|
|
||||||
member: true,
|
|
||||||
admin: true,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
orgPermissions.value = await apiClient.getOrgPermissions(org.value.id);
|
orgPermissions.value = await apiClient.getOrgPermissions(org.value.id);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(load);
|
onMounted(load);
|
||||||
watch(orgId, load);
|
watch(orgId, load);
|
||||||
|
|
Loading…
Reference in a new issue