feat(teams): add public flags to teams to allow easier sharing with other teams (#2179)
Resolves #2173 Co-authored-by: Daniel Herrmann <daniel.herrmann1@gmail.com> Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2179 Reviewed-by: konrad <k@knt.li> Co-authored-by: waza-ari <daniel.herrmann@makerspace-darmstadt.de> Co-committed-by: waza-ari <daniel.herrmann@makerspace-darmstadt.de>
This commit is contained in:
parent
d7fdefcead
commit
ffa82556e0
@ -62,6 +62,9 @@ service:
|
|||||||
allowiconchanges: true
|
allowiconchanges: true
|
||||||
# Allow using a custom logo via external URL.
|
# Allow using a custom logo via external URL.
|
||||||
customlogourl: ''
|
customlogourl: ''
|
||||||
|
# Enables the public team feature. If enabled, it is possible to configure teams to be public, which makes them
|
||||||
|
# discoverable when sharing a project, therefore not only showing teams the user is member of.
|
||||||
|
enablepublicteams: false
|
||||||
|
|
||||||
sentry:
|
sentry:
|
||||||
# If set to true, enables anonymous error tracking of api errors via Sentry. This allows us to gather more
|
# If set to true, enables anonymous error tracking of api errors via Sentry. This allows us to gather more
|
||||||
|
@ -346,6 +346,17 @@ Full path: `service.customlogourl`
|
|||||||
Environment path: `VIKUNJA_SERVICE_CUSTOMLOGOURL`
|
Environment path: `VIKUNJA_SERVICE_CUSTOMLOGOURL`
|
||||||
|
|
||||||
|
|
||||||
|
### enablepublicteams
|
||||||
|
|
||||||
|
discoverable when sharing a project, therefore not only showing teams the user is member of.
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Full path: `service.enablepublicteams`
|
||||||
|
|
||||||
|
Environment path: `VIKUNJA_SERVICE_ENABLEPUBLICTEAMS`
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## sentry
|
## sentry
|
||||||
|
@ -99,7 +99,7 @@ It depends on the provider being used as well as the preferences of the administ
|
|||||||
Typically you'd want to request an additional scope (e.g. `vikunja_scope`) which then triggers the identity provider to add the claim.
|
Typically you'd want to request an additional scope (e.g. `vikunja_scope`) which then triggers the identity provider to add the claim.
|
||||||
If the `vikunja_groups` is part of the **ID token**, Vikunja will start the procedure and import teams and team memberships.
|
If the `vikunja_groups` is part of the **ID token**, Vikunja will start the procedure and import teams and team memberships.
|
||||||
|
|
||||||
The claim structure expexted by Vikunja is as follows:
|
The minimal claim structure expected by Vikunja is as follows:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -116,6 +116,21 @@ The claim structure expexted by Vikunja is as follows:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
It also also possible to pass the description and isPublic flag as optional parameter. If not present, the description will be empty and project visibility defaults to false.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"vikunja_groups": [
|
||||||
|
{
|
||||||
|
"name": "team 3",
|
||||||
|
"oidcID": 33349,
|
||||||
|
"description": "My Team Description",
|
||||||
|
"isPublic": true
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
For each team, you need to define a team `name` and an `oidcID`, where the `oidcID` can be any string with a length of less than 250 characters.
|
For each team, you need to define a team `name` and an `oidcID`, where the `oidcID` can be any string with a length of less than 250 characters.
|
||||||
The `oidcID` is used to uniquely identify the team, so please make sure to keep this unique.
|
The `oidcID` is used to uniquely identify the team, so please make sure to keep this unique.
|
||||||
|
|
||||||
|
@ -172,6 +172,7 @@ import Multiselect from '@/components/input/multiselect.vue'
|
|||||||
import Nothing from '@/components/misc/nothing.vue'
|
import Nothing from '@/components/misc/nothing.vue'
|
||||||
import {success} from '@/message'
|
import {success} from '@/message'
|
||||||
import {useAuthStore} from '@/stores/auth'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
import {useConfigStore} from '@/stores/config'
|
||||||
|
|
||||||
// FIXME: I think this whole thing can now only manage user/team sharing for projects? Maybe remove a little generalization?
|
// FIXME: I think this whole thing can now only manage user/team sharing for projects? Maybe remove a little generalization?
|
||||||
|
|
||||||
@ -210,8 +211,8 @@ const selectedRight = ref({})
|
|||||||
const sharables = ref([])
|
const sharables = ref([])
|
||||||
const showDeleteModal = ref(false)
|
const showDeleteModal = ref(false)
|
||||||
|
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
const configStore = useConfigStore()
|
||||||
const userInfo = computed(() => authStore.info)
|
const userInfo = computed(() => authStore.info)
|
||||||
|
|
||||||
function createShareTypeNameComputed(count: number) {
|
function createShareTypeNameComputed(count: number) {
|
||||||
@ -360,7 +361,15 @@ async function find(query: string) {
|
|||||||
found.value = []
|
found.value = []
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const results = await searchService.getAll({}, {s: query})
|
|
||||||
|
// Include public teams here if we are sharing with teams and its enabled in the config
|
||||||
|
let results = []
|
||||||
|
if (props.shareType === 'team' && configStore.publicTeamsEnabled) {
|
||||||
|
results = await searchService.getAll({}, {s: query, includePublic: true})
|
||||||
|
} else {
|
||||||
|
results = await searchService.getAll({}, {s: query})
|
||||||
|
}
|
||||||
|
|
||||||
found.value = results
|
found.value = results
|
||||||
.filter(m => {
|
.filter(m => {
|
||||||
if(props.shareType === 'user' && m.id === currentUserId.value) {
|
if(props.shareType === 'user' && m.id === currentUserId.value) {
|
||||||
|
@ -986,7 +986,9 @@
|
|||||||
"description": "Description",
|
"description": "Description",
|
||||||
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
|
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"member": "Member"
|
"member": "Member",
|
||||||
|
"isPublic": "Public Team",
|
||||||
|
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
|
@ -10,6 +10,7 @@ export interface ITeam extends IAbstract {
|
|||||||
members: ITeamMember[]
|
members: ITeamMember[]
|
||||||
right: Right
|
right: Right
|
||||||
oidcId: string
|
oidcId: string
|
||||||
|
isPublic: boolean
|
||||||
|
|
||||||
createdBy: IUser
|
createdBy: IUser
|
||||||
created: Date
|
created: Date
|
||||||
|
@ -14,6 +14,7 @@ export default class TeamModel extends AbstractModel<ITeam> implements ITeam {
|
|||||||
members: ITeamMember[] = []
|
members: ITeamMember[] = []
|
||||||
right: Right = RIGHTS.READ
|
right: Right = RIGHTS.READ
|
||||||
oidcId = ''
|
oidcId = ''
|
||||||
|
isPublic: boolean = false
|
||||||
|
|
||||||
createdBy: IUser = {} // FIXME: seems wrong
|
createdBy: IUser = {} // FIXME: seems wrong
|
||||||
created: Date = null
|
created: Date = null
|
||||||
|
@ -37,6 +37,7 @@ export interface ConfigState {
|
|||||||
providers: IProvider[],
|
providers: IProvider[],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
publicTeamsEnabled: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useConfigStore = defineStore('config', () => {
|
export const useConfigStore = defineStore('config', () => {
|
||||||
@ -70,6 +71,7 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
providers: [],
|
providers: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
publicTeamsEnabled: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const migratorsEnabled = computed(() => state.availableMigrators?.length > 0)
|
const migratorsEnabled = computed(() => state.availableMigrators?.length > 0)
|
||||||
|
@ -33,6 +33,27 @@
|
|||||||
>
|
>
|
||||||
{{ $t('team.attributes.nameRequired') }}
|
{{ $t('team.attributes.nameRequired') }}
|
||||||
</p>
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="configStore.publicTeamsEnabled"
|
||||||
|
class="field"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="label"
|
||||||
|
for="teamIsPublic"
|
||||||
|
>{{ $t('team.attributes.isPublic') }}</label>
|
||||||
|
<div
|
||||||
|
class="control is-expanded"
|
||||||
|
:class="{ 'is-loading': teamService.loading }"
|
||||||
|
>
|
||||||
|
<Fancycheckbox
|
||||||
|
v-model="team.isPublic"
|
||||||
|
:disabled="teamMemberService.loading || undefined"
|
||||||
|
:class="{ 'disabled': teamService.loading }"
|
||||||
|
>
|
||||||
|
{{ $t('team.attributes.isPublicDescription') }}
|
||||||
|
</Fancycheckbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label
|
<label
|
||||||
class="label"
|
class="label"
|
||||||
@ -242,6 +263,7 @@ import {useI18n} from 'vue-i18n'
|
|||||||
import {useRoute, useRouter} from 'vue-router'
|
import {useRoute, useRouter} from 'vue-router'
|
||||||
|
|
||||||
import Editor from '@/components/input/AsyncEditor'
|
import Editor from '@/components/input/AsyncEditor'
|
||||||
|
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||||
import Multiselect from '@/components/input/multiselect.vue'
|
import Multiselect from '@/components/input/multiselect.vue'
|
||||||
import User from '@/components/misc/user.vue'
|
import User from '@/components/misc/user.vue'
|
||||||
|
|
||||||
@ -254,12 +276,14 @@ import {RIGHTS as Rights} from '@/constants/rights'
|
|||||||
import {useTitle} from '@/composables/useTitle'
|
import {useTitle} from '@/composables/useTitle'
|
||||||
import {success} from '@/message'
|
import {success} from '@/message'
|
||||||
import {useAuthStore} from '@/stores/auth'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
import {useConfigStore} from '@/stores/config'
|
||||||
|
|
||||||
import type {ITeam} from '@/modelTypes/ITeam'
|
import type {ITeam} from '@/modelTypes/ITeam'
|
||||||
import type {IUser} from '@/modelTypes/IUser'
|
import type {IUser} from '@/modelTypes/IUser'
|
||||||
import type {ITeamMember} from '@/modelTypes/ITeamMember'
|
import type {ITeamMember} from '@/modelTypes/ITeamMember'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
const configStore = useConfigStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const {t} = useI18n({useScope: 'global'})
|
const {t} = useI18n({useScope: 'global'})
|
||||||
|
@ -25,6 +25,26 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="configStore.publicTeamsEnabled"
|
||||||
|
class="field"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="label"
|
||||||
|
for="teamIsPublic"
|
||||||
|
>{{ $t('team.attributes.isPublic') }}</label>
|
||||||
|
<div
|
||||||
|
class="control is-expanded"
|
||||||
|
:class="{ 'is-loading': teamService.loading }"
|
||||||
|
>
|
||||||
|
<Fancycheckbox
|
||||||
|
v-model="team.isPublic"
|
||||||
|
:class="{ 'disabled': teamService.loading }"
|
||||||
|
>
|
||||||
|
{{ $t('team.attributes.isPublicDescription') }}
|
||||||
|
</Fancycheckbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p
|
<p
|
||||||
v-if="showError && team.name === ''"
|
v-if="showError && team.name === ''"
|
||||||
class="help is-danger"
|
class="help is-danger"
|
||||||
@ -46,11 +66,14 @@ import TeamModel from '@/models/team'
|
|||||||
import TeamService from '@/services/team'
|
import TeamService from '@/services/team'
|
||||||
|
|
||||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||||
|
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||||
|
|
||||||
import {useTitle} from '@/composables/useTitle'
|
import {useTitle} from '@/composables/useTitle'
|
||||||
import {useRouter} from 'vue-router'
|
import {useRouter} from 'vue-router'
|
||||||
import {success} from '@/message'
|
import {success} from '@/message'
|
||||||
|
|
||||||
|
import {useConfigStore} from '@/stores/config'
|
||||||
|
|
||||||
const {t} = useI18n()
|
const {t} = useI18n()
|
||||||
const title = computed(() => t('team.create.title'))
|
const title = computed(() => t('team.create.title'))
|
||||||
useTitle(title)
|
useTitle(title)
|
||||||
@ -60,6 +83,8 @@ const teamService = shallowReactive(new TeamService())
|
|||||||
const team = reactive(new TeamModel())
|
const team = reactive(new TeamModel())
|
||||||
const showError = ref(false)
|
const showError = ref(false)
|
||||||
|
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
async function newTeam() {
|
async function newTeam() {
|
||||||
if (team.name === '') {
|
if (team.name === '') {
|
||||||
showError.value = true
|
showError.value = true
|
||||||
|
@ -64,6 +64,7 @@ const (
|
|||||||
ServiceMaxAvatarSize Key = `service.maxavatarsize`
|
ServiceMaxAvatarSize Key = `service.maxavatarsize`
|
||||||
ServiceAllowIconChanges Key = `service.allowiconchanges`
|
ServiceAllowIconChanges Key = `service.allowiconchanges`
|
||||||
ServiceCustomLogoURL Key = `service.customlogourl`
|
ServiceCustomLogoURL Key = `service.customlogourl`
|
||||||
|
ServiceEnablePublicTeams Key = `service.enablepublicteams`
|
||||||
|
|
||||||
SentryEnabled Key = `sentry.enabled`
|
SentryEnabled Key = `sentry.enabled`
|
||||||
SentryDsn Key = `sentry.dsn`
|
SentryDsn Key = `sentry.dsn`
|
||||||
@ -312,6 +313,7 @@ func InitDefaultConfig() {
|
|||||||
ServiceMaxAvatarSize.setDefault(1024)
|
ServiceMaxAvatarSize.setDefault(1024)
|
||||||
ServiceDemoMode.setDefault(false)
|
ServiceDemoMode.setDefault(false)
|
||||||
ServiceAllowIconChanges.setDefault(true)
|
ServiceAllowIconChanges.setDefault(true)
|
||||||
|
ServiceEnablePublicTeams.setDefault(false)
|
||||||
|
|
||||||
// Sentry
|
// Sentry
|
||||||
SentryDsn.setDefault("https://440eedc957d545a795c17bbaf477497c@o1047380.ingest.sentry.io/4504254983634944")
|
SentryDsn.setDefault("https://440eedc957d545a795c17bbaf477497c@o1047380.ingest.sentry.io/4504254983634944")
|
||||||
|
@ -1,61 +1,49 @@
|
|||||||
-
|
- team_id: 1
|
||||||
team_id: 1
|
|
||||||
user_id: 1
|
user_id: 1
|
||||||
admin: true
|
admin: true
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
- team_id: 1
|
||||||
team_id: 1
|
|
||||||
user_id: 2
|
user_id: 2
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
- team_id: 2
|
||||||
team_id: 2
|
|
||||||
user_id: 1
|
user_id: 1
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
- team_id: 3
|
||||||
team_id: 3
|
|
||||||
user_id: 1
|
user_id: 1
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
- team_id: 4
|
||||||
team_id: 4
|
|
||||||
user_id: 1
|
user_id: 1
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
- team_id: 5
|
||||||
team_id: 5
|
|
||||||
user_id: 1
|
user_id: 1
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
- team_id: 6
|
||||||
team_id: 6
|
|
||||||
user_id: 1
|
user_id: 1
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
- team_id: 7
|
||||||
team_id: 7
|
|
||||||
user_id: 1
|
user_id: 1
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
- team_id: 8
|
||||||
team_id: 8
|
|
||||||
user_id: 1
|
user_id: 1
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
- team_id: 9
|
||||||
team_id: 9
|
|
||||||
user_id: 2
|
user_id: 2
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
- team_id: 10
|
||||||
team_id: 10
|
|
||||||
user_id: 3
|
user_id: 3
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
- team_id: 11
|
||||||
team_id: 11
|
|
||||||
user_id: 8
|
user_id: 8
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
- team_id: 12
|
||||||
team_id: 12
|
|
||||||
user_id: 9
|
user_id: 9
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
- team_id: 13
|
||||||
team_id: 13
|
|
||||||
user_id: 10
|
user_id: 10
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
- team_id: 14
|
||||||
team_id: 14
|
user_id: 10
|
||||||
|
created: 2018-12-01 15:13:12
|
||||||
|
- team_id: 15
|
||||||
user_id: 10
|
user_id: 10
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
@ -29,8 +29,16 @@
|
|||||||
- id: 13
|
- id: 13
|
||||||
name: testteam13
|
name: testteam13
|
||||||
created_by_id: 7
|
created_by_id: 7
|
||||||
|
is_public: true
|
||||||
- id: 14
|
- id: 14
|
||||||
name: testteam14
|
name: testteam14
|
||||||
created_by_id: 7
|
created_by_id: 7
|
||||||
oidc_id: 14
|
oidc_id: 14
|
||||||
issuer: "https://some.issuer"
|
issuer: "https://some.issuer"
|
||||||
|
- id: 15
|
||||||
|
name: testteam15
|
||||||
|
created_by_id: 7
|
||||||
|
oidc_id: 15
|
||||||
|
issuer: "https://some.issuer"
|
||||||
|
is_public: true
|
||||||
|
description: "This is a public team"
|
||||||
|
43
pkg/migration/20240309111148.go
Normal file
43
pkg/migration/20240309111148.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public Licensee for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public Licensee
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package migration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"src.techknowlogick.com/xormigrate"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type teams20240309111148 struct {
|
||||||
|
IsPublic bool `xorm:"not null default false" json:"is_public"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (teams20240309111148) TableName() string {
|
||||||
|
return "teams"
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
migrations = append(migrations, &xormigrate.Migration{
|
||||||
|
ID: "20240309111148",
|
||||||
|
Description: "Add IsPublic field to teams table to control discoverability of teams.",
|
||||||
|
Migrate: func(tx *xorm.Engine) error {
|
||||||
|
return tx.Sync2(teams20240309111148{})
|
||||||
|
},
|
||||||
|
Rollback: func(tx *xorm.Engine) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
@ -19,6 +19,7 @@ package models
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.vikunja.io/api/pkg/config"
|
||||||
"code.vikunja.io/api/pkg/db"
|
"code.vikunja.io/api/pkg/db"
|
||||||
|
|
||||||
"code.vikunja.io/api/pkg/events"
|
"code.vikunja.io/api/pkg/events"
|
||||||
@ -53,6 +54,12 @@ type Team struct {
|
|||||||
// A timestamp when this relation was last updated. You cannot change this value.
|
// A timestamp when this relation was last updated. You cannot change this value.
|
||||||
Updated time.Time `xorm:"updated" json:"updated"`
|
Updated time.Time `xorm:"updated" json:"updated"`
|
||||||
|
|
||||||
|
// Defines wether the team should be publicly discoverable when sharing a project
|
||||||
|
IsPublic bool `xorm:"not null default false" json:"is_public"`
|
||||||
|
|
||||||
|
// Query parameter controlling whether to include public projects or not
|
||||||
|
IncludePublic bool `xorm:"-" query:"include_public" json:"include_public"`
|
||||||
|
|
||||||
web.CRUDable `xorm:"-" json:"-"`
|
web.CRUDable `xorm:"-" json:"-"`
|
||||||
web.Rights `xorm:"-" json:"-"`
|
web.Rights `xorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
@ -100,6 +107,7 @@ type OIDCTeam struct {
|
|||||||
Name string
|
Name string
|
||||||
OidcID string
|
OidcID string
|
||||||
Description string
|
Description string
|
||||||
|
IsPublic bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTeamByID gets a team by its ID
|
// GetTeamByID gets a team by its ID
|
||||||
@ -287,11 +295,24 @@ func (t *Team) ReadAll(s *xorm.Session, a web.Auth, search string, page int, per
|
|||||||
|
|
||||||
limit, start := getLimitFromPageIndex(page, perPage)
|
limit, start := getLimitFromPageIndex(page, perPage)
|
||||||
all := []*Team{}
|
all := []*Team{}
|
||||||
|
|
||||||
query := s.Select("teams.*").
|
query := s.Select("teams.*").
|
||||||
Table("teams").
|
Table("teams").
|
||||||
Join("INNER", "team_members", "team_members.team_id = teams.id").
|
Join("INNER", "team_members", "team_members.team_id = teams.id").
|
||||||
Where("team_members.user_id = ?", a.GetID()).
|
|
||||||
Where(db.ILIKE("teams.name", search))
|
Where(db.ILIKE("teams.name", search))
|
||||||
|
|
||||||
|
// If public teams are enabled, we want to include them in the result
|
||||||
|
if config.ServiceEnablePublicTeams.GetBool() && t.IncludePublic {
|
||||||
|
query = query.Where(
|
||||||
|
builder.Or(
|
||||||
|
builder.Eq{"teams.is_public": true},
|
||||||
|
builder.Eq{"team_members.user_id": a.GetID()},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
query = query.Where("team_members.user_id = ?", a.GetID())
|
||||||
|
}
|
||||||
|
|
||||||
if limit > 0 {
|
if limit > 0 {
|
||||||
query = query.Limit(limit, start)
|
query = query.Limit(limit, start)
|
||||||
}
|
}
|
||||||
@ -398,7 +419,7 @@ func (t *Team) Update(s *xorm.Session, _ web.Auth) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = s.ID(t.ID).Update(t)
|
_, err = s.ID(t.ID).UseBool("is_public").Update(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.vikunja.io/api/pkg/config"
|
||||||
"code.vikunja.io/api/pkg/db"
|
"code.vikunja.io/api/pkg/db"
|
||||||
"code.vikunja.io/api/pkg/user"
|
"code.vikunja.io/api/pkg/user"
|
||||||
|
|
||||||
@ -49,6 +50,7 @@ func TestTeam_Create(t *testing.T) {
|
|||||||
"id": team.ID,
|
"id": team.ID,
|
||||||
"name": "Testteam293",
|
"name": "Testteam293",
|
||||||
"description": "Lorem Ispum",
|
"description": "Lorem Ispum",
|
||||||
|
"is_public": false,
|
||||||
}, false)
|
}, false)
|
||||||
})
|
})
|
||||||
t.Run("empty name", func(t *testing.T) {
|
t.Run("empty name", func(t *testing.T) {
|
||||||
@ -61,6 +63,27 @@ func TestTeam_Create(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.True(t, IsErrTeamNameCannotBeEmpty(err))
|
assert.True(t, IsErrTeamNameCannotBeEmpty(err))
|
||||||
})
|
})
|
||||||
|
t.Run("public", func(t *testing.T) {
|
||||||
|
db.LoadAndAssertFixtures(t)
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
team := &Team{
|
||||||
|
Name: "Testteam293_Public",
|
||||||
|
Description: "Lorem Ispum",
|
||||||
|
IsPublic: true,
|
||||||
|
}
|
||||||
|
err := team.Create(s, doer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = s.Commit()
|
||||||
|
require.NoError(t, err)
|
||||||
|
db.AssertExists(t, "teams", map[string]interface{}{
|
||||||
|
"id": team.ID,
|
||||||
|
"name": "Testteam293_Public",
|
||||||
|
"description": "Lorem Ispum",
|
||||||
|
"is_public": true,
|
||||||
|
}, false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTeam_ReadOne(t *testing.T) {
|
func TestTeam_ReadOne(t *testing.T) {
|
||||||
@ -126,6 +149,58 @@ func TestTeam_ReadAll(t *testing.T) {
|
|||||||
assert.Len(t, ts, 1)
|
assert.Len(t, ts, 1)
|
||||||
assert.Equal(t, int64(2), ts[0].ID)
|
assert.Equal(t, int64(2), ts[0].ID)
|
||||||
})
|
})
|
||||||
|
t.Run("public discovery disabled", func(t *testing.T) {
|
||||||
|
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
team := &Team{}
|
||||||
|
|
||||||
|
// Default setting is having ServiceEnablePublicTeams disabled
|
||||||
|
// In this default case, fetching teams with or without public flag should return the same result
|
||||||
|
|
||||||
|
// Fetch without public flag
|
||||||
|
teams, _, _, err := team.ReadAll(s, doer, "", 1, 50)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, reflect.Slice, reflect.TypeOf(teams).Kind())
|
||||||
|
ts := teams.([]*Team)
|
||||||
|
assert.Len(t, ts, 5)
|
||||||
|
|
||||||
|
// Fetch with public flag
|
||||||
|
team.IncludePublic = true
|
||||||
|
teams, _, _, err = team.ReadAll(s, doer, "", 1, 50)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, reflect.Slice, reflect.TypeOf(teams).Kind())
|
||||||
|
ts = teams.([]*Team)
|
||||||
|
assert.Len(t, ts, 5)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("public discovery enabled", func(t *testing.T) {
|
||||||
|
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
team := &Team{}
|
||||||
|
|
||||||
|
// Enable ServiceEnablePublicTeams feature
|
||||||
|
config.ServiceEnablePublicTeams.Set(true)
|
||||||
|
|
||||||
|
// Fetch without public flag should be the same as before
|
||||||
|
team.IncludePublic = false
|
||||||
|
teams, _, _, err := team.ReadAll(s, doer, "", 1, 50)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, reflect.Slice, reflect.TypeOf(teams).Kind())
|
||||||
|
ts := teams.([]*Team)
|
||||||
|
assert.Len(t, ts, 5)
|
||||||
|
|
||||||
|
// Fetch with public flag should return more teams
|
||||||
|
team.IncludePublic = true
|
||||||
|
teams, _, _, err = team.ReadAll(s, doer, "", 1, 50)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, reflect.Slice, reflect.TypeOf(teams).Kind())
|
||||||
|
ts = teams.([]*Team)
|
||||||
|
assert.Len(t, ts, 7)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTeam_Update(t *testing.T) {
|
func TestTeam_Update(t *testing.T) {
|
||||||
|
@ -298,14 +298,27 @@ func getTeamDataFromToken(groups []map[string]interface{}, provider *Provider) (
|
|||||||
var name string
|
var name string
|
||||||
var description string
|
var description string
|
||||||
var oidcID string
|
var oidcID string
|
||||||
|
var IsPublic bool
|
||||||
|
|
||||||
|
// Read name
|
||||||
_, exists := team["name"]
|
_, exists := team["name"]
|
||||||
if exists {
|
if exists {
|
||||||
name = team["name"].(string)
|
name = team["name"].(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read description
|
||||||
_, exists = team["description"]
|
_, exists = team["description"]
|
||||||
if exists {
|
if exists {
|
||||||
description = team["description"].(string)
|
description = team["description"].(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read isPublic flag
|
||||||
|
_, exists = team["isPublic"]
|
||||||
|
if exists {
|
||||||
|
IsPublic = team["isPublic"].(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read oidcID
|
||||||
_, exists = team["oidcID"]
|
_, exists = team["oidcID"]
|
||||||
if exists {
|
if exists {
|
||||||
switch t := team["oidcID"].(type) {
|
switch t := team["oidcID"].(type) {
|
||||||
@ -324,7 +337,7 @@ func getTeamDataFromToken(groups []map[string]interface{}, provider *Provider) (
|
|||||||
errs = append(errs, &user.ErrOpenIDCustomScopeMalformed{})
|
errs = append(errs, &user.ErrOpenIDCustomScopeMalformed{})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
teamData = append(teamData, &models.OIDCTeam{Name: name, OidcID: oidcID, Description: description})
|
teamData = append(teamData, &models.OIDCTeam{Name: name, OidcID: oidcID, Description: description, IsPublic: IsPublic})
|
||||||
}
|
}
|
||||||
return teamData, errs
|
return teamData, errs
|
||||||
}
|
}
|
||||||
@ -339,6 +352,7 @@ func CreateOIDCTeam(s *xorm.Session, teamData *models.OIDCTeam, u *user.User, is
|
|||||||
Description: teamData.Description,
|
Description: teamData.Description,
|
||||||
OidcID: teamData.OidcID,
|
OidcID: teamData.OidcID,
|
||||||
Issuer: issuer,
|
Issuer: issuer,
|
||||||
|
IsPublic: teamData.IsPublic,
|
||||||
}
|
}
|
||||||
err = team.CreateNewTeam(s, u, false)
|
err = team.CreateNewTeam(s, u, false)
|
||||||
return team, err
|
return team, err
|
||||||
@ -363,12 +377,24 @@ func GetOrCreateTeamsByOIDC(s *xorm.Session, teamData []*models.OIDCTeam, u *use
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compare the name and update if it changed
|
||||||
if team.Name != getOIDCTeamName(oidcTeam.Name) {
|
if team.Name != getOIDCTeamName(oidcTeam.Name) {
|
||||||
team.Name = getOIDCTeamName(oidcTeam.Name)
|
team.Name = getOIDCTeamName(oidcTeam.Name)
|
||||||
err = team.Update(s, u)
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
// Compare the description and update if it changed
|
||||||
}
|
if team.Description != oidcTeam.Description {
|
||||||
|
team.Description = oidcTeam.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare the isPublic flag and update if it changed
|
||||||
|
if team.IsPublic != oidcTeam.IsPublic {
|
||||||
|
team.IsPublic = oidcTeam.IsPublic
|
||||||
|
}
|
||||||
|
|
||||||
|
err = team.Update(s, u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Team with oidc_id %v and name %v already exists.", team.OidcID, team.Name)
|
log.Debugf("Team with oidc_id %v and name %v already exists.", team.OidcID, team.Name)
|
||||||
|
@ -128,8 +128,41 @@ func TestGetOrCreateUser(t *testing.T) {
|
|||||||
"email": cl.Email,
|
"email": cl.Email,
|
||||||
}, false)
|
}, false)
|
||||||
db.AssertExists(t, "teams", map[string]interface{}{
|
db.AssertExists(t, "teams", map[string]interface{}{
|
||||||
"id": oidcTeams,
|
"id": oidcTeams,
|
||||||
"name": team + " (OIDC)",
|
"name": team + " (OIDC)",
|
||||||
|
"is_public": false,
|
||||||
|
}, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Update IsPublic flag for existing team", func(t *testing.T) {
|
||||||
|
db.LoadAndAssertFixtures(t)
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
team := "testteam15"
|
||||||
|
oidcID := "15"
|
||||||
|
cl := &claims{
|
||||||
|
Email: "other-email-address@some.service.com",
|
||||||
|
VikunjaGroups: []map[string]interface{}{
|
||||||
|
{"name": team, "oidcID": oidcID, "isPublic": true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := getOrCreateUser(s, cl, "https://some.service.com", "12345")
|
||||||
|
require.NoError(t, err)
|
||||||
|
teamData, errs := getTeamDataFromToken(cl.VikunjaGroups, nil)
|
||||||
|
for _, err := range errs {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
oidcTeams, err := AssignOrCreateUserToTeams(s, u, teamData, "https://some.issuer")
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = s.Commit()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
db.AssertExists(t, "teams", map[string]interface{}{
|
||||||
|
"id": oidcTeams,
|
||||||
|
"name": team + " (OIDC)",
|
||||||
|
"is_public": true,
|
||||||
}, false)
|
}, false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -51,6 +51,7 @@ type vikunjaInfos struct {
|
|||||||
TaskCommentsEnabled bool `json:"task_comments_enabled"`
|
TaskCommentsEnabled bool `json:"task_comments_enabled"`
|
||||||
DemoModeEnabled bool `json:"demo_mode_enabled"`
|
DemoModeEnabled bool `json:"demo_mode_enabled"`
|
||||||
WebhooksEnabled bool `json:"webhooks_enabled"`
|
WebhooksEnabled bool `json:"webhooks_enabled"`
|
||||||
|
PublicTeamsEnabled bool `json:"public_teams_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type authInfo struct {
|
type authInfo struct {
|
||||||
@ -95,6 +96,7 @@ func Info(c echo.Context) error {
|
|||||||
TaskCommentsEnabled: config.ServiceEnableTaskComments.GetBool(),
|
TaskCommentsEnabled: config.ServiceEnableTaskComments.GetBool(),
|
||||||
DemoModeEnabled: config.ServiceDemoMode.GetBool(),
|
DemoModeEnabled: config.ServiceDemoMode.GetBool(),
|
||||||
WebhooksEnabled: config.WebhooksEnabled.GetBool(),
|
WebhooksEnabled: config.WebhooksEnabled.GetBool(),
|
||||||
|
PublicTeamsEnabled: config.ServiceEnablePublicTeams.GetBool(),
|
||||||
AvailableMigrators: []string{
|
AvailableMigrators: []string{
|
||||||
(&vikunja_file.FileMigrator{}).Name(),
|
(&vikunja_file.FileMigrator{}).Name(),
|
||||||
(&ticktick.Migrator{}).Name(),
|
(&ticktick.Migrator{}).Name(),
|
||||||
|
@ -8229,6 +8229,14 @@ const docTemplate = `{
|
|||||||
"description": "The unique, numeric id of this team.",
|
"description": "The unique, numeric id of this team.",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"include_public": {
|
||||||
|
"description": "Query parameter controlling whether to include public projects or not",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"is_public": {
|
||||||
|
"description": "Defines wether the team should be publicly discoverable when sharing a project",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"members": {
|
"members": {
|
||||||
"description": "An array of all members in this team.",
|
"description": "An array of all members in this team.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@ -8364,6 +8372,14 @@ const docTemplate = `{
|
|||||||
"description": "The unique, numeric id of this team.",
|
"description": "The unique, numeric id of this team.",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"include_public": {
|
||||||
|
"description": "Query parameter controlling whether to include public projects or not",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"is_public": {
|
||||||
|
"description": "Defines wether the team should be publicly discoverable when sharing a project",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"members": {
|
"members": {
|
||||||
"description": "An array of all members in this team.",
|
"description": "An array of all members in this team.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@ -8886,6 +8902,9 @@ const docTemplate = `{
|
|||||||
"motd": {
|
"motd": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"public_teams_enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"registration_enabled": {
|
"registration_enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
@ -8221,6 +8221,14 @@
|
|||||||
"description": "The unique, numeric id of this team.",
|
"description": "The unique, numeric id of this team.",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"include_public": {
|
||||||
|
"description": "Query parameter controlling whether to include public projects or not",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"is_public": {
|
||||||
|
"description": "Defines wether the team should be publicly discoverable when sharing a project",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"members": {
|
"members": {
|
||||||
"description": "An array of all members in this team.",
|
"description": "An array of all members in this team.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@ -8356,6 +8364,14 @@
|
|||||||
"description": "The unique, numeric id of this team.",
|
"description": "The unique, numeric id of this team.",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"include_public": {
|
||||||
|
"description": "Query parameter controlling whether to include public projects or not",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"is_public": {
|
||||||
|
"description": "Defines wether the team should be publicly discoverable when sharing a project",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"members": {
|
"members": {
|
||||||
"description": "An array of all members in this team.",
|
"description": "An array of all members in this team.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@ -8878,6 +8894,9 @@
|
|||||||
"motd": {
|
"motd": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"public_teams_enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"registration_enabled": {
|
"registration_enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
@ -877,6 +877,14 @@ definitions:
|
|||||||
id:
|
id:
|
||||||
description: The unique, numeric id of this team.
|
description: The unique, numeric id of this team.
|
||||||
type: integer
|
type: integer
|
||||||
|
include_public:
|
||||||
|
description: Query parameter controlling whether to include public projects
|
||||||
|
or not
|
||||||
|
type: boolean
|
||||||
|
is_public:
|
||||||
|
description: Defines wether the team should be publicly discoverable when
|
||||||
|
sharing a project
|
||||||
|
type: boolean
|
||||||
members:
|
members:
|
||||||
description: An array of all members in this team.
|
description: An array of all members in this team.
|
||||||
items:
|
items:
|
||||||
@ -984,6 +992,14 @@ definitions:
|
|||||||
id:
|
id:
|
||||||
description: The unique, numeric id of this team.
|
description: The unique, numeric id of this team.
|
||||||
type: integer
|
type: integer
|
||||||
|
include_public:
|
||||||
|
description: Query parameter controlling whether to include public projects
|
||||||
|
or not
|
||||||
|
type: boolean
|
||||||
|
is_public:
|
||||||
|
description: Defines wether the team should be publicly discoverable when
|
||||||
|
sharing a project
|
||||||
|
type: boolean
|
||||||
members:
|
members:
|
||||||
description: An array of all members in this team.
|
description: An array of all members in this team.
|
||||||
items:
|
items:
|
||||||
@ -1369,6 +1385,8 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
motd:
|
motd:
|
||||||
type: string
|
type: string
|
||||||
|
public_teams_enabled:
|
||||||
|
type: boolean
|
||||||
registration_enabled:
|
registration_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
task_attachments_enabled:
|
task_attachments_enabled:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user