forked from vikunja/vikunja
feat(projects): remove namespaces
This commit is contained in:
parent
0795828a9f
commit
16de7cd591
@ -188,7 +188,7 @@ var userCreateCmd = &cobra.Command{
|
||||
log.Fatalf("Error creating new user: %s", err)
|
||||
}
|
||||
|
||||
err = models.CreateNewNamespaceForUser(s, newUser)
|
||||
err = models.CreateNewProjectForUser(s, newUser)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
log.Fatalf("Error creating new namespace for user: %s", err)
|
||||
|
@ -77,7 +77,7 @@ func (l *Label) hasAccessToLabel(s *xorm.Session, a web.Auth) (has bool, maxRigh
|
||||
builder.
|
||||
Select("id").
|
||||
From("tasks").
|
||||
Where(builder.In("project_id", getUserProjectsStatement(u.ID).Select("l.id"))),
|
||||
Where(builder.In("project_id", getUserProjectsStatement(u.ID, "", false).Select("l.id"))),
|
||||
)
|
||||
|
||||
ll := &LabelTask{}
|
||||
|
@ -180,7 +180,7 @@ func GetLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*Lab
|
||||
builder.
|
||||
Select("id").
|
||||
From("tasks").
|
||||
Where(builder.In("project_id", getUserProjectsStatement(opts.GetForUser).Select("l.id"))),
|
||||
Where(builder.In("project_id", getUserProjectsStatement(opts.GetForUser, "", false).Select("l.id"))),
|
||||
), cond)
|
||||
}
|
||||
if opts.GetUnusedLabels {
|
||||
|
@ -1,774 +0,0 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 models
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// Namespace holds informations about a namespace
|
||||
type Namespace struct {
|
||||
// The unique, numeric id of this namespace.
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"`
|
||||
// The name of this namespace.
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
|
||||
// The description of the namespace
|
||||
Description string `xorm:"longtext null" json:"description"`
|
||||
OwnerID int64 `xorm:"bigint not null INDEX" json:"-"`
|
||||
|
||||
// The hex color of this namespace
|
||||
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
|
||||
|
||||
// Whether or not a namespace is archived.
|
||||
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
|
||||
|
||||
// The user who owns this namespace
|
||||
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
|
||||
|
||||
// The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.
|
||||
// Will only returned when retreiving one namespace.
|
||||
Subscription *Subscription `xorm:"-" json:"subscription,omitempty"`
|
||||
|
||||
// A timestamp when this namespace was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this namespace was last updated. You cannot change this value.
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
|
||||
// If set to true, will only return the namespaces, not their projects.
|
||||
NamespacesOnly bool `xorm:"-" json:"-" query:"namespaces_only"`
|
||||
|
||||
web.CRUDable `xorm:"-" json:"-"`
|
||||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// SharedProjectsPseudoNamespace is a pseudo namespace used to hold shared projects
|
||||
var SharedProjectsPseudoNamespace = Namespace{
|
||||
ID: -1,
|
||||
Title: "Shared Projects",
|
||||
Description: "Projects of other users shared with you via teams or directly.",
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
// FavoritesPseudoNamespace is a pseudo namespace used to hold favorited projects and tasks
|
||||
var FavoritesPseudoNamespace = Namespace{
|
||||
ID: -2,
|
||||
Title: "Favorites",
|
||||
Description: "Favorite projects and tasks.",
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
// SavedFiltersPseudoNamespace is a pseudo namespace used to hold saved filters
|
||||
var SavedFiltersPseudoNamespace = Namespace{
|
||||
ID: -3,
|
||||
Title: "Filters",
|
||||
Description: "Saved filters.",
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
// TableName makes beautiful table names
|
||||
func (Namespace) TableName() string {
|
||||
return "namespaces"
|
||||
}
|
||||
|
||||
// GetSimpleByID gets a namespace without things like the owner, it more or less only checks if it exists.
|
||||
func getNamespaceSimpleByID(s *xorm.Session, id int64) (namespace *Namespace, err error) {
|
||||
if id == 0 {
|
||||
return nil, ErrNamespaceDoesNotExist{ID: id}
|
||||
}
|
||||
|
||||
// Get the namesapce with shared projects
|
||||
if id == -1 {
|
||||
return &SharedProjectsPseudoNamespace, nil
|
||||
}
|
||||
|
||||
if id == FavoritesPseudoNamespace.ID {
|
||||
return &FavoritesPseudoNamespace, nil
|
||||
}
|
||||
|
||||
if id == SavedFiltersPseudoNamespace.ID {
|
||||
return &SavedFiltersPseudoNamespace, nil
|
||||
}
|
||||
|
||||
namespace = &Namespace{}
|
||||
|
||||
exists, err := s.Where("id = ?", id).Get(namespace)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
return nil, ErrNamespaceDoesNotExist{ID: id}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetNamespaceByID returns a namespace object by its ID
|
||||
func GetNamespaceByID(s *xorm.Session, id int64) (namespace *Namespace, err error) {
|
||||
namespace, err = getNamespaceSimpleByID(s, id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the namespace Owner
|
||||
namespace.Owner, err = user.GetUserByID(s, namespace.OwnerID)
|
||||
return
|
||||
}
|
||||
|
||||
// CheckIsArchived returns an ErrNamespaceIsArchived if the namepace is archived.
|
||||
func (n *Namespace) CheckIsArchived(s *xorm.Session) error {
|
||||
exists, err := s.
|
||||
Where("id = ? AND is_archived = true", n.ID).
|
||||
Exist(&Namespace{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return ErrNamespaceIsArchived{NamespaceID: n.ID}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadOne gets one namespace
|
||||
// @Summary Gets one namespace
|
||||
// @Description Returns a namespace by its ID.
|
||||
// @tags namespace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Namespace ID"
|
||||
// @Success 200 {object} models.Namespace "The Namespace"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to that namespace."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{id} [get]
|
||||
func (n *Namespace) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
nn, err := GetNamespaceByID(s, n.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*n = *nn
|
||||
|
||||
n.Subscription, err = GetSubscription(s, SubscriptionEntityNamespace, n.ID, a)
|
||||
return
|
||||
}
|
||||
|
||||
// NamespaceWithProjects represents a namespace with project meta informations
|
||||
type NamespaceWithProjects struct {
|
||||
Namespace `xorm:"extends"`
|
||||
Projects []*Project `xorm:"-" json:"projects"`
|
||||
}
|
||||
|
||||
type NamespaceWithProjectsAndTasks struct {
|
||||
Namespace
|
||||
Projects []*ProjectWithTasksAndBuckets `xorm:"-" json:"projects"`
|
||||
}
|
||||
|
||||
func makeNamespaceSlice(namespaces map[int64]*NamespaceWithProjects, userMap map[int64]*user.User, subscriptions map[int64]*Subscription) []*NamespaceWithProjects {
|
||||
all := make([]*NamespaceWithProjects, 0, len(namespaces))
|
||||
for _, n := range namespaces {
|
||||
n.Owner = userMap[n.OwnerID]
|
||||
n.Subscription = subscriptions[n.ID]
|
||||
all = append(all, n)
|
||||
for _, l := range n.Projects {
|
||||
if n.Subscription != nil && l.Subscription == nil {
|
||||
l.Subscription = n.Subscription
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(all, func(i, j int) bool {
|
||||
return all[i].ID < all[j].ID
|
||||
})
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
func getNamespaceFilterCond(search string) (filterCond builder.Cond) {
|
||||
filterCond = db.ILIKE("namespaces.title", search)
|
||||
|
||||
if search == "" {
|
||||
return
|
||||
}
|
||||
|
||||
vals := strings.Split(search, ",")
|
||||
|
||||
if len(vals) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ids := []int64{}
|
||||
for _, val := range vals {
|
||||
v, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
log.Debugf("Namespace search string part '%s' is not a number: %s", val, err)
|
||||
continue
|
||||
}
|
||||
ids = append(ids, v)
|
||||
}
|
||||
|
||||
if len(ids) > 0 {
|
||||
filterCond = builder.In("namespaces.id", ids)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getNamespaceArchivedCond(archived bool) builder.Cond {
|
||||
// Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions
|
||||
var isArchivedCond builder.Cond = builder.Eq{"1": 1}
|
||||
if !archived {
|
||||
isArchivedCond = builder.And(
|
||||
builder.Eq{"namespaces.is_archived": false},
|
||||
)
|
||||
}
|
||||
|
||||
return isArchivedCond
|
||||
}
|
||||
|
||||
func getNamespacesWithProjects(s *xorm.Session, namespaces *map[int64]*NamespaceWithProjects, search string, isArchived bool, page, perPage int, userID int64) (numberOfTotalItems int64, err error) {
|
||||
isArchivedCond := getNamespaceArchivedCond(isArchived)
|
||||
filterCond := getNamespaceFilterCond(search)
|
||||
|
||||
limit, start := getLimitFromPageIndex(page, perPage)
|
||||
query := s.Select("namespaces.*").
|
||||
Table("namespaces").
|
||||
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
|
||||
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
|
||||
Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id").
|
||||
Where("team_members.user_id = ?", userID).
|
||||
Or("namespaces.owner_id = ?", userID).
|
||||
Or("users_namespaces.user_id = ?", userID).
|
||||
GroupBy("namespaces.id").
|
||||
Where(filterCond).
|
||||
Where(isArchivedCond)
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
}
|
||||
err = query.Find(namespaces)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
numberOfTotalItems, err = s.
|
||||
Table("namespaces").
|
||||
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
|
||||
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
|
||||
Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id").
|
||||
Where("team_members.user_id = ?", userID).
|
||||
Or("namespaces.owner_id = ?", userID).
|
||||
Or("users_namespaces.user_id = ?", userID).
|
||||
And("namespaces.is_archived = false").
|
||||
GroupBy("namespaces.id").
|
||||
Where(filterCond).
|
||||
Where(isArchivedCond).
|
||||
Count(&NamespaceWithProjects{})
|
||||
return numberOfTotalItems, err
|
||||
}
|
||||
|
||||
func getNamespaceOwnerIDs(namespaces map[int64]*NamespaceWithProjects) (namespaceIDs, ownerIDs []int64) {
|
||||
for _, nsp := range namespaces {
|
||||
namespaceIDs = append(namespaceIDs, nsp.ID)
|
||||
ownerIDs = append(ownerIDs, nsp.OwnerID)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getNamespaceSubscriptions(s *xorm.Session, namespaceIDs []int64, userID int64) (map[int64]*Subscription, error) {
|
||||
subscriptionsMap := make(map[int64]*Subscription)
|
||||
if len(namespaceIDs) == 0 {
|
||||
return subscriptionsMap, nil
|
||||
}
|
||||
|
||||
subscriptions := []*Subscription{}
|
||||
err := s.
|
||||
Where("entity_type = ? AND user_id = ?", SubscriptionEntityNamespace, userID).
|
||||
In("entity_id", namespaceIDs).
|
||||
Find(&subscriptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, sub := range subscriptions {
|
||||
sub.Entity = sub.EntityType.String()
|
||||
subscriptionsMap[sub.EntityID] = sub
|
||||
}
|
||||
|
||||
return subscriptionsMap, err
|
||||
}
|
||||
|
||||
func getProjectsForNamespaces(s *xorm.Session, namespaceIDs []int64, archived bool) ([]*Project, error) {
|
||||
projects := []*Project{}
|
||||
projectQuery := s.
|
||||
OrderBy("position").
|
||||
In("namespace_id", namespaceIDs)
|
||||
|
||||
if !archived {
|
||||
projectQuery.And("is_archived = false")
|
||||
}
|
||||
err := projectQuery.Find(&projects)
|
||||
return projects, err
|
||||
}
|
||||
|
||||
func getSharedProjectsInNamespace(s *xorm.Session, archived bool, doer *user.User) (sharedProjectsNamespace *NamespaceWithProjects, err error) {
|
||||
// Create our pseudo namespace to hold the shared projects
|
||||
sharedProjectsPseudonamespace := SharedProjectsPseudoNamespace
|
||||
sharedProjectsPseudonamespace.OwnerID = doer.ID
|
||||
sharedProjectsNamespace = &NamespaceWithProjects{
|
||||
sharedProjectsPseudonamespace,
|
||||
[]*Project{},
|
||||
}
|
||||
|
||||
// Get all projects individually shared with our user (not via a namespace)
|
||||
individualProjects := []*Project{}
|
||||
iProjectQuery := s.Select("l.*").
|
||||
Table("projects").
|
||||
Alias("l").
|
||||
Join("LEFT", []string{"team_projects", "tl"}, "l.id = tl.project_id").
|
||||
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tl.team_id").
|
||||
Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = l.id").
|
||||
Where(builder.And(
|
||||
builder.Eq{"tm.user_id": doer.ID},
|
||||
builder.Neq{"l.owner_id": doer.ID},
|
||||
)).
|
||||
Or(builder.And(
|
||||
builder.Eq{"ul.user_id": doer.ID},
|
||||
builder.Neq{"l.owner_id": doer.ID},
|
||||
)).
|
||||
GroupBy("l.id")
|
||||
if !archived {
|
||||
iProjectQuery.And("l.is_archived = false")
|
||||
}
|
||||
err = iProjectQuery.Find(&individualProjects)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Make the namespace -1 so we now later which one it was
|
||||
// + Append it to all projects we already have
|
||||
for _, l := range individualProjects {
|
||||
l.NamespaceID = sharedProjectsNamespace.ID
|
||||
}
|
||||
|
||||
sharedProjectsNamespace.Projects = individualProjects
|
||||
|
||||
// Remove the sharedProjectsPseudonamespace if we don't have any shared projects
|
||||
if len(individualProjects) == 0 {
|
||||
sharedProjectsNamespace = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getFavoriteProjects(s *xorm.Session, projects []*Project, namespaceIDs []int64, doer *user.User) (favoriteNamespace *NamespaceWithProjects, err error) {
|
||||
// Create our pseudo namespace with favorite projects
|
||||
pseudoFavoriteNamespace := FavoritesPseudoNamespace
|
||||
pseudoFavoriteNamespace.OwnerID = doer.ID
|
||||
favoriteNamespace = &NamespaceWithProjects{
|
||||
Namespace: pseudoFavoriteNamespace,
|
||||
Projects: []*Project{{}},
|
||||
}
|
||||
*favoriteNamespace.Projects[0] = FavoritesPseudoProject // Copying the project to be able to modify it later
|
||||
favoriteNamespace.Projects[0].Owner = doer
|
||||
|
||||
for _, project := range projects {
|
||||
if !project.IsFavorite {
|
||||
continue
|
||||
}
|
||||
favoriteNamespace.Projects = append(favoriteNamespace.Projects, project)
|
||||
}
|
||||
|
||||
// Check if we have any favorites or favorited projects and remove the favorites namespace from the project if not
|
||||
cond := builder.
|
||||
Select("tasks.id").
|
||||
From("tasks").
|
||||
Join("INNER", "projects", "tasks.project_id = projects.id").
|
||||
Join("INNER", "namespaces", "projects.namespace_id = namespaces.id").
|
||||
Where(builder.In("namespaces.id", namespaceIDs))
|
||||
|
||||
var favoriteCount int64
|
||||
favoriteCount, err = s.
|
||||
Where(builder.And(
|
||||
builder.Eq{"user_id": doer.ID},
|
||||
builder.Eq{"kind": FavoriteKindTask},
|
||||
builder.In("entity_id", cond),
|
||||
)).
|
||||
Count(&Favorite{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If we don't have any favorites in the favorites pseudo project, remove that pseudo project from the namespace
|
||||
if favoriteCount == 0 {
|
||||
for in, l := range favoriteNamespace.Projects {
|
||||
if l.ID == FavoritesPseudoProject.ID {
|
||||
favoriteNamespace.Projects = append(favoriteNamespace.Projects[:in], favoriteNamespace.Projects[in+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have any favorites in the namespace, remove it
|
||||
if len(favoriteNamespace.Projects) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getSavedFilters(s *xorm.Session, doer *user.User) (savedFiltersNamespace *NamespaceWithProjects, err error) {
|
||||
savedFilters, err := getSavedFiltersForUser(s, doer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(savedFilters) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
savedFiltersPseudoNamespace := SavedFiltersPseudoNamespace
|
||||
savedFiltersPseudoNamespace.OwnerID = doer.ID
|
||||
savedFiltersNamespace = &NamespaceWithProjects{
|
||||
Namespace: savedFiltersPseudoNamespace,
|
||||
Projects: make([]*Project, 0, len(savedFilters)),
|
||||
}
|
||||
|
||||
for _, filter := range savedFilters {
|
||||
filterProject := filter.toProject()
|
||||
filterProject.NamespaceID = savedFiltersNamespace.ID
|
||||
filterProject.Owner = doer
|
||||
savedFiltersNamespace.Projects = append(savedFiltersNamespace.Projects, filterProject)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll gets all namespaces a user has access to
|
||||
// @Summary Get all namespaces a user has access to
|
||||
// @Description Returns all namespaces a user has access to.
|
||||
// @tags namespace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
|
||||
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
|
||||
// @Param s query string false "Search namespaces by name."
|
||||
// @Param is_archived query bool false "If true, also returns all archived namespaces."
|
||||
// @Param namespaces_only query bool false "If true, also returns only namespaces without their projects."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.NamespaceWithProjects "The Namespaces."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces [get]
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return nil, 0, 0, ErrGenericForbidden{}
|
||||
}
|
||||
|
||||
// This map will hold all namespaces and their projects. The key is usually the id of the namespace.
|
||||
// We're using a map here because it makes a few things like adding projects or removing pseudo namespaces easier.
|
||||
namespaces := make(map[int64]*NamespaceWithProjects)
|
||||
|
||||
//////////////////////////////
|
||||
// Projects with their namespaces
|
||||
|
||||
doer, err := user.GetFromAuth(a)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
numberOfTotalItems, err = getNamespacesWithProjects(s, &namespaces, search, n.IsArchived, page, perPage, doer.ID)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
namespaceIDs, ownerIDs := getNamespaceOwnerIDs(namespaces)
|
||||
|
||||
if len(namespaceIDs) == 0 {
|
||||
return nil, 0, 0, nil
|
||||
}
|
||||
|
||||
subscriptionsMap, err := getNamespaceSubscriptions(s, namespaceIDs, doer.ID)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
ownerMap, err := user.GetUsersByIDs(s, ownerIDs)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
ownerMap[doer.ID] = doer
|
||||
|
||||
if n.NamespacesOnly {
|
||||
all := makeNamespaceSlice(namespaces, ownerMap, subscriptionsMap)
|
||||
return all, len(all), numberOfTotalItems, nil
|
||||
}
|
||||
|
||||
// Get all projects
|
||||
projects, err := getProjectsForNamespaces(s, namespaceIDs, n.IsArchived)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
///////////////
|
||||
// Shared Projects
|
||||
|
||||
sharedProjectsNamespace, err := getSharedProjectsInNamespace(s, n.IsArchived, doer)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if sharedProjectsNamespace != nil {
|
||||
namespaces[sharedProjectsNamespace.ID] = sharedProjectsNamespace
|
||||
projects = append(projects, sharedProjectsNamespace.Projects...)
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Saved Filters
|
||||
|
||||
savedFiltersNamespace, err := getSavedFilters(s, doer)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if savedFiltersNamespace != nil {
|
||||
namespaces[savedFiltersNamespace.ID] = savedFiltersNamespace
|
||||
projects = append(projects, savedFiltersNamespace.Projects...)
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Add project details (favorite state, among other things)
|
||||
err = addProjectDetails(s, projects, a)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Favorite projects
|
||||
|
||||
favoritesNamespace, err := getFavoriteProjects(s, projects, namespaceIDs, doer)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if favoritesNamespace != nil {
|
||||
namespaces[favoritesNamespace.ID] = favoritesNamespace
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// Put it all together
|
||||
|
||||
for _, project := range projects {
|
||||
if project.NamespaceID == SharedProjectsPseudoNamespace.ID || project.NamespaceID == SavedFiltersPseudoNamespace.ID {
|
||||
// Shared projects and filtered projects are already in the namespace
|
||||
continue
|
||||
}
|
||||
namespaces[project.NamespaceID].Projects = append(namespaces[project.NamespaceID].Projects, project)
|
||||
}
|
||||
|
||||
all := makeNamespaceSlice(namespaces, ownerMap, subscriptionsMap)
|
||||
return all, len(all), numberOfTotalItems, err
|
||||
}
|
||||
|
||||
// Create implements the creation method via the interface
|
||||
// @Summary Creates a new namespace
|
||||
// @Description Creates a new namespace.
|
||||
// @tags namespace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param namespace body models.Namespace true "The namespace you want to create."
|
||||
// @Success 201 {object} models.Namespace "The created namespace."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid namespace object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces [put]
|
||||
func (n *Namespace) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// Check if we have at least a title
|
||||
if n.Title == "" {
|
||||
return ErrNamespaceNameCannotBeEmpty{NamespaceID: 0, UserID: a.GetID()}
|
||||
}
|
||||
|
||||
n.Owner, err = user.GetUserByID(s, a.GetID())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
n.OwnerID = n.Owner.ID
|
||||
|
||||
if _, err = s.Insert(n); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = events.Dispatch(&NamespaceCreatedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CreateNewNamespaceForUser creates a new namespace for a user. To prevent import cycles, we can't do that
|
||||
// directly in the user.Create function.
|
||||
func CreateNewNamespaceForUser(s *xorm.Session, user *user.User) (err error) {
|
||||
newN := &Namespace{
|
||||
Title: user.Username,
|
||||
Description: user.Username + "'s namespace.",
|
||||
}
|
||||
return newN.Create(s, user)
|
||||
}
|
||||
|
||||
// Delete deletes a namespace
|
||||
// @Summary Deletes a namespace
|
||||
// @Description Delets a namespace
|
||||
// @tags namespace
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Namespace ID"
|
||||
// @Success 200 {object} models.Message "The namespace was successfully deleted."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid namespace object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{id} [delete]
|
||||
func (n *Namespace) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
return deleteNamespace(s, n, a, true)
|
||||
}
|
||||
|
||||
func deleteNamespace(s *xorm.Session, n *Namespace, a web.Auth, withProjects bool) (err error) {
|
||||
// Check if the namespace exists
|
||||
_, err = GetNamespaceByID(s, n.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the namespace
|
||||
_, err = s.ID(n.ID).Delete(&Namespace{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
namespaceDeleted := &NamespaceDeletedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
}
|
||||
|
||||
if !withProjects {
|
||||
return events.Dispatch(namespaceDeleted)
|
||||
}
|
||||
|
||||
// Delete all projects with their tasks
|
||||
projects, err := GetProjectsByNamespaceID(s, n.ID, &user.User{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(projects) == 0 {
|
||||
return events.Dispatch(namespaceDeleted)
|
||||
}
|
||||
|
||||
// Looping over all projects to let the project handle properly cleaning up the tasks and everything else associated with it.
|
||||
for _, project := range projects {
|
||||
err = project.Delete(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return events.Dispatch(namespaceDeleted)
|
||||
}
|
||||
|
||||
// Update implements the update method via the interface
|
||||
// @Summary Updates a namespace
|
||||
// @Description Updates a namespace.
|
||||
// @tags namespace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Namespace ID"
|
||||
// @Param namespace body models.Namespace true "The namespace with updated values you want to update."
|
||||
// @Success 200 {object} models.Namespace "The updated namespace."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid namespace object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespace/{id} [post]
|
||||
func (n *Namespace) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
// Check if we have at least a name
|
||||
if n.Title == "" {
|
||||
return ErrNamespaceNameCannotBeEmpty{NamespaceID: n.ID}
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
currentNamespace, err := GetNamespaceByID(s, n.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the namespace is archived and the update is not un-archiving it
|
||||
if currentNamespace.IsArchived && n.IsArchived {
|
||||
return ErrNamespaceIsArchived{NamespaceID: n.ID}
|
||||
}
|
||||
|
||||
// Check if the (new) owner exists
|
||||
if n.Owner != nil {
|
||||
n.OwnerID = n.Owner.ID
|
||||
if currentNamespace.OwnerID != n.OwnerID {
|
||||
n.Owner, err = user.GetUserByID(s, n.OwnerID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to specify the cols we want to update here to be able to un-archive projects
|
||||
colsToUpdate := []string{
|
||||
"title",
|
||||
"is_archived",
|
||||
"hex_color",
|
||||
}
|
||||
if n.Description != "" {
|
||||
colsToUpdate = append(colsToUpdate, "description")
|
||||
}
|
||||
|
||||
// Do the actual update
|
||||
_, err = s.
|
||||
ID(currentNamespace.ID).
|
||||
Cols(colsToUpdate...).
|
||||
Update(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return events.Dispatch(&NamespaceUpdatedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CanWrite checks if a user has write access to a namespace
|
||||
func (n *Namespace) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
can, _, err := n.checkRight(s, a, RightWrite, RightAdmin)
|
||||
return can, err
|
||||
}
|
||||
|
||||
// IsAdmin returns true or false if the user is admin on that namespace or not
|
||||
func (n *Namespace) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
is, _, err := n.checkRight(s, a, RightAdmin)
|
||||
return is, err
|
||||
}
|
||||
|
||||
// CanRead checks if a user has read access to that namespace
|
||||
func (n *Namespace) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
||||
return n.checkRight(s, a, RightRead, RightWrite, RightAdmin)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update the namespace
|
||||
func (n *Namespace) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return n.IsAdmin(s, a)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a namespace
|
||||
func (n *Namespace) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return n.IsAdmin(s, a)
|
||||
}
|
||||
|
||||
// CanCreate checks if the user can create a new namespace
|
||||
func (n *Namespace) CanCreate(_ *xorm.Session, a web.Auth) (bool, error) {
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// This is currently a dummy function, later on we could imagine global limits etc.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (n *Namespace) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) {
|
||||
|
||||
// If the auth is a link share, don't do anything
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return false, 0, nil
|
||||
}
|
||||
|
||||
// Get the namespace and check the right
|
||||
nn, err := getNamespaceSimpleByID(s, n.ID)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
if a.GetID() == nn.OwnerID ||
|
||||
nn.ID == SharedProjectsPseudoNamespace.ID ||
|
||||
nn.ID == FavoritesPseudoNamespace.ID ||
|
||||
nn.ID == SavedFiltersPseudoNamespace.ID {
|
||||
return true, int(RightAdmin), nil
|
||||
}
|
||||
|
||||
/*
|
||||
The following loop creates an sql condition like this one:
|
||||
|
||||
namespaces.owner_id = 1 OR
|
||||
(users_namespaces.user_id = 1 AND users_namespaces.right = 1) OR
|
||||
(team_members.user_id = 1 AND team_namespaces.right = 1) OR
|
||||
|
||||
|
||||
for each passed right. That way, we can check with a single sql query (instead if 8)
|
||||
if the user has the right to see the project or not.
|
||||
*/
|
||||
|
||||
var conds []builder.Cond
|
||||
conds = append(conds, builder.Eq{"namespaces.owner_id": a.GetID()})
|
||||
for _, r := range rights {
|
||||
// User conditions
|
||||
// If the namespace was shared directly with the user and the user has the right
|
||||
conds = append(conds, builder.And(
|
||||
builder.Eq{"users_namespaces.user_id": a.GetID()},
|
||||
builder.Eq{"users_namespaces.right": r},
|
||||
))
|
||||
|
||||
// Team rights
|
||||
// If the namespace was shared directly with the team and the team has the right
|
||||
conds = append(conds, builder.And(
|
||||
builder.Eq{"team_members.user_id": a.GetID()},
|
||||
builder.Eq{"team_namespaces.right": r},
|
||||
))
|
||||
}
|
||||
|
||||
type allRights struct {
|
||||
UserNamespace NamespaceUser `xorm:"extends"`
|
||||
TeamNamespace TeamNamespace `xorm:"extends"`
|
||||
}
|
||||
|
||||
var maxRights = 0
|
||||
r := &allRights{}
|
||||
exists, err := s.
|
||||
Select("*").
|
||||
Table("namespaces").
|
||||
// User stuff
|
||||
Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id").
|
||||
// Teams stuff
|
||||
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
|
||||
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
|
||||
// The actual condition
|
||||
Where(builder.And(
|
||||
builder.Or(
|
||||
conds...,
|
||||
),
|
||||
builder.Eq{"namespaces.id": n.ID},
|
||||
)).
|
||||
Exist(r)
|
||||
|
||||
// Figure out the max right and return it
|
||||
if int(r.UserNamespace.Right) > maxRights {
|
||||
maxRights = int(r.UserNamespace.Right)
|
||||
}
|
||||
if int(r.TeamNamespace.Right) > maxRights {
|
||||
maxRights = int(r.TeamNamespace.Right)
|
||||
}
|
||||
|
||||
return exists, maxRights, err
|
||||
}
|
@ -1,244 +0,0 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/web"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// TeamNamespace defines the relationship between a Team and a Namespace
|
||||
type TeamNamespace struct {
|
||||
// The unique, numeric id of this namespace <-> team relation.
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
|
||||
// The team id.
|
||||
TeamID int64 `xorm:"bigint not null INDEX" json:"team_id" param:"team"`
|
||||
// The namespace id.
|
||||
NamespaceID int64 `xorm:"bigint not null INDEX" json:"-" param:"namespace"`
|
||||
// The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
|
||||
Right Right `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
|
||||
|
||||
// A timestamp when this relation was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this relation was last updated. You cannot change this value.
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
|
||||
web.CRUDable `xorm:"-" json:"-"`
|
||||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName makes beautiful table names
|
||||
func (TeamNamespace) TableName() string {
|
||||
return "team_namespaces"
|
||||
}
|
||||
|
||||
// Create creates a new team <-> namespace relation
|
||||
// @Summary Add a team to a namespace
|
||||
// @Description Gives a team access to a namespace.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Namespace ID"
|
||||
// @Param namespace body models.TeamNamespace true "The team you want to add to the namespace."
|
||||
// @Success 201 {object} models.TeamNamespace "The created team<->namespace relation."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid team namespace object provided."
|
||||
// @Failure 404 {object} web.HTTPError "The team does not exist."
|
||||
// @Failure 403 {object} web.HTTPError "The team does not have access to the namespace"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{id}/teams [put]
|
||||
func (tn *TeamNamespace) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the rights are valid
|
||||
if err = tn.Right.isValid(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the team exists
|
||||
team, err := GetTeamByID(s, tn.TeamID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
namespace, err := GetNamespaceByID(s, tn.NamespaceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the team already has access to the namespace
|
||||
exists, err := s.
|
||||
Where("team_id = ?", tn.TeamID).
|
||||
And("namespace_id = ?", tn.NamespaceID).
|
||||
Get(&TeamNamespace{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
return ErrTeamAlreadyHasAccess{tn.TeamID, tn.NamespaceID}
|
||||
}
|
||||
|
||||
// Insert the new team
|
||||
_, err = s.Insert(tn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return events.Dispatch(&NamespaceSharedWithTeamEvent{
|
||||
Namespace: namespace,
|
||||
Team: team,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete deletes a team <-> namespace relation based on the namespace & team id
|
||||
// @Summary Delete a team from a namespace
|
||||
// @Description Delets a team from a namespace. The team won't have access to the namespace anymore.
|
||||
// @tags sharing
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param namespaceID path int true "Namespace ID"
|
||||
// @Param teamID path int true "team ID"
|
||||
// @Success 200 {object} models.Message "The team was successfully deleted."
|
||||
// @Failure 403 {object} web.HTTPError "The team does not have access to the namespace"
|
||||
// @Failure 404 {object} web.HTTPError "team or namespace does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/teams/{teamID} [delete]
|
||||
func (tn *TeamNamespace) Delete(s *xorm.Session, _ web.Auth) (err error) {
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(s, tn.TeamID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the team has access to the namespace
|
||||
has, err := s.
|
||||
Where("team_id = ? AND namespace_id = ?", tn.TeamID, tn.NamespaceID).
|
||||
Get(&TeamNamespace{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
return ErrTeamDoesNotHaveAccessToNamespace{TeamID: tn.TeamID, NamespaceID: tn.NamespaceID}
|
||||
}
|
||||
|
||||
// Delete the relation
|
||||
_, err = s.
|
||||
Where("team_id = ?", tn.TeamID).
|
||||
And("namespace_id = ?", tn.NamespaceID).
|
||||
Delete(TeamNamespace{})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll implements the method to read all teams of a namespace
|
||||
// @Summary Get teams on a namespace
|
||||
// @Description Returns a namespace with all teams which have access on a given namespace.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Namespace ID"
|
||||
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
|
||||
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
|
||||
// @Param s query string false "Search teams by its name."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.TeamWithRight "The teams with the right they have."
|
||||
// @Failure 403 {object} web.HTTPError "No right to see the namespace."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{id}/teams [get]
|
||||
func (tn *TeamNamespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
// Check if the user can read the namespace
|
||||
n := Namespace{ID: tn.NamespaceID}
|
||||
canRead, _, err := n.CanRead(s, a)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
if !canRead {
|
||||
return nil, 0, 0, ErrNeedToHaveNamespaceReadAccess{NamespaceID: tn.NamespaceID, UserID: a.GetID()}
|
||||
}
|
||||
|
||||
// Get the teams
|
||||
all := []*TeamWithRight{}
|
||||
limit, start := getLimitFromPageIndex(page, perPage)
|
||||
query := s.
|
||||
Table("teams").
|
||||
Join("INNER", "team_namespaces", "team_id = teams.id").
|
||||
Where("team_namespaces.namespace_id = ?", tn.NamespaceID).
|
||||
Where(db.ILIKE("teams.name", search))
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
}
|
||||
err = query.Find(&all)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
teams := []*Team{}
|
||||
for _, t := range all {
|
||||
teams = append(teams, &t.Team)
|
||||
}
|
||||
|
||||
err = addMoreInfoToTeams(s, teams)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
numberOfTotalItems, err = s.
|
||||
Table("teams").
|
||||
Join("INNER", "team_namespaces", "team_id = teams.id").
|
||||
Where("team_namespaces.namespace_id = ?", tn.NamespaceID).
|
||||
Where("teams.name LIKE ?", "%"+search+"%").
|
||||
Count(&TeamWithRight{})
|
||||
|
||||
return all, len(all), numberOfTotalItems, err
|
||||
}
|
||||
|
||||
// Update updates a team <-> namespace relation
|
||||
// @Summary Update a team <-> namespace relation
|
||||
// @Description Update a team <-> namespace relation. Mostly used to update the right that team has.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param namespaceID path int true "Namespace ID"
|
||||
// @Param teamID path int true "Team ID"
|
||||
// @Param namespace body models.TeamNamespace true "The team you want to update."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {object} models.TeamNamespace "The updated team <-> namespace relation."
|
||||
// @Failure 403 {object} web.HTTPError "The team does not have admin-access to the namespace"
|
||||
// @Failure 404 {object} web.HTTPError "Team or namespace does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/teams/{teamID} [post]
|
||||
func (tn *TeamNamespace) Update(s *xorm.Session, _ web.Auth) (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := tn.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = s.
|
||||
Where("namespace_id = ? AND team_id = ?", tn.NamespaceID, tn.TeamID).
|
||||
Cols("right").
|
||||
Update(tn)
|
||||
return
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CanCreate checks if one can create a new team <-> namespace relation
|
||||
func (tn *TeamNamespace) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
n := &Namespace{ID: tn.NamespaceID}
|
||||
return n.IsAdmin(s, a)
|
||||
}
|
||||
|
||||
// CanDelete checks if a user can remove a team from a namespace. Only namespace admins can do that.
|
||||
func (tn *TeamNamespace) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
n := &Namespace{ID: tn.NamespaceID}
|
||||
return n.IsAdmin(s, a)
|
||||
}
|
||||
|
||||
// CanUpdate checks if a user can update a team from a Only namespace admins can do that.
|
||||
func (tn *TeamNamespace) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
n := &Namespace{ID: tn.NamespaceID}
|
||||
return n.IsAdmin(s, a)
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"code.vikunja.io/web"
|
||||
)
|
||||
|
||||
func TestTeamNamespace_CanDoSomething(t *testing.T) {
|
||||
type fields struct {
|
||||
ID int64
|
||||
TeamID int64
|
||||
NamespaceID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
type args struct {
|
||||
a web.Auth
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want map[string]bool
|
||||
}{
|
||||
{
|
||||
name: "CanDoSomething Normally",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 3},
|
||||
},
|
||||
want: map[string]bool{"CanCreate": true, "CanDelete": true, "CanUpdate": true},
|
||||
},
|
||||
{
|
||||
name: "CanDoSomething for a nonexistant namespace",
|
||||
fields: fields{
|
||||
NamespaceID: 300,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 3},
|
||||
},
|
||||
want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false},
|
||||
},
|
||||
{
|
||||
name: "CanDoSomething where the user does not have the rights",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 4},
|
||||
},
|
||||
want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
||||
tn := &TeamNamespace{
|
||||
ID: tt.fields.ID,
|
||||
TeamID: tt.fields.TeamID,
|
||||
NamespaceID: tt.fields.NamespaceID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
if got, _ := tn.CanCreate(s, tt.args.a); got != tt.want["CanCreate"] {
|
||||
t.Errorf("TeamNamespace.CanCreate() = %v, want %v", got, tt.want["CanCreate"])
|
||||
}
|
||||
if got, _ := tn.CanDelete(s, tt.args.a); got != tt.want["CanDelete"] {
|
||||
t.Errorf("TeamNamespace.CanDelete() = %v, want %v", got, tt.want["CanDelete"])
|
||||
}
|
||||
if got, _ := tn.CanUpdate(s, tt.args.a); got != tt.want["CanUpdate"] {
|
||||
t.Errorf("TeamNamespace.CanUpdate() = %v, want %v", got, tt.want["CanUpdate"])
|
||||
}
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
}
|
@ -1,298 +0,0 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 models
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTeamNamespace_ReadAll(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
NamespaceID: 3,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
teams, _, _, err := tn.ReadAll(s, u, "", 1, 50)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
|
||||
ts := reflect.ValueOf(teams)
|
||||
assert.Equal(t, ts.Len(), 2)
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("nonexistant namespace", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
NamespaceID: 9999,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
_, _, _, err := tn.ReadAll(s, u, "", 1, 50)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("no right for namespace", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
NamespaceID: 17,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
_, _, _, err := tn.ReadAll(s, u, "", 1, 50)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNeedToHaveNamespaceReadAccess(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("search", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
NamespaceID: 3,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
teams, _, _, err := tn.ReadAll(s, u, "READ_only_on_project6", 1, 50)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
|
||||
ts := teams.([]*TeamWithRight)
|
||||
assert.Len(t, ts, 1)
|
||||
assert.Equal(t, int64(2), ts[0].ID)
|
||||
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestTeamNamespace_Create(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
TeamID: 1,
|
||||
NamespaceID: 1,
|
||||
Right: RightAdmin,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
allowed, _ := tn.CanCreate(s, u)
|
||||
assert.True(t, allowed)
|
||||
err := tn.Create(s, u)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
db.AssertExists(t, "team_namespaces", map[string]interface{}{
|
||||
"team_id": 1,
|
||||
"namespace_id": 1,
|
||||
"right": RightAdmin,
|
||||
}, false)
|
||||
})
|
||||
t.Run("team already has access", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
TeamID: 1,
|
||||
NamespaceID: 3,
|
||||
Right: RightRead,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := tn.Create(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamAlreadyHasAccess(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("invalid team right", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
TeamID: 1,
|
||||
NamespaceID: 3,
|
||||
Right: RightUnknown,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := tn.Create(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrInvalidRight(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("nonexistant team", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
TeamID: 9999,
|
||||
NamespaceID: 1,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := tn.Create(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("nonexistant namespace", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
TeamID: 1,
|
||||
NamespaceID: 9999,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := tn.Create(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestTeamNamespace_Delete(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
TeamID: 7,
|
||||
NamespaceID: 9,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
allowed, _ := tn.CanDelete(s, u)
|
||||
assert.True(t, allowed)
|
||||
err := tn.Delete(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
db.AssertMissing(t, "team_namespaces", map[string]interface{}{
|
||||
"team_id": 7,
|
||||
"namespace_id": 9,
|
||||
})
|
||||
})
|
||||
t.Run("nonexistant team", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
TeamID: 9999,
|
||||
NamespaceID: 3,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := tn.Delete(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("nonexistant namespace", func(t *testing.T) {
|
||||
tn := TeamNamespace{
|
||||
TeamID: 1,
|
||||
NamespaceID: 9999,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := tn.Delete(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotHaveAccessToNamespace(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestTeamNamespace_Update(t *testing.T) {
|
||||
type fields struct {
|
||||
ID int64
|
||||
TeamID int64
|
||||
NamespaceID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
errType func(err error) bool
|
||||
}{
|
||||
{
|
||||
name: "Test Update Normally",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
TeamID: 1,
|
||||
Right: RightAdmin,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update to write",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
TeamID: 1,
|
||||
Right: RightWrite,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update to Read",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
TeamID: 1,
|
||||
Right: RightRead,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update with invalid right",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
TeamID: 1,
|
||||
Right: 500,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrInvalidRight,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
||||
tl := &TeamNamespace{
|
||||
ID: tt.fields.ID,
|
||||
TeamID: tt.fields.TeamID,
|
||||
NamespaceID: tt.fields.NamespaceID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := tl.Update(s, &user.User{ID: 1})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("TeamNamespace.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||
t.Errorf("TeamNamespace.Update() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
}
|
||||
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
if !tt.wantErr {
|
||||
db.AssertExists(t, "team_namespaces", map[string]interface{}{
|
||||
"team_id": tt.fields.TeamID,
|
||||
"namespace_id": tt.fields.NamespaceID,
|
||||
"right": tt.fields.Right,
|
||||
}, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,372 +0,0 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNamespace_Create(t *testing.T) {
|
||||
|
||||
// Dummy namespace
|
||||
dummynamespace := Namespace{
|
||||
Title: "Test",
|
||||
Description: "Lorem Ipsum",
|
||||
}
|
||||
|
||||
user1 := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := dummynamespace.Create(s, user1)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
db.AssertExists(t, "namespaces", map[string]interface{}{
|
||||
"title": "Test",
|
||||
"description": "Lorem Ipsum",
|
||||
}, false)
|
||||
})
|
||||
t.Run("no title", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
n2 := Namespace{}
|
||||
err := n2.Create(s, user1)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceNameCannotBeEmpty(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("nonexistant user", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
nUser := &user.User{ID: 9482385}
|
||||
dnsp2 := dummynamespace
|
||||
err := dnsp2.Create(s, nUser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, user.IsErrUserDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestNamespace_ReadOne(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
n := &Namespace{ID: 1}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
err := n.ReadOne(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, n.Title, "testnamespace")
|
||||
})
|
||||
t.Run("nonexistant", func(t *testing.T) {
|
||||
n := &Namespace{ID: 99999}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
err := n.ReadOne(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
})
|
||||
t.Run("with subscription", func(t *testing.T) {
|
||||
n := &Namespace{ID: 8}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
err := n.ReadOne(s, &user.User{ID: 6})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, n.Subscription)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNamespace_Update(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
n := &Namespace{
|
||||
ID: 1,
|
||||
Title: "Lorem Ipsum",
|
||||
}
|
||||
err := n.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
db.AssertExists(t, "namespaces", map[string]interface{}{
|
||||
"id": 1,
|
||||
"title": "Lorem Ipsum",
|
||||
}, false)
|
||||
})
|
||||
t.Run("nonexisting", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
n := &Namespace{
|
||||
ID: 99999,
|
||||
Title: "Lorem Ipsum",
|
||||
}
|
||||
err := n.Update(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("nonexisting owner", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
n := &Namespace{
|
||||
ID: 1,
|
||||
Title: "Lorem Ipsum",
|
||||
Owner: &user.User{ID: 99999},
|
||||
}
|
||||
err := n.Update(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, user.IsErrUserDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("no title", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
n := &Namespace{
|
||||
ID: 1,
|
||||
}
|
||||
err := n.Update(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceNameCannotBeEmpty(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestNamespace_Delete(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
n := &Namespace{
|
||||
ID: 1,
|
||||
}
|
||||
err := n.Delete(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
db.AssertMissing(t, "namespaces", map[string]interface{}{
|
||||
"id": 1,
|
||||
})
|
||||
})
|
||||
t.Run("nonexisting", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
n := &Namespace{
|
||||
ID: 9999,
|
||||
}
|
||||
err := n.Delete(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestNamespace_ReadAll(t *testing.T) {
|
||||
user1 := &user.User{ID: 1}
|
||||
user6 := &user.User{ID: 6}
|
||||
user7 := &user.User{ID: 7}
|
||||
user11 := &user.User{ID: 11}
|
||||
user12 := &user.User{ID: 12}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(s, user1, "", 1, -1)
|
||||
assert.NoError(t, err)
|
||||
namespaces := nn.([]*NamespaceWithProjects)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Len(t, namespaces, 11) // Total of 11 including shared, favorites and saved filters
|
||||
assert.Equal(t, int64(-3), namespaces[0].ID) // The first one should be the one with saved filters
|
||||
assert.Equal(t, int64(-2), namespaces[1].ID) // The second one should be the one with favorites
|
||||
assert.Equal(t, int64(-1), namespaces[2].ID) // The third one should be the one with the shared namespaces
|
||||
// Ensure every project and namespace are not archived
|
||||
for _, namespace := range namespaces {
|
||||
assert.False(t, namespace.IsArchived)
|
||||
for _, project := range namespace.Projects {
|
||||
assert.False(t, project.IsArchived)
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("no own shared projects", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(s, user6, "", 1, -1)
|
||||
assert.NoError(t, err)
|
||||
namespaces := nn.([]*NamespaceWithProjects)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Equal(t, int64(-1), namespaces[1].ID) // The third one should be the one with the shared namespaces
|
||||
|
||||
sharedProjectOccurences := make(map[int64]int64)
|
||||
for _, project := range namespaces[1].Projects {
|
||||
assert.NotEqual(t, user1.ID, project.OwnerID)
|
||||
sharedProjectOccurences[project.ID]++
|
||||
}
|
||||
|
||||
for projectID, occ := range sharedProjectOccurences {
|
||||
assert.Equal(t, int64(1), occ, "shared project %d is present %d times, should be 1", projectID, occ)
|
||||
}
|
||||
})
|
||||
t.Run("namespaces only", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{
|
||||
NamespacesOnly: true,
|
||||
}
|
||||
nn, _, _, err := n.ReadAll(s, user1, "", 1, -1)
|
||||
assert.NoError(t, err)
|
||||
namespaces := nn.([]*NamespaceWithProjects)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Len(t, namespaces, 8) // Total of 8 - excluding shared, favorites and saved filters (normally 11)
|
||||
// Ensure every namespace does not contain projects
|
||||
for _, namespace := range namespaces {
|
||||
assert.Nil(t, namespace.Projects)
|
||||
}
|
||||
})
|
||||
t.Run("ids only", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{
|
||||
NamespacesOnly: true,
|
||||
}
|
||||
nn, _, _, err := n.ReadAll(s, user7, "13,14", 1, -1)
|
||||
assert.NoError(t, err)
|
||||
namespaces := nn.([]*NamespaceWithProjects)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Len(t, namespaces, 2)
|
||||
assert.Equal(t, int64(13), namespaces[0].ID)
|
||||
assert.Equal(t, int64(14), namespaces[1].ID)
|
||||
})
|
||||
t.Run("ids only but ids with other people's namespace", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{
|
||||
NamespacesOnly: true,
|
||||
}
|
||||
nn, _, _, err := n.ReadAll(s, user1, "1,w", 1, -1)
|
||||
assert.NoError(t, err)
|
||||
namespaces := nn.([]*NamespaceWithProjects)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Len(t, namespaces, 1)
|
||||
assert.Equal(t, int64(1), namespaces[0].ID)
|
||||
})
|
||||
t.Run("archived", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{
|
||||
IsArchived: true,
|
||||
}
|
||||
nn, _, _, err := n.ReadAll(s, user1, "", 1, -1)
|
||||
namespaces := nn.([]*NamespaceWithProjects)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Len(t, namespaces, 12) // Total of 12 including shared & favorites, one is archived
|
||||
assert.Equal(t, int64(-3), namespaces[0].ID) // The first one should be the one with shared filters
|
||||
assert.Equal(t, int64(-2), namespaces[1].ID) // The second one should be the one with favorites
|
||||
assert.Equal(t, int64(-1), namespaces[2].ID) // The third one should be the one with the shared namespaces
|
||||
})
|
||||
t.Run("no favorites", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(s, user11, "", 1, -1)
|
||||
namespaces := nn.([]*NamespaceWithProjects)
|
||||
assert.NoError(t, err)
|
||||
// Assert the first namespace is not the favorites namespace
|
||||
assert.NotEqual(t, FavoritesPseudoNamespace.ID, namespaces[0].ID)
|
||||
})
|
||||
t.Run("no favorite tasks but namespace", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(s, user12, "", 1, -1)
|
||||
namespaces := nn.([]*NamespaceWithProjects)
|
||||
assert.NoError(t, err)
|
||||
// Assert the first namespace is the favorites namespace and contains projects
|
||||
assert.Equal(t, FavoritesPseudoNamespace.ID, namespaces[0].ID)
|
||||
assert.NotEqual(t, 0, namespaces[0].Projects)
|
||||
})
|
||||
t.Run("no saved filters", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(s, user11, "", 1, -1)
|
||||
namespaces := nn.([]*NamespaceWithProjects)
|
||||
assert.NoError(t, err)
|
||||
// Assert the first namespace is not the favorites namespace
|
||||
assert.NotEqual(t, SavedFiltersPseudoNamespace.ID, namespaces[0].ID)
|
||||
})
|
||||
t.Run("no results", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(s, user1, "some search string which will never return results", 1, -1)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, nn)
|
||||
})
|
||||
t.Run("search", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(s, user6, "NamespACE7", 1, -1)
|
||||
assert.NoError(t, err)
|
||||
namespaces := nn.([]*NamespaceWithProjects)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Len(t, namespaces, 2)
|
||||
assert.Equal(t, int64(7), namespaces[1].ID)
|
||||
})
|
||||
}
|
@ -1,251 +0,0 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
user2 "code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// NamespaceUser represents a namespace <-> user relation
|
||||
type NamespaceUser struct {
|
||||
// The unique, numeric id of this namespace <-> user relation.
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"`
|
||||
// The username.
|
||||
Username string `xorm:"-" json:"user_id" param:"user"`
|
||||
UserID int64 `xorm:"bigint not null INDEX" json:"-"`
|
||||
// The namespace id
|
||||
NamespaceID int64 `xorm:"bigint not null INDEX" json:"-" param:"namespace"`
|
||||
// The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
|
||||
Right Right `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
|
||||
|
||||
// A timestamp when this relation was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this relation was last updated. You cannot change this value.
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
|
||||
web.CRUDable `xorm:"-" json:"-"`
|
||||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName is the table name for NamespaceUser
|
||||
func (NamespaceUser) TableName() string {
|
||||
return "users_namespaces"
|
||||
}
|
||||
|
||||
// Create creates a new namespace <-> user relation
|
||||
// @Summary Add a user to a namespace
|
||||
// @Description Gives a user access to a namespace.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Namespace ID"
|
||||
// @Param namespace body models.NamespaceUser true "The user you want to add to the namespace."
|
||||
// @Success 201 {object} models.NamespaceUser "The created user<->namespace relation."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid user namespace object provided."
|
||||
// @Failure 404 {object} web.HTTPError "The user does not exist."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{id}/users [put]
|
||||
func (nu *NamespaceUser) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// Reset the id
|
||||
nu.ID = 0
|
||||
|
||||
// Check if the right is valid
|
||||
if err := nu.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
n, err := GetNamespaceByID(s, nu.NamespaceID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
user, err := user2.GetUserByUsername(s, nu.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nu.UserID = user.ID
|
||||
|
||||
// Check if the user already has access or is owner of that namespace
|
||||
// We explicitly DO NOT check for teams here
|
||||
if n.OwnerID == nu.UserID {
|
||||
return ErrUserAlreadyHasNamespaceAccess{UserID: nu.UserID, NamespaceID: nu.NamespaceID}
|
||||
}
|
||||
|
||||
exist, err := s.
|
||||
Where("namespace_id = ? AND user_id = ?", nu.NamespaceID, nu.UserID).
|
||||
Get(&NamespaceUser{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if exist {
|
||||
return ErrUserAlreadyHasNamespaceAccess{UserID: nu.UserID, NamespaceID: nu.NamespaceID}
|
||||
}
|
||||
|
||||
// Insert user <-> namespace relation
|
||||
_, err = s.Insert(nu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return events.Dispatch(&NamespaceSharedWithUserEvent{
|
||||
Namespace: n,
|
||||
User: user,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete deletes a namespace <-> user relation
|
||||
// @Summary Delete a user from a namespace
|
||||
// @Description Delets a user from a namespace. The user won't have access to the namespace anymore.
|
||||
// @tags sharing
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param namespaceID path int true "Namespace ID"
|
||||
// @Param userID path int true "user ID"
|
||||
// @Success 200 {object} models.Message "The user was successfully deleted."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
|
||||
// @Failure 404 {object} web.HTTPError "user or namespace does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/users/{userID} [delete]
|
||||
func (nu *NamespaceUser) Delete(s *xorm.Session, _ web.Auth) (err error) {
|
||||
|
||||
// Check if the user exists
|
||||
user, err := user2.GetUserByUsername(s, nu.Username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
nu.UserID = user.ID
|
||||
|
||||
// Check if the user has access to the namespace
|
||||
has, err := s.
|
||||
Where("user_id = ? AND namespace_id = ?", nu.UserID, nu.NamespaceID).
|
||||
Get(&NamespaceUser{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
return ErrUserDoesNotHaveAccessToNamespace{NamespaceID: nu.NamespaceID, UserID: nu.UserID}
|
||||
}
|
||||
|
||||
_, err = s.
|
||||
Where("user_id = ? AND namespace_id = ?", nu.UserID, nu.NamespaceID).
|
||||
Delete(&NamespaceUser{})
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll gets all users who have access to a namespace
|
||||
// @Summary Get users on a namespace
|
||||
// @Description Returns a namespace with all users which have access on a given namespace.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Namespace ID"
|
||||
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
|
||||
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
|
||||
// @Param s query string false "Search users by its name."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.UserWithRight "The users with the right they have."
|
||||
// @Failure 403 {object} web.HTTPError "No right to see the namespace."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{id}/users [get]
|
||||
func (nu *NamespaceUser) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
// Check if the user has access to the namespace
|
||||
l := Namespace{ID: nu.NamespaceID}
|
||||
canRead, _, err := l.CanRead(s, a)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
if !canRead {
|
||||
return nil, 0, 0, ErrNeedToHaveNamespaceReadAccess{}
|
||||
}
|
||||
|
||||
// Get all users
|
||||
all := []*UserWithRight{}
|
||||
limit, start := getLimitFromPageIndex(page, perPage)
|
||||
query := s.
|
||||
Join("INNER", "users_namespaces", "user_id = users.id").
|
||||
Where("users_namespaces.namespace_id = ?", nu.NamespaceID).
|
||||
Where(db.ILIKE("users.username", search))
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
}
|
||||
err = query.Find(&all)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
// Obfuscate all user emails
|
||||
for _, u := range all {
|
||||
u.Email = ""
|
||||
}
|
||||
|
||||
numberOfTotalItems, err = s.
|
||||
Join("INNER", "users_namespaces", "user_id = users.id").
|
||||
Where("users_namespaces.namespace_id = ?", nu.NamespaceID).
|
||||
Where("users.username LIKE ?", "%"+search+"%").
|
||||
Count(&UserWithRight{})
|
||||
|
||||
return all, len(all), numberOfTotalItems, err
|
||||
}
|
||||
|
||||
// Update updates a user <-> namespace relation
|
||||
// @Summary Update a user <-> namespace relation
|
||||
// @Description Update a user <-> namespace relation. Mostly used to update the right that user has.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param namespaceID path int true "Namespace ID"
|
||||
// @Param userID path int true "User ID"
|
||||
// @Param namespace body models.NamespaceUser true "The user you want to update."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {object} models.NamespaceUser "The updated user <-> namespace relation."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have admin-access to the namespace"
|
||||
// @Failure 404 {object} web.HTTPError "User or namespace does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/users/{userID} [post]
|
||||
func (nu *NamespaceUser) Update(s *xorm.Session, _ web.Auth) (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := nu.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
user, err := user2.GetUserByUsername(s, nu.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nu.UserID = user.ID
|
||||
|
||||
_, err = s.
|
||||
Where("namespace_id = ? AND user_id = ?", nu.NamespaceID, nu.UserID).
|
||||
Cols("right").
|
||||
Update(nu)
|
||||
return
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CanCreate checks if the user can create a new user <-> namespace relation
|
||||
func (nu *NamespaceUser) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return nu.canDoNamespaceUser(s, a)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a user <-> namespace relation
|
||||
func (nu *NamespaceUser) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return nu.canDoNamespaceUser(s, a)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update a user <-> namespace relation
|
||||
func (nu *NamespaceUser) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return nu.canDoNamespaceUser(s, a)
|
||||
}
|
||||
|
||||
func (nu *NamespaceUser) canDoNamespaceUser(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
n := &Namespace{ID: nu.NamespaceID}
|
||||
return n.IsAdmin(s, a)
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"code.vikunja.io/web"
|
||||
)
|
||||
|
||||
func TestNamespaceUser_CanDoSomething(t *testing.T) {
|
||||
type fields struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
NamespaceID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
type args struct {
|
||||
a web.Auth
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want map[string]bool
|
||||
}{
|
||||
{
|
||||
name: "CanDoSomething Normally",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 3},
|
||||
},
|
||||
want: map[string]bool{"CanCreate": true, "CanDelete": true, "CanUpdate": true},
|
||||
},
|
||||
{
|
||||
name: "CanDoSomething for a nonexistant namespace",
|
||||
fields: fields{
|
||||
NamespaceID: 300,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 3},
|
||||
},
|
||||
want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false},
|
||||
},
|
||||
{
|
||||
name: "CanDoSomething where the user does not have the rights",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 4},
|
||||
},
|
||||
want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
nu := &NamespaceUser{
|
||||
ID: tt.fields.ID,
|
||||
UserID: tt.fields.UserID,
|
||||
NamespaceID: tt.fields.NamespaceID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
if got, _ := nu.CanCreate(s, tt.args.a); got != tt.want["CanCreate"] {
|
||||
t.Errorf("NamespaceUser.CanCreate() = %v, want %v", got, tt.want["CanCreate"])
|
||||
}
|
||||
if got, _ := nu.CanDelete(s, tt.args.a); got != tt.want["CanDelete"] {
|
||||
t.Errorf("NamespaceUser.CanDelete() = %v, want %v", got, tt.want["CanDelete"])
|
||||
}
|
||||
if got, _ := nu.CanUpdate(s, tt.args.a); got != tt.want["CanUpdate"] {
|
||||
t.Errorf("NamespaceUser.CanUpdate() = %v, want %v", got, tt.want["CanUpdate"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,436 +0,0 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 models
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/d4l3k/messagediff.v1"
|
||||
)
|
||||
|
||||
func TestNamespaceUser_Create(t *testing.T) {
|
||||
type fields struct {
|
||||
ID int64
|
||||
Username string
|
||||
UserID int64
|
||||
NamespaceID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
type args struct {
|
||||
a web.Auth
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
errType func(err error) bool
|
||||
}{
|
||||
{
|
||||
name: "NamespaceUsers Create normally",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
UserID: 1,
|
||||
NamespaceID: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NamespaceUsers Create for duplicate",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
NamespaceID: 3,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrUserAlreadyHasNamespaceAccess,
|
||||
},
|
||||
{
|
||||
name: "NamespaceUsers Create with invalid right",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
NamespaceID: 2,
|
||||
Right: 500,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrInvalidRight,
|
||||
},
|
||||
{
|
||||
name: "NamespaceUsers Create with inexisting project",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
NamespaceID: 2000,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrNamespaceDoesNotExist,
|
||||
},
|
||||
{
|
||||
name: "NamespaceUsers Create with inexisting user",
|
||||
fields: fields{
|
||||
Username: "user500",
|
||||
NamespaceID: 2,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: user.IsErrUserDoesNotExist,
|
||||
},
|
||||
{
|
||||
name: "NamespaceUsers Create with the owner as shared user",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
NamespaceID: 1,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrUserAlreadyHasNamespaceAccess,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
||||
un := &NamespaceUser{
|
||||
ID: tt.fields.ID,
|
||||
Username: tt.fields.Username,
|
||||
NamespaceID: tt.fields.NamespaceID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := un.Create(s, tt.args.a)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NamespaceUser.Create() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||
t.Errorf("NamespaceUser.Create() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
}
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
if !tt.wantErr {
|
||||
db.AssertExists(t, "users_namespaces", map[string]interface{}{
|
||||
"user_id": tt.fields.UserID,
|
||||
"namespace_id": tt.fields.NamespaceID,
|
||||
}, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceUser_ReadAll(t *testing.T) {
|
||||
user1 := &UserWithRight{
|
||||
User: user.User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Right: RightRead,
|
||||
}
|
||||
user2 := &UserWithRight{
|
||||
User: user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Right: RightRead,
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
NamespaceID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
type args struct {
|
||||
search string
|
||||
a web.Auth
|
||||
page int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want interface{}
|
||||
wantErr bool
|
||||
errType func(err error) bool
|
||||
}{
|
||||
{
|
||||
name: "Test readall normal",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 3},
|
||||
},
|
||||
want: []*UserWithRight{
|
||||
user1,
|
||||
user2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test ReadAll by a user who does not have access to the project",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 4},
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrNeedToHaveNamespaceReadAccess,
|
||||
},
|
||||
{
|
||||
name: "Search",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 3},
|
||||
search: "usER2",
|
||||
},
|
||||
want: []*UserWithRight{
|
||||
user2,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
un := &NamespaceUser{
|
||||
ID: tt.fields.ID,
|
||||
UserID: tt.fields.UserID,
|
||||
NamespaceID: tt.fields.NamespaceID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
got, _, _, err := un.ReadAll(s, tt.args.a, tt.args.search, tt.args.page, 50)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NamespaceUser.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||
t.Errorf("NamespaceUser.ReadAll() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
}
|
||||
if diff, equal := messagediff.PrettyDiff(got, tt.want); !equal {
|
||||
t.Errorf("NamespaceUser.ReadAll() = %v, want %v, diff: %v", got, tt.want, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceUser_Update(t *testing.T) {
|
||||
type fields struct {
|
||||
ID int64
|
||||
Username string
|
||||
UserID int64
|
||||
NamespaceID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
errType func(err error) bool
|
||||
}{
|
||||
{
|
||||
name: "Test Update Normally",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
Username: "user1",
|
||||
UserID: 1,
|
||||
Right: RightAdmin,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update to write",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
Username: "user1",
|
||||
UserID: 1,
|
||||
Right: RightWrite,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update to Read",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
Username: "user1",
|
||||
UserID: 1,
|
||||
Right: RightRead,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Update with invalid right",
|
||||
fields: fields{
|
||||
NamespaceID: 3,
|
||||
Username: "user1",
|
||||
Right: 500,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrInvalidRight,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
||||
nu := &NamespaceUser{
|
||||
ID: tt.fields.ID,
|
||||
Username: tt.fields.Username,
|
||||
NamespaceID: tt.fields.NamespaceID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := nu.Update(s, &user.User{ID: 1})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NamespaceUser.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||
t.Errorf("NamespaceUser.Update() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
}
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
if !tt.wantErr {
|
||||
db.AssertExists(t, "users_namespaces", map[string]interface{}{
|
||||
"user_id": tt.fields.UserID,
|
||||
"namespace_id": tt.fields.NamespaceID,
|
||||
"right": tt.fields.Right,
|
||||
}, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceUser_Delete(t *testing.T) {
|
||||
type fields struct {
|
||||
ID int64
|
||||
Username string
|
||||
UserID int64
|
||||
NamespaceID int64
|
||||
Right Right
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CRUDable web.CRUDable
|
||||
Rights web.Rights
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
errType func(err error) bool
|
||||
}{
|
||||
{
|
||||
name: "Try deleting some unexistant user",
|
||||
fields: fields{
|
||||
Username: "user1000",
|
||||
NamespaceID: 2,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: user.IsErrUserDoesNotExist,
|
||||
},
|
||||
{
|
||||
name: "Try deleting a user which does not has access but exists",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
NamespaceID: 4,
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrUserDoesNotHaveAccessToNamespace,
|
||||
},
|
||||
{
|
||||
name: "Try deleting normally",
|
||||
fields: fields{
|
||||
Username: "user1",
|
||||
UserID: 1,
|
||||
NamespaceID: 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
||||
nu := &NamespaceUser{
|
||||
ID: tt.fields.ID,
|
||||
Username: tt.fields.Username,
|
||||
NamespaceID: tt.fields.NamespaceID,
|
||||
Right: tt.fields.Right,
|
||||
Created: tt.fields.Created,
|
||||
Updated: tt.fields.Updated,
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := nu.Delete(s, &user.User{ID: 1})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NamespaceUser.Delete() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||
t.Errorf("NamespaceUser.Delete() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
}
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
if !tt.wantErr {
|
||||
db.AssertMissing(t, "users_namespaces", map[string]interface{}{
|
||||
"user_id": tt.fields.UserID,
|
||||
"namespace_id": tt.fields.NamespaceID,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -47,13 +47,14 @@ type Project struct {
|
||||
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
|
||||
|
||||
OwnerID int64 `xorm:"bigint INDEX not null" json:"-"`
|
||||
NamespaceID int64 `xorm:"bigint INDEX not null" json:"namespace_id" param:"namespace"`
|
||||
ParentProjectID int64 `xorm:"bigint INDEX null" json:"parent_project_id"`
|
||||
|
||||
ChildProjects []*Project `xorm:"-" json:"child_projects"`
|
||||
|
||||
// The user who created this project.
|
||||
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
|
||||
|
||||
// Whether or not a project is archived.
|
||||
// Whether a project is archived.
|
||||
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
|
||||
|
||||
// The id of the file this project has set as background
|
||||
@ -92,7 +93,7 @@ type ProjectWithTasksAndBuckets struct {
|
||||
}
|
||||
|
||||
// TableName returns a better name for the projects table
|
||||
func (l *Project) TableName() string {
|
||||
func (p *Project) TableName() string {
|
||||
return "projects"
|
||||
}
|
||||
|
||||
@ -104,70 +105,42 @@ type ProjectBackgroundType struct {
|
||||
// ProjectBackgroundUpload represents the project upload background type
|
||||
const ProjectBackgroundUpload string = "upload"
|
||||
|
||||
// FavoritesPseudoProject holds all tasks marked as favorites
|
||||
var FavoritesPseudoProject = Project{
|
||||
// SharedProjectsPseudoProject is a pseudo project used to hold shared projects
|
||||
var SharedProjectsPseudoProject = &Project{
|
||||
ID: -1,
|
||||
Title: "Favorites",
|
||||
Description: "This project has all tasks marked as favorites.",
|
||||
NamespaceID: FavoritesPseudoNamespace.ID,
|
||||
IsFavorite: true,
|
||||
Title: "Shared Projects",
|
||||
Description: "Projects of other users shared with you via teams or directly.",
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
// GetProjectsByNamespaceID gets all projects in a namespace
|
||||
func GetProjectsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (projects []*Project, err error) {
|
||||
switch nID {
|
||||
case SharedProjectsPseudoNamespace.ID:
|
||||
nnn, err := getSharedProjectsInNamespace(s, false, doer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nnn != nil && nnn.Projects != nil {
|
||||
projects = nnn.Projects
|
||||
}
|
||||
case FavoritesPseudoNamespace.ID:
|
||||
namespaces := make(map[int64]*NamespaceWithProjects)
|
||||
_, err := getNamespacesWithProjects(s, &namespaces, "", false, 0, -1, doer.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
namespaceIDs, _ := getNamespaceOwnerIDs(namespaces)
|
||||
ls, err := getProjectsForNamespaces(s, namespaceIDs, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nnn, err := getFavoriteProjects(s, ls, namespaceIDs, doer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nnn != nil && nnn.Projects != nil {
|
||||
projects = nnn.Projects
|
||||
}
|
||||
case SavedFiltersPseudoNamespace.ID:
|
||||
nnn, err := getSavedFilters(s, doer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nnn != nil && nnn.Projects != nil {
|
||||
projects = nnn.Projects
|
||||
}
|
||||
default:
|
||||
err = s.Select("l.*").
|
||||
Alias("l").
|
||||
Join("LEFT", []string{"namespaces", "n"}, "l.namespace_id = n.id").
|
||||
Where("l.is_archived = false").
|
||||
Where("n.is_archived = false OR n.is_archived IS NULL").
|
||||
Where("namespace_id = ?", nID).
|
||||
Find(&projects)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// FavoriteProjectsPseudoProject is a pseudo namespace used to hold favorite projects and tasks
|
||||
var FavoriteProjectsPseudoProject = &Project{
|
||||
ID: -2,
|
||||
Title: "Favorites",
|
||||
Description: "Favorite projects and tasks.",
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
// get more project details
|
||||
err = addProjectDetails(s, projects, doer)
|
||||
return projects, err
|
||||
// SavedFiltersPseudoProject is a pseudo namespace used to hold saved filters
|
||||
var SavedFiltersPseudoProject = &Project{
|
||||
ID: -3,
|
||||
Title: "Filters",
|
||||
Description: "Saved filters.",
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
// FavoritesPseudoProject holds all tasks marked as favorites
|
||||
var FavoritesPseudoProject = Project{
|
||||
ID: -1,
|
||||
Title: "Favorites",
|
||||
Description: "This project has all tasks marked as favorites.",
|
||||
ParentProjectID: FavoriteProjectsPseudoProject.ID,
|
||||
IsFavorite: true,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
// ReadAll gets all projects a user has access to
|
||||
@ -185,7 +158,7 @@ func GetProjectsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (proj
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects [get]
|
||||
func (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
|
||||
func (p *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
|
||||
// Check if we're dealing with a share auth
|
||||
shareAuth, ok := a.(*LinkSharing)
|
||||
if ok {
|
||||
@ -193,26 +166,67 @@ func (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int,
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
projects := []*Project{project}
|
||||
projects := map[int64]*Project{}
|
||||
projects[project.ID] = project
|
||||
err = addProjectDetails(s, projects, a)
|
||||
return projects, 0, 0, err
|
||||
}
|
||||
|
||||
projects, resultCount, totalItems, err := getRawProjectsForUser(
|
||||
doer, err := user.GetFromAuth(a)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
allProjects, resultCount, totalItems, err := getRawProjectsForUser(
|
||||
s,
|
||||
&projectOptions{
|
||||
search: search,
|
||||
user: &user.User{ID: a.GetID()},
|
||||
page: page,
|
||||
perPage: perPage,
|
||||
isArchived: l.IsArchived,
|
||||
search: search,
|
||||
user: doer,
|
||||
page: page,
|
||||
perPage: perPage,
|
||||
getArchived: p.IsArchived,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
// Add more project details
|
||||
err = addProjectDetails(s, projects, a)
|
||||
/////////////////
|
||||
// Saved Filters
|
||||
|
||||
savedFiltersProject, err := getSavedFilterProjects(s, doer)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if savedFiltersProject != nil {
|
||||
allProjects[savedFiltersProject.ID] = savedFiltersProject
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Add project details (favorite state, among other things)
|
||||
err = addProjectDetails(s, allProjects, a)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//////////////////////////
|
||||
// Putting it all together
|
||||
|
||||
var projects []*Project
|
||||
for _, p := range allProjects {
|
||||
if p.ParentProjectID != 0 {
|
||||
if allProjects[p.ParentProjectID].ChildProjects == nil {
|
||||
allProjects[p.ParentProjectID].ChildProjects = []*Project{}
|
||||
}
|
||||
allProjects[p.ParentProjectID].ChildProjects = append(allProjects[p.ParentProjectID].ChildProjects, p)
|
||||
continue
|
||||
}
|
||||
|
||||
// The projects variable will contain all projects which have no parents
|
||||
// And because we're using the same pointers for everything, those will contain child projects
|
||||
projects = append(projects, p)
|
||||
}
|
||||
|
||||
return projects, resultCount, totalItems, err
|
||||
}
|
||||
|
||||
@ -228,61 +242,61 @@ func (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int,
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{id} [get]
|
||||
func (l *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (p *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
if l.ID == FavoritesPseudoProject.ID {
|
||||
if p.ID == FavoritesPseudoProject.ID {
|
||||
// Already "built" the project in CanRead
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for saved filters
|
||||
if getSavedFilterIDFromProjectID(l.ID) > 0 {
|
||||
sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(l.ID))
|
||||
if getSavedFilterIDFromProjectID(p.ID) > 0 {
|
||||
sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(p.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.Title = sf.Title
|
||||
l.Description = sf.Description
|
||||
l.Created = sf.Created
|
||||
l.Updated = sf.Updated
|
||||
l.OwnerID = sf.OwnerID
|
||||
p.Title = sf.Title
|
||||
p.Description = sf.Description
|
||||
p.Created = sf.Created
|
||||
p.Updated = sf.Updated
|
||||
p.OwnerID = sf.OwnerID
|
||||
}
|
||||
|
||||
// Get project owner
|
||||
l.Owner, err = user.GetUserByID(s, l.OwnerID)
|
||||
p.Owner, err = user.GetUserByID(s, p.OwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Check if the namespace is archived and set the namespace to archived if it is not already archived individually.
|
||||
if !l.IsArchived {
|
||||
err = l.CheckIsArchived(s)
|
||||
if !p.IsArchived {
|
||||
err = p.CheckIsArchived(s)
|
||||
if err != nil {
|
||||
if !IsErrNamespaceIsArchived(err) && !IsErrProjectIsArchived(err) {
|
||||
return
|
||||
}
|
||||
l.IsArchived = true
|
||||
p.IsArchived = true
|
||||
}
|
||||
}
|
||||
|
||||
// Get any background information if there is one set
|
||||
if l.BackgroundFileID != 0 {
|
||||
if p.BackgroundFileID != 0 {
|
||||
// Unsplash image
|
||||
l.BackgroundInformation, err = GetUnsplashPhotoByFileID(s, l.BackgroundFileID)
|
||||
p.BackgroundInformation, err = GetUnsplashPhotoByFileID(s, p.BackgroundFileID)
|
||||
if err != nil && !files.IsErrFileIsNotUnsplashFile(err) {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil && files.IsErrFileIsNotUnsplashFile(err) {
|
||||
l.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
|
||||
p.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
|
||||
}
|
||||
}
|
||||
|
||||
l.IsFavorite, err = isFavorite(s, l.ID, a, FavoriteKindProject)
|
||||
p.IsFavorite, err = isFavorite(s, p.ID, a, FavoriteKindProject)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
l.Subscription, err = GetSubscription(s, SubscriptionEntityProject, l.ID, a)
|
||||
p.Subscription, err = GetSubscription(s, SubscriptionEntityProject, p.ID, a)
|
||||
return
|
||||
}
|
||||
|
||||
@ -345,62 +359,32 @@ func GetProjectsByIDs(s *xorm.Session, projectIDs []int64) (projects map[int64]*
|
||||
}
|
||||
|
||||
type projectOptions struct {
|
||||
search string
|
||||
user *user.User
|
||||
page int
|
||||
perPage int
|
||||
isArchived bool
|
||||
search string
|
||||
user *user.User
|
||||
page int
|
||||
perPage int
|
||||
getArchived bool
|
||||
}
|
||||
|
||||
func getUserProjectsStatement(userID int64) *builder.Builder {
|
||||
func getUserProjectsStatement(userID int64, search string, getArchived bool) *builder.Builder {
|
||||
dialect := config.DatabaseType.GetString()
|
||||
if dialect == "sqlite" {
|
||||
dialect = builder.SQLITE
|
||||
}
|
||||
|
||||
return builder.Dialect(dialect).
|
||||
Select("l.*").
|
||||
From("projects", "l").
|
||||
Join("INNER", "namespaces n", "l.namespace_id = n.id").
|
||||
Join("LEFT", "team_namespaces tn", "tn.namespace_id = n.id").
|
||||
Join("LEFT", "team_members tm", "tm.team_id = tn.team_id").
|
||||
Join("LEFT", "team_projects tl", "l.id = tl.project_id").
|
||||
Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
|
||||
Join("LEFT", "users_projects ul", "ul.project_id = l.id").
|
||||
Join("LEFT", "users_namespaces un", "un.namespace_id = l.namespace_id").
|
||||
Where(builder.Or(
|
||||
builder.Eq{"tm.user_id": userID},
|
||||
builder.Eq{"tm2.user_id": userID},
|
||||
builder.Eq{"ul.user_id": userID},
|
||||
builder.Eq{"un.user_id": userID},
|
||||
builder.Eq{"l.owner_id": userID},
|
||||
)).
|
||||
OrderBy("position").
|
||||
GroupBy("l.id")
|
||||
}
|
||||
|
||||
// Gets the projects only, without any tasks or so
|
||||
func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects []*Project, resultCount int, totalItems int64, err error) {
|
||||
fullUser, err := user.GetUserByID(s, opts.user.ID)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
// Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions
|
||||
var isArchivedCond builder.Cond = builder.Eq{"1": 1}
|
||||
if !opts.isArchived {
|
||||
isArchivedCond = builder.And(
|
||||
var getArchivedCond builder.Cond = builder.Eq{"1": 1}
|
||||
if !getArchived {
|
||||
getArchivedCond = builder.And(
|
||||
builder.Eq{"l.is_archived": false},
|
||||
builder.Eq{"n.is_archived": false},
|
||||
)
|
||||
}
|
||||
|
||||
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
|
||||
|
||||
var filterCond builder.Cond
|
||||
ids := []int64{}
|
||||
if opts.search != "" {
|
||||
vals := strings.Split(opts.search, ",")
|
||||
if search != "" {
|
||||
vals := strings.Split(search, ",")
|
||||
for _, val := range vals {
|
||||
v, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
@ -411,65 +395,114 @@ func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects []*P
|
||||
}
|
||||
}
|
||||
|
||||
filterCond = db.ILIKE("l.title", opts.search)
|
||||
filterCond = db.ILIKE("l.title", search)
|
||||
if len(ids) > 0 {
|
||||
filterCond = builder.In("l.id", ids)
|
||||
}
|
||||
|
||||
// Gets all Projects where the user is either owner or in a team which has access to the project
|
||||
// Or in a team which has namespace read access
|
||||
return builder.Dialect(dialect).
|
||||
Select("l.*").
|
||||
From("projects", "l").
|
||||
// TODO: remove namespaces
|
||||
Join("INNER", "namespaces n", "l.namespace_id = n.id").
|
||||
Join("LEFT", "team_namespaces tn", "tn.namespace_id = n.id").
|
||||
Join("LEFT", "team_members tm", "tm.team_id = tn.team_id").
|
||||
Join("LEFT", "team_projects tl", "l.id = tl.project_id").
|
||||
Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
|
||||
Join("LEFT", "users_projects ul", "ul.project_id = l.id").
|
||||
Join("LEFT", "users_namespaces un", "un.namespace_id = l.namespace_id").
|
||||
Where(builder.And(
|
||||
builder.Or(
|
||||
builder.Eq{"tm.user_id": userID},
|
||||
builder.Eq{"tm2.user_id": userID},
|
||||
builder.Eq{"ul.user_id": userID},
|
||||
builder.Eq{"un.user_id": userID},
|
||||
builder.Eq{"l.owner_id": userID},
|
||||
),
|
||||
filterCond,
|
||||
getArchivedCond,
|
||||
)).
|
||||
OrderBy("position").
|
||||
GroupBy("l.id")
|
||||
}
|
||||
|
||||
query := getUserProjectsStatement(fullUser.ID).
|
||||
Where(filterCond).
|
||||
Where(isArchivedCond)
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
}
|
||||
err = s.SQL(query).Find(&projects)
|
||||
// Gets the projects with their children without any tasks
|
||||
func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects map[int64]*Project, resultCount int, totalItems int64, err error) {
|
||||
fullUser, err := user.GetUserByID(s, opts.user.ID)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
query = getUserProjectsStatement(fullUser.ID).
|
||||
Where(filterCond).
|
||||
Where(isArchivedCond)
|
||||
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
|
||||
|
||||
// Gets all projects where the user is either owner or it was shared to them
|
||||
|
||||
allProjects := make(map[int64]*Project)
|
||||
|
||||
query := getUserProjectsStatement(fullUser.ID, opts.search, opts.getArchived)
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
}
|
||||
err = s.SQL(query).Find(&allProjects)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
query = getUserProjectsStatement(fullUser.ID, opts.search, opts.getArchived)
|
||||
totalItems, err = s.
|
||||
SQL(query.Select("count(*)")).
|
||||
Count(&Project{})
|
||||
return projects, len(projects), totalItems, err
|
||||
|
||||
if len(allProjects) == 0 {
|
||||
return nil, 0, totalItems, nil
|
||||
}
|
||||
|
||||
return projects, len(allProjects), totalItems, err
|
||||
}
|
||||
|
||||
func getSavedFilterProjects(s *xorm.Session, doer *user.User) (savedFiltersNamespace *Project, err error) {
|
||||
savedFilters, err := getSavedFiltersForUser(s, doer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(savedFilters) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
savedFiltersPseudoNamespace := SavedFiltersPseudoProject
|
||||
savedFiltersPseudoNamespace.OwnerID = doer.ID
|
||||
*savedFiltersNamespace = *savedFiltersPseudoNamespace
|
||||
savedFiltersNamespace.ChildProjects = make([]*Project, 0, len(savedFilters))
|
||||
|
||||
for _, filter := range savedFilters {
|
||||
filterProject := filter.toProject()
|
||||
filterProject.ParentProjectID = savedFiltersNamespace.ID
|
||||
filterProject.Owner = doer
|
||||
savedFiltersNamespace.ChildProjects = append(savedFiltersNamespace.ChildProjects, filterProject)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// addProjectDetails adds owner user objects and project tasks to all projects in the slice
|
||||
func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err error) {
|
||||
func addProjectDetails(s *xorm.Session, projects map[int64]*Project, a web.Auth) (err error) {
|
||||
if len(projects) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var ownerIDs []int64
|
||||
for _, l := range projects {
|
||||
ownerIDs = append(ownerIDs, l.OwnerID)
|
||||
}
|
||||
|
||||
// Get all project owners
|
||||
owners := map[int64]*user.User{}
|
||||
if len(ownerIDs) > 0 {
|
||||
err = s.In("id", ownerIDs).Find(&owners)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var fileIDs []int64
|
||||
var projectIDs []int64
|
||||
for _, l := range projects {
|
||||
projectIDs = append(projectIDs, l.ID)
|
||||
if o, exists := owners[l.OwnerID]; exists {
|
||||
l.Owner = o
|
||||
}
|
||||
if l.BackgroundFileID != 0 {
|
||||
l.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
|
||||
}
|
||||
fileIDs = append(fileIDs, l.BackgroundFileID)
|
||||
var fileIDs []int64
|
||||
for _, p := range projects {
|
||||
ownerIDs = append(ownerIDs, p.OwnerID)
|
||||
projectIDs = append(projectIDs, p.ID)
|
||||
fileIDs = append(fileIDs, p.BackgroundFileID)
|
||||
}
|
||||
|
||||
owners, err := user.GetUsersByIDs(s, ownerIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
favs, err := getFavorites(s, projectIDs, a, FavoriteKindProject)
|
||||
@ -479,19 +512,26 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
|
||||
|
||||
subscriptions, err := GetSubscriptions(s, SubscriptionEntityProject, projectIDs, a)
|
||||
if err != nil {
|
||||
log.Errorf("An error occurred while getting project subscriptions for a namespace item: %s", err.Error())
|
||||
log.Errorf("An error occurred while getting project subscriptions for a project: %s", err.Error())
|
||||
subscriptions = make(map[int64]*Subscription)
|
||||
}
|
||||
|
||||
for _, project := range projects {
|
||||
for _, p := range projects {
|
||||
if o, exists := owners[p.OwnerID]; exists {
|
||||
p.Owner = o
|
||||
}
|
||||
if p.BackgroundFileID != 0 {
|
||||
p.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
|
||||
}
|
||||
|
||||
// Don't override the favorite state if it was already set from before (favorite saved filters do this)
|
||||
if project.IsFavorite {
|
||||
if p.IsFavorite {
|
||||
continue
|
||||
}
|
||||
project.IsFavorite = favs[project.ID]
|
||||
p.IsFavorite = favs[p.ID]
|
||||
|
||||
if subscription, exists := subscriptions[project.ID]; exists {
|
||||
project.Subscription = subscription
|
||||
if subscription, exists := subscriptions[p.ID]; exists {
|
||||
p.Subscription = subscription
|
||||
}
|
||||
}
|
||||
|
||||
@ -521,46 +561,36 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
|
||||
return
|
||||
}
|
||||
|
||||
// NamespaceProject is a meta type to be able to join a project with its namespace
|
||||
type NamespaceProject struct {
|
||||
Project Project `xorm:"extends"`
|
||||
Namespace Namespace `xorm:"extends"`
|
||||
}
|
||||
|
||||
// CheckIsArchived returns an ErrProjectIsArchived or ErrNamespaceIsArchived if the project or its namespace is archived.
|
||||
func (l *Project) CheckIsArchived(s *xorm.Session) (err error) {
|
||||
// When creating a new project, we check if the namespace is archived
|
||||
if l.ID == 0 {
|
||||
n := &Namespace{ID: l.NamespaceID}
|
||||
return n.CheckIsArchived(s)
|
||||
// CheckIsArchived returns an ErrProjectIsArchived or ErrNamespaceIsArchived if the project or any of its parent projects is archived.
|
||||
func (p *Project) CheckIsArchived(s *xorm.Session) (err error) {
|
||||
// When creating a new project, we check if the parent is archived
|
||||
if p.ID == 0 {
|
||||
p := &Project{ID: p.ParentProjectID}
|
||||
return p.CheckIsArchived(s)
|
||||
}
|
||||
|
||||
nl := &NamespaceProject{}
|
||||
exists, err := s.
|
||||
Table("projects").
|
||||
Join("LEFT", "namespaces", "projects.namespace_id = namespaces.id").
|
||||
Where("projects.id = ? AND (projects.is_archived = true OR namespaces.is_archived = true)", l.ID).
|
||||
Get(nl)
|
||||
p, err := GetProjectSimpleByID(s, p.ID)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
if exists && nl.Project.ID != 0 && nl.Project.IsArchived {
|
||||
return ErrProjectIsArchived{ProjectID: l.ID}
|
||||
}
|
||||
if exists && nl.Namespace.ID != 0 && nl.Namespace.IsArchived {
|
||||
return ErrNamespaceIsArchived{NamespaceID: nl.Namespace.ID}
|
||||
|
||||
// TODO: parent project
|
||||
|
||||
if p.IsArchived {
|
||||
return ErrProjectIsArchived{ProjectID: p.ID}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkProjectBeforeUpdateOrDelete(s *xorm.Session, project *Project) error {
|
||||
if project.NamespaceID < 0 {
|
||||
return &ErrProjectCannotBelongToAPseudoNamespace{ProjectID: project.ID, NamespaceID: project.NamespaceID}
|
||||
if project.ParentProjectID < 0 {
|
||||
return &ErrProjectCannotBelongToAPseudoNamespace{ProjectID: project.ID, NamespaceID: project.ParentProjectID}
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
if project.NamespaceID > 0 {
|
||||
_, err := GetNamespaceByID(s, project.NamespaceID)
|
||||
// Check if the parent project exists
|
||||
if project.ParentProjectID > 0 {
|
||||
_, err := GetProjectSimpleByID(s, project.ParentProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -635,19 +665,21 @@ func CreateProject(s *xorm.Session, project *Project, auth web.Auth) (err error)
|
||||
})
|
||||
}
|
||||
|
||||
// CreateNewProjectForUser creates a new inbox project for a user. To prevent import cycles, we can't do that
|
||||
// directly in the user.Create function.
|
||||
func CreateNewProjectForUser(s *xorm.Session, user *user.User) (err error) {
|
||||
p := &Project{
|
||||
Title: "Inbox",
|
||||
}
|
||||
return p.Create(s, user)
|
||||
}
|
||||
|
||||
func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProjectBackground bool) (err error) {
|
||||
err = checkProjectBeforeUpdateOrDelete(s, project)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if project.NamespaceID == 0 {
|
||||
return &ErrProjectMustBelongToANamespace{
|
||||
ProjectID: project.ID,
|
||||
NamespaceID: project.NamespaceID,
|
||||
}
|
||||
}
|
||||
|
||||
// We need to specify the cols we want to update here to be able to un-archive projects
|
||||
colsToUpdate := []string{
|
||||
"title",
|
||||
@ -721,27 +753,27 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{id} [post]
|
||||
func (l *Project) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
fid := getSavedFilterIDFromProjectID(l.ID)
|
||||
func (p *Project) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
fid := getSavedFilterIDFromProjectID(p.ID)
|
||||
if fid > 0 {
|
||||
f, err := getSavedFilterSimpleByID(s, fid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.Title = l.Title
|
||||
f.Description = l.Description
|
||||
f.IsFavorite = l.IsFavorite
|
||||
f.Title = p.Title
|
||||
f.Description = p.Description
|
||||
f.IsFavorite = p.IsFavorite
|
||||
err = f.Update(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*l = *f.toProject()
|
||||
*p = *f.toProject()
|
||||
return nil
|
||||
}
|
||||
|
||||
return UpdateProject(s, l, a, false)
|
||||
return UpdateProject(s, p, a, false)
|
||||
}
|
||||
|
||||
func updateProjectLastUpdated(s *xorm.Session, project *Project) error {
|
||||
@ -773,13 +805,13 @@ func updateProjectByTaskID(s *xorm.Session, taskID int64) (err error) {
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/projects [put]
|
||||
func (l *Project) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
err = CreateProject(s, l, a)
|
||||
func (p *Project) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
err = CreateProject(s, p, a)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return l.ReadOne(s, a)
|
||||
return p.ReadOne(s, a)
|
||||
}
|
||||
|
||||
// Delete implements the delete method of CRUDable
|
||||
@ -794,7 +826,7 @@ func (l *Project) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{id} [delete]
|
||||
func (l *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (p *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
fullList, err := GetProjectSimpleByID(s, l.ID)
|
||||
if err != nil {
|
||||
@ -802,14 +834,14 @@ func (l *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
}
|
||||
|
||||
// Delete the project
|
||||
_, err = s.ID(l.ID).Delete(&Project{})
|
||||
_, err = s.ID(p.ID).Delete(&Project{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete all tasks on that project
|
||||
// Using the loop to make sure all related entities to all tasks are properly deleted as well.
|
||||
tasks, _, _, err := getRawTasksForProjects(s, []*Project{l}, a, &taskOptions{})
|
||||
tasks, _, _, err := getRawTasksForProjects(s, []*Project{p}, a, &taskOptions{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -827,7 +859,7 @@ func (l *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
}
|
||||
|
||||
return events.Dispatch(&ProjectDeletedEvent{
|
||||
Project: l,
|
||||
Project: p,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
@ -24,15 +24,15 @@ import (
|
||||
)
|
||||
|
||||
// CanWrite return whether the user can write on that project or not
|
||||
func (l *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
func (p *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
|
||||
// The favorite project can't be edited
|
||||
if l.ID == FavoritesPseudoProject.ID {
|
||||
if p.ID == FavoritesPseudoProject.ID {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Get the project and check the right
|
||||
originalProject, err := GetProjectSimpleByID(s, l.ID)
|
||||
originalProject, err := GetProjectSimpleByID(s, p.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -67,66 +67,66 @@ func (l *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
}
|
||||
|
||||
// CanRead checks if a user has read access to a project
|
||||
func (l *Project) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
||||
func (p *Project) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
||||
|
||||
// The favorite project needs a special treatment
|
||||
if l.ID == FavoritesPseudoProject.ID {
|
||||
if p.ID == FavoritesPseudoProject.ID {
|
||||
owner, err := user.GetFromAuth(a)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
*l = FavoritesPseudoProject
|
||||
l.Owner = owner
|
||||
*p = FavoritesPseudoProject
|
||||
p.Owner = owner
|
||||
return true, int(RightRead), nil
|
||||
}
|
||||
|
||||
// Saved Filter Projects need a special case
|
||||
if getSavedFilterIDFromProjectID(l.ID) > 0 {
|
||||
sf := &SavedFilter{ID: getSavedFilterIDFromProjectID(l.ID)}
|
||||
if getSavedFilterIDFromProjectID(p.ID) > 0 {
|
||||
sf := &SavedFilter{ID: getSavedFilterIDFromProjectID(p.ID)}
|
||||
return sf.CanRead(s, a)
|
||||
}
|
||||
|
||||
// Check if the user is either owner or can read
|
||||
var err error
|
||||
originalProject, err := GetProjectSimpleByID(s, l.ID)
|
||||
originalProject, err := GetProjectSimpleByID(s, p.ID)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
*l = *originalProject
|
||||
*p = *originalProject
|
||||
|
||||
// Check if we're dealing with a share auth
|
||||
shareAuth, ok := a.(*LinkSharing)
|
||||
if ok {
|
||||
return l.ID == shareAuth.ProjectID &&
|
||||
return p.ID == shareAuth.ProjectID &&
|
||||
(shareAuth.Right == RightRead || shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), int(shareAuth.Right), nil
|
||||
}
|
||||
|
||||
if l.isOwner(&user.User{ID: a.GetID()}) {
|
||||
if p.isOwner(&user.User{ID: a.GetID()}) {
|
||||
return true, int(RightAdmin), nil
|
||||
}
|
||||
return l.checkRight(s, a, RightRead, RightWrite, RightAdmin)
|
||||
return p.checkRight(s, a, RightRead, RightWrite, RightAdmin)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update a project
|
||||
func (l *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err error) {
|
||||
func (p *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err error) {
|
||||
// The favorite project can't be edited
|
||||
if l.ID == FavoritesPseudoProject.ID {
|
||||
if p.ID == FavoritesPseudoProject.ID {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Get the project
|
||||
ol, err := GetProjectSimpleByID(s, l.ID)
|
||||
ol, err := GetProjectSimpleByID(s, p.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check if we're moving the project into a different namespace.
|
||||
// Check if we're moving the project to a different parent project.
|
||||
// If that is the case, we need to verify permissions to do so.
|
||||
if l.NamespaceID != 0 && l.NamespaceID != ol.NamespaceID {
|
||||
newNamespace := &Namespace{ID: l.NamespaceID}
|
||||
can, err := newNamespace.CanWrite(s, a)
|
||||
if p.ParentProjectID != 0 && p.ParentProjectID != ol.ParentProjectID {
|
||||
newProject := &Project{ID: p.ParentProjectID}
|
||||
can, err := newProject.CanWrite(s, a)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -135,7 +135,7 @@ func (l *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err er
|
||||
}
|
||||
}
|
||||
|
||||
fid := getSavedFilterIDFromProjectID(l.ID)
|
||||
fid := getSavedFilterIDFromProjectID(p.ID)
|
||||
if fid > 0 {
|
||||
sf, err := getSavedFilterSimpleByID(s, fid)
|
||||
if err != nil {
|
||||
@ -145,34 +145,40 @@ func (l *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err er
|
||||
return sf.CanUpdate(s, a)
|
||||
}
|
||||
|
||||
canUpdate, err = l.CanWrite(s, a)
|
||||
canUpdate, err = p.CanWrite(s, a)
|
||||
// If the project is archived and the user tries to un-archive it, let the request through
|
||||
if IsErrProjectIsArchived(err) && !l.IsArchived {
|
||||
if IsErrProjectIsArchived(err) && !p.IsArchived {
|
||||
err = nil
|
||||
}
|
||||
return canUpdate, err
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a project
|
||||
func (l *Project) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return l.IsAdmin(s, a)
|
||||
func (p *Project) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return p.IsAdmin(s, a)
|
||||
}
|
||||
|
||||
// CanCreate checks if the user can create a project
|
||||
func (l *Project) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
// A user can create a project if they have write access to the namespace
|
||||
n := &Namespace{ID: l.NamespaceID}
|
||||
return n.CanWrite(s, a)
|
||||
func (p *Project) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
// If the new namespace has a parent, check that
|
||||
if p.ParentProjectID != 0 {
|
||||
// TODO: check the parent's parent (and so on)
|
||||
parent := &Project{ID: p.ParentProjectID}
|
||||
return parent.CanCreate(s, a)
|
||||
}
|
||||
|
||||
// Otherwise just allow
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// IsAdmin returns whether the user has admin rights on the project or not
|
||||
func (l *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
func (p *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
// The favorite project can't be edited
|
||||
if l.ID == FavoritesPseudoProject.ID {
|
||||
if p.ID == FavoritesPseudoProject.ID {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
originalProject, err := GetProjectSimpleByID(s, l.ID)
|
||||
originalProject, err := GetProjectSimpleByID(s, p.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -194,12 +200,12 @@ func (l *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
}
|
||||
|
||||
// Little helper function to check if a user is project owner
|
||||
func (l *Project) isOwner(u *user.User) bool {
|
||||
return l.OwnerID == u.ID
|
||||
func (p *Project) isOwner(u *user.User) bool {
|
||||
return p.OwnerID == u.ID
|
||||
}
|
||||
|
||||
// Checks n different rights for any given user
|
||||
func (l *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) {
|
||||
func (p *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) {
|
||||
|
||||
/*
|
||||
The following loop creates a sql condition like this one:
|
||||
@ -242,53 +248,44 @@ func (l *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool
|
||||
conds = append(conds, builder.Eq{"n.owner_id": a.GetID()})
|
||||
|
||||
type allProjectRights struct {
|
||||
UserNamespace *NamespaceUser `xorm:"extends"`
|
||||
UserProject *ProjectUser `xorm:"extends"`
|
||||
UserProject *ProjectUser `xorm:"extends"`
|
||||
TeamProject *TeamProject `xorm:"extends"`
|
||||
|
||||
TeamNamespace *TeamNamespace `xorm:"extends"`
|
||||
TeamProject *TeamProject `xorm:"extends"`
|
||||
|
||||
NamespaceOwnerID int64 `xorm:"namespaces_owner_id"`
|
||||
OwnerID int64 `xorm:"namespaces_owner_id"`
|
||||
}
|
||||
|
||||
r := &allProjectRights{}
|
||||
var maxRight = 0
|
||||
exists, err := s.
|
||||
Select("l.*, un.right, ul.right, tn.right, tl.right, n.owner_id as namespaces_owner_id").
|
||||
Select("p.*, un.right, ul.right, tn.right, tl.right, n.owner_id as namespaces_owner_id").
|
||||
Table("projects").
|
||||
Alias("l").
|
||||
Alias("p").
|
||||
// User stuff
|
||||
Join("LEFT", []string{"users_namespaces", "un"}, "un.namespace_id = l.namespace_id").
|
||||
Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = l.id").
|
||||
Join("LEFT", []string{"namespaces", "n"}, "n.id = l.namespace_id").
|
||||
Join("LEFT", []string{"users_namespaces", "un"}, "un.namespace_id = p.namespace_id").
|
||||
Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = p.id").
|
||||
Join("LEFT", []string{"namespaces", "n"}, "n.id = p.namespace_id").
|
||||
// Team stuff
|
||||
Join("LEFT", []string{"team_namespaces", "tn"}, " l.namespace_id = tn.namespace_id").
|
||||
Join("LEFT", []string{"team_namespaces", "tn"}, " p.namespace_id = tn.namespace_id").
|
||||
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tn.team_id").
|
||||
Join("LEFT", []string{"team_projects", "tl"}, "l.id = tl.project_id").
|
||||
Join("LEFT", []string{"team_projects", "tl"}, "p.id = tl.project_id").
|
||||
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
|
||||
// The actual condition
|
||||
Where(builder.And(
|
||||
builder.Or(
|
||||
conds...,
|
||||
),
|
||||
builder.Eq{"l.id": l.ID},
|
||||
builder.Eq{"p.id": p.ID},
|
||||
)).
|
||||
Get(r)
|
||||
|
||||
// Figure out the max right and return it
|
||||
if int(r.UserNamespace.Right) > maxRight {
|
||||
maxRight = int(r.UserNamespace.Right)
|
||||
}
|
||||
if int(r.UserProject.Right) > maxRight {
|
||||
maxRight = int(r.UserProject.Right)
|
||||
}
|
||||
if int(r.TeamNamespace.Right) > maxRight {
|
||||
maxRight = int(r.TeamNamespace.Right)
|
||||
}
|
||||
if int(r.TeamProject.Right) > maxRight {
|
||||
maxRight = int(r.TeamProject.Right)
|
||||
}
|
||||
if r.NamespaceOwnerID == a.GetID() {
|
||||
if r.OwnerID == a.GetID() {
|
||||
maxRight = int(RightAdmin)
|
||||
}
|
||||
|
||||
|
@ -102,7 +102,7 @@ func (sf *SavedFilter) toProject() *Project {
|
||||
Created: sf.Created,
|
||||
Updated: sf.Updated,
|
||||
Owner: sf.Owner,
|
||||
NamespaceID: SavedFiltersPseudoNamespace.ID,
|
||||
NamespaceID: SavedFiltersPseudoProject.ID,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,45 +87,6 @@ func deleteUsers() {
|
||||
}
|
||||
}
|
||||
|
||||
func getNamespacesToDelete(s *xorm.Session, u *user.User) (namespacesToDelete []*Namespace, err error) {
|
||||
namespacesToDelete = []*Namespace{}
|
||||
nm := &Namespace{IsArchived: true}
|
||||
res, _, _, err := nm.ReadAll(s, u, "", 1, -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
namespaces := res.([]*NamespaceWithProjects)
|
||||
for _, n := range namespaces {
|
||||
if n.ID < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
hadUsers, err := ensureNamespaceAdminUser(s, &n.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hadUsers {
|
||||
continue
|
||||
}
|
||||
hadTeams, err := ensureNamespaceAdminTeam(s, &n.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hadTeams {
|
||||
continue
|
||||
}
|
||||
|
||||
namespacesToDelete = append(namespacesToDelete, &n.Namespace)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getProjectsToDelete(s *xorm.Session, u *user.User) (projectsToDelete []*Project, err error) {
|
||||
projectsToDelete = []*Project{}
|
||||
lm := &Project{IsArchived: true}
|
||||
@ -170,24 +131,11 @@ func getProjectsToDelete(s *xorm.Session, u *user.User) (projectsToDelete []*Pro
|
||||
// This action is irrevocable.
|
||||
// Public to allow deletion from the CLI.
|
||||
func DeleteUser(s *xorm.Session, u *user.User) (err error) {
|
||||
namespacesToDelete, err := getNamespacesToDelete(s, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
projectsToDelete, err := getProjectsToDelete(s, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete everything not shared with anybody else
|
||||
for _, n := range namespacesToDelete {
|
||||
err = deleteNamespace(s, n, u, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, l := range projectsToDelete {
|
||||
err = l.Delete(s, u)
|
||||
if err != nil {
|
||||
@ -205,58 +153,6 @@ func DeleteUser(s *xorm.Session, u *user.User) (err error) {
|
||||
})
|
||||
}
|
||||
|
||||
func ensureNamespaceAdminUser(s *xorm.Session, n *Namespace) (hadUsers bool, err error) {
|
||||
namespaceUsers := []*NamespaceUser{}
|
||||
err = s.Where("namespace_id = ?", n.ID).Find(&namespaceUsers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(namespaceUsers) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, lu := range namespaceUsers {
|
||||
if lu.Right == RightAdmin {
|
||||
// Project already has more than one admin, no need to do anything
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
firstUser := namespaceUsers[0]
|
||||
firstUser.Right = RightAdmin
|
||||
_, err = s.Where("id = ?", firstUser.ID).
|
||||
Cols("right").
|
||||
Update(firstUser)
|
||||
return true, err
|
||||
}
|
||||
|
||||
func ensureNamespaceAdminTeam(s *xorm.Session, n *Namespace) (hadTeams bool, err error) {
|
||||
namespaceTeams := []*TeamNamespace{}
|
||||
err = s.Where("namespace_id = ?", n.ID).Find(&namespaceTeams)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(namespaceTeams) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, lu := range namespaceTeams {
|
||||
if lu.Right == RightAdmin {
|
||||
// Project already has more than one admin, no need to do anything
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
firstTeam := namespaceTeams[0]
|
||||
firstTeam.Right = RightAdmin
|
||||
_, err = s.Where("id = ?", firstTeam.ID).
|
||||
Cols("right").
|
||||
Update(firstTeam)
|
||||
return true, err
|
||||
}
|
||||
|
||||
func ensureProjectAdminUser(s *xorm.Session, l *Project) (hadUsers bool, err error) {
|
||||
projectUsers := []*ProjectUser{}
|
||||
err = s.Where("project_id = ?", l.ID).Find(&projectUsers)
|
||||
|
@ -244,7 +244,7 @@ func getOrCreateUser(s *xorm.Session, cl *claims, issuer, subject string) (u *us
|
||||
}
|
||||
|
||||
// And create its namespace
|
||||
err = models.CreateNewNamespaceForUser(s, u)
|
||||
err = models.CreateNewProjectForUser(s, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1,97 +0,0 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"xorm.io/xorm"
|
||||
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web/handler"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// GetProjectsByNamespaceID is the web handler to get all projects belonging to a namespace
|
||||
// TODO: deprecate this in favour of namespace.ReadOne() <-- should also return the projects
|
||||
// @Summary Get all projects in a namespace
|
||||
// @Description Returns all projects inside of a namespace.
|
||||
// @tags namespace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param namespaceID path int true "Namespace ID"
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.Project "The projects."
|
||||
// @Failure 403 {object} models.Message "No access to that namespace."
|
||||
// @Failure 404 {object} models.Message "The namespace does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/projects [get]
|
||||
func GetProjectsByNamespaceID(c echo.Context) error {
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
// Get our namespace
|
||||
namespace, err := getNamespace(s, c)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
// Get the projects
|
||||
doer, err := user.GetCurrentUser(c)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
projects, err := models.GetProjectsByNamespaceID(s, namespace.ID, doer)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
return c.JSON(http.StatusOK, projects)
|
||||
}
|
||||
|
||||
func getNamespace(s *xorm.Session, c echo.Context) (namespace *models.Namespace, err error) {
|
||||
// Check if we have our ID
|
||||
id := c.Param("namespace")
|
||||
// Make int
|
||||
namespaceID, err := strconv.ParseInt(id, 10, 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if namespaceID == -1 {
|
||||
namespace = &models.SharedProjectsPseudoNamespace
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user has acces to that namespace
|
||||
u, err := user.GetCurrentUser(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
namespace = &models.Namespace{ID: namespaceID}
|
||||
canRead, _, err := namespace.CanRead(s, u)
|
||||
if err != nil {
|
||||
return namespace, err
|
||||
}
|
||||
if !canRead {
|
||||
return nil, echo.ErrForbidden
|
||||
}
|
||||
return
|
||||
}
|
@ -63,7 +63,7 @@ func RegisterUser(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Add its namespace
|
||||
err = models.CreateNewNamespaceForUser(s, newUser)
|
||||
err = models.CreateNewProjectForUser(s, newUser)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
|
@ -22,7 +22,7 @@
|
||||
// @description * `x-pagination-total-pages`: The total number of available pages for this request
|
||||
// @description * `x-pagination-result-count`: The number of items returned for this request.
|
||||
// @description # Rights
|
||||
// @description All endpoints which return a single item (project, task, namespace, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`.
|
||||
// @description All endpoints which return a single item (project, task, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`.
|
||||
// @description This can be used to show or hide ui elements based on the rights the user has.
|
||||
// @description # Authorization
|
||||
// @description **JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer <jwt-token>`-header to authenticate successfully.
|
||||
@ -488,38 +488,6 @@ func registerAPIRoutes(a *echo.Group) {
|
||||
a.DELETE("/filters/:filter", savedFiltersHandler.DeleteWeb)
|
||||
a.POST("/filters/:filter", savedFiltersHandler.UpdateWeb)
|
||||
|
||||
namespaceHandler := &handler.WebHandler{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
return &models.Namespace{}
|
||||
},
|
||||
}
|
||||
a.GET("/namespaces", namespaceHandler.ReadAllWeb)
|
||||
a.PUT("/namespaces", namespaceHandler.CreateWeb)
|
||||
a.GET("/namespaces/:namespace", namespaceHandler.ReadOneWeb)
|
||||
a.POST("/namespaces/:namespace", namespaceHandler.UpdateWeb)
|
||||
a.DELETE("/namespaces/:namespace", namespaceHandler.DeleteWeb)
|
||||
a.GET("/namespaces/:namespace/projects", apiv1.GetProjectsByNamespaceID)
|
||||
|
||||
namespaceTeamHandler := &handler.WebHandler{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
return &models.TeamNamespace{}
|
||||
},
|
||||
}
|
||||
a.GET("/namespaces/:namespace/teams", namespaceTeamHandler.ReadAllWeb)
|
||||
a.PUT("/namespaces/:namespace/teams", namespaceTeamHandler.CreateWeb)
|
||||
a.DELETE("/namespaces/:namespace/teams/:team", namespaceTeamHandler.DeleteWeb)
|
||||
a.POST("/namespaces/:namespace/teams/:team", namespaceTeamHandler.UpdateWeb)
|
||||
|
||||
namespaceUserHandler := &handler.WebHandler{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
return &models.NamespaceUser{}
|
||||
},
|
||||
}
|
||||
a.GET("/namespaces/:namespace/users", namespaceUserHandler.ReadAllWeb)
|
||||
a.PUT("/namespaces/:namespace/users", namespaceUserHandler.CreateWeb)
|
||||
a.DELETE("/namespaces/:namespace/users/:user", namespaceUserHandler.DeleteWeb)
|
||||
a.POST("/namespaces/:namespace/users/:user", namespaceUserHandler.UpdateWeb)
|
||||
|
||||
teamHandler := &handler.WebHandler{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
return &models.Team{}
|
||||
|
Loading…
x
Reference in New Issue
Block a user