New structure (#7)
This commit is contained in:
73
pkg/config/config.go
Normal file
73
pkg/config/config.go
Normal file
@ -0,0 +1,73 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"github.com/spf13/viper"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InitConfig initializes the config, sets defaults etc.
|
||||
func InitConfig() (err error) {
|
||||
|
||||
// Set defaults
|
||||
// Service config
|
||||
random, err := random(32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Service
|
||||
viper.SetDefault("service.JWTSecret", random)
|
||||
viper.SetDefault("service.interface", ":3456")
|
||||
viper.SetDefault("service.frontendurl", "")
|
||||
// Database
|
||||
viper.SetDefault("database.type", "sqlite")
|
||||
viper.SetDefault("database.host", "localhost")
|
||||
viper.SetDefault("database.user", "vikunja")
|
||||
viper.SetDefault("database.password", "")
|
||||
viper.SetDefault("database.database", "vikunja")
|
||||
viper.SetDefault("database.path", "./vikunja.db")
|
||||
viper.SetDefault("database.showqueries", false)
|
||||
viper.SetDefault("database.openconnections", 100)
|
||||
// Cacher
|
||||
viper.SetDefault("cache.enabled", false)
|
||||
viper.SetDefault("cache.type", "memory")
|
||||
viper.SetDefault("cache.maxelementsize", 1000)
|
||||
viper.SetDefault("cache.redishost", "localhost:6379")
|
||||
viper.SetDefault("cache.redispassword", "")
|
||||
// Mailer
|
||||
viper.SetDefault("mailer.host", "")
|
||||
viper.SetDefault("mailer.port", "587")
|
||||
viper.SetDefault("mailer.user", "user")
|
||||
viper.SetDefault("mailer.password", "")
|
||||
viper.SetDefault("mailer.skiptlsverify", false)
|
||||
viper.SetDefault("mailer.fromemail", "mail@vikunja")
|
||||
viper.SetDefault("mailer.queuelength", 100)
|
||||
viper.SetDefault("mailer.queuetimeout", 30)
|
||||
|
||||
// Init checking for environment variables
|
||||
viper.SetEnvPrefix("vikunja")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.AutomaticEnv()
|
||||
|
||||
// Load the config file
|
||||
viper.AddConfigPath(".")
|
||||
viper.SetConfigName("config")
|
||||
err = viper.ReadInConfig()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("Using defaults.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func random(length int) (string, error) {
|
||||
b := make([]byte, length)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%X", b), nil
|
||||
}
|
20
pkg/log/logging.go
Normal file
20
pkg/log/logging.go
Normal file
@ -0,0 +1,20 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"github.com/op/go-logging"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Log is the handler for the logger
|
||||
var Log = logging.MustGetLogger("vikunja")
|
||||
|
||||
var format = logging.MustStringFormatter(
|
||||
`%{color}%{time:2006-01-02 15:04:05.000} %{shortfunc} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
|
||||
)
|
||||
|
||||
// InitLogger initializes the global log handler
|
||||
func InitLogger() {
|
||||
backend := logging.NewLogBackend(os.Stderr, "", 0)
|
||||
backendFormatter := logging.NewBackendFormatter(backend, format)
|
||||
logging.SetBackend(backendFormatter)
|
||||
}
|
63
pkg/mail/mail.go
Normal file
63
pkg/mail/mail.go
Normal file
@ -0,0 +1,63 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"crypto/tls"
|
||||
"github.com/spf13/viper"
|
||||
"gopkg.in/gomail.v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Queue is the mail queue
|
||||
var Queue chan *gomail.Message
|
||||
|
||||
// StartMailDaemon starts the mail daemon
|
||||
func StartMailDaemon() {
|
||||
Queue = make(chan *gomail.Message, viper.GetInt("mailer.queuelength"))
|
||||
|
||||
if viper.GetString("mailer.host") == "" {
|
||||
log.Log.Warning("Mailer seems to be not configured! Please see the config docs for more details.")
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
d := gomail.NewDialer(viper.GetString("mailer.host"), viper.GetInt("mailer.port"), viper.GetString("mailer.username"), viper.GetString("mailer.password"))
|
||||
d.TLSConfig = &tls.Config{InsecureSkipVerify: viper.GetBool("mailer.skiptlsverify")}
|
||||
|
||||
var s gomail.SendCloser
|
||||
var err error
|
||||
open := false
|
||||
for {
|
||||
select {
|
||||
case m, ok := <-Queue:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !open {
|
||||
if s, err = d.Dial(); err != nil {
|
||||
log.Log.Error("Error during connect to smtp server: %s", err)
|
||||
}
|
||||
open = true
|
||||
}
|
||||
if err := gomail.Send(s, m); err != nil {
|
||||
log.Log.Error("Error when sending mail: %s", err)
|
||||
}
|
||||
// Close the connection to the SMTP server if no email was sent in
|
||||
// the last 30 seconds.
|
||||
case <-time.After(viper.GetDuration("mailer.queuetimeout") * time.Second):
|
||||
if open {
|
||||
if err := s.Close(); err != nil {
|
||||
log.Log.Error("Error closing the mail server connection: %s\n", err)
|
||||
}
|
||||
log.Log.Infof("Closed connection to mailserver")
|
||||
open = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// StopMailDaemon closes the mail queue channel, aka stops the daemon
|
||||
func StopMailDaemon() {
|
||||
close(Queue)
|
||||
}
|
101
pkg/mail/send_mail.go
Normal file
101
pkg/mail/send_mail.go
Normal file
@ -0,0 +1,101 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"github.com/labstack/gommon/log"
|
||||
"github.com/spf13/viper"
|
||||
"gopkg.in/gomail.v2"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// Opts holds infos for a mail
|
||||
type Opts struct {
|
||||
To string
|
||||
Subject string
|
||||
Message string
|
||||
HTMLMessage string
|
||||
ContentType ContentType
|
||||
Boundary string
|
||||
Headers []*header
|
||||
}
|
||||
|
||||
// ContentType represents mail content types
|
||||
type ContentType int
|
||||
|
||||
// Enumerate all the team rights
|
||||
const (
|
||||
ContentTypePlain ContentType = iota
|
||||
ContentTypeHTML
|
||||
ContentTypeMultipart
|
||||
)
|
||||
|
||||
type header struct {
|
||||
Field string
|
||||
Content string
|
||||
}
|
||||
|
||||
// SendMail puts a mail in the queue
|
||||
func SendMail(opts *Opts) {
|
||||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", viper.GetString("mailer.fromemail"))
|
||||
m.SetHeader("To", opts.To)
|
||||
m.SetHeader("Subject", opts.Subject)
|
||||
for _, h := range opts.Headers {
|
||||
m.SetHeader(h.Field, h.Content)
|
||||
}
|
||||
|
||||
switch opts.ContentType {
|
||||
case ContentTypePlain:
|
||||
m.SetBody("text/plain", opts.Message)
|
||||
case ContentTypeHTML:
|
||||
m.SetBody("text/html", opts.Message)
|
||||
case ContentTypeMultipart:
|
||||
m.SetBody("text/plain", opts.Message)
|
||||
m.AddAlternative("text/html", opts.HTMLMessage)
|
||||
}
|
||||
|
||||
Queue <- m
|
||||
}
|
||||
|
||||
// Template holds a pointer about a template
|
||||
type Template struct {
|
||||
Templates *template.Template
|
||||
}
|
||||
|
||||
// SendMailWithTemplate parses a template and sends it via mail
|
||||
func SendMailWithTemplate(to, subject, tpl string, data map[string]interface{}) {
|
||||
var htmlContent bytes.Buffer
|
||||
var plainContent bytes.Buffer
|
||||
|
||||
t := &Template{
|
||||
Templates: template.Must(template.ParseGlob("templates/mail/*.tmpl")),
|
||||
}
|
||||
|
||||
boundary := "np" + utils.MakeRandomString(13)
|
||||
|
||||
data["Boundary"] = boundary
|
||||
data["FrontendURL"] = viper.GetString("service.frontendurl")
|
||||
|
||||
if err := t.Templates.ExecuteTemplate(&htmlContent, tpl+".html.tmpl", data); err != nil {
|
||||
log.Error(3, "Template: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := t.Templates.ExecuteTemplate(&plainContent, tpl+".plain.tmpl", data); err != nil {
|
||||
log.Error(3, "Template: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
opts := &Opts{
|
||||
To: to,
|
||||
Subject: subject,
|
||||
Message: plainContent.String(),
|
||||
HTMLMessage: htmlContent.String(),
|
||||
ContentType: ContentTypeMultipart,
|
||||
Boundary: boundary,
|
||||
Headers: []*header{{Field: "MIME-Version", Content: "1.0"}},
|
||||
}
|
||||
|
||||
SendMail(opts)
|
||||
}
|
10
pkg/models/crudable.go
Normal file
10
pkg/models/crudable.go
Normal file
@ -0,0 +1,10 @@
|
||||
package models
|
||||
|
||||
// CRUDable defines the crud methods
|
||||
type CRUDable interface {
|
||||
Create(*User) error
|
||||
ReadOne() error
|
||||
ReadAll(*User) (interface{}, error)
|
||||
Update() error
|
||||
Delete() error
|
||||
}
|
734
pkg/models/error.go
Normal file
734
pkg/models/error.go
Normal file
@ -0,0 +1,734 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HTTPErrorProcessor is executed when the defined error is thrown, it will make sure the user sees an appropriate error message and http status code
|
||||
type HTTPErrorProcessor interface {
|
||||
HTTPError() HTTPError
|
||||
}
|
||||
|
||||
// HTTPError holds informations about an http error
|
||||
type HTTPError struct {
|
||||
HTTPCode int `json:"-"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// =====================
|
||||
// User Operation Errors
|
||||
// =====================
|
||||
|
||||
// ErrUsernameExists represents a "UsernameAlreadyExists" kind of error.
|
||||
type ErrUsernameExists struct {
|
||||
UserID int64
|
||||
Username string
|
||||
}
|
||||
|
||||
// IsErrUsernameExists checks if an error is a ErrUsernameExists.
|
||||
func IsErrUsernameExists(err error) bool {
|
||||
_, ok := err.(ErrUsernameExists)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUsernameExists) Error() string {
|
||||
return fmt.Sprintf("User with that username already exists [user id: %d, username: %s]", err.UserID, err.Username)
|
||||
}
|
||||
|
||||
// ErrorCodeUsernameExists holds the unique world-error code of this error
|
||||
const ErrorCodeUsernameExists = 1001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUsernameExists) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrorCodeUsernameExists, Message: "A user with this username already exists."}
|
||||
}
|
||||
|
||||
// ErrUserEmailExists represents a "UserEmailExists" kind of error.
|
||||
type ErrUserEmailExists struct {
|
||||
UserID int64
|
||||
Email string
|
||||
}
|
||||
|
||||
// IsErrUserEmailExists checks if an error is a ErrUserEmailExists.
|
||||
func IsErrUserEmailExists(err error) bool {
|
||||
_, ok := err.(ErrUserEmailExists)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserEmailExists) Error() string {
|
||||
return fmt.Sprintf("User with that email already exists [user id: %d, email: %s]", err.UserID, err.Email)
|
||||
}
|
||||
|
||||
// ErrorCodeUserEmailExists holds the unique world-error code of this error
|
||||
const ErrorCodeUserEmailExists = 1002
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserEmailExists) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrorCodeUserEmailExists, Message: "A user with this email address already exists."}
|
||||
}
|
||||
|
||||
// ErrNoUsernamePassword represents a "NoUsernamePassword" kind of error.
|
||||
type ErrNoUsernamePassword struct{}
|
||||
|
||||
// IsErrNoUsernamePassword checks if an error is a ErrNoUsernamePassword.
|
||||
func IsErrNoUsernamePassword(err error) bool {
|
||||
_, ok := err.(ErrNoUsernamePassword)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNoUsernamePassword) Error() string {
|
||||
return fmt.Sprintf("No username and password provided")
|
||||
}
|
||||
|
||||
// ErrCodeNoUsernamePassword holds the unique world-error code of this error
|
||||
const ErrCodeNoUsernamePassword = 1004
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrNoUsernamePassword) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeNoUsernamePassword, Message: "Please specify a username and a password."}
|
||||
}
|
||||
|
||||
// ErrUserDoesNotExist represents a "UserDoesNotExist" kind of error.
|
||||
type ErrUserDoesNotExist struct {
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrUserDoesNotExist checks if an error is a ErrUserDoesNotExist.
|
||||
func IsErrUserDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrUserDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("User does not exist [user id: %d]", err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeUserDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeUserDoesNotExist = 1005
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserDoesNotExist) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeUserDoesNotExist, Message: "The user does not exist."}
|
||||
}
|
||||
|
||||
// ErrCouldNotGetUserID represents a "ErrCouldNotGetUserID" kind of error.
|
||||
type ErrCouldNotGetUserID struct{}
|
||||
|
||||
// IsErrCouldNotGetUserID checks if an error is a ErrCouldNotGetUserID.
|
||||
func IsErrCouldNotGetUserID(err error) bool {
|
||||
_, ok := err.(ErrCouldNotGetUserID)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrCouldNotGetUserID) Error() string {
|
||||
return fmt.Sprintf("Could not get user ID")
|
||||
}
|
||||
|
||||
// ErrCodeCouldNotGetUserID holds the unique world-error code of this error
|
||||
const ErrCodeCouldNotGetUserID = 1006
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrCouldNotGetUserID) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeCouldNotGetUserID, Message: "Could not get user id."}
|
||||
}
|
||||
|
||||
// ErrNoPasswordResetToken represents an error where no password reset token exists for that user
|
||||
type ErrNoPasswordResetToken struct {
|
||||
UserID int64
|
||||
}
|
||||
|
||||
func (err ErrNoPasswordResetToken) Error() string {
|
||||
return fmt.Sprintf("No token to reset a password [UserID: %d]", err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeNoPasswordResetToken holds the unique world-error code of this error
|
||||
const ErrCodeNoPasswordResetToken = 1008
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrNoPasswordResetToken) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeNoPasswordResetToken, Message: "No token to reset a user's password provided."}
|
||||
}
|
||||
|
||||
// ErrInvalidPasswordResetToken is an error where the password reset token is invalid
|
||||
type ErrInvalidPasswordResetToken struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
func (err ErrInvalidPasswordResetToken) Error() string {
|
||||
return fmt.Sprintf("Invalid token to reset a password [Token: %s]", err.Token)
|
||||
}
|
||||
|
||||
// ErrCodeInvalidPasswordResetToken holds the unique world-error code of this error
|
||||
const ErrCodeInvalidPasswordResetToken = 1009
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrInvalidPasswordResetToken) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeInvalidPasswordResetToken, Message: "Invalid token to reset a user's password."}
|
||||
}
|
||||
|
||||
// IsErrInvalidPasswordResetToken checks if an error is a ErrInvalidPasswordResetToken.
|
||||
func IsErrInvalidPasswordResetToken(err error) bool {
|
||||
_, ok := err.(ErrInvalidPasswordResetToken)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ErrInvalidEmailConfirmToken is an error where the email confirm token is invalid
|
||||
type ErrInvalidEmailConfirmToken struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
func (err ErrInvalidEmailConfirmToken) Error() string {
|
||||
return fmt.Sprintf("Invalid email confirm token [Token: %s]", err.Token)
|
||||
}
|
||||
|
||||
// ErrCodeInvalidEmailConfirmToken holds the unique world-error code of this error
|
||||
const ErrCodeInvalidEmailConfirmToken = 1010
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrInvalidEmailConfirmToken) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeInvalidEmailConfirmToken, Message: "Invalid email confirm token."}
|
||||
}
|
||||
|
||||
// IsErrInvalidEmailConfirmToken checks if an error is a ErrInvalidEmailConfirmToken.
|
||||
func IsErrInvalidEmailConfirmToken(err error) bool {
|
||||
_, ok := err.(ErrInvalidEmailConfirmToken)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ===================
|
||||
// Empty things errors
|
||||
// ===================
|
||||
|
||||
// ErrIDCannotBeZero represents a "IDCannotBeZero" kind of error. Used if an ID (of something, not defined) is 0 where it should not.
|
||||
type ErrIDCannotBeZero struct{}
|
||||
|
||||
// IsErrIDCannotBeZero checks if an error is a ErrIDCannotBeZero.
|
||||
func IsErrIDCannotBeZero(err error) bool {
|
||||
_, ok := err.(ErrIDCannotBeZero)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrIDCannotBeZero) Error() string {
|
||||
return fmt.Sprintf("ID cannot be empty or 0")
|
||||
}
|
||||
|
||||
// ErrCodeIDCannotBeZero holds the unique world-error code of this error
|
||||
const ErrCodeIDCannotBeZero = 2001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrIDCannotBeZero) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeIDCannotBeZero, Message: "The ID cannot be empty or 0."}
|
||||
}
|
||||
|
||||
// ===========
|
||||
// List errors
|
||||
// ===========
|
||||
|
||||
// ErrListDoesNotExist represents a "ErrListDoesNotExist" kind of error. Used if the list does not exist.
|
||||
type ErrListDoesNotExist struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
// IsErrListDoesNotExist checks if an error is a ErrListDoesNotExist.
|
||||
func IsErrListDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrListDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrListDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("List does not exist [ID: %d]", err.ID)
|
||||
}
|
||||
|
||||
// ErrCodeListDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeListDoesNotExist = 3001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrListDoesNotExist) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeListDoesNotExist, Message: "This list does not exist."}
|
||||
}
|
||||
|
||||
// ErrNeedToHaveListReadAccess represents an error, where the user dont has read access to that List
|
||||
type ErrNeedToHaveListReadAccess struct {
|
||||
ListID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrNeedToHaveListReadAccess checks if an error is a ErrListDoesNotExist.
|
||||
func IsErrNeedToHaveListReadAccess(err error) bool {
|
||||
_, ok := err.(ErrNeedToHaveListReadAccess)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNeedToHaveListReadAccess) Error() string {
|
||||
return fmt.Sprintf("User needs to have read access to that list [ListID: %d, UserID: %d]", err.ListID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeNeedToHaveListReadAccess holds the unique world-error code of this error
|
||||
const ErrCodeNeedToHaveListReadAccess = 3004
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrNeedToHaveListReadAccess) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeNeedToHaveListReadAccess, Message: "You need to have read access to this list."}
|
||||
}
|
||||
|
||||
// ErrListTitleCannotBeEmpty represents a "ErrListTitleCannotBeEmpty" kind of error. Used if the list does not exist.
|
||||
type ErrListTitleCannotBeEmpty struct{}
|
||||
|
||||
// IsErrListTitleCannotBeEmpty checks if an error is a ErrListTitleCannotBeEmpty.
|
||||
func IsErrListTitleCannotBeEmpty(err error) bool {
|
||||
_, ok := err.(ErrListTitleCannotBeEmpty)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrListTitleCannotBeEmpty) Error() string {
|
||||
return fmt.Sprintf("List title cannot be empty.")
|
||||
}
|
||||
|
||||
// ErrCodeListTitleCannotBeEmpty holds the unique world-error code of this error
|
||||
const ErrCodeListTitleCannotBeEmpty = 3005
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrListTitleCannotBeEmpty) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeListTitleCannotBeEmpty, Message: "You must provide at least a list title."}
|
||||
}
|
||||
|
||||
// ================
|
||||
// List task errors
|
||||
// ================
|
||||
|
||||
// ErrListTaskCannotBeEmpty represents a "ErrListDoesNotExist" kind of error. Used if the list does not exist.
|
||||
type ErrListTaskCannotBeEmpty struct{}
|
||||
|
||||
// IsErrListTaskCannotBeEmpty checks if an error is a ErrListDoesNotExist.
|
||||
func IsErrListTaskCannotBeEmpty(err error) bool {
|
||||
_, ok := err.(ErrListTaskCannotBeEmpty)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrListTaskCannotBeEmpty) Error() string {
|
||||
return fmt.Sprintf("List task text cannot be empty.")
|
||||
}
|
||||
|
||||
// ErrCodeListTaskCannotBeEmpty holds the unique world-error code of this error
|
||||
const ErrCodeListTaskCannotBeEmpty = 4001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrListTaskCannotBeEmpty) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeListTaskCannotBeEmpty, Message: "You must provide at least a list task text."}
|
||||
}
|
||||
|
||||
// ErrListTaskDoesNotExist represents a "ErrListDoesNotExist" kind of error. Used if the list does not exist.
|
||||
type ErrListTaskDoesNotExist struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
// IsErrListTaskDoesNotExist checks if an error is a ErrListDoesNotExist.
|
||||
func IsErrListTaskDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrListTaskDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrListTaskDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("List task does not exist. [ID: %d]", err.ID)
|
||||
}
|
||||
|
||||
// ErrCodeListTaskDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeListTaskDoesNotExist = 4002
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrListTaskDoesNotExist) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeListTaskDoesNotExist, Message: "This list task does not exist"}
|
||||
}
|
||||
|
||||
// =================
|
||||
// Namespace errors
|
||||
// =================
|
||||
|
||||
// ErrNamespaceDoesNotExist represents a "ErrNamespaceDoesNotExist" kind of error. Used if the namespace does not exist.
|
||||
type ErrNamespaceDoesNotExist struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
// IsErrNamespaceDoesNotExist checks if an error is a ErrNamespaceDoesNotExist.
|
||||
func IsErrNamespaceDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrNamespaceDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNamespaceDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("Namespace does not exist [ID: %d]", err.ID)
|
||||
}
|
||||
|
||||
// ErrCodeNamespaceDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeNamespaceDoesNotExist = 5001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrNamespaceDoesNotExist) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeNamespaceDoesNotExist, Message: "Namespace not found."}
|
||||
}
|
||||
|
||||
// ErrUserDoesNotHaveAccessToNamespace represents an error, where the user is not the owner of that namespace (used i.e. when deleting a namespace)
|
||||
type ErrUserDoesNotHaveAccessToNamespace struct {
|
||||
NamespaceID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrUserDoesNotHaveAccessToNamespace checks if an error is a ErrNamespaceDoesNotExist.
|
||||
func IsErrUserDoesNotHaveAccessToNamespace(err error) bool {
|
||||
_, ok := err.(ErrUserDoesNotHaveAccessToNamespace)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserDoesNotHaveAccessToNamespace) Error() string {
|
||||
return fmt.Sprintf("User does not have access to the namespace [NamespaceID: %d, UserID: %d]", err.NamespaceID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeUserDoesNotHaveAccessToNamespace holds the unique world-error code of this error
|
||||
const ErrCodeUserDoesNotHaveAccessToNamespace = 5003
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserDoesNotHaveAccessToNamespace) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeUserDoesNotHaveAccessToNamespace, Message: "This user does not have access to the namespace."}
|
||||
}
|
||||
|
||||
// ErrNamespaceNameCannotBeEmpty represents an error, where a namespace name is empty.
|
||||
type ErrNamespaceNameCannotBeEmpty struct {
|
||||
NamespaceID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrNamespaceNameCannotBeEmpty checks if an error is a ErrNamespaceDoesNotExist.
|
||||
func IsErrNamespaceNameCannotBeEmpty(err error) bool {
|
||||
_, ok := err.(ErrNamespaceNameCannotBeEmpty)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNamespaceNameCannotBeEmpty) Error() string {
|
||||
return fmt.Sprintf("Namespace name cannot be empty [NamespaceID: %d, UserID: %d]", err.NamespaceID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeNamespaceNameCannotBeEmpty holds the unique world-error code of this error
|
||||
const ErrCodeNamespaceNameCannotBeEmpty = 5006
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrNamespaceNameCannotBeEmpty) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeNamespaceNameCannotBeEmpty, Message: "The namespace name cannot be empty."}
|
||||
}
|
||||
|
||||
// ErrNeedToHaveNamespaceReadAccess represents an error, where the user is not the owner of that namespace (used i.e. when deleting a namespace)
|
||||
type ErrNeedToHaveNamespaceReadAccess struct {
|
||||
NamespaceID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrNeedToHaveNamespaceReadAccess checks if an error is a ErrNamespaceDoesNotExist.
|
||||
func IsErrNeedToHaveNamespaceReadAccess(err error) bool {
|
||||
_, ok := err.(ErrNeedToHaveNamespaceReadAccess)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNeedToHaveNamespaceReadAccess) Error() string {
|
||||
return fmt.Sprintf("User does not have access to that namespace [NamespaceID: %d, UserID: %d]", err.NamespaceID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeNeedToHaveNamespaceReadAccess holds the unique world-error code of this error
|
||||
const ErrCodeNeedToHaveNamespaceReadAccess = 5009
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrNeedToHaveNamespaceReadAccess) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeNeedToHaveNamespaceReadAccess, Message: "You need to have namespace read access to do this."}
|
||||
}
|
||||
|
||||
// ErrTeamDoesNotHaveAccessToNamespace represents an error, where the Team is not the owner of that namespace (used i.e. when deleting a namespace)
|
||||
type ErrTeamDoesNotHaveAccessToNamespace struct {
|
||||
NamespaceID int64
|
||||
TeamID int64
|
||||
}
|
||||
|
||||
// IsErrTeamDoesNotHaveAccessToNamespace checks if an error is a ErrNamespaceDoesNotExist.
|
||||
func IsErrTeamDoesNotHaveAccessToNamespace(err error) bool {
|
||||
_, ok := err.(ErrTeamDoesNotHaveAccessToNamespace)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTeamDoesNotHaveAccessToNamespace) Error() string {
|
||||
return fmt.Sprintf("Team does not have access to that namespace [NamespaceID: %d, TeamID: %d]", err.NamespaceID, err.TeamID)
|
||||
}
|
||||
|
||||
// ErrCodeTeamDoesNotHaveAccessToNamespace holds the unique world-error code of this error
|
||||
const ErrCodeTeamDoesNotHaveAccessToNamespace = 5010
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrTeamDoesNotHaveAccessToNamespace) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeTeamDoesNotHaveAccessToNamespace, Message: "You need to have access to this namespace to do this."}
|
||||
}
|
||||
|
||||
// ErrUserAlreadyHasNamespaceAccess represents an error where a user already has access to a namespace
|
||||
type ErrUserAlreadyHasNamespaceAccess struct {
|
||||
UserID int64
|
||||
NamespaceID int64
|
||||
}
|
||||
|
||||
// IsErrUserAlreadyHasNamespaceAccess checks if an error is ErrUserAlreadyHasNamespaceAccess.
|
||||
func IsErrUserAlreadyHasNamespaceAccess(err error) bool {
|
||||
_, ok := err.(ErrUserAlreadyHasNamespaceAccess)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserAlreadyHasNamespaceAccess) Error() string {
|
||||
return fmt.Sprintf("User already has access to that namespace. [User ID: %d, Namespace ID: %d]", err.UserID, err.NamespaceID)
|
||||
}
|
||||
|
||||
// ErrCodeUserAlreadyHasNamespaceAccess holds the unique world-error code of this error
|
||||
const ErrCodeUserAlreadyHasNamespaceAccess = 5011
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserAlreadyHasNamespaceAccess) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusConflict, Code: ErrCodeUserAlreadyHasNamespaceAccess, Message: "This user already has access to this namespace."}
|
||||
}
|
||||
|
||||
// ============
|
||||
// Team errors
|
||||
// ============
|
||||
|
||||
// ErrTeamNameCannotBeEmpty represents an error where a team name is empty.
|
||||
type ErrTeamNameCannotBeEmpty struct {
|
||||
TeamID int64
|
||||
}
|
||||
|
||||
// IsErrTeamNameCannotBeEmpty checks if an error is a ErrNamespaceDoesNotExist.
|
||||
func IsErrTeamNameCannotBeEmpty(err error) bool {
|
||||
_, ok := err.(ErrTeamNameCannotBeEmpty)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTeamNameCannotBeEmpty) Error() string {
|
||||
return fmt.Sprintf("Team name cannot be empty [Team ID: %d]", err.TeamID)
|
||||
}
|
||||
|
||||
// ErrCodeTeamNameCannotBeEmpty holds the unique world-error code of this error
|
||||
const ErrCodeTeamNameCannotBeEmpty = 6001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrTeamNameCannotBeEmpty) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeTeamNameCannotBeEmpty, Message: "The team name cannot be empty"}
|
||||
}
|
||||
|
||||
// ErrTeamDoesNotExist represents an error where a team does not exist
|
||||
type ErrTeamDoesNotExist struct {
|
||||
TeamID int64
|
||||
}
|
||||
|
||||
// IsErrTeamDoesNotExist checks if an error is ErrTeamDoesNotExist.
|
||||
func IsErrTeamDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrTeamDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTeamDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("Team does not exist [Team ID: %d]", err.TeamID)
|
||||
}
|
||||
|
||||
// ErrCodeTeamDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeTeamDoesNotExist = 6002
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrTeamDoesNotExist) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "This team does not exist."}
|
||||
}
|
||||
|
||||
// ErrInvalidTeamRight represents an error where a team right is invalid
|
||||
type ErrInvalidTeamRight struct {
|
||||
Right TeamRight
|
||||
}
|
||||
|
||||
// IsErrInvalidTeamRight checks if an error is ErrInvalidTeamRight.
|
||||
func IsErrInvalidTeamRight(err error) bool {
|
||||
_, ok := err.(ErrInvalidTeamRight)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrInvalidTeamRight) Error() string {
|
||||
return fmt.Sprintf("Team right invalid [Right: %d]", err.Right)
|
||||
}
|
||||
|
||||
// ErrCodeInvalidTeamRight holds the unique world-error code of this error
|
||||
const ErrCodeInvalidTeamRight = 6003
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrInvalidTeamRight) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeInvalidTeamRight, Message: "The team right is invalid."}
|
||||
}
|
||||
|
||||
// ErrTeamAlreadyHasAccess represents an error where a team already has access to a list/namespace
|
||||
type ErrTeamAlreadyHasAccess struct {
|
||||
TeamID int64
|
||||
ID int64
|
||||
}
|
||||
|
||||
// IsErrTeamAlreadyHasAccess checks if an error is ErrTeamAlreadyHasAccess.
|
||||
func IsErrTeamAlreadyHasAccess(err error) bool {
|
||||
_, ok := err.(ErrTeamAlreadyHasAccess)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTeamAlreadyHasAccess) Error() string {
|
||||
return fmt.Sprintf("Team already has access. [Team ID: %d, ID: %d]", err.TeamID, err.ID)
|
||||
}
|
||||
|
||||
// ErrCodeTeamAlreadyHasAccess holds the unique world-error code of this error
|
||||
const ErrCodeTeamAlreadyHasAccess = 6004
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrTeamAlreadyHasAccess) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusConflict, Code: ErrCodeTeamAlreadyHasAccess, Message: "This team already has access."}
|
||||
}
|
||||
|
||||
// ErrUserIsMemberOfTeam represents an error where a user is already member of a team.
|
||||
type ErrUserIsMemberOfTeam struct {
|
||||
TeamID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrUserIsMemberOfTeam checks if an error is ErrUserIsMemberOfTeam.
|
||||
func IsErrUserIsMemberOfTeam(err error) bool {
|
||||
_, ok := err.(ErrUserIsMemberOfTeam)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserIsMemberOfTeam) Error() string {
|
||||
return fmt.Sprintf("User is already a member of that team. [Team ID: %d, User ID: %d]", err.TeamID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeUserIsMemberOfTeam holds the unique world-error code of this error
|
||||
const ErrCodeUserIsMemberOfTeam = 6005
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserIsMemberOfTeam) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusConflict, Code: ErrCodeUserIsMemberOfTeam, Message: "This user is already a member of that team."}
|
||||
}
|
||||
|
||||
// ErrCannotDeleteLastTeamMember represents an error where a user wants to delete the last member of a team (probably himself)
|
||||
type ErrCannotDeleteLastTeamMember struct {
|
||||
TeamID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrCannotDeleteLastTeamMember checks if an error is ErrCannotDeleteLastTeamMember.
|
||||
func IsErrCannotDeleteLastTeamMember(err error) bool {
|
||||
_, ok := err.(ErrCannotDeleteLastTeamMember)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrCannotDeleteLastTeamMember) Error() string {
|
||||
return fmt.Sprintf("Cannot delete last team member. [Team ID: %d, User ID: %d]", err.TeamID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeCannotDeleteLastTeamMember holds the unique world-error code of this error
|
||||
const ErrCodeCannotDeleteLastTeamMember = 6006
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrCannotDeleteLastTeamMember) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeCannotDeleteLastTeamMember, Message: "You cannot delete the last member of a team."}
|
||||
}
|
||||
|
||||
// ErrTeamDoesNotHaveAccessToList represents an error, where the Team is not the owner of that List (used i.e. when deleting a List)
|
||||
type ErrTeamDoesNotHaveAccessToList struct {
|
||||
ListID int64
|
||||
TeamID int64
|
||||
}
|
||||
|
||||
// IsErrTeamDoesNotHaveAccessToList checks if an error is a ErrListDoesNotExist.
|
||||
func IsErrTeamDoesNotHaveAccessToList(err error) bool {
|
||||
_, ok := err.(ErrTeamDoesNotHaveAccessToList)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTeamDoesNotHaveAccessToList) Error() string {
|
||||
return fmt.Sprintf("Team does not have access to the list [ListID: %d, TeamID: %d]", err.ListID, err.TeamID)
|
||||
}
|
||||
|
||||
// ErrCodeTeamDoesNotHaveAccessToList holds the unique world-error code of this error
|
||||
const ErrCodeTeamDoesNotHaveAccessToList = 6007
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrTeamDoesNotHaveAccessToList) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeTeamDoesNotHaveAccessToList, Message: "This team does not have access to the list."}
|
||||
}
|
||||
|
||||
// ====================
|
||||
// User <-> List errors
|
||||
// ====================
|
||||
|
||||
// ErrInvalidUserRight represents an error where a user right is invalid
|
||||
type ErrInvalidUserRight struct {
|
||||
Right UserRight
|
||||
}
|
||||
|
||||
// IsErrInvalidUserRight checks if an error is ErrInvalidUserRight.
|
||||
func IsErrInvalidUserRight(err error) bool {
|
||||
_, ok := err.(ErrInvalidUserRight)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrInvalidUserRight) Error() string {
|
||||
return fmt.Sprintf("User right is invalid [Right: %d]", err.Right)
|
||||
}
|
||||
|
||||
// ErrCodeInvalidUserRight holds the unique world-error code of this error
|
||||
const ErrCodeInvalidUserRight = 7001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrInvalidUserRight) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeInvalidUserRight, Message: "The user right is invalid."}
|
||||
}
|
||||
|
||||
// ErrUserAlreadyHasAccess represents an error where a user already has access to a list/namespace
|
||||
type ErrUserAlreadyHasAccess struct {
|
||||
UserID int64
|
||||
ListID int64
|
||||
}
|
||||
|
||||
// IsErrUserAlreadyHasAccess checks if an error is ErrUserAlreadyHasAccess.
|
||||
func IsErrUserAlreadyHasAccess(err error) bool {
|
||||
_, ok := err.(ErrUserAlreadyHasAccess)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserAlreadyHasAccess) Error() string {
|
||||
return fmt.Sprintf("User already has access to that list. [User ID: %d, List ID: %d]", err.UserID, err.ListID)
|
||||
}
|
||||
|
||||
// ErrCodeUserAlreadyHasAccess holds the unique world-error code of this error
|
||||
const ErrCodeUserAlreadyHasAccess = 7002
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserAlreadyHasAccess) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusConflict, Code: ErrCodeUserAlreadyHasAccess, Message: "This user already has access to this list."}
|
||||
}
|
||||
|
||||
// ErrUserDoesNotHaveAccessToList represents an error, where the user is not the owner of that List (used i.e. when deleting a List)
|
||||
type ErrUserDoesNotHaveAccessToList struct {
|
||||
ListID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrUserDoesNotHaveAccessToList checks if an error is a ErrListDoesNotExist.
|
||||
func IsErrUserDoesNotHaveAccessToList(err error) bool {
|
||||
_, ok := err.(ErrUserDoesNotHaveAccessToList)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserDoesNotHaveAccessToList) Error() string {
|
||||
return fmt.Sprintf("User does not have access to the list [ListID: %d, UserID: %d]", err.ListID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeUserDoesNotHaveAccessToList holds the unique world-error code of this error
|
||||
const ErrCodeUserDoesNotHaveAccessToList = 7003
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserDoesNotHaveAccessToList) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeUserDoesNotHaveAccessToList, Message: "This user does not have access to the list."}
|
||||
}
|
18
pkg/models/fixtures/list.yml
Normal file
18
pkg/models/fixtures/list.yml
Normal file
@ -0,0 +1,18 @@
|
||||
-
|
||||
id: 1
|
||||
title: Test1
|
||||
description: Lorem Ipsum
|
||||
owner_id: 1
|
||||
namespace_id: 1
|
||||
-
|
||||
id: 2
|
||||
title: Test2
|
||||
description: Lorem Ipsum
|
||||
owner_id: 3
|
||||
namespace_id: 1
|
||||
-
|
||||
id: 3
|
||||
title: Test3
|
||||
description: Lorem Ipsum
|
||||
owner_id: 3
|
||||
namespace_id: 2
|
10
pkg/models/fixtures/namespaces.yml
Normal file
10
pkg/models/fixtures/namespaces.yml
Normal file
@ -0,0 +1,10 @@
|
||||
-
|
||||
id: 1
|
||||
name: testnamespace
|
||||
description: Lorem Ipsum
|
||||
owner_id: 1
|
||||
-
|
||||
id: 2
|
||||
name: testnamespace2
|
||||
description: Lorem Ipsum
|
||||
owner_id: 2
|
7
pkg/models/fixtures/team_members.yml
Normal file
7
pkg/models/fixtures/team_members.yml
Normal file
@ -0,0 +1,7 @@
|
||||
-
|
||||
team_id: 1
|
||||
user_id: 1
|
||||
admin: true
|
||||
-
|
||||
team_id: 1
|
||||
user_id: 2
|
5
pkg/models/fixtures/teams.yml
Normal file
5
pkg/models/fixtures/teams.yml
Normal file
@ -0,0 +1,5 @@
|
||||
-
|
||||
id: 1
|
||||
name: testteam1
|
||||
description: Lorem Ipsum
|
||||
created_by_id: 1
|
15
pkg/models/fixtures/users.yml
Normal file
15
pkg/models/fixtures/users.yml
Normal file
@ -0,0 +1,15 @@
|
||||
-
|
||||
id: 1
|
||||
username: 'user1'
|
||||
password: '1234'
|
||||
email: 'johndoe@example.com'
|
||||
-
|
||||
id: 2
|
||||
username: 'user2'
|
||||
password: '1234'
|
||||
email: 'johndoe@example.com'
|
||||
-
|
||||
id: 3
|
||||
username: 'user3'
|
||||
password: '1234'
|
||||
email: 'johndoe@example.com'
|
143
pkg/models/list.go
Normal file
143
pkg/models/list.go
Normal file
@ -0,0 +1,143 @@
|
||||
package models
|
||||
|
||||
// List represents a list of tasks
|
||||
type List struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"list"`
|
||||
Title string `xorm:"varchar(250)" json:"title"`
|
||||
Description string `xorm:"varchar(1000)" json:"description"`
|
||||
OwnerID int64 `xorm:"int(11) INDEX" json:"-"`
|
||||
NamespaceID int64 `xorm:"int(11) INDEX" json:"-" param:"namespace"`
|
||||
|
||||
Owner User `xorm:"-" json:"owner"`
|
||||
Tasks []*ListTask `xorm:"-" json:"tasks"`
|
||||
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// GetListsByNamespaceID gets all lists in a namespace
|
||||
func GetListsByNamespaceID(nID int64) (lists []*List, err error) {
|
||||
err = x.Where("namespace_id = ?", nID).Find(&lists)
|
||||
return lists, err
|
||||
}
|
||||
|
||||
// ReadAll gets all lists a user has access to
|
||||
func (l *List) ReadAll(u *User) (interface{}, error) {
|
||||
lists := []*List{}
|
||||
fullUser, err := GetUserByID(u.ID)
|
||||
if err != nil {
|
||||
return lists, err
|
||||
}
|
||||
|
||||
// Gets all Lists where the user is either owner or in a team which has access to the list
|
||||
// Or in a team which has namespace read access
|
||||
err = x.Select("l.*").
|
||||
Table("list").
|
||||
Alias("l").
|
||||
Join("INNER", []string{"namespaces", "n"}, "l.namespace_id = n.id").
|
||||
Join("LEFT", []string{"team_namespaces", "tn"}, "tn.namespace_id = n.id").
|
||||
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tn.team_id").
|
||||
Join("LEFT", []string{"team_list", "tl"}, "l.id = tl.list_id").
|
||||
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
|
||||
Join("LEFT", []string{"users_list", "ul"}, "ul.list_id = l.id").
|
||||
Join("LEFT", []string{"users_namespace", "un"}, "un.namespace_id = l.namespace_id").
|
||||
Where("tm.user_id = ?", fullUser.ID).
|
||||
Or("tm2.user_id = ?", fullUser.ID).
|
||||
Or("l.owner_id = ?", fullUser.ID).
|
||||
Or("ul.user_id = ?", fullUser.ID).
|
||||
Or("un.user_id = ?", fullUser.ID).
|
||||
GroupBy("l.id").
|
||||
Find(&lists)
|
||||
|
||||
// Add more list details
|
||||
AddListDetails(lists)
|
||||
|
||||
return lists, err
|
||||
}
|
||||
|
||||
// ReadOne gets one list by its ID
|
||||
func (l *List) ReadOne() (err error) {
|
||||
err = l.GetSimpleByID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get list tasks
|
||||
l.Tasks, err = GetTasksByListID(l.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get list owner
|
||||
l.Owner, err = GetUserByID(l.OwnerID)
|
||||
return
|
||||
}
|
||||
|
||||
// GetSimpleByID gets a list with only the basic items, aka no tasks or user objects. Returns an error if the list does not exist.
|
||||
func (l *List) GetSimpleByID() (err error) {
|
||||
if l.ID < 1 {
|
||||
return ErrListDoesNotExist{ID: l.ID}
|
||||
}
|
||||
|
||||
// We need to re-init our list object, because otherwise xorm creates a "where for every item in that list object,
|
||||
// leading to not finding anything if the id is good, but for example the title is different.
|
||||
id := l.ID
|
||||
*l = List{}
|
||||
exists, err := x.Where("id = ?", id).Get(l)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return ErrListDoesNotExist{ID: l.ID}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// AddListDetails adds owner user objects and list tasks to all lists in the slice
|
||||
func AddListDetails(lists []*List) (err error) {
|
||||
var listIDs []int64
|
||||
var ownerIDs []int64
|
||||
for _, l := range lists {
|
||||
listIDs = append(listIDs, l.ID)
|
||||
ownerIDs = append(ownerIDs, l.OwnerID)
|
||||
}
|
||||
|
||||
// Get all tasks
|
||||
ts := []*ListTask{}
|
||||
err = x.In("list_id", listIDs).Find(&ts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get all list owners
|
||||
owners := []*User{}
|
||||
err = x.In("id", ownerIDs).Find(&owners)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Build it all into the lists slice
|
||||
for in, list := range lists {
|
||||
// Owner
|
||||
for _, owner := range owners {
|
||||
if list.OwnerID == owner.ID {
|
||||
lists[in].Owner = *owner
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Tasks
|
||||
for _, task := range ts {
|
||||
if task.ListID == list.ID {
|
||||
lists[in].Tasks = append(lists[in].Tasks, task)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
85
pkg/models/list_create_test.go
Normal file
85
pkg/models/list_create_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestList_Create(t *testing.T) {
|
||||
// Create test database
|
||||
//assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
// Get our doer
|
||||
doer, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Dummy list for testing
|
||||
dummylist := List{
|
||||
Title: "test",
|
||||
Description: "Lorem Ipsum",
|
||||
NamespaceID: 1,
|
||||
}
|
||||
|
||||
// Check if the user can create
|
||||
assert.True(t, dummylist.CanCreate(&doer))
|
||||
|
||||
// Create it
|
||||
err = dummylist.Create(&doer)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get the list
|
||||
newdummy := List{ID: dummylist.ID}
|
||||
err = newdummy.ReadOne()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dummylist.Title, newdummy.Title)
|
||||
assert.Equal(t, dummylist.Description, newdummy.Description)
|
||||
assert.Equal(t, dummylist.OwnerID, doer.ID)
|
||||
|
||||
// Check if the user can see it
|
||||
assert.True(t, dummylist.CanRead(&doer))
|
||||
|
||||
// Try updating a list
|
||||
assert.True(t, dummylist.CanUpdate(&doer))
|
||||
dummylist.Description = "Lorem Ipsum dolor sit amet."
|
||||
err = dummylist.Update()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Delete it
|
||||
assert.True(t, dummylist.CanDelete(&doer))
|
||||
|
||||
err = dummylist.Delete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try updating a nonexistant list
|
||||
err = dummylist.Update()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListDoesNotExist(err))
|
||||
|
||||
// Delete a nonexistant list
|
||||
err = dummylist.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListDoesNotExist(err))
|
||||
|
||||
// Check failing with no title
|
||||
list2 := List{}
|
||||
err = list2.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListTitleCannotBeEmpty(err))
|
||||
|
||||
// Check creation with a nonexistant namespace
|
||||
list3 := List{
|
||||
Title: "test",
|
||||
Description: "Lorem Ipsum",
|
||||
NamespaceID: 876694,
|
||||
}
|
||||
|
||||
err = list3.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
|
||||
// Try creating with a nonexistant owner
|
||||
nUser := &User{ID: 9482385}
|
||||
err = dummylist.Create(nUser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
}
|
57
pkg/models/list_create_update.go
Normal file
57
pkg/models/list_create_update.go
Normal file
@ -0,0 +1,57 @@
|
||||
package models
|
||||
|
||||
// CreateOrUpdateList updates a list or creates it if it doesn't exist
|
||||
func CreateOrUpdateList(list *List) (err error) {
|
||||
|
||||
// Check we have at least a title
|
||||
if list.Title == "" {
|
||||
return ErrListTitleCannotBeEmpty{}
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
if list.NamespaceID != 0 {
|
||||
_, err = GetNamespaceByID(list.NamespaceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if list.ID == 0 {
|
||||
_, err = x.Insert(list)
|
||||
} else {
|
||||
_, err = x.ID(list.ID).Update(list)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = list.ReadOne()
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// Update implements the update method of CRUDable
|
||||
func (l *List) Update() (err error) {
|
||||
// Check if it exists
|
||||
if err = l.GetSimpleByID(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return CreateOrUpdateList(l)
|
||||
}
|
||||
|
||||
// Create implements the create method of CRUDable
|
||||
func (l *List) Create(doer *User) (err error) {
|
||||
// Check rights
|
||||
u, err := GetUserByID(doer.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
l.OwnerID = u.ID
|
||||
l.Owner.ID = u.ID
|
||||
l.ID = 0 // Otherwise only the first time a new list would be created
|
||||
|
||||
return CreateOrUpdateList(l)
|
||||
}
|
19
pkg/models/list_delete.go
Normal file
19
pkg/models/list_delete.go
Normal file
@ -0,0 +1,19 @@
|
||||
package models
|
||||
|
||||
// Delete implements the delete method of CRUDable
|
||||
func (l *List) Delete() (err error) {
|
||||
// Check if the list exists
|
||||
if err = l.GetSimpleByID(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the list
|
||||
_, err = x.ID(l.ID).Delete(&List{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete all todotasks on that list
|
||||
_, err = x.Where("list_id = ?", l.ID).Delete(&ListTask{})
|
||||
return
|
||||
}
|
33
pkg/models/list_read_test.go
Normal file
33
pkg/models/list_read_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestList_ReadAll(t *testing.T) {
|
||||
// Create test database
|
||||
//assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
// Get all lists for our namespace
|
||||
lists, err := GetListsByNamespaceID(1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(lists), 2)
|
||||
|
||||
// Get all lists our user has access to
|
||||
u, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
lists2 := List{}
|
||||
lists3, err := lists2.ReadAll(&u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(lists3).Kind(), reflect.Slice)
|
||||
s := reflect.ValueOf(lists3)
|
||||
assert.Equal(t, s.Len(), 1)
|
||||
|
||||
// Try getting lists for a nonexistant user
|
||||
_, err = lists2.ReadAll(&User{ID: 984234})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
}
|
115
pkg/models/list_rights.go
Normal file
115
pkg/models/list_rights.go
Normal file
@ -0,0 +1,115 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// IsAdmin returns whether the user has admin rights on the list or not
|
||||
func (l *List) IsAdmin(u *User) bool {
|
||||
// Owners are always admins
|
||||
if l.Owner.ID == u.ID {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check individual rights
|
||||
if l.checkListUserRight(u, UserRightAdmin) {
|
||||
return true
|
||||
}
|
||||
|
||||
return l.checkListTeamRight(u, TeamRightAdmin)
|
||||
}
|
||||
|
||||
// CanWrite return whether the user can write on that list or not
|
||||
func (l *List) CanWrite(user *User) bool {
|
||||
// Admins always have write access
|
||||
if l.IsAdmin(user) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check individual rights
|
||||
if l.checkListUserRight(user, UserRightWrite) {
|
||||
return true
|
||||
}
|
||||
|
||||
return l.checkListTeamRight(user, TeamRightWrite)
|
||||
}
|
||||
|
||||
// CanRead checks if a user has read access to a list
|
||||
func (l *List) CanRead(user *User) bool {
|
||||
// Admins always have read access
|
||||
if l.IsAdmin(user) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check individual rights
|
||||
if l.checkListUserRight(user, UserRightRead) {
|
||||
return true
|
||||
}
|
||||
|
||||
return l.checkListTeamRight(user, TeamRightRead)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a list
|
||||
func (l *List) CanDelete(doer *User) bool {
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
log.Log.Error("Error occurred during CanDelete for List: %s", err)
|
||||
return false
|
||||
}
|
||||
return l.IsAdmin(doer)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update a list
|
||||
func (l *List) CanUpdate(doer *User) bool {
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for List: %s", err)
|
||||
return false
|
||||
}
|
||||
return l.CanWrite(doer)
|
||||
}
|
||||
|
||||
// CanCreate checks if the user can update a list
|
||||
func (l *List) CanCreate(doer *User) bool {
|
||||
// A user can create a list if he has write access to the namespace
|
||||
n, _ := GetNamespaceByID(l.NamespaceID)
|
||||
return n.CanWrite(doer)
|
||||
}
|
||||
|
||||
func (l *List) checkListTeamRight(user *User, r TeamRight) bool {
|
||||
exists, err := x.Select("l.*").
|
||||
Table("list").
|
||||
Alias("l").
|
||||
Join("LEFT", []string{"team_namespaces", "tn"}, " l.namespace_id = tn.namespace_id").
|
||||
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tn.team_id").
|
||||
Join("LEFT", []string{"team_list", "tl"}, "l.id = tl.list_id").
|
||||
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
|
||||
Where("((tm.user_id = ? AND tn.right = ?) OR (tm2.user_id = ? AND tl.right = ?)) AND l.id = ?",
|
||||
user.ID, r, user.ID, r, l.ID).
|
||||
Exist(&List{})
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during checkListTeamRight for List: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return exists
|
||||
}
|
||||
|
||||
func (l *List) checkListUserRight(user *User, r UserRight) bool {
|
||||
exists, err := x.Select("l.*").
|
||||
Table("list").
|
||||
Alias("l").
|
||||
Join("LEFT", []string{"users_namespace", "un"}, "un.namespace_id = l.namespace_id").
|
||||
Join("LEFT", []string{"users_list", "ul"}, "ul.list_id = l.id").
|
||||
Join("LEFT", []string{"namespaces", "n"}, "n.id = l.namespace_id").
|
||||
Where("((ul.user_id = ? AND ul.right = ?) "+
|
||||
"OR (un.user_id = ? AND un.right = ?) "+
|
||||
"OR n.owner_id = ?)"+
|
||||
"AND l.id = ?",
|
||||
user.ID, r, user.ID, r, user.ID, l.ID).
|
||||
Exist(&List{})
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during checkListUserRight for List: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return exists
|
||||
}
|
98
pkg/models/list_tasks.go
Normal file
98
pkg/models/list_tasks.go
Normal file
@ -0,0 +1,98 @@
|
||||
package models
|
||||
|
||||
// ListTask represents an task in a todolist
|
||||
type ListTask struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"listtask"`
|
||||
Text string `xorm:"varchar(250)" json:"text"`
|
||||
Description string `xorm:"varchar(250)" json:"description"`
|
||||
Done bool `xorm:"INDEX" json:"done"`
|
||||
DueDateUnix int64 `xorm:"int(11) INDEX" json:"dueDate"`
|
||||
ReminderUnix int64 `xorm:"int(11) INDEX" json:"reminderDate"`
|
||||
CreatedByID int64 `xorm:"int(11)" json:"-"` // ID of the user who put that task on the list
|
||||
ListID int64 `xorm:"int(11) INDEX" json:"listID" param:"list"`
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CreatedBy User `xorm:"-" json:"createdBy"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for listtasks
|
||||
func (ListTask) TableName() string {
|
||||
return "tasks"
|
||||
}
|
||||
|
||||
// GetTasksByListID gets all todotasks for a list
|
||||
func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
|
||||
err = x.Where("list_id = ?", listID).Find(&tasks)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// No need to iterate over users if the list doesn't has tasks
|
||||
if len(tasks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Get all users and put them into the array
|
||||
var userIDs []int64
|
||||
for _, i := range tasks {
|
||||
found := false
|
||||
for _, u := range userIDs {
|
||||
if i.CreatedByID == u {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
userIDs = append(userIDs, i.CreatedByID)
|
||||
}
|
||||
}
|
||||
|
||||
var users []User
|
||||
err = x.In("id", userIDs).Find(&users)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for in, task := range tasks {
|
||||
for _, u := range users {
|
||||
if task.CreatedByID == u.ID {
|
||||
tasks[in].CreatedBy = u
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// obsfucate the user password
|
||||
tasks[in].CreatedBy.Password = ""
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetListTaskByID returns all tasks a list has
|
||||
func GetListTaskByID(listTaskID int64) (listTask ListTask, err error) {
|
||||
if listTaskID < 1 {
|
||||
return ListTask{}, ErrListTaskDoesNotExist{listTaskID}
|
||||
}
|
||||
|
||||
exists, err := x.ID(listTaskID).Get(&listTask)
|
||||
if err != nil {
|
||||
return ListTask{}, err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return ListTask{}, ErrListTaskDoesNotExist{listTaskID}
|
||||
}
|
||||
|
||||
u, err := GetUserByID(listTask.CreatedByID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
listTask.CreatedBy = u
|
||||
|
||||
return
|
||||
}
|
56
pkg/models/list_tasks_create_update.go
Normal file
56
pkg/models/list_tasks_create_update.go
Normal file
@ -0,0 +1,56 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/imdario/mergo"
|
||||
)
|
||||
|
||||
// Create is the implementation to create a list task
|
||||
func (i *ListTask) Create(doer *User) (err error) {
|
||||
i.ID = 0
|
||||
|
||||
// Check if we have at least a text
|
||||
if i.Text == "" {
|
||||
return ErrListTaskCannotBeEmpty{}
|
||||
}
|
||||
|
||||
// Check if the list exists
|
||||
l := &List{ID: i.ListID}
|
||||
if err = l.GetSimpleByID(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
u, err := GetUserByID(doer.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i.CreatedByID = u.ID
|
||||
i.CreatedBy = u
|
||||
_, err = x.Cols("text", "description", "done", "due_date_unix", "reminder_unix", "created_by_id", "list_id", "created", "updated").Insert(i)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update updates a list task
|
||||
func (i *ListTask) Update() (err error) {
|
||||
// Check if the task exists
|
||||
ot, err := GetListTaskByID(i.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// For whatever reason, xorm dont detect if done is updated, so we need to update this every time by hand
|
||||
// Which is why we merge the actual task struct with the one we got from the
|
||||
// The user struct overrides values in the actual one.
|
||||
if err := mergo.Merge(&ot, i, mergo.WithOverride); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// And because a false is considered to be a null value, we need to explicitly check that case here.
|
||||
if i.Done == false {
|
||||
ot.Done = false
|
||||
}
|
||||
|
||||
_, err = x.ID(i.ID).Cols("text", "description", "done", "due_date_unix", "reminder_unix").Update(ot)
|
||||
*i = ot
|
||||
return
|
||||
}
|
14
pkg/models/list_tasks_delete.go
Normal file
14
pkg/models/list_tasks_delete.go
Normal file
@ -0,0 +1,14 @@
|
||||
package models
|
||||
|
||||
// Delete implements the delete method for listTask
|
||||
func (i *ListTask) Delete() (err error) {
|
||||
|
||||
// Check if it exists
|
||||
_, err = GetListTaskByID(i.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = x.ID(i.ID).Delete(ListTask{})
|
||||
return
|
||||
}
|
43
pkg/models/list_tasks_rights.go
Normal file
43
pkg/models/list_tasks_rights.go
Normal file
@ -0,0 +1,43 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// CanDelete checks if the user can delete an task
|
||||
func (i *ListTask) CanDelete(doer *User) bool {
|
||||
// Get the task
|
||||
lI, err := GetListTaskByID(i.ID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanDelete for ListTask: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// A user can delete an task if he has write acces to its list
|
||||
l := &List{ID: lI.ListID}
|
||||
l.ReadOne()
|
||||
return l.CanWrite(doer)
|
||||
}
|
||||
|
||||
// CanUpdate determines if a user has the right to update a list task
|
||||
func (i *ListTask) CanUpdate(doer *User) bool {
|
||||
// Get the task
|
||||
lI, err := GetListTaskByID(i.ID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanDelete for ListTask: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// A user can update an task if he has write acces to its list
|
||||
l := &List{ID: lI.ListID}
|
||||
l.ReadOne()
|
||||
return l.CanWrite(doer)
|
||||
}
|
||||
|
||||
// CanCreate determines if a user has the right to create a list task
|
||||
func (i *ListTask) CanCreate(doer *User) bool {
|
||||
// A user can create an task if he has write acces to its list
|
||||
l := &List{ID: i.ListID}
|
||||
l.ReadOne()
|
||||
return l.CanWrite(doer)
|
||||
}
|
74
pkg/models/list_tasks_test.go
Normal file
74
pkg/models/list_tasks_test.go
Normal file
@ -0,0 +1,74 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListTask_Create(t *testing.T) {
|
||||
//assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
// Fake list task
|
||||
listtask := ListTask{
|
||||
Text: "Lorem",
|
||||
Description: "Lorem Ipsum BACKERY",
|
||||
ListID: 1,
|
||||
}
|
||||
|
||||
// Add one point to a list
|
||||
doer, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.True(t, listtask.CanCreate(&doer))
|
||||
|
||||
err = listtask.Create(&doer)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Update it
|
||||
listtask.Text = "Test34"
|
||||
assert.True(t, listtask.CanUpdate(&doer))
|
||||
err = listtask.Update()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check if it was updated
|
||||
li, err := GetListTaskByID(listtask.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, li.Text, "Test34")
|
||||
|
||||
// Delete the task
|
||||
assert.True(t, listtask.CanDelete(&doer))
|
||||
err = listtask.Delete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Delete a nonexistant task
|
||||
listtask.ID = 0
|
||||
err = listtask.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListTaskDoesNotExist(err))
|
||||
|
||||
// Try adding a list task with an empty text
|
||||
listtask.Text = ""
|
||||
err = listtask.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListTaskCannotBeEmpty(err))
|
||||
|
||||
// Try adding one to a nonexistant list
|
||||
listtask.ListID = 99993939
|
||||
listtask.Text = "Lorem Ipsum"
|
||||
err = listtask.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListDoesNotExist(err))
|
||||
|
||||
// Try updating a nonexistant task
|
||||
listtask.ID = 94829352
|
||||
err = listtask.Update()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListTaskDoesNotExist(err))
|
||||
|
||||
// Try inserting an task with a nonexistant user
|
||||
nUser := &User{ID: 9482385}
|
||||
listtask.ListID = 1
|
||||
err = listtask.Create(nUser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
}
|
25
pkg/models/list_users.go
Normal file
25
pkg/models/list_users.go
Normal file
@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
// ListUser represents a list <-> user relation
|
||||
type ListUser struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"namespace"`
|
||||
UserID int64 `xorm:"int(11) not null INDEX" json:"user_id" param:"user"`
|
||||
ListID int64 `xorm:"int(11) not null INDEX" json:"list_id" param:"list"`
|
||||
Right UserRight `xorm:"int(11) INDEX" json:"right"`
|
||||
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName is the table name for ListUser
|
||||
func (ListUser) TableName() string {
|
||||
return "users_list"
|
||||
}
|
||||
|
||||
type userWithRight struct {
|
||||
User `xorm:"extends"`
|
||||
Right UserRight `json:"right"`
|
||||
}
|
40
pkg/models/list_users_create.go
Normal file
40
pkg/models/list_users_create.go
Normal file
@ -0,0 +1,40 @@
|
||||
package models
|
||||
|
||||
// Create creates a new list <-> user relation
|
||||
func (ul *ListUser) Create(u *User) (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := ul.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the list exists
|
||||
l := &List{ID: ul.ListID}
|
||||
if err = l.GetSimpleByID(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
if _, err = GetUserByID(ul.UserID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the user already has access or is owner of that list
|
||||
// We explicitly DONT check for teams here
|
||||
if l.OwnerID == ul.UserID {
|
||||
return ErrUserAlreadyHasAccess{UserID: ul.UserID, ListID: ul.ListID}
|
||||
}
|
||||
|
||||
exist, err := x.Where("list_id = ? AND user_id = ?", ul.ListID, ul.UserID).Get(&ListUser{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if exist {
|
||||
return ErrUserAlreadyHasAccess{UserID: ul.UserID, ListID: ul.ListID}
|
||||
}
|
||||
|
||||
// Insert user <-> list relation
|
||||
_, err = x.Insert(ul)
|
||||
|
||||
return
|
||||
}
|
25
pkg/models/list_users_delete.go
Normal file
25
pkg/models/list_users_delete.go
Normal file
@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
// Delete deletes a list <-> user relation
|
||||
func (lu *ListUser) Delete() (err error) {
|
||||
|
||||
// Check if the user exists
|
||||
_, err = GetUserByID(lu.UserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user has access to the list
|
||||
has, err := x.Where("user_id = ? AND list_id = ?", lu.UserID, lu.ListID).
|
||||
Get(&ListUser{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
return ErrUserDoesNotHaveAccessToList{ListID: lu.ListID, UserID: lu.UserID}
|
||||
}
|
||||
|
||||
_, err = x.Where("user_id = ? AND list_id = ?", lu.UserID, lu.ListID).
|
||||
Delete(&ListUser{})
|
||||
return
|
||||
}
|
22
pkg/models/list_users_readall.go
Normal file
22
pkg/models/list_users_readall.go
Normal file
@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
// ReadAll gets all users who have access to a list
|
||||
func (ul *ListUser) ReadAll(u *User) (interface{}, error) {
|
||||
// Check if the user has access to the list
|
||||
l := &List{ID: ul.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !l.CanRead(u) {
|
||||
return nil, ErrNeedToHaveListReadAccess{}
|
||||
}
|
||||
|
||||
// Get all users
|
||||
all := []*userWithRight{}
|
||||
err := x.
|
||||
Join("INNER", "users_list", "user_id = users.id").
|
||||
Where("users_list.list_id = ?", ul.ListID).
|
||||
Find(&all)
|
||||
|
||||
return all, err
|
||||
}
|
38
pkg/models/list_users_rights.go
Normal file
38
pkg/models/list_users_rights.go
Normal file
@ -0,0 +1,38 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// CanCreate checks if the user can create a new user <-> list relation
|
||||
func (lu *ListUser) CanCreate(doer *User) bool {
|
||||
// Get the list and check if the user has write access on it
|
||||
l := List{ID: lu.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
log.Log.Error("Error occurred during CanCreate for ListUser: %s", err)
|
||||
return false
|
||||
}
|
||||
return l.CanWrite(doer)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a user <-> list relation
|
||||
func (lu *ListUser) CanDelete(doer *User) bool {
|
||||
// Get the list and check if the user has write access on it
|
||||
l := List{ID: lu.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
log.Log.Error("Error occurred during CanDelete for ListUser: %s", err)
|
||||
return false
|
||||
}
|
||||
return l.CanWrite(doer)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update a user <-> list relation
|
||||
func (lu *ListUser) CanUpdate(doer *User) bool {
|
||||
// Get the list and check if the user has write access on it
|
||||
l := List{ID: lu.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for ListUser: %s", err)
|
||||
return false
|
||||
}
|
||||
return l.CanWrite(doer)
|
||||
}
|
16
pkg/models/list_users_update.go
Normal file
16
pkg/models/list_users_update.go
Normal file
@ -0,0 +1,16 @@
|
||||
package models
|
||||
|
||||
// Update updates a user <-> list relation
|
||||
func (lu *ListUser) Update() (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := lu.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = x.
|
||||
Where("list_id = ? AND user_id = ?", lu.ListID, lu.UserID).
|
||||
Cols("right").
|
||||
Update(lu)
|
||||
return
|
||||
}
|
7
pkg/models/main_test.go
Normal file
7
pkg/models/main_test.go
Normal file
@ -0,0 +1,7 @@
|
||||
package models
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
MainTest(m, "..")
|
||||
}
|
6
pkg/models/message.go
Normal file
6
pkg/models/message.go
Normal file
@ -0,0 +1,6 @@
|
||||
package models
|
||||
|
||||
// Message is a standard message
|
||||
type Message struct {
|
||||
Message string `json:"message"`
|
||||
}
|
92
pkg/models/models.go
Normal file
92
pkg/models/models.go
Normal file
@ -0,0 +1,92 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
_ "github.com/go-sql-driver/mysql" // Because.
|
||||
"github.com/go-xorm/core"
|
||||
"github.com/go-xorm/xorm"
|
||||
xrc "github.com/go-xorm/xorm-redis-cache"
|
||||
_ "github.com/mattn/go-sqlite3" // Because.
|
||||
|
||||
"encoding/gob"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
x *xorm.Engine
|
||||
|
||||
tables []interface{}
|
||||
)
|
||||
|
||||
func getEngine() (*xorm.Engine, error) {
|
||||
// Use Mysql if set
|
||||
if viper.GetString("database.type") == "mysql" {
|
||||
connStr := fmt.Sprintf(
|
||||
"%s:%s@tcp(%s)/%s?charset=utf8&parseTime=true",
|
||||
viper.GetString("database.user"),
|
||||
viper.GetString("database.password"),
|
||||
viper.GetString("database.host"),
|
||||
viper.GetString("database.database"))
|
||||
e, err := xorm.NewEngine("mysql", connStr)
|
||||
e.SetMaxOpenConns(viper.GetInt("database.openconnections"))
|
||||
return e, err
|
||||
}
|
||||
|
||||
// Otherwise use sqlite
|
||||
path := viper.GetString("database.path")
|
||||
if path == "" {
|
||||
path = "./db.db"
|
||||
}
|
||||
return xorm.NewEngine("sqlite3", path)
|
||||
}
|
||||
|
||||
func init() {
|
||||
tables = append(tables,
|
||||
new(User),
|
||||
new(List),
|
||||
new(ListTask),
|
||||
new(Team),
|
||||
new(TeamMember),
|
||||
new(TeamList),
|
||||
new(TeamNamespace),
|
||||
new(Namespace),
|
||||
new(ListUser),
|
||||
new(NamespaceUser),
|
||||
)
|
||||
}
|
||||
|
||||
// SetEngine sets the xorm.Engine
|
||||
func SetEngine() (err error) {
|
||||
x, err = getEngine()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
// Cache
|
||||
if viper.GetBool("cache.enabled") {
|
||||
switch viper.GetString("cache.type") {
|
||||
case "memory":
|
||||
cacher := xorm.NewLRUCacher(xorm.NewMemoryStore(), viper.GetInt("cache.maxelementsize"))
|
||||
x.SetDefaultCacher(cacher)
|
||||
break
|
||||
case "redis":
|
||||
cacher := xrc.NewRedisCacher(viper.GetString("cache.redishost"), viper.GetString("cache.redispassword"), xrc.DEFAULT_EXPIRATION, x.Logger())
|
||||
x.SetDefaultCacher(cacher)
|
||||
gob.Register(tables)
|
||||
break
|
||||
default:
|
||||
fmt.Println("Did not find a valid cache type. Caching disabled. Please refer to the docs for poosible cache types.")
|
||||
}
|
||||
}
|
||||
|
||||
x.SetMapper(core.GonicMapper{})
|
||||
|
||||
// Sync dat shit
|
||||
if err = x.StoreEngine("InnoDB").Sync2(tables...); err != nil {
|
||||
return fmt.Errorf("sync database struct error: %v", err)
|
||||
}
|
||||
|
||||
x.ShowSQL(viper.GetBool("database.showqueries"))
|
||||
|
||||
return nil
|
||||
}
|
13
pkg/models/models_test.go
Normal file
13
pkg/models/models_test.go
Normal file
@ -0,0 +1,13 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSetEngine(t *testing.T) {
|
||||
viper.Set("database.path", "file::memory:?cache=shared")
|
||||
err := SetEngine()
|
||||
assert.NoError(t, err)
|
||||
}
|
134
pkg/models/namespace.go
Normal file
134
pkg/models/namespace.go
Normal file
@ -0,0 +1,134 @@
|
||||
package models
|
||||
|
||||
// Namespace holds informations about a namespace
|
||||
type Namespace struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"namespace"`
|
||||
Name string `xorm:"varchar(250)" json:"name"`
|
||||
Description string `xorm:"varchar(1000)" json:"description"`
|
||||
OwnerID int64 `xorm:"int(11) not null INDEX" json:"-"`
|
||||
|
||||
Owner User `xorm:"-" json:"owner"`
|
||||
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName makes beautiful table names
|
||||
func (Namespace) TableName() string {
|
||||
return "namespaces"
|
||||
}
|
||||
|
||||
// GetNamespaceByID returns a namespace object by its ID
|
||||
func GetNamespaceByID(id int64) (namespace Namespace, err error) {
|
||||
if id < 1 {
|
||||
return namespace, ErrNamespaceDoesNotExist{ID: id}
|
||||
}
|
||||
|
||||
namespace.ID = id
|
||||
exists, err := x.Get(&namespace)
|
||||
if err != nil {
|
||||
return namespace, err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return namespace, ErrNamespaceDoesNotExist{ID: id}
|
||||
}
|
||||
|
||||
// Get the namespace Owner
|
||||
namespace.Owner, err = GetUserByID(namespace.OwnerID)
|
||||
if err != nil {
|
||||
return namespace, err
|
||||
}
|
||||
|
||||
return namespace, err
|
||||
}
|
||||
|
||||
// ReadOne gets one namespace
|
||||
func (n *Namespace) ReadOne() (err error) {
|
||||
*n, err = GetNamespaceByID(n.ID)
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll gets all namespaces a user has access to
|
||||
func (n *Namespace) ReadAll(doer *User) (interface{}, error) {
|
||||
|
||||
type namespaceWithLists struct {
|
||||
Namespace `xorm:"extends"`
|
||||
Lists []*List `xorm:"-" json:"lists"`
|
||||
}
|
||||
|
||||
all := []*namespaceWithLists{}
|
||||
|
||||
err := x.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_namespace", "users_namespace.namespace_id = namespaces.id").
|
||||
Where("team_members.user_id = ?", doer.ID).
|
||||
Or("namespaces.owner_id = ?", doer.ID).
|
||||
Or("users_namespace.user_id = ?", doer.ID).
|
||||
GroupBy("namespaces.id").
|
||||
Find(&all)
|
||||
|
||||
if err != nil {
|
||||
return all, err
|
||||
}
|
||||
|
||||
// Get all users
|
||||
users := []*User{}
|
||||
err = x.Select("users.*").
|
||||
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("INNER", "users", "users.id = namespaces.owner_id").
|
||||
Where("team_members.user_id = ?", doer.ID).
|
||||
Or("namespaces.owner_id = ?", doer.ID).
|
||||
GroupBy("users.id").
|
||||
Find(&users)
|
||||
|
||||
if err != nil {
|
||||
return all, err
|
||||
}
|
||||
|
||||
// Make a list of namespace ids
|
||||
var namespaceids []int64
|
||||
for _, nsp := range all {
|
||||
namespaceids = append(namespaceids, nsp.ID)
|
||||
}
|
||||
|
||||
// Get all lists
|
||||
lists := []*List{}
|
||||
err = x.Table(&lists).
|
||||
In("namespace_id", namespaceids).
|
||||
Find(&lists)
|
||||
if err != nil {
|
||||
return all, err
|
||||
}
|
||||
|
||||
// More details for the lists
|
||||
AddListDetails(lists)
|
||||
|
||||
// Put objects in our namespace list
|
||||
for i, n := range all {
|
||||
|
||||
// Users
|
||||
for _, u := range users {
|
||||
if n.OwnerID == u.ID {
|
||||
all[i].Owner = *u
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// List infos
|
||||
for _, l := range lists {
|
||||
if n.ID == l.NamespaceID {
|
||||
all[i].Lists = append(all[i].Lists, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return all, nil
|
||||
}
|
21
pkg/models/namespace_create.go
Normal file
21
pkg/models/namespace_create.go
Normal file
@ -0,0 +1,21 @@
|
||||
package models
|
||||
|
||||
// Create implements the creation method via the interface
|
||||
func (n *Namespace) Create(doer *User) (err error) {
|
||||
// Check if we have at least a name
|
||||
if n.Name == "" {
|
||||
return ErrNamespaceNameCannotBeEmpty{NamespaceID: 0, UserID: doer.ID}
|
||||
}
|
||||
n.ID = 0 // This would otherwise prevent the creation of new lists after one was created
|
||||
|
||||
// Check if the User exists
|
||||
n.Owner, err = GetUserByID(doer.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
n.OwnerID = n.Owner.ID
|
||||
|
||||
// Insert
|
||||
_, err = x.Insert(n)
|
||||
return
|
||||
}
|
41
pkg/models/namespace_delete.go
Normal file
41
pkg/models/namespace_delete.go
Normal file
@ -0,0 +1,41 @@
|
||||
package models
|
||||
|
||||
// Delete deletes a namespace
|
||||
func (n *Namespace) Delete() (err error) {
|
||||
|
||||
// Check if the namespace exists
|
||||
_, err = GetNamespaceByID(n.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the namespace
|
||||
_, err = x.ID(n.ID).Delete(&Namespace{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete all lists with their tasks
|
||||
lists, err := GetListsByNamespaceID(n.ID)
|
||||
var listIDs []int64
|
||||
// We need to do that for here because we need the list ids to delete two times:
|
||||
// 1) to delete the lists itself
|
||||
// 2) to delete the list tasks
|
||||
for _, l := range lists {
|
||||
listIDs = append(listIDs, l.ID)
|
||||
}
|
||||
|
||||
// Delete tasks
|
||||
_, err = x.In("list_id", listIDs).Delete(&ListTask{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the lists
|
||||
_, err = x.In("id", listIDs).Delete(&List{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
112
pkg/models/namespace_rights.go
Normal file
112
pkg/models/namespace_rights.go
Normal file
@ -0,0 +1,112 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// IsAdmin returns true or false if the user is admin on that namespace or not
|
||||
func (n *Namespace) IsAdmin(u *User) bool {
|
||||
// Owners always have admin rights
|
||||
if u.ID == n.Owner.ID {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check user rights
|
||||
if n.checkUserRights(u, UserRightAdmin) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if that user is in a team which has admin rights to that namespace
|
||||
return n.checkTeamRights(u, TeamRightAdmin)
|
||||
}
|
||||
|
||||
// CanWrite checks if a user has write access to a namespace
|
||||
func (n *Namespace) CanWrite(u *User) bool {
|
||||
// Admins always have write access
|
||||
if n.IsAdmin(u) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check user rights
|
||||
if n.checkUserRights(u, UserRightWrite) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if that user is in a team which has write rights to that namespace
|
||||
return n.checkTeamRights(u, TeamRightWrite)
|
||||
}
|
||||
|
||||
// CanRead checks if a user has read access to that namespace
|
||||
func (n *Namespace) CanRead(u *User) bool {
|
||||
// Admins always have read access
|
||||
if n.IsAdmin(u) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check user rights
|
||||
if n.checkUserRights(u, UserRightRead) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if the user is in a team which has access to the namespace
|
||||
return n.checkTeamRights(u, TeamRightRead)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update the namespace
|
||||
func (n *Namespace) CanUpdate(u *User) bool {
|
||||
nn, err := GetNamespaceByID(n.ID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for Namespace: %s", err)
|
||||
return false
|
||||
}
|
||||
return nn.IsAdmin(u)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a namespace
|
||||
func (n *Namespace) CanDelete(u *User) bool {
|
||||
nn, err := GetNamespaceByID(n.ID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanDelete for Namespace: %s", err)
|
||||
return false
|
||||
}
|
||||
return nn.IsAdmin(u)
|
||||
}
|
||||
|
||||
// CanCreate checks if the user can create a new namespace
|
||||
func (n *Namespace) CanCreate(u *User) bool {
|
||||
// This is currently a dummy function, later on we could imagine global limits etc.
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *Namespace) checkTeamRights(u *User, r TeamRight) bool {
|
||||
exists, err := x.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").
|
||||
Where("namespaces.id = ? "+
|
||||
"AND (team_members.user_id = ? AND team_namespaces.right = ?) "+
|
||||
"OR namespaces.owner_id = ? ", n.ID, u.ID, r, u.ID).
|
||||
Get(&Namespace{})
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during checkTeamRights for Namespace: %s, TeamRight: %d", err, r)
|
||||
return false
|
||||
}
|
||||
|
||||
return exists
|
||||
}
|
||||
|
||||
func (n *Namespace) checkUserRights(u *User, r UserRight) bool {
|
||||
exists, err := x.Select("namespaces.*").
|
||||
Table("namespaces").
|
||||
Join("LEFT", "users_namespace", "users_namespace.namespace_id = namespaces.id").
|
||||
Where("namespaces.id = ? AND ("+
|
||||
"namespaces.owner_id = ? "+
|
||||
"OR (users_namespace.user_id = ? AND users_namespace.right = ?))", n.ID, u.ID, u.ID, r).
|
||||
Get(&Namespace{})
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during checkUserRights for Namespace: %s, UserRight: %d", err, r)
|
||||
return false
|
||||
}
|
||||
|
||||
return exists
|
||||
}
|
93
pkg/models/namespace_test.go
Normal file
93
pkg/models/namespace_test.go
Normal file
@ -0,0 +1,93 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNamespace_Create(t *testing.T) {
|
||||
// Create test database
|
||||
//assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
// Dummy namespace
|
||||
dummynamespace := Namespace{
|
||||
Name: "Test",
|
||||
Description: "Lorem Ipsum",
|
||||
}
|
||||
|
||||
// Doer
|
||||
doer, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try creating it
|
||||
assert.True(t, dummynamespace.CanCreate(&doer))
|
||||
err = dummynamespace.Create(&doer)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check if it really exists
|
||||
assert.True(t, dummynamespace.CanRead(&doer))
|
||||
newOne := Namespace{ID: dummynamespace.ID}
|
||||
err = newOne.ReadOne()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, newOne.Name, "Test")
|
||||
|
||||
// Try creating one without a name
|
||||
n2 := Namespace{}
|
||||
err = n2.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceNameCannotBeEmpty(err))
|
||||
|
||||
// Try inserting one with a nonexistant user
|
||||
nUser := &User{ID: 9482385}
|
||||
dnsp2 := dummynamespace
|
||||
err = dnsp2.Create(nUser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
|
||||
// Update it
|
||||
assert.True(t, dummynamespace.CanUpdate(&doer))
|
||||
dummynamespace.Description = "Dolor sit amet."
|
||||
err = dummynamespace.Update()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try updating one with a nonexistant owner
|
||||
dummynamespace.Owner.ID = 94829838572
|
||||
err = dummynamespace.Update()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
|
||||
// Try updating without a name
|
||||
dummynamespace.Name = ""
|
||||
err = dummynamespace.Update()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceNameCannotBeEmpty(err))
|
||||
|
||||
// Try updating a nonexistant one
|
||||
n := Namespace{ID: 284729, Name: "Lorem"}
|
||||
err = n.Update()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
|
||||
// Delete it
|
||||
assert.True(t, dummynamespace.CanDelete(&doer))
|
||||
err = dummynamespace.Delete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try deleting a nonexistant one
|
||||
err = n.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
|
||||
// Check if it was successfully deleted
|
||||
err = dummynamespace.ReadOne()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
|
||||
// Get all namespaces of a user
|
||||
nsps, err := n.ReadAll(&doer)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(nsps).Kind(), reflect.Slice)
|
||||
s := reflect.ValueOf(nsps)
|
||||
assert.Equal(t, 1, s.Len())
|
||||
}
|
28
pkg/models/namespace_update.go
Normal file
28
pkg/models/namespace_update.go
Normal file
@ -0,0 +1,28 @@
|
||||
package models
|
||||
|
||||
// Update implements the update method via the interface
|
||||
func (n *Namespace) Update() (err error) {
|
||||
// Check if we have at least a name
|
||||
if n.Name == "" {
|
||||
return ErrNamespaceNameCannotBeEmpty{NamespaceID: n.ID}
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
currentNamespace, err := GetNamespaceByID(n.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the (new) owner exists
|
||||
n.OwnerID = n.Owner.ID
|
||||
if currentNamespace.OwnerID != n.OwnerID {
|
||||
n.Owner, err = GetUserByID(n.OwnerID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Do the actual update
|
||||
_, err = x.ID(currentNamespace.ID).Update(n)
|
||||
return
|
||||
}
|
20
pkg/models/namespace_users.go
Normal file
20
pkg/models/namespace_users.go
Normal file
@ -0,0 +1,20 @@
|
||||
package models
|
||||
|
||||
// NamespaceUser represents a namespace <-> user relation
|
||||
type NamespaceUser struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"namespace"`
|
||||
UserID int64 `xorm:"int(11) not null INDEX" json:"user_id" param:"user"`
|
||||
NamespaceID int64 `xorm:"int(11) not null INDEX" json:"namespace_id" param:"namespace"`
|
||||
Right UserRight `xorm:"int(11) INDEX" json:"right"`
|
||||
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName is the table name for NamespaceUser
|
||||
func (NamespaceUser) TableName() string {
|
||||
return "users_namespace"
|
||||
}
|
43
pkg/models/namespace_users_create.go
Normal file
43
pkg/models/namespace_users_create.go
Normal file
@ -0,0 +1,43 @@
|
||||
package models
|
||||
|
||||
// Create creates a new namespace <-> user relation
|
||||
func (un *NamespaceUser) Create(u *User) (err error) {
|
||||
|
||||
// Reset the id
|
||||
un.ID = 0
|
||||
|
||||
// Check if the right is valid
|
||||
if err := un.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
l, err := GetNamespaceByID(un.NamespaceID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
if _, err = GetUserByID(un.UserID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the user already has access or is owner of that namespace
|
||||
// We explicitly DO NOT check for teams here
|
||||
if l.OwnerID == un.UserID {
|
||||
return ErrUserAlreadyHasNamespaceAccess{UserID: un.UserID, NamespaceID: un.NamespaceID}
|
||||
}
|
||||
|
||||
exist, err := x.Where("namespace_id = ? AND user_id = ?", un.NamespaceID, un.UserID).Get(&NamespaceUser{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if exist {
|
||||
return ErrUserAlreadyHasNamespaceAccess{UserID: un.UserID, NamespaceID: un.NamespaceID}
|
||||
}
|
||||
|
||||
// Insert user <-> namespace relation
|
||||
_, err = x.Insert(un)
|
||||
|
||||
return
|
||||
}
|
25
pkg/models/namespace_users_delete.go
Normal file
25
pkg/models/namespace_users_delete.go
Normal file
@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
// Delete deletes a namespace <-> user relation
|
||||
func (nu *NamespaceUser) Delete() (err error) {
|
||||
|
||||
// Check if the user exists
|
||||
_, err = GetUserByID(nu.UserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user has access to the namespace
|
||||
has, err := x.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 = x.Where("user_id = ? AND namespace_id = ?", nu.UserID, nu.NamespaceID).
|
||||
Delete(&NamespaceUser{})
|
||||
return
|
||||
}
|
22
pkg/models/namespace_users_readall.go
Normal file
22
pkg/models/namespace_users_readall.go
Normal file
@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
// ReadAll gets all users who have access to a namespace
|
||||
func (un *NamespaceUser) ReadAll(u *User) (interface{}, error) {
|
||||
// Check if the user has access to the namespace
|
||||
l, err := GetNamespaceByID(un.NamespaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !l.CanRead(u) {
|
||||
return nil, ErrNeedToHaveNamespaceReadAccess{}
|
||||
}
|
||||
|
||||
// Get all users
|
||||
all := []*userWithRight{}
|
||||
err = x.
|
||||
Join("INNER", "users_namespace", "user_id = users.id").
|
||||
Where("users_namespace.namespace_id = ?", un.NamespaceID).
|
||||
Find(&all)
|
||||
|
||||
return all, err
|
||||
}
|
38
pkg/models/namespace_users_rights.go
Normal file
38
pkg/models/namespace_users_rights.go
Normal file
@ -0,0 +1,38 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// CanCreate checks if the user can create a new user <-> namespace relation
|
||||
func (nu *NamespaceUser) CanCreate(doer *User) bool {
|
||||
// Get the namespace and check if the user has write access on it
|
||||
n, err := GetNamespaceByID(nu.NamespaceID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanCreate for NamespaceUser: %s", err)
|
||||
return false
|
||||
}
|
||||
return n.CanWrite(doer)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a user <-> namespace relation
|
||||
func (nu *NamespaceUser) CanDelete(doer *User) bool {
|
||||
// Get the namespace and check if the user has write access on it
|
||||
n, err := GetNamespaceByID(nu.NamespaceID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanDelete for NamespaceUser: %s", err)
|
||||
return false
|
||||
}
|
||||
return n.CanWrite(doer)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update a user <-> namespace relation
|
||||
func (nu *NamespaceUser) CanUpdate(doer *User) bool {
|
||||
// Get the namespace and check if the user has write access on it
|
||||
n, err := GetNamespaceByID(nu.NamespaceID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for NamespaceUser: %s", err)
|
||||
return false
|
||||
}
|
||||
return n.CanWrite(doer)
|
||||
}
|
16
pkg/models/namespace_users_update.go
Normal file
16
pkg/models/namespace_users_update.go
Normal file
@ -0,0 +1,16 @@
|
||||
package models
|
||||
|
||||
// Update updates a user <-> namespace relation
|
||||
func (nu *NamespaceUser) Update() (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := nu.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = x.
|
||||
Where("namespace_id = ? AND user_id = ?", nu.NamespaceID, nu.UserID).
|
||||
Cols("right").
|
||||
Update(nu)
|
||||
return
|
||||
}
|
11
pkg/models/rights.go
Normal file
11
pkg/models/rights.go
Normal file
@ -0,0 +1,11 @@
|
||||
package models
|
||||
|
||||
// Rights defines rights methods
|
||||
type Rights interface {
|
||||
IsAdmin(*User) bool
|
||||
CanWrite(*User) bool
|
||||
CanRead(*User) bool
|
||||
CanDelete(*User) bool
|
||||
CanUpdate(*User) bool
|
||||
CanCreate(*User) bool
|
||||
}
|
25
pkg/models/team_list.go
Normal file
25
pkg/models/team_list.go
Normal file
@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
// TeamList defines the relation between a team and a list
|
||||
type TeamList struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
|
||||
TeamID int64 `xorm:"int(11) not null INDEX" json:"team_id" param:"team"`
|
||||
ListID int64 `xorm:"int(11) not null INDEX" json:"list_id" param:"list"`
|
||||
Right TeamRight `xorm:"int(11) INDEX" json:"right"`
|
||||
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName makes beautiful table names
|
||||
func (TeamList) TableName() string {
|
||||
return "team_list"
|
||||
}
|
||||
|
||||
type teamWithRight struct {
|
||||
Team `xorm:"extends"`
|
||||
Right TeamRight `json:"right"`
|
||||
}
|
37
pkg/models/team_list_create.go
Normal file
37
pkg/models/team_list_create.go
Normal file
@ -0,0 +1,37 @@
|
||||
package models
|
||||
|
||||
// Create creates a new team <-> list relation
|
||||
func (tl *TeamList) Create(doer *User) (err error) {
|
||||
|
||||
// Check if the rights are valid
|
||||
if err = tl.Right.isValid(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(tl.TeamID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the list exists
|
||||
l := &List{ID: tl.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the team is already on the list
|
||||
exists, err := x.Where("team_id = ?", tl.TeamID).
|
||||
And("list_id = ?", tl.ListID).
|
||||
Get(&TeamList{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
return ErrTeamAlreadyHasAccess{tl.TeamID, tl.ListID}
|
||||
}
|
||||
|
||||
// Insert the new team
|
||||
_, err = x.Insert(tl)
|
||||
return
|
||||
}
|
28
pkg/models/team_list_delete.go
Normal file
28
pkg/models/team_list_delete.go
Normal file
@ -0,0 +1,28 @@
|
||||
package models
|
||||
|
||||
// Delete deletes a team <-> list relation based on the list & team id
|
||||
func (tl *TeamList) Delete() (err error) {
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(tl.TeamID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the team has access to the list
|
||||
has, err := x.Where("team_id = ? AND list_id = ?", tl.TeamID, tl.ListID).
|
||||
Get(&TeamList{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
return ErrTeamDoesNotHaveAccessToList{TeamID: tl.TeamID, ListID: tl.ListID}
|
||||
}
|
||||
|
||||
// Delete the relation
|
||||
_, err = x.Where("team_id = ?", tl.TeamID).
|
||||
And("list_id = ?", tl.ListID).
|
||||
Delete(TeamList{})
|
||||
|
||||
return
|
||||
}
|
23
pkg/models/team_list_readall.go
Normal file
23
pkg/models/team_list_readall.go
Normal file
@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
// ReadAll implements the method to read all teams of a list
|
||||
func (tl *TeamList) ReadAll(u *User) (interface{}, error) {
|
||||
// Check if the user can read the namespace
|
||||
l := &List{ID: tl.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !l.CanRead(u) {
|
||||
return nil, ErrNeedToHaveListReadAccess{ListID: tl.ListID, UserID: u.ID}
|
||||
}
|
||||
|
||||
// Get the teams
|
||||
all := []*teamWithRight{}
|
||||
err := x.
|
||||
Table("teams").
|
||||
Join("INNER", "team_list", "team_id = teams.id").
|
||||
Where("team_list.list_id = ?", tl.ListID).
|
||||
Find(&all)
|
||||
|
||||
return all, err
|
||||
}
|
35
pkg/models/team_list_rights.go
Normal file
35
pkg/models/team_list_rights.go
Normal file
@ -0,0 +1,35 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// CanCreate checks if the user can create a team <-> list relation
|
||||
func (tl *TeamList) CanCreate(u *User) bool {
|
||||
l := List{ID: tl.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
log.Log.Error("Error occurred during CanCreate for TeamList: %s", err)
|
||||
return false
|
||||
}
|
||||
return l.IsAdmin(u)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a team <-> list relation
|
||||
func (tl *TeamList) CanDelete(user *User) bool {
|
||||
l := List{ID: tl.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
log.Log.Error("Error occurred during CanDelete for TeamList: %s", err)
|
||||
return false
|
||||
}
|
||||
return l.IsAdmin(user)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update a team <-> list relation
|
||||
func (tl *TeamList) CanUpdate(user *User) bool {
|
||||
l := List{ID: tl.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for TeamList: %s", err)
|
||||
return false
|
||||
}
|
||||
return l.IsAdmin(user)
|
||||
}
|
92
pkg/models/team_list_test.go
Normal file
92
pkg/models/team_list_test.go
Normal file
@ -0,0 +1,92 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTeamList(t *testing.T) {
|
||||
// Dummy relation
|
||||
tl := TeamList{
|
||||
TeamID: 1,
|
||||
ListID: 1,
|
||||
Right: TeamRightAdmin,
|
||||
}
|
||||
|
||||
// Dummyuser
|
||||
u, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check normal creation
|
||||
assert.True(t, tl.CanCreate(&u))
|
||||
err = tl.Create(&u)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check again
|
||||
err = tl.Create(&u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamAlreadyHasAccess(err))
|
||||
|
||||
// Check with wrong rights
|
||||
tl2 := tl
|
||||
tl2.Right = TeamRightUnknown
|
||||
err = tl2.Create(&u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrInvalidTeamRight(err))
|
||||
|
||||
// Check with inexistant team
|
||||
tl3 := tl
|
||||
tl3.TeamID = 3253
|
||||
err = tl3.Create(&u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
|
||||
// Check with inexistant list
|
||||
tl4 := tl
|
||||
tl4.ListID = 3252
|
||||
err = tl4.Create(&u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListDoesNotExist(err))
|
||||
|
||||
// Test Read all
|
||||
teams, err := tl.ReadAll(&u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
|
||||
s := reflect.ValueOf(teams)
|
||||
assert.Equal(t, s.Len(), 1)
|
||||
|
||||
// Test Read all for nonexistant list
|
||||
_, err = tl4.ReadAll(&u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListDoesNotExist(err))
|
||||
|
||||
// Test Read all for a list where the user is owner of the namespace this list belongs to
|
||||
tl5 := tl
|
||||
tl5.ListID = 2
|
||||
_, err = tl5.ReadAll(&u)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test read all for a list where the user not has access
|
||||
tl6 := tl
|
||||
tl6.ListID = 3
|
||||
_, err = tl6.ReadAll(&u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNeedToHaveListReadAccess(err))
|
||||
|
||||
// Delete
|
||||
assert.True(t, tl.CanDelete(&u))
|
||||
err = tl.Delete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Delete a nonexistant team
|
||||
err = tl3.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
|
||||
// Delete with a nonexistant list
|
||||
err = tl4.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotHaveAccessToList(err))
|
||||
|
||||
}
|
16
pkg/models/team_list_update.go
Normal file
16
pkg/models/team_list_update.go
Normal file
@ -0,0 +1,16 @@
|
||||
package models
|
||||
|
||||
// Update updates a team <-> list relation
|
||||
func (tl *TeamList) Update() (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := tl.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = x.
|
||||
Where("list_id = ? AND team_id = ?", tl.ListID, tl.TeamID).
|
||||
Cols("right").
|
||||
Update(tl)
|
||||
return
|
||||
}
|
27
pkg/models/team_members_create.go
Normal file
27
pkg/models/team_members_create.go
Normal file
@ -0,0 +1,27 @@
|
||||
package models
|
||||
|
||||
// Create implements the create method to assign a user to a team
|
||||
func (tm *TeamMember) Create(doer *User) (err error) {
|
||||
// Check if the team extst
|
||||
_, err = GetTeamByID(tm.TeamID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
_, err = GetUserByID(tm.UserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if that user is already part of the team
|
||||
exists, err := x.Where("team_id = ? AND user_id = ?", tm.TeamID, tm.UserID).
|
||||
Get(&TeamMember{})
|
||||
if exists {
|
||||
return ErrUserIsMemberOfTeam{tm.TeamID, tm.UserID}
|
||||
}
|
||||
|
||||
// Insert the user
|
||||
_, err = x.Insert(tm)
|
||||
return
|
||||
}
|
16
pkg/models/team_members_delete.go
Normal file
16
pkg/models/team_members_delete.go
Normal file
@ -0,0 +1,16 @@
|
||||
package models
|
||||
|
||||
// Delete deletes a user from a team
|
||||
func (tm *TeamMember) Delete() (err error) {
|
||||
|
||||
total, err := x.Where("team_id = ?", tm.TeamID).Count(&TeamMember{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if total == 1 {
|
||||
return ErrCannotDeleteLastTeamMember{tm.TeamID, tm.UserID}
|
||||
}
|
||||
|
||||
_, err = x.Where("team_id = ? AND user_id = ?", tm.TeamID, tm.UserID).Delete(&TeamMember{})
|
||||
return
|
||||
}
|
27
pkg/models/team_members_rights.go
Normal file
27
pkg/models/team_members_rights.go
Normal file
@ -0,0 +1,27 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// CanCreate checks if the user can add a new tem member
|
||||
func (tm *TeamMember) CanCreate(u *User) bool {
|
||||
return tm.IsAdmin(u)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a new team member
|
||||
func (tm *TeamMember) CanDelete(u *User) bool {
|
||||
return tm.IsAdmin(u)
|
||||
}
|
||||
|
||||
// IsAdmin checks if the user is team admin
|
||||
func (tm *TeamMember) IsAdmin(u *User) bool {
|
||||
// A user can add a member to a team if he is admin of that team
|
||||
exists, err := x.Where("user_id = ? AND team_id = ? AND admin = ?", u.ID, tm.TeamID, true).
|
||||
Get(&TeamMember{})
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during IsAdmin for TeamMember: %s", err)
|
||||
return false
|
||||
}
|
||||
return exists
|
||||
}
|
63
pkg/models/team_members_test.go
Normal file
63
pkg/models/team_members_test.go
Normal file
@ -0,0 +1,63 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTeamMember_Create(t *testing.T) {
|
||||
|
||||
// Dummy team member
|
||||
dummyteammember := TeamMember{
|
||||
TeamID: 1,
|
||||
UserID: 3,
|
||||
}
|
||||
|
||||
// Doer
|
||||
doer, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Insert a new team member
|
||||
assert.True(t, dummyteammember.CanCreate(&doer))
|
||||
err = dummyteammember.Create(&doer)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check he's in there
|
||||
team := Team{ID: 1}
|
||||
err = team.ReadOne()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, len(team.Members))
|
||||
|
||||
// Try inserting a user twice
|
||||
err = dummyteammember.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserIsMemberOfTeam(err))
|
||||
|
||||
// Delete it
|
||||
assert.True(t, dummyteammember.CanDelete(&doer))
|
||||
err = dummyteammember.Delete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Delete the other one
|
||||
tm := TeamMember{TeamID: 1, UserID: 2}
|
||||
err = tm.Delete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try deleting the last one
|
||||
tm = TeamMember{TeamID: 1, UserID: 1}
|
||||
err = tm.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrCannotDeleteLastTeamMember(err))
|
||||
|
||||
// Try inserting a user which does not exist
|
||||
dummyteammember.UserID = 9484
|
||||
err = dummyteammember.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
|
||||
// Try adding a user to a team which does not exist
|
||||
tm = TeamMember{TeamID: 94824, UserID: 1}
|
||||
err = tm.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
}
|
20
pkg/models/team_namespace.go
Normal file
20
pkg/models/team_namespace.go
Normal file
@ -0,0 +1,20 @@
|
||||
package models
|
||||
|
||||
// TeamNamespace defines the relationship between a Team and a Namespace
|
||||
type TeamNamespace struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
|
||||
TeamID int64 `xorm:"int(11) not null INDEX" json:"team_id" param:"team"`
|
||||
NamespaceID int64 `xorm:"int(11) not null INDEX" json:"namespace_id" param:"namespace"`
|
||||
Right TeamRight `xorm:"int(11) INDEX" json:"right"`
|
||||
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName makes beautiful table names
|
||||
func (TeamNamespace) TableName() string {
|
||||
return "team_namespaces"
|
||||
}
|
37
pkg/models/team_namespace_create.go
Normal file
37
pkg/models/team_namespace_create.go
Normal file
@ -0,0 +1,37 @@
|
||||
package models
|
||||
|
||||
// Create creates a new team <-> namespace relation
|
||||
func (tn *TeamNamespace) Create(doer *User) (err error) {
|
||||
|
||||
// Check if the rights are valid
|
||||
if err = tn.Right.isValid(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(tn.TeamID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
_, err = GetNamespaceByID(tn.NamespaceID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the team already has access to the namespace
|
||||
exists, err := x.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 = x.Insert(tn)
|
||||
return
|
||||
}
|
28
pkg/models/team_namespace_delete.go
Normal file
28
pkg/models/team_namespace_delete.go
Normal file
@ -0,0 +1,28 @@
|
||||
package models
|
||||
|
||||
// Delete deletes a team <-> namespace relation based on the namespace & team id
|
||||
func (tn *TeamNamespace) Delete() (err error) {
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(tn.TeamID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the team has access to the namespace
|
||||
has, err := x.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 = x.Where("team_id = ?", tn.TeamID).
|
||||
And("namespace_id = ?", tn.NamespaceID).
|
||||
Delete(TeamNamespace{})
|
||||
|
||||
return
|
||||
}
|
23
pkg/models/team_namespace_readall.go
Normal file
23
pkg/models/team_namespace_readall.go
Normal file
@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
// ReadAll implements the method to read all teams of a namespace
|
||||
func (tn *TeamNamespace) ReadAll(user *User) (interface{}, error) {
|
||||
// Check if the user can read the namespace
|
||||
n, err := GetNamespaceByID(tn.NamespaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !n.CanRead(user) {
|
||||
return nil, ErrNeedToHaveNamespaceReadAccess{NamespaceID: tn.NamespaceID, UserID: user.ID}
|
||||
}
|
||||
|
||||
// Get the teams
|
||||
all := []*teamWithRight{}
|
||||
|
||||
err = x.Table("teams").
|
||||
Join("INNER", "team_namespaces", "team_id = teams.id").
|
||||
Where("team_namespaces.namespace_id = ?", tn.NamespaceID).
|
||||
Find(&all)
|
||||
|
||||
return all, err
|
||||
}
|
35
pkg/models/team_namespace_rights.go
Normal file
35
pkg/models/team_namespace_rights.go
Normal file
@ -0,0 +1,35 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// CanCreate checks if one can create a new team <-> namespace relation
|
||||
func (tn *TeamNamespace) CanCreate(user *User) bool {
|
||||
n, err := GetNamespaceByID(tn.NamespaceID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanCreate for TeamNamespace: %s", err)
|
||||
return false
|
||||
}
|
||||
return n.IsAdmin(user)
|
||||
}
|
||||
|
||||
// CanDelete checks if a user can remove a team from a namespace. Only namespace admins can do that.
|
||||
func (tn *TeamNamespace) CanDelete(user *User) bool {
|
||||
n, err := GetNamespaceByID(tn.NamespaceID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanDelete for TeamNamespace: %s", err)
|
||||
return false
|
||||
}
|
||||
return n.IsAdmin(user)
|
||||
}
|
||||
|
||||
// CanUpdate checks if a user can update a team from a Only namespace admins can do that.
|
||||
func (tn *TeamNamespace) CanUpdate(user *User) bool {
|
||||
n, err := GetNamespaceByID(tn.NamespaceID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for TeamNamespace: %s", err)
|
||||
return false
|
||||
}
|
||||
return n.IsAdmin(user)
|
||||
}
|
84
pkg/models/team_namespace_test.go
Normal file
84
pkg/models/team_namespace_test.go
Normal file
@ -0,0 +1,84 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTeamNamespace(t *testing.T) {
|
||||
// Dummy team <-> namespace relation
|
||||
tn := TeamNamespace{
|
||||
TeamID: 1,
|
||||
NamespaceID: 1,
|
||||
Right: TeamRightAdmin,
|
||||
}
|
||||
|
||||
dummyuser, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test normal creation
|
||||
assert.True(t, tn.CanCreate(&dummyuser))
|
||||
err = tn.Create(&dummyuser)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test again (should fail)
|
||||
err = tn.Create(&dummyuser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamAlreadyHasAccess(err))
|
||||
|
||||
// Test with invalid team right
|
||||
tn2 := tn
|
||||
tn2.Right = TeamRightUnknown
|
||||
err = tn2.Create(&dummyuser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrInvalidTeamRight(err))
|
||||
|
||||
// Check with inexistant team
|
||||
tn3 := tn
|
||||
tn3.TeamID = 324
|
||||
err = tn3.Create(&dummyuser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
|
||||
// Check with a namespace which does not exist
|
||||
tn4 := tn
|
||||
tn4.NamespaceID = 423
|
||||
err = tn4.Create(&dummyuser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
|
||||
// Check readall
|
||||
teams, err := tn.ReadAll(&dummyuser)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
|
||||
s := reflect.ValueOf(teams)
|
||||
assert.Equal(t, s.Len(), 1)
|
||||
|
||||
// Check readall for a nonexistant namespace
|
||||
_, err = tn4.ReadAll(&dummyuser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
|
||||
// Check with no right to read the namespace
|
||||
nouser := &User{ID: 393}
|
||||
_, err = tn.ReadAll(nouser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNeedToHaveNamespaceReadAccess(err))
|
||||
|
||||
// Delete it
|
||||
assert.True(t, tn.CanDelete(&dummyuser))
|
||||
err = tn.Delete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try deleting with a nonexisting team
|
||||
err = tn3.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
|
||||
// Try deleting with a nonexistant namespace
|
||||
err = tn4.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotHaveAccessToNamespace(err))
|
||||
|
||||
}
|
16
pkg/models/team_namespace_update.go
Normal file
16
pkg/models/team_namespace_update.go
Normal file
@ -0,0 +1,16 @@
|
||||
package models
|
||||
|
||||
// Update updates a team <-> namespace relation
|
||||
func (tl *TeamNamespace) Update() (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := tl.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = x.
|
||||
Where("namespace_id = ? AND team_id = ?", tl.TeamID, tl.TeamID).
|
||||
Cols("right").
|
||||
Update(tl)
|
||||
return
|
||||
}
|
27
pkg/models/team_right.go
Normal file
27
pkg/models/team_right.go
Normal file
@ -0,0 +1,27 @@
|
||||
package models
|
||||
|
||||
// TeamRight defines the rights teams can have for lists/namespaces
|
||||
type TeamRight int
|
||||
|
||||
// define unknown team right
|
||||
const (
|
||||
TeamRightUnknown = -1
|
||||
)
|
||||
|
||||
// Enumerate all the team rights
|
||||
const (
|
||||
// Can read lists in a Team
|
||||
TeamRightRead TeamRight = iota
|
||||
// Can write tasks in a Team like lists and todo tasks. Cannot create new lists.
|
||||
TeamRightWrite
|
||||
// Can manage a list/namespace, can do everything
|
||||
TeamRightAdmin
|
||||
)
|
||||
|
||||
func (r TeamRight) isValid() error {
|
||||
if r != TeamRightAdmin && r != TeamRightRead && r != TeamRightWrite {
|
||||
return ErrInvalidTeamRight{r}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
96
pkg/models/teams.go
Normal file
96
pkg/models/teams.go
Normal file
@ -0,0 +1,96 @@
|
||||
package models
|
||||
|
||||
// Team holds a team object
|
||||
type Team struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"team"`
|
||||
Name string `xorm:"varchar(250) not null" json:"name"`
|
||||
Description string `xorm:"varchar(250)" json:"description"`
|
||||
CreatedByID int64 `xorm:"int(11) not null INDEX" json:"-"`
|
||||
|
||||
CreatedBy User `xorm:"-" json:"created_by"`
|
||||
Members []*TeamUser `xorm:"-" json:"members"`
|
||||
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName makes beautiful table names
|
||||
func (Team) TableName() string {
|
||||
return "teams"
|
||||
}
|
||||
|
||||
// AfterLoad gets the created by user object
|
||||
func (t *Team) AfterLoad() {
|
||||
// Get the owner
|
||||
t.CreatedBy, _ = GetUserByID(t.CreatedByID)
|
||||
|
||||
// Get all members
|
||||
x.Select("*").
|
||||
Table("users").
|
||||
Join("INNER", "team_members", "team_members.user_id = users.id").
|
||||
Where("team_id = ?", t.ID).
|
||||
Find(&t.Members)
|
||||
}
|
||||
|
||||
// TeamMember defines the relationship between a user and a team
|
||||
type TeamMember struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
|
||||
TeamID int64 `xorm:"int(11) not null INDEX" json:"team_id" param:"team"`
|
||||
UserID int64 `xorm:"int(11) not null INDEX" json:"user_id" param:"user"`
|
||||
Admin bool `xorm:"tinyint(1) INDEX" json:"admin"`
|
||||
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName makes beautiful table names
|
||||
func (TeamMember) TableName() string {
|
||||
return "team_members"
|
||||
}
|
||||
|
||||
// TeamUser is the team member type
|
||||
type TeamUser struct {
|
||||
User `xorm:"extends"`
|
||||
Admin bool `json:"admin"`
|
||||
}
|
||||
|
||||
// GetTeamByID gets a team by its ID
|
||||
func GetTeamByID(id int64) (team Team, err error) {
|
||||
if id < 1 {
|
||||
return team, ErrTeamDoesNotExist{id}
|
||||
}
|
||||
|
||||
exists, err := x.Where("id = ?", id).Get(&team)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
return team, ErrTeamDoesNotExist{id}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ReadOne implements the CRUD method to get one team
|
||||
func (t *Team) ReadOne() (err error) {
|
||||
*t, err = GetTeamByID(t.ID)
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll gets all teams the user is part of
|
||||
func (t *Team) ReadAll(user *User) (teams interface{}, err error) {
|
||||
all := []*Team{}
|
||||
err = x.Select("teams.*").
|
||||
Table("teams").
|
||||
Join("INNER", "team_members", "team_members.team_id = teams.id").
|
||||
Where("team_members.user_id = ?", user.ID).
|
||||
Find(&all)
|
||||
|
||||
return all, err
|
||||
}
|
22
pkg/models/teams_create.go
Normal file
22
pkg/models/teams_create.go
Normal file
@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
// Create is the handler to create a team
|
||||
func (t *Team) Create(doer *User) (err error) {
|
||||
// Check if we have a name
|
||||
if t.Name == "" {
|
||||
return ErrTeamNameCannotBeEmpty{}
|
||||
}
|
||||
|
||||
t.CreatedByID = doer.ID
|
||||
t.CreatedBy = *doer
|
||||
|
||||
_, err = x.Insert(t)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Insert the current user as member and admin
|
||||
tm := TeamMember{TeamID: t.ID, UserID: doer.ID, Admin: true}
|
||||
err = tm.Create(doer)
|
||||
return
|
||||
}
|
33
pkg/models/teams_delete.go
Normal file
33
pkg/models/teams_delete.go
Normal file
@ -0,0 +1,33 @@
|
||||
package models
|
||||
|
||||
// Delete deletes a team
|
||||
func (t *Team) Delete() (err error) {
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(t.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the team
|
||||
_, err = x.ID(t.ID).Delete(&Team{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete team members
|
||||
_, err = x.Where("team_id = ?", t.ID).Delete(&TeamMember{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete team <-> namespace relations
|
||||
_, err = x.Where("team_id = ?", t.ID).Delete(&TeamNamespace{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete team <-> lists relations
|
||||
_, err = x.Where("team_id = ?", t.ID).Delete(&TeamList{})
|
||||
return
|
||||
}
|
58
pkg/models/teams_rights.go
Normal file
58
pkg/models/teams_rights.go
Normal file
@ -0,0 +1,58 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// CanCreate checks if the user can create a new team
|
||||
func (t *Team) CanCreate(u *User) bool {
|
||||
// This is currently a dummy function, later on we could imagine global limits etc.
|
||||
return true
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update a team
|
||||
func (t *Team) CanUpdate(u *User) bool {
|
||||
|
||||
// Check if the current user is in the team and has admin rights in it
|
||||
exists, err := x.Where("team_id = ?", t.ID).
|
||||
And("user_id = ?", u.ID).
|
||||
And("admin = ?", true).
|
||||
Get(&TeamMember{})
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for Team: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return exists
|
||||
}
|
||||
|
||||
// CanDelete checks if a user can delete a team
|
||||
func (t *Team) CanDelete(u *User) bool {
|
||||
return t.IsAdmin(u)
|
||||
}
|
||||
|
||||
// IsAdmin returns true when the user is admin of a team
|
||||
func (t *Team) IsAdmin(u *User) bool {
|
||||
exists, err := x.Where("team_id = ?", t.ID).
|
||||
And("user_id = ?", u.ID).
|
||||
And("admin = ?", true).
|
||||
Get(&TeamMember{})
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for Team: %s", err)
|
||||
return false
|
||||
}
|
||||
return exists
|
||||
}
|
||||
|
||||
// CanRead returns true if the user has read access to the team
|
||||
func (t *Team) CanRead(user *User) bool {
|
||||
// Check if the user is in the team
|
||||
exists, err := x.Where("team_id = ?", t.ID).
|
||||
And("user_id = ?", user.ID).
|
||||
Get(&TeamMember{})
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for Team: %s", err)
|
||||
return false
|
||||
}
|
||||
return exists
|
||||
}
|
85
pkg/models/teams_test.go
Normal file
85
pkg/models/teams_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTeam_Create(t *testing.T) {
|
||||
//Dummyteam
|
||||
dummyteam := Team{
|
||||
Name: "Testteam293",
|
||||
Description: "Lorem Ispum",
|
||||
}
|
||||
|
||||
// Doer
|
||||
doer, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Insert it
|
||||
assert.True(t, dummyteam.CanCreate(&doer))
|
||||
err = dummyteam.Create(&doer)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check if it was inserted and we're admin
|
||||
tm := Team{ID: dummyteam.ID}
|
||||
err = tm.ReadOne()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(tm.Members))
|
||||
assert.Equal(t, doer.ID, tm.Members[0].User.ID)
|
||||
assert.True(t, tm.Members[0].Admin)
|
||||
assert.True(t, dummyteam.CanRead(&doer))
|
||||
|
||||
// Get all teams the user is part of
|
||||
ts, err := tm.ReadAll(&doer)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(ts).Kind(), reflect.Slice)
|
||||
s := reflect.ValueOf(ts)
|
||||
assert.Equal(t, 2, s.Len())
|
||||
|
||||
// Check inserting it with an empty name
|
||||
dummyteam.Name = ""
|
||||
err = dummyteam.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamNameCannotBeEmpty(err))
|
||||
|
||||
// update it (still no name, should fail)
|
||||
assert.True(t, dummyteam.CanUpdate(&doer))
|
||||
err = dummyteam.Update()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamNameCannotBeEmpty(err))
|
||||
|
||||
// Update it, this time with a name
|
||||
dummyteam.Name = "Lorem"
|
||||
err = dummyteam.Update()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Delete it
|
||||
assert.True(t, dummyteam.CanDelete(&doer))
|
||||
err = dummyteam.Delete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try deleting a (now) nonexistant team
|
||||
err = dummyteam.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
|
||||
// Try updating the (now) nonexistant team
|
||||
err = dummyteam.Update()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
}
|
||||
|
||||
func TestIsErrInvalidTeamRight(t *testing.T) {
|
||||
assert.NoError(t, TeamRightAdmin.isValid())
|
||||
assert.NoError(t, TeamRightRead.isValid())
|
||||
assert.NoError(t, TeamRightWrite.isValid())
|
||||
|
||||
// Check invalid
|
||||
var tr TeamRight
|
||||
tr = 938
|
||||
err := tr.isValid()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrInvalidTeamRight(err))
|
||||
}
|
25
pkg/models/teams_update.go
Normal file
25
pkg/models/teams_update.go
Normal file
@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
// Update is the handler to create a team
|
||||
func (t *Team) Update() (err error) {
|
||||
// Check if we have a name
|
||||
if t.Name == "" {
|
||||
return ErrTeamNameCannotBeEmpty{}
|
||||
}
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(t.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = x.ID(t.ID).Update(t)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the newly updated team
|
||||
*t, err = GetTeamByID(t.ID)
|
||||
|
||||
return
|
||||
}
|
19
pkg/models/test_fixtures.go
Normal file
19
pkg/models/test_fixtures.go
Normal file
@ -0,0 +1,19 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"gopkg.in/testfixtures.v2"
|
||||
)
|
||||
|
||||
var fixtures *testfixtures.Context
|
||||
|
||||
// InitFixtures initialize test fixtures for a test database
|
||||
func InitFixtures(helper testfixtures.Helper, dir string) (err error) {
|
||||
testfixtures.SkipDatabaseNameCheck(true)
|
||||
fixtures, err = testfixtures.NewFolder(x.DB().DB, helper, dir)
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadFixtures load fixtures for a test database
|
||||
func LoadFixtures() error {
|
||||
return fixtures.Load()
|
||||
}
|
63
pkg/models/unit_tests.go
Normal file
63
pkg/models/unit_tests.go
Normal file
@ -0,0 +1,63 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/mail"
|
||||
"fmt"
|
||||
"github.com/go-xorm/core"
|
||||
"github.com/go-xorm/xorm"
|
||||
"gopkg.in/testfixtures.v2"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// IsTesting is set to true when we're running tests.
|
||||
// We don't have a good solution to test email sending yet, so we disable email sending when testing
|
||||
var IsTesting bool
|
||||
|
||||
// MainTest creates the test engine
|
||||
func MainTest(m *testing.M, pathToRoot string) {
|
||||
var err error
|
||||
fixturesDir := filepath.Join(pathToRoot, "models", "fixtures")
|
||||
if err = createTestEngine(fixturesDir); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating test engine: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
IsTesting = true
|
||||
|
||||
// Start the pseudo mail queue
|
||||
mail.StartMailDaemon()
|
||||
|
||||
// Create test database
|
||||
PrepareTestDatabase()
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func createTestEngine(fixturesDir string) error {
|
||||
var err error
|
||||
x, err = xorm.NewEngine("sqlite3", "file::memory:?cache=shared")
|
||||
//x, err = xorm.NewEngine("sqlite3", "db.db")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
x.SetMapper(core.GonicMapper{})
|
||||
|
||||
// Sync dat shit
|
||||
if err = x.StoreEngine("InnoDB").Sync2(tables...); err != nil {
|
||||
return fmt.Errorf("sync database struct error: %v", err)
|
||||
}
|
||||
|
||||
// Show SQL-Queries if necessary
|
||||
if os.Getenv("UNIT_TESTS_VERBOSE") == "1" {
|
||||
x.ShowSQL(true)
|
||||
}
|
||||
|
||||
return InitFixtures(&testfixtures.SQLite{}, fixturesDir)
|
||||
}
|
||||
|
||||
// PrepareTestDatabase load test fixtures into test database
|
||||
func PrepareTestDatabase() error {
|
||||
return LoadFixtures()
|
||||
}
|
109
pkg/models/user.go
Normal file
109
pkg/models/user.go
Normal file
@ -0,0 +1,109 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/labstack/echo"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// UserLogin Object to recive user credentials in JSON format
|
||||
type UserLogin struct {
|
||||
Username string `json:"username" form:"username"`
|
||||
Password string `json:"password" form:"password"`
|
||||
}
|
||||
|
||||
// User holds information about an user
|
||||
type User struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
|
||||
Username string `xorm:"varchar(250) not null unique" json:"username"`
|
||||
Password string `xorm:"varchar(250) not null" json:"-"`
|
||||
Email string `xorm:"varchar(250)" json:"email"`
|
||||
IsActive bool `json:"-"`
|
||||
|
||||
PasswordResetToken string `xorm:"varchar(450)" json:"-"`
|
||||
EmailConfirmToken string `xorm:"varchar(450)" json:"-"`
|
||||
|
||||
Created int64 `xorm:"created" json:"-"`
|
||||
Updated int64 `xorm:"updated" json:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for users
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
// APIUserPassword represents a user object without timestamps and a json password field.
|
||||
type APIUserPassword struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// APIFormat formats an API User into a normal user struct
|
||||
func (apiUser *APIUserPassword) APIFormat() User {
|
||||
return User{
|
||||
ID: apiUser.ID,
|
||||
Username: apiUser.Username,
|
||||
Password: apiUser.Password,
|
||||
Email: apiUser.Email,
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserByID gets informations about a user by its ID
|
||||
func GetUserByID(id int64) (user User, err error) {
|
||||
// Apparently xorm does otherwise look for all users but return only one, which leads to returing one even if the ID is 0
|
||||
if id < 1 {
|
||||
return User{}, ErrUserDoesNotExist{}
|
||||
}
|
||||
|
||||
return GetUser(User{ID: id})
|
||||
}
|
||||
|
||||
// GetUser gets a user object
|
||||
func GetUser(user User) (userOut User, err error) {
|
||||
userOut = user
|
||||
exists, err := x.Get(&userOut)
|
||||
|
||||
if !exists {
|
||||
return User{}, ErrUserDoesNotExist{UserID: user.ID}
|
||||
}
|
||||
|
||||
return userOut, err
|
||||
}
|
||||
|
||||
// CheckUserCredentials checks user credentials
|
||||
func CheckUserCredentials(u *UserLogin) (User, error) {
|
||||
|
||||
// Check if the user exists
|
||||
user, err := GetUser(User{Username: u.Username})
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Check the users password
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(u.Password))
|
||||
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetCurrentUser returns the current user based on its jwt token
|
||||
func GetCurrentUser(c echo.Context) (user User, err error) {
|
||||
jwtinf := c.Get("user").(*jwt.Token)
|
||||
claims := jwtinf.Claims.(jwt.MapClaims)
|
||||
userID, ok := claims["id"].(float64)
|
||||
if !ok {
|
||||
return user, ErrCouldNotGetUserID{}
|
||||
}
|
||||
user = User{
|
||||
ID: int64(userID),
|
||||
Email: claims["email"].(string),
|
||||
Username: claims["username"].(string),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
154
pkg/models/user_add_update.go
Normal file
154
pkg/models/user_add_update.go
Normal file
@ -0,0 +1,154 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/mail"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// CreateUser creates a new user and inserts it into the database
|
||||
func CreateUser(user User) (newUser User, err error) {
|
||||
|
||||
newUser = user
|
||||
|
||||
// Check if we have all needed informations
|
||||
if newUser.Password == "" || newUser.Username == "" {
|
||||
return User{}, ErrNoUsernamePassword{}
|
||||
}
|
||||
|
||||
// Check if the user already existst with that username
|
||||
exists := true
|
||||
existingUser, err := GetUser(User{Username: newUser.Username})
|
||||
if err != nil {
|
||||
if IsErrUserDoesNotExist(err) {
|
||||
exists = false
|
||||
} else {
|
||||
return User{}, err
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
return User{}, ErrUsernameExists{newUser.ID, newUser.Username}
|
||||
}
|
||||
|
||||
// Check if the user already existst with that email
|
||||
exists = true
|
||||
existingUser, err = GetUser(User{Email: newUser.Email})
|
||||
if err != nil {
|
||||
if IsErrUserDoesNotExist(err) {
|
||||
exists = false
|
||||
} else {
|
||||
return User{}, err
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
return User{}, ErrUserEmailExists{existingUser.ID, existingUser.Email}
|
||||
}
|
||||
|
||||
// Hash the password
|
||||
newUser.Password, err = hashPassword(user.Password)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Generate a confirm token
|
||||
newUser.EmailConfirmToken = utils.MakeRandomString(400)
|
||||
|
||||
// The new user should not be activated until it confirms his mail address
|
||||
newUser.IsActive = false
|
||||
|
||||
// Insert it
|
||||
_, err = x.Insert(newUser)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Get the full new User
|
||||
newUserOut, err := GetUser(newUser)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Create the user's namespace
|
||||
newN := &Namespace{Name: newUserOut.Username, Description: newUserOut.Username + "'s namespace.", Owner: newUserOut}
|
||||
err = newN.Create(&newUserOut)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Dont send a mail if we're testing
|
||||
if IsTesting {
|
||||
return newUserOut, err
|
||||
}
|
||||
|
||||
// Send the user a mail with a link to confirm the mail
|
||||
data := map[string]interface{}{
|
||||
"User": newUserOut,
|
||||
}
|
||||
|
||||
mail.SendMailWithTemplate(user.Email, newUserOut.Username+" + Vikunja = <3", "confirm-email", data)
|
||||
|
||||
return newUserOut, err
|
||||
}
|
||||
|
||||
// HashPassword hashes a password
|
||||
func hashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// UpdateUser updates a user
|
||||
func UpdateUser(user User) (updatedUser User, err error) {
|
||||
|
||||
// Check if it exists
|
||||
theUser, err := GetUserByID(user.ID)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Check if we have at least a username
|
||||
if user.Username == "" {
|
||||
//return User{}, ErrNoUsername{user.ID}
|
||||
user.Username = theUser.Username // Dont change the username if we dont have one
|
||||
}
|
||||
|
||||
user.Password = theUser.Password // set the password to the one in the database to not accedently resetting it
|
||||
|
||||
// Update it
|
||||
_, err = x.Id(user.ID).Update(user)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Get the newly updated user
|
||||
updatedUser, err = GetUserByID(user.ID)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
return updatedUser, err
|
||||
}
|
||||
|
||||
// UpdateUserPassword updates the password of a user
|
||||
func UpdateUserPassword(user *User, newPassword string) (err error) {
|
||||
|
||||
// Get all user details
|
||||
theUser, err := GetUserByID(user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Hash the new password and set it
|
||||
hashed, err := hashPassword(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
theUser.Password = hashed
|
||||
|
||||
// Update it
|
||||
_, err = x.Id(user.ID).Update(theUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
18
pkg/models/user_delete.go
Normal file
18
pkg/models/user_delete.go
Normal file
@ -0,0 +1,18 @@
|
||||
package models
|
||||
|
||||
// DeleteUserByID deletes a user by its ID
|
||||
func DeleteUserByID(id int64, doer *User) error {
|
||||
// Check if the id is 0
|
||||
if id == 0 {
|
||||
return ErrIDCannotBeZero{}
|
||||
}
|
||||
|
||||
// Delete the user
|
||||
_, err := x.Id(id).Delete(&User{})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
25
pkg/models/user_email_confirm.go
Normal file
25
pkg/models/user_email_confirm.go
Normal file
@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
// EmailConfirm holds the token to confirm a mail address
|
||||
type EmailConfirm struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// UserEmailConfirm handles the confirmation of an email address
|
||||
func UserEmailConfirm(c *EmailConfirm) (err error) {
|
||||
|
||||
user := User{}
|
||||
has, err := x.Where("email_confirm_token = ?", c.Token).Get(&user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !has {
|
||||
return ErrInvalidEmailConfirmToken{Token: c.Token}
|
||||
}
|
||||
|
||||
user.IsActive = true
|
||||
user.EmailConfirmToken = ""
|
||||
_, err = x.Where("id = ?", user.ID).Cols("is_active", "email_confirm_token").Update(&user)
|
||||
return
|
||||
}
|
94
pkg/models/user_password_reset.go
Normal file
94
pkg/models/user_password_reset.go
Normal file
@ -0,0 +1,94 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/mail"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
)
|
||||
|
||||
// PasswordReset holds the data to reset a password
|
||||
type PasswordReset struct {
|
||||
Token string `json:"token"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
// UserPasswordReset resets a users password
|
||||
func UserPasswordReset(reset *PasswordReset) (err error) {
|
||||
|
||||
// Check if the password is not empty
|
||||
if reset.NewPassword == "" {
|
||||
return ErrNoUsernamePassword{}
|
||||
}
|
||||
|
||||
// Check if we have a token
|
||||
var user User
|
||||
exists, err := x.Where("password_reset_token = ?", reset.Token).Get(&user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return ErrInvalidPasswordResetToken{Token: reset.Token}
|
||||
}
|
||||
|
||||
// Hash the password
|
||||
user.Password, err = hashPassword(reset.NewPassword)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Save it
|
||||
_, err = x.Where("id = ?", user.ID).Update(&user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Dont send a mail if we're testing
|
||||
if IsTesting {
|
||||
return
|
||||
}
|
||||
|
||||
// Send a mail to the user to notify it his password was changed.
|
||||
data := map[string]interface{}{
|
||||
"User": user,
|
||||
}
|
||||
|
||||
mail.SendMailWithTemplate(user.Email, "Your password on Vikunja was changed", "password-changed", data)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// PasswordTokenRequest defines the request format for password reset resqest
|
||||
type PasswordTokenRequest struct {
|
||||
Username string `json:"user_name"`
|
||||
}
|
||||
|
||||
// RequestUserPasswordResetToken inserts a random token to reset a users password into the databsse
|
||||
func RequestUserPasswordResetToken(tr *PasswordTokenRequest) (err error) {
|
||||
// Check if the user exists
|
||||
user, err := GetUser(User{Username: tr.Username})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a token and save it
|
||||
user.PasswordResetToken = utils.MakeRandomString(400)
|
||||
|
||||
// Save it
|
||||
_, err = x.Where("id = ?", user.ID).Update(&user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Dont send a mail if we're testing
|
||||
if IsTesting {
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"User": user,
|
||||
}
|
||||
|
||||
// Send the user a mail with the reset token
|
||||
mail.SendMailWithTemplate(user.Email, "Reset your password on Vikunja", "reset-password", data)
|
||||
return
|
||||
}
|
27
pkg/models/user_right.go
Normal file
27
pkg/models/user_right.go
Normal file
@ -0,0 +1,27 @@
|
||||
package models
|
||||
|
||||
// UserRight defines the rights users can have for lists/namespaces
|
||||
type UserRight int
|
||||
|
||||
// define unknown user right
|
||||
const (
|
||||
UserRightUnknown = -1
|
||||
)
|
||||
|
||||
// Enumerate all the user rights
|
||||
const (
|
||||
// Can read lists in a User
|
||||
UserRightRead UserRight = iota
|
||||
// Can write tasks in a User like lists and todo tasks. Cannot create new lists.
|
||||
UserRightWrite
|
||||
// Can manage a list/namespace, can do everything
|
||||
UserRightAdmin
|
||||
)
|
||||
|
||||
func (r UserRight) isValid() error {
|
||||
if r != UserRightAdmin && r != UserRightRead && r != UserRightWrite {
|
||||
return ErrInvalidUserRight{r}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
154
pkg/models/user_test.go
Normal file
154
pkg/models/user_test.go
Normal file
@ -0,0 +1,154 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateUser(t *testing.T) {
|
||||
// Create test database
|
||||
//assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
// Get our doer
|
||||
doer, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Our dummy user for testing
|
||||
dummyuser := User{
|
||||
Username: "testuu",
|
||||
Password: "1234",
|
||||
Email: "noone@example.com",
|
||||
}
|
||||
|
||||
// Create a new user
|
||||
createdUser, err := CreateUser(dummyuser)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create a second new user
|
||||
_, err = CreateUser(User{Username: dummyuser.Username + "2", Email: dummyuser.Email + "m", Password: dummyuser.Password})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check if it fails to create the same user again
|
||||
_, err = CreateUser(dummyuser)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Check if it fails to create a user with just the same username
|
||||
_, err = CreateUser(User{Username: dummyuser.Username, Password: "fsdf"})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUsernameExists(err))
|
||||
|
||||
// Check if it fails to create one with the same email
|
||||
_, err = CreateUser(User{Username: "noone", Password: "1234", Email: dummyuser.Email})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserEmailExists(err))
|
||||
|
||||
// Check if it fails to create a user without password and username
|
||||
_, err = CreateUser(User{})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNoUsernamePassword(err))
|
||||
|
||||
// Check if he exists
|
||||
theuser, err := GetUser(createdUser)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get by his ID
|
||||
_, err = GetUserByID(theuser.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Passing 0 as ID should return an error
|
||||
_, err = GetUserByID(0)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
|
||||
// Check the user credentials
|
||||
user, err := CheckUserCredentials(&UserLogin{"testuu", "1234"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "testuu", user.Username)
|
||||
|
||||
// Check wrong password (should also fail)
|
||||
_, err = CheckUserCredentials(&UserLogin{"testuu", "12345"})
|
||||
assert.Error(t, err)
|
||||
|
||||
// Check usercredentials for a nonexistent user (should fail)
|
||||
_, err = CheckUserCredentials(&UserLogin{"dfstestuu", "1234"})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
|
||||
// Update the user
|
||||
uuser, err := UpdateUser(User{ID: theuser.ID, Password: "444444"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, theuser.Password, uuser.Password) // Password should not change
|
||||
assert.Equal(t, theuser.Username, uuser.Username) // Username should not change either
|
||||
|
||||
// Try updating one which does not exist
|
||||
_, err = UpdateUser(User{ID: 99999, Username: "dg"})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
|
||||
// Update a users password
|
||||
newpassword := "55555"
|
||||
err = UpdateUserPassword(&theuser, newpassword)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check if it was changed
|
||||
user, err = CheckUserCredentials(&UserLogin{theuser.Username, newpassword})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check if the searchterm works
|
||||
all, err := ListUsers("test")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, len(all) > 0)
|
||||
|
||||
all, err = ListUsers("")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, len(all) > 0)
|
||||
|
||||
// Try updating the password of a nonexistent user (should fail)
|
||||
err = UpdateUserPassword(&User{ID: 9999}, newpassword)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
|
||||
// Delete it
|
||||
err = DeleteUserByID(theuser.ID, &doer)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try deleting one with ID = 0
|
||||
err = DeleteUserByID(0, &doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrIDCannotBeZero(err))
|
||||
}
|
||||
|
||||
func TestUserPasswordReset(t *testing.T) {
|
||||
// Request a new token
|
||||
tr := &PasswordTokenRequest{
|
||||
Username: "user1",
|
||||
}
|
||||
err := RequestUserPasswordResetToken(tr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get the token / inside the user object
|
||||
userWithToken, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try resetting it
|
||||
reset := &PasswordReset{
|
||||
Token: userWithToken.PasswordResetToken,
|
||||
}
|
||||
|
||||
// Try resetting it without a password
|
||||
reset.NewPassword = ""
|
||||
err = UserPasswordReset(reset)
|
||||
assert.True(t, IsErrNoUsernamePassword(err))
|
||||
|
||||
// Reset it
|
||||
reset.NewPassword = "1234"
|
||||
err = UserPasswordReset(reset)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try resetting it with a wrong token
|
||||
reset.Token = utils.MakeRandomString(400)
|
||||
err = UserPasswordReset(reset)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrInvalidPasswordResetToken(err))
|
||||
}
|
19
pkg/models/users_list.go
Normal file
19
pkg/models/users_list.go
Normal file
@ -0,0 +1,19 @@
|
||||
package models
|
||||
|
||||
// ListUsers returns a list with all users, filtered by an optional searchstring
|
||||
func ListUsers(searchterm string) (users []User, err error) {
|
||||
|
||||
if searchterm == "" {
|
||||
err = x.Find(&users)
|
||||
} else {
|
||||
err = x.
|
||||
Where("username LIKE ?", "%"+searchterm+"%").
|
||||
Find(&users)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return []User{}, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
81
pkg/routes/api/v1/list_by_namespace.go
Normal file
81
pkg/routes/api/v1/list_by_namespace.go
Normal file
@ -0,0 +1,81 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// GetListsByNamespaceID is the web handler to delete a namespace
|
||||
func GetListsByNamespaceID(c echo.Context) error {
|
||||
// swagger:operation GET /namespaces/{namespaceID}/lists namespaces getListsByNamespace
|
||||
// ---
|
||||
// summary: gets all lists belonging to that namespace
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: namespaceID
|
||||
// in: path
|
||||
// description: ID of the namespace
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Namespace"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// Get our namespace
|
||||
namespace, err := getNamespace(c)
|
||||
if err != nil {
|
||||
if models.IsErrNamespaceDoesNotExist(err) {
|
||||
return c.JSON(http.StatusNotFound, models.Message{"Namespace not found."})
|
||||
}
|
||||
if models.IsErrUserDoesNotHaveAccessToNamespace(err) {
|
||||
return c.JSON(http.StatusForbidden, models.Message{"You don't have access to this namespace."})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, models.Message{"An error occurred."})
|
||||
}
|
||||
|
||||
// Get the lists
|
||||
lists, err := models.GetListsByNamespaceID(namespace.ID)
|
||||
if err != nil {
|
||||
if models.IsErrNamespaceDoesNotExist(err) {
|
||||
return c.JSON(http.StatusNotFound, models.Message{"Namespace not found."})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, models.Message{"An error occurred."})
|
||||
}
|
||||
return c.JSON(http.StatusOK, lists)
|
||||
}
|
||||
|
||||
func getNamespace(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
|
||||
}
|
||||
|
||||
// Get the namespace
|
||||
namespace, err = models.GetNamespaceByID(namespaceID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user has acces to that namespace
|
||||
user, err := models.GetCurrentUser(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !namespace.CanRead(&user) {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
69
pkg/routes/api/v1/login.go
Normal file
69
pkg/routes/api/v1/login.go
Normal file
@ -0,0 +1,69 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/labstack/echo"
|
||||
"github.com/spf13/viper"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Login is the login handler
|
||||
func Login(c echo.Context) error {
|
||||
// swagger:operation POST /login user login
|
||||
// ---
|
||||
// summary: Logs a user in. Returns a JWT-Token to authenticate requests
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/UserLogin"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Token"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
u := models.UserLogin{}
|
||||
if err := c.Bind(&u); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"Please provide a username and password."})
|
||||
}
|
||||
|
||||
// Check user
|
||||
user, err := models.CheckUserCredentials(&u)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusUnauthorized, models.Message{"Wrong username or password."})
|
||||
}
|
||||
|
||||
// Create token
|
||||
token := jwt.New(jwt.SigningMethodHS256)
|
||||
|
||||
// Set claims
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
claims["username"] = user.Username
|
||||
claims["email"] = user.Email
|
||||
claims["id"] = user.ID
|
||||
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
|
||||
|
||||
avatar := md5.Sum([]byte(user.Email))
|
||||
claims["avatar"] = hex.EncodeToString(avatar[:])
|
||||
|
||||
// Generate encoded token and send it as response.
|
||||
t, err := token.SignedString([]byte(viper.GetString("service.JWTSecret")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]string{
|
||||
"token": t,
|
||||
})
|
||||
}
|
54
pkg/routes/api/v1/swagger/options.go
Normal file
54
pkg/routes/api/v1/swagger/options.go
Normal file
@ -0,0 +1,54 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
)
|
||||
|
||||
// not actually a response, just a hack to get go-swagger to include definitions
|
||||
// of the various XYZOption structs
|
||||
|
||||
// parameterBodies
|
||||
// swagger:response parameterBodies
|
||||
type swaggerParameterBodies struct {
|
||||
// in:body
|
||||
UserLogin models.UserLogin
|
||||
|
||||
// in:body
|
||||
APIUserPassword models.APIUserPassword
|
||||
|
||||
// in:body
|
||||
List models.List
|
||||
|
||||
// in:body
|
||||
ListTask models.ListTask
|
||||
|
||||
// in:body
|
||||
Namespace models.Namespace
|
||||
|
||||
// in:body
|
||||
Team models.Team
|
||||
|
||||
// in:body
|
||||
TeamMember models.TeamMember
|
||||
|
||||
// in:body
|
||||
TeamList models.TeamList
|
||||
|
||||
// in:body
|
||||
TeamNamespace models.TeamNamespace
|
||||
|
||||
// in:body
|
||||
ListUser models.ListUser
|
||||
|
||||
// in:body
|
||||
NamespaceUser models.NamespaceUser
|
||||
|
||||
// in:body
|
||||
PasswordReset models.PasswordReset
|
||||
|
||||
// in:body
|
||||
PasswordTokenRequest models.PasswordTokenRequest
|
||||
|
||||
// in:body
|
||||
EmailConfirm models.EmailConfirm
|
||||
}
|
111
pkg/routes/api/v1/swagger/responses.go
Normal file
111
pkg/routes/api/v1/swagger/responses.go
Normal file
@ -0,0 +1,111 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
)
|
||||
|
||||
// Message
|
||||
// swagger:response Message
|
||||
type swaggerResponseMessage struct {
|
||||
// in:body
|
||||
Body models.Message `json:"body"`
|
||||
}
|
||||
|
||||
// ================
|
||||
// User definitions
|
||||
// ================
|
||||
|
||||
// User Object
|
||||
// swagger:response User
|
||||
type swaggerResponseUser struct {
|
||||
// in:body
|
||||
Body models.User `json:"body"`
|
||||
}
|
||||
|
||||
// Token
|
||||
// swagger:response Token
|
||||
type swaggerResponseToken struct {
|
||||
// The body message
|
||||
// in:body
|
||||
Body struct {
|
||||
// The token
|
||||
//
|
||||
// Required: true
|
||||
Token string `json:"token"`
|
||||
} `json:"body"`
|
||||
}
|
||||
|
||||
// ================
|
||||
// List definitions
|
||||
// ================
|
||||
|
||||
// List
|
||||
// swagger:response List
|
||||
type swaggerResponseLIst struct {
|
||||
// in:body
|
||||
Body models.List `json:"body"`
|
||||
}
|
||||
|
||||
// ListTask
|
||||
// swagger:response ListTask
|
||||
type swaggerResponseLIstTask struct {
|
||||
// in:body
|
||||
Body models.ListTask `json:"body"`
|
||||
}
|
||||
|
||||
// ================
|
||||
// Namespace definitions
|
||||
// ================
|
||||
|
||||
// Namespace
|
||||
// swagger:response Namespace
|
||||
type swaggerResponseNamespace struct {
|
||||
// in:body
|
||||
Body models.Namespace `json:"body"`
|
||||
}
|
||||
|
||||
// ================
|
||||
// Team definitions
|
||||
// ================
|
||||
|
||||
// Team
|
||||
// swagger:response Team
|
||||
type swaggerResponseTeam struct {
|
||||
// in:body
|
||||
Body models.Team `json:"body"`
|
||||
}
|
||||
|
||||
// TeamMember
|
||||
// swagger:response TeamMember
|
||||
type swaggerResponseTeamMember struct {
|
||||
// in:body
|
||||
Body models.TeamMember `json:"body"`
|
||||
}
|
||||
|
||||
// TeamList
|
||||
// swagger:response TeamList
|
||||
type swaggerResponseTeamList struct {
|
||||
// in:body
|
||||
Body models.TeamList `json:"body"`
|
||||
}
|
||||
|
||||
// TeamNamespace
|
||||
// swagger:response TeamNamespace
|
||||
type swaggerResponseTeamNamespace struct {
|
||||
// in:body
|
||||
Body models.TeamNamespace `json:"body"`
|
||||
}
|
||||
|
||||
// UserList
|
||||
// swagger:response UserList
|
||||
type swaggerResponseUserList struct {
|
||||
// in:body
|
||||
Body models.ListUser `json:"body"`
|
||||
}
|
||||
|
||||
// UserNamespace
|
||||
// swagger:response UserNamespace
|
||||
type swaggerResponseUserNamespace struct {
|
||||
// in:body
|
||||
Body models.NamespaceUser `json:"body"`
|
||||
}
|
915
pkg/routes/api/v1/swaggerdocs.go
Normal file
915
pkg/routes/api/v1/swaggerdocs.go
Normal file
@ -0,0 +1,915 @@
|
||||
package v1
|
||||
|
||||
// swagger:operation DELETE /tasks/{taskID} lists deleteListTask
|
||||
// ---
|
||||
// summary: Deletes a list task
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: taskID
|
||||
// in: path
|
||||
// description: ID of the list task to delete
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "404":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation DELETE /lists/{listID} lists deleteList
|
||||
// ---
|
||||
// summary: Deletes a list with all tasks on it
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: listID
|
||||
// in: path
|
||||
// description: ID of the list to delete
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "404":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation PUT /lists/{listID} lists addListTask
|
||||
// ---
|
||||
// summary: Adds an task to a list
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: listID
|
||||
// in: path
|
||||
// description: ID of the list to use
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ListTask"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ListTask"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation POST /tasks/{taskID} lists updateListTask
|
||||
// ---
|
||||
// summary: Updates a list task
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: taskID
|
||||
// in: path
|
||||
// description: ID of the task to update
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ListTask"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ListTask"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation GET /lists/{listID} lists getList
|
||||
// ---
|
||||
// summary: gets one list with all todo tasks
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: listID
|
||||
// in: path
|
||||
// description: ID of the list to show
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/List"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation PUT /namespaces/{namespaceID}/lists lists addList
|
||||
// ---
|
||||
// summary: Creates a new list owned by the currently logged in user in that namespace
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: namespaceID
|
||||
// in: path
|
||||
// description: ID of the namespace that list should belong to
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/List"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/List"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation POST /lists/{listID} lists upadteList
|
||||
// ---
|
||||
// summary: Updates a list
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: listID
|
||||
// in: path
|
||||
// description: ID of the list to update
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/List"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/List"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation GET /lists lists getLists
|
||||
// ---
|
||||
// summary: Gets all lists owned by the current user
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/List"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation PUT /namespaces namespaces addNamespace
|
||||
// ---
|
||||
// summary: Creates a new namespace owned by the currently logged in user
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/Namespace"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Namespace"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation POST /namespaces/{namespaceID} namespaces upadteNamespace
|
||||
// ---
|
||||
// summary: Updates a namespace
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: namespaceID
|
||||
// in: path
|
||||
// description: ID of the namespace to update
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/Namespace"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Namespace"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation DELETE /namespaces/{namespaceID} namespaces deleteNamespace
|
||||
// ---
|
||||
// summary: Deletes a namespace with all lists
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: namespaceID
|
||||
// in: path
|
||||
// description: ID of the namespace to delete
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "404":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation GET /namespaces/{namespaceID} namespaces getNamespace
|
||||
// ---
|
||||
// summary: gets one namespace with all todo tasks
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: namespaceID
|
||||
// in: path
|
||||
// description: ID of the namespace to show
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Namespace"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation GET /namespaces/{namespaceID}/lists lists getNamespaceLists
|
||||
// ---
|
||||
// summary: gets all lists in that namespace
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: namespaceID
|
||||
// in: path
|
||||
// description: ID of the namespace to show
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/List"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation GET /namespaces namespaces getNamespaces
|
||||
// ---
|
||||
// summary: Get all namespaces the currently logged in user has at least read access
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Namespace"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation GET /teams teams getTeams
|
||||
// ---
|
||||
// summary: gets all teams the current user is part of
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Team"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation GET /teams/{teamID} teams getTeamByID
|
||||
// ---
|
||||
// summary: gets infos about the team
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: teamID
|
||||
// in: path
|
||||
// description: ID of the team
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Team"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation PUT /teams teams createTeam
|
||||
// ---
|
||||
// summary: Creates a team
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/Team"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Team"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation POST /teams/{teamID} teams updateTeam
|
||||
// ---
|
||||
// summary: Updates a team
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: teamID
|
||||
// in: path
|
||||
// description: ID of the team you want to update
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/Team"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Team"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation DELETE /teams/{teamID} teams deleteTeam
|
||||
// ---
|
||||
// summary: Deletes a team
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: teamID
|
||||
// in: path
|
||||
// description: ID of the team you want to delete
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation PUT /teams/{teamID}/members teams addTeamMember
|
||||
// ---
|
||||
// summary: Adds a member to a team
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: teamID
|
||||
// in: path
|
||||
// description: ID of the team you want to add a member to
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/TeamMember"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TeamMember"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation DELETE /teams/{teamID}/members/{userID} teams removeTeamMember
|
||||
// ---
|
||||
// summary: Removes a member from a team
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: teamID
|
||||
// in: path
|
||||
// description: ID of the team you want to delete a member
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: userID
|
||||
// in: path
|
||||
// description: ID of the user you want to remove from the team
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation GET /namespaces/{namespaceID}/teams sharing getNamespaceTeams
|
||||
// ---
|
||||
// summary: gets all teams which have access to that namespace
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: namespaceID
|
||||
// in: path
|
||||
// description: ID of the namespace to show
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Team"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation GET /lists/{listID}/teams sharing getTeamsByList
|
||||
// ---
|
||||
// summary: gets all teams which have access to the list
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: listID
|
||||
// in: path
|
||||
// description: ID of the list to show
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Team"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation PUT /lists/{listID}/teams sharing addTeamToList
|
||||
// ---
|
||||
// summary: adds a team to a list
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: listID
|
||||
// in: path
|
||||
// description: ID of the list to use
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/TeamList"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TeamList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation DELETE /lists/{listID}/teams/{teamID} sharing deleteTeamFromList
|
||||
// ---
|
||||
// summary: removes a team from a list
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: listID
|
||||
// in: path
|
||||
// description: ID of the list to use
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: teamID
|
||||
// in: path
|
||||
// description: ID of the team to remove
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation PUT /namespaces/{namespaceID}/teams sharing giveTeamAccessToNamespace
|
||||
// ---
|
||||
// summary: Gives a team access to a namespace
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: namespaceID
|
||||
// in: path
|
||||
// description: ID of the namespace to use
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/TeamNamespace"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TeamNamespace"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation DELETE /namespaces/{namespaceID}/teams/{teamID} sharing removeTeamFromNamespace
|
||||
// ---
|
||||
// summary: Removes a team from a namespace
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: namespaceID
|
||||
// in: path
|
||||
// description: ID of the namespace to use
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: teamID
|
||||
// in: path
|
||||
// description: ID of the team you want to remove
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation GET /lists/{listID}/users sharing getUsersByList
|
||||
// ---
|
||||
// summary: gets all users which have access to the list
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: listID
|
||||
// in: path
|
||||
// description: ID of the list to show
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/User"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation PUT /lists/{listID}/users sharing addUserToList
|
||||
// ---
|
||||
// summary: adds a user to a list
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: listID
|
||||
// in: path
|
||||
// description: ID of the list to use
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/UserList"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation DELETE /lists/{listID}/users/{userID} sharing deleteUserFromList
|
||||
// ---
|
||||
// summary: removes a user from a list
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: listID
|
||||
// in: path
|
||||
// description: ID of the list to use
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: userID
|
||||
// in: path
|
||||
// description: ID of the user to remove
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation GET /namespaces/{namespaceID}/users sharing getNamespaceUsers
|
||||
// ---
|
||||
// summary: gets all users which have access to that namespace
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: namespaceID
|
||||
// in: path
|
||||
// description: ID of the namespace to show
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/User"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation PUT /namespaces/{namespaceID}/users sharing giveUserAccessToNamespace
|
||||
// ---
|
||||
// summary: Gives a user access to a namespace
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: namespaceID
|
||||
// in: path
|
||||
// description: ID of the namespace to use
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/UserNamespace"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserNamespace"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation DELETE /namespaces/{namespaceID}/users/{userID} sharing removeUserFromNamespace
|
||||
// ---
|
||||
// summary: Removes a user from a namespace
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: namespaceID
|
||||
// in: path
|
||||
// description: ID of the namespace to use
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: userID
|
||||
// in: path
|
||||
// description: ID of the user you want to remove
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation POST /namespaces/{namespaceID}/users/{userID} sharing updateUserAccessToNamespace
|
||||
// ---
|
||||
// summary: Updates a users access to a namespace
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: namespaceID
|
||||
// in: path
|
||||
// description: ID of the namespace to use
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: userID
|
||||
// in: path
|
||||
// description: ID of the user to use
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/NamespaceUser"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/NamespaceUser"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation POST /namespaces/{namespaceID}/teams/{teamID} sharing updateTeamAccessToNamespace
|
||||
// ---
|
||||
// summary: Updates a teams access to a namespace
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: namespaceID
|
||||
// in: path
|
||||
// description: ID of the namespace to use
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: teamID
|
||||
// in: path
|
||||
// description: ID of the team to use
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/TeamNamespace"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TeamNamespace"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation POST /lists/{listID}/users/{userID} sharing updateUserAccessToList
|
||||
// ---
|
||||
// summary: Updates a users access to a list
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: listID
|
||||
// in: path
|
||||
// description: ID of the list to use
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: userID
|
||||
// in: path
|
||||
// description: ID of the user to use
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/UserList"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// swagger:operation POST /lists/{listID}/teams/{teamID} sharing updateTeamAccessToList
|
||||
// ---
|
||||
// summary: Updates a teams access to a list
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: listID
|
||||
// in: path
|
||||
// description: ID of the list to use
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: teamID
|
||||
// in: path
|
||||
// description: ID of the team to use
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/TeamList"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TeamList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "403":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
18
pkg/routes/api/v1/token_check.go
Normal file
18
pkg/routes/api/v1/token_check.go
Normal file
@ -0,0 +1,18 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/labstack/echo"
|
||||
)
|
||||
|
||||
// CheckToken checks prints a message if the token is valid or not. Currently only used for testing pourposes.
|
||||
func CheckToken(c echo.Context) error {
|
||||
|
||||
user := c.Get("user").(*jwt.Token)
|
||||
|
||||
fmt.Println(user.Valid)
|
||||
|
||||
return c.JSON(418, models.Message{"🍵"})
|
||||
}
|
85
pkg/routes/api/v1/user_add_update.go
Normal file
85
pkg/routes/api/v1/user_add_update.go
Normal file
@ -0,0 +1,85 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/routes/crud"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// RegisterUser ...
|
||||
func RegisterUser(c echo.Context) error {
|
||||
|
||||
// swagger:operation POST /register user register
|
||||
// ---
|
||||
// summary: Creates a new user account
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/APIUserPassword"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/User"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
return userAddOrUpdate(c)
|
||||
}
|
||||
|
||||
// userAddOrUpdate is the handler to add a user
|
||||
func userAddOrUpdate(c echo.Context) error {
|
||||
|
||||
// TODO: prevent everyone from updating users
|
||||
|
||||
// Check for Request Content
|
||||
var datUser *models.APIUserPassword
|
||||
|
||||
if err := c.Bind(&datUser); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"No user model provided."})
|
||||
}
|
||||
|
||||
// Check if we have an ID other than the one in the struct
|
||||
id := c.Param("id")
|
||||
if id != "" {
|
||||
// Make int
|
||||
userID, err := strconv.ParseInt(id, 10, 64)
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"Invalid ID."})
|
||||
}
|
||||
datUser.ID = userID
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
var exists = true
|
||||
_, err := models.GetUserByID(datUser.ID)
|
||||
if err != nil {
|
||||
if models.IsErrUserDoesNotExist(err) {
|
||||
exists = false
|
||||
} else {
|
||||
return c.JSON(http.StatusInternalServerError, models.Message{"Could not check if the user exists."})
|
||||
}
|
||||
}
|
||||
|
||||
// Insert or update the user
|
||||
var newUser models.User
|
||||
if exists {
|
||||
newUser, err = models.UpdateUser(datUser.APIFormat())
|
||||
} else {
|
||||
newUser, err = models.CreateUser(datUser.APIFormat())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return crud.HandleHTTPError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, newUser)
|
||||
}
|
46
pkg/routes/api/v1/user_confirm_email.go
Normal file
46
pkg/routes/api/v1/user_confirm_email.go
Normal file
@ -0,0 +1,46 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/routes/crud"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// UserConfirmEmail is the handler to confirm a user email
|
||||
func UserConfirmEmail(c echo.Context) error {
|
||||
// swagger:operation POST /user/confirm user confirmEmail
|
||||
// ---
|
||||
// summary: Confirms a users email address
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EmailConfirm"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "404":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// Check for Request Content
|
||||
var emailConfirm models.EmailConfirm
|
||||
if err := c.Bind(&emailConfirm); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No token provided.")
|
||||
}
|
||||
|
||||
err := models.UserEmailConfirm(&emailConfirm)
|
||||
if err != nil {
|
||||
return crud.HandleHTTPError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, models.Message{"The email was confirmed successfully."})
|
||||
}
|
49
pkg/routes/api/v1/user_delete.go
Normal file
49
pkg/routes/api/v1/user_delete.go
Normal file
@ -0,0 +1,49 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/routes/crud"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// UserDelete is the handler to delete a user
|
||||
func UserDelete(c echo.Context) error {
|
||||
|
||||
// TODO: only allow users to allow itself
|
||||
|
||||
id := c.Param("id")
|
||||
|
||||
// Make int
|
||||
userID, err := strconv.ParseInt(id, 10, 64)
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{"User ID is invalid."})
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
_, err = models.GetUserByID(userID)
|
||||
|
||||
if err != nil {
|
||||
if models.IsErrUserDoesNotExist(err) {
|
||||
return c.JSON(http.StatusNotFound, models.Message{"The user does not exist."})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get user."})
|
||||
}
|
||||
|
||||
// Get the doer options
|
||||
doer, err := models.GetCurrentUser(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete it
|
||||
err = models.DeleteUserByID(userID, &doer)
|
||||
|
||||
if err != nil {
|
||||
return crud.HandleHTTPError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, models.Message{"success"})
|
||||
}
|
44
pkg/routes/api/v1/user_list.go
Normal file
44
pkg/routes/api/v1/user_list.go
Normal file
@ -0,0 +1,44 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/routes/crud"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// UserList gets all information about a user
|
||||
func UserList(c echo.Context) error {
|
||||
|
||||
// swagger:operation GET /users user list
|
||||
// ---
|
||||
// summary: Lists all users
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: s
|
||||
// description: A searchterm to search for a user by its username
|
||||
// in: query
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/User"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
s := c.QueryParam("s")
|
||||
users, err := models.ListUsers(s)
|
||||
if err != nil {
|
||||
return crud.HandleHTTPError(err)
|
||||
}
|
||||
|
||||
// Obfuscate the mailadresses
|
||||
for in := range users {
|
||||
users[in].Email = ""
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, users)
|
||||
}
|
84
pkg/routes/api/v1/user_password_reset.go
Normal file
84
pkg/routes/api/v1/user_password_reset.go
Normal file
@ -0,0 +1,84 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/routes/crud"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// UserResetPassword is the handler to change a users password
|
||||
func UserResetPassword(c echo.Context) error {
|
||||
// swagger:operation POST /user/password/reset user updatePassword
|
||||
// ---
|
||||
// summary: Resets a users password
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/PasswordReset"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "404":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// Check for Request Content
|
||||
var pwReset models.PasswordReset
|
||||
if err := c.Bind(&pwReset); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
|
||||
}
|
||||
|
||||
err := models.UserPasswordReset(&pwReset)
|
||||
if err != nil {
|
||||
return crud.HandleHTTPError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, models.Message{"The password was updated successfully."})
|
||||
}
|
||||
|
||||
// UserRequestResetPasswordToken is the handler to change a users password
|
||||
func UserRequestResetPasswordToken(c echo.Context) error {
|
||||
// swagger:operation POST /user/password/token user requestUpdatePasswordToken
|
||||
// ---
|
||||
// summary: Requests a token to reset a users password
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/PasswordTokenRequest"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "404":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// Check for Request Content
|
||||
var pwTokenReset models.PasswordTokenRequest
|
||||
if err := c.Bind(&pwTokenReset); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No user ID provided.")
|
||||
}
|
||||
|
||||
err := models.RequestUserPasswordResetToken(&pwTokenReset)
|
||||
if err != nil {
|
||||
return crud.HandleHTTPError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, models.Message{"Token was sent."})
|
||||
}
|
38
pkg/routes/api/v1/user_show.go
Normal file
38
pkg/routes/api/v1/user_show.go
Normal file
@ -0,0 +1,38 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/routes/crud"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// UserShow gets all informations about the current user
|
||||
func UserShow(c echo.Context) error {
|
||||
// swagger:operation GET /user user showUser
|
||||
// ---
|
||||
// summary: Shows the current user
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/User"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
userInfos, err := models.GetCurrentUser(c)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Error getting current user.")
|
||||
}
|
||||
|
||||
user, err := models.GetUserByID(userInfos.ID)
|
||||
if err != nil {
|
||||
return crud.HandleHTTPError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, user)
|
||||
}
|
63
pkg/routes/api/v1/user_update_password.go
Normal file
63
pkg/routes/api/v1/user_update_password.go
Normal file
@ -0,0 +1,63 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/routes/crud"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// UserPassword holds a user password. Used to update it.
|
||||
type UserPassword struct {
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
// UserChangePassword is the handler to change a users password
|
||||
func UserChangePassword(c echo.Context) error {
|
||||
// swagger:operation POST /user/password user updatePassword
|
||||
// ---
|
||||
// summary: Shows the current user
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/Password"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "400":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "404":
|
||||
// "$ref": "#/responses/Message"
|
||||
// "500":
|
||||
// "$ref": "#/responses/Message"
|
||||
|
||||
// Check if the user is itself
|
||||
doer, err := models.GetCurrentUser(c)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Error getting current user.")
|
||||
}
|
||||
|
||||
// Check for Request Content
|
||||
var newPW UserPassword
|
||||
if err := c.Bind(&newPW); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
|
||||
}
|
||||
|
||||
// Check the current password
|
||||
if _, err = models.CheckUserCredentials(&models.UserLogin{Username: doer.Username, Password: newPW.OldPassword}); err != nil {
|
||||
return crud.HandleHTTPError(err)
|
||||
}
|
||||
|
||||
// Update the password
|
||||
if err = models.UpdateUserPassword(&doer, newPW.NewPassword); err != nil {
|
||||
return crud.HandleHTTPError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, models.Message{"The password was updated successfully."})
|
||||
}
|
39
pkg/routes/crud/create.go
Normal file
39
pkg/routes/crud/create.go
Normal file
@ -0,0 +1,39 @@
|
||||
package crud
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// CreateWeb is the handler to create an object
|
||||
func (c *WebHandler) CreateWeb(ctx echo.Context) error {
|
||||
// Get our model
|
||||
currentStruct := c.EmptyStruct()
|
||||
|
||||
// Get the object & bind params to struct
|
||||
if err := ParamBinder(currentStruct, ctx); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No or invalid model provided.")
|
||||
}
|
||||
|
||||
// Get the user to pass for later checks
|
||||
currentUser, err := models.GetCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Could not determine the current user.")
|
||||
}
|
||||
|
||||
// Check rights
|
||||
if !currentStruct.CanCreate(¤tUser) {
|
||||
log.Log.Noticef("%s [ID: %d] tried to create while not having the rights for it", currentUser.Username, currentUser.ID)
|
||||
return echo.NewHTTPError(http.StatusForbidden)
|
||||
}
|
||||
|
||||
// Create
|
||||
err = currentStruct.Create(¤tUser)
|
||||
if err != nil {
|
||||
return HandleHTTPError(err)
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusCreated, currentStruct)
|
||||
}
|
37
pkg/routes/crud/delete.go
Normal file
37
pkg/routes/crud/delete.go
Normal file
@ -0,0 +1,37 @@
|
||||
package crud
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// DeleteWeb is the web handler to delete something
|
||||
func (c *WebHandler) DeleteWeb(ctx echo.Context) error {
|
||||
|
||||
// Get our model
|
||||
currentStruct := c.EmptyStruct()
|
||||
|
||||
// Bind params to struct
|
||||
if err := ParamBinder(currentStruct, ctx); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid URL param.")
|
||||
}
|
||||
|
||||
// Check if the user has the right to delete
|
||||
currentUser, err := models.GetCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
}
|
||||
if !currentStruct.CanDelete(¤tUser) {
|
||||
log.Log.Noticef("%s [ID: %d] tried to delete while not having the rights for it", currentUser.Username, currentUser.ID)
|
||||
return echo.NewHTTPError(http.StatusForbidden)
|
||||
}
|
||||
|
||||
err = currentStruct.Delete()
|
||||
if err != nil {
|
||||
return HandleHTTPError(err)
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, models.Message{"Successfully deleted."})
|
||||
}
|
30
pkg/routes/crud/helper.go
Normal file
30
pkg/routes/crud/helper.go
Normal file
@ -0,0 +1,30 @@
|
||||
package crud
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// WebHandler defines the webhandler object
|
||||
// This does web stuff, aka returns json etc. Uses CRUDable Methods to get the data
|
||||
type WebHandler struct {
|
||||
EmptyStruct func() CObject
|
||||
}
|
||||
|
||||
// CObject is the definition of our object, holds the structs
|
||||
type CObject interface {
|
||||
models.CRUDable
|
||||
models.Rights
|
||||
}
|
||||
|
||||
// HandleHTTPError does what it says
|
||||
func HandleHTTPError(err error) *echo.HTTPError {
|
||||
if a, has := err.(models.HTTPErrorProcessor); has {
|
||||
errDetails := a.HTTPError()
|
||||
return echo.NewHTTPError(errDetails.HTTPCode, errDetails)
|
||||
}
|
||||
log.Log.Error(err.Error())
|
||||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
}
|
272
pkg/routes/crud/paramBinder.go
Normal file
272
pkg/routes/crud/paramBinder.go
Normal file
@ -0,0 +1,272 @@
|
||||
package crud
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/labstack/echo"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const paramTagName = "param"
|
||||
|
||||
// ParamBinder binds parameters to a struct.
|
||||
// Currently a working implementation, waiting to implement this officially into echo.
|
||||
func ParamBinder(i interface{}, c echo.Context) (err error) {
|
||||
|
||||
// Default binder
|
||||
db := new(echo.DefaultBinder)
|
||||
if err = db.Bind(i, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
paramNames := c.ParamNames()
|
||||
paramValues := c.ParamValues()
|
||||
paramVars := make(map[string][]string)
|
||||
for in, name := range paramNames {
|
||||
// Hotfix for an echo bug where a param name would show up which dont exist
|
||||
names := strings.Split(name, ",")
|
||||
for _, n := range names {
|
||||
paramVars[n] = append(paramVars[name], paramValues[in])
|
||||
}
|
||||
}
|
||||
|
||||
b := Binder{}
|
||||
err = b.bindData(i, paramVars, paramTagName)
|
||||
|
||||
/*
|
||||
// Our custom magic starts here
|
||||
paramNames := c.ParamNames()
|
||||
paramValues := c.ParamValues()
|
||||
|
||||
v := reflect.ValueOf(i)
|
||||
t := reflect.TypeOf(i)
|
||||
s := reflect.ValueOf(i).Elem()
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
f := s.Field(i)
|
||||
|
||||
// Check if it has a param tag
|
||||
tag := field.Tag.Get(paramTagName)
|
||||
if tag != "" {
|
||||
// If it has one, range over all url parameters to see if we have a match
|
||||
for in, name := range paramNames {
|
||||
// Found match
|
||||
if tag == name {
|
||||
// Put the value of that match in our sruct
|
||||
switch field.Type.Name() {
|
||||
case "int64": // SetInt only accepts int64, so the struct field can only have int64 of int (no int32/16/int...)
|
||||
intParam, err := strconv.ParseInt(paramValues[in], 10, 64)
|
||||
f.SetInt(intParam)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "string":
|
||||
f.SetString(paramValues[in])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//f.SetString("blub")
|
||||
|
||||
}*/
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Binder represents a binder
|
||||
type Binder struct{}
|
||||
|
||||
func (b *Binder) bindData(ptr interface{}, data map[string][]string, tag string) error {
|
||||
typ := reflect.TypeOf(ptr).Elem()
|
||||
val := reflect.ValueOf(ptr).Elem()
|
||||
|
||||
if typ.Kind() != reflect.Struct {
|
||||
return errors.New("Binding element must be a struct")
|
||||
}
|
||||
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
typeField := typ.Field(i)
|
||||
structField := val.Field(i)
|
||||
if !structField.CanSet() {
|
||||
continue
|
||||
}
|
||||
structFieldKind := structField.Kind()
|
||||
inputFieldName := typeField.Tag.Get(tag)
|
||||
|
||||
if inputFieldName == "" {
|
||||
inputFieldName = typeField.Name
|
||||
// If tag is nil, we inspect if the field is a struct.
|
||||
if _, ok := bindUnmarshaler(structField); !ok && structFieldKind == reflect.Struct {
|
||||
err := b.bindData(structField.Addr().Interface(), data, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
inputValue, exists := data[inputFieldName]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// Call this first, in case we're dealing with an alias to an array type
|
||||
if ok, err := unmarshalField(typeField.Type.Kind(), inputValue[0], structField); ok {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
numElems := len(inputValue)
|
||||
if structFieldKind == reflect.Slice && numElems > 0 {
|
||||
sliceOf := structField.Type().Elem().Kind()
|
||||
slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
|
||||
for j := 0; j < numElems; j++ {
|
||||
if err := setWithProperType(sliceOf, inputValue[j], slice.Index(j)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
val.Field(i).Set(slice)
|
||||
} else {
|
||||
if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error {
|
||||
// But also call it here, in case we're dealing with an array of BindUnmarshalers
|
||||
if ok, err := unmarshalField(valueKind, val, structField); ok {
|
||||
return err
|
||||
}
|
||||
|
||||
switch valueKind {
|
||||
case reflect.Int:
|
||||
return setIntField(val, 0, structField)
|
||||
case reflect.Int8:
|
||||
return setIntField(val, 8, structField)
|
||||
case reflect.Int16:
|
||||
return setIntField(val, 16, structField)
|
||||
case reflect.Int32:
|
||||
return setIntField(val, 32, structField)
|
||||
case reflect.Int64:
|
||||
return setIntField(val, 64, structField)
|
||||
case reflect.Uint:
|
||||
return setUintField(val, 0, structField)
|
||||
case reflect.Uint8:
|
||||
return setUintField(val, 8, structField)
|
||||
case reflect.Uint16:
|
||||
return setUintField(val, 16, structField)
|
||||
case reflect.Uint32:
|
||||
return setUintField(val, 32, structField)
|
||||
case reflect.Uint64:
|
||||
return setUintField(val, 64, structField)
|
||||
case reflect.Bool:
|
||||
return setBoolField(val, structField)
|
||||
case reflect.Float32:
|
||||
return setFloatField(val, 32, structField)
|
||||
case reflect.Float64:
|
||||
return setFloatField(val, 64, structField)
|
||||
case reflect.String:
|
||||
structField.SetString(val)
|
||||
default:
|
||||
return errors.New("unknown type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setIntField(value string, bitSize int, field reflect.Value) error {
|
||||
if value == "" {
|
||||
value = "0"
|
||||
}
|
||||
intVal, err := strconv.ParseInt(value, 10, bitSize)
|
||||
if err == nil {
|
||||
field.SetInt(intVal)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func setUintField(value string, bitSize int, field reflect.Value) error {
|
||||
if value == "" {
|
||||
value = "0"
|
||||
}
|
||||
uintVal, err := strconv.ParseUint(value, 10, bitSize)
|
||||
if err == nil {
|
||||
field.SetUint(uintVal)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func setBoolField(value string, field reflect.Value) error {
|
||||
if value == "" {
|
||||
value = "false"
|
||||
}
|
||||
boolVal, err := strconv.ParseBool(value)
|
||||
if err == nil {
|
||||
field.SetBool(boolVal)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func setFloatField(value string, bitSize int, field reflect.Value) error {
|
||||
if value == "" {
|
||||
value = "0.0"
|
||||
}
|
||||
floatVal, err := strconv.ParseFloat(value, bitSize)
|
||||
if err == nil {
|
||||
field.SetFloat(floatVal)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// BindUnmarshaler type
|
||||
type BindUnmarshaler interface {
|
||||
// UnmarshalParam decodes and assigns a value from an form or query param.
|
||||
UnmarshalParam(param string) error
|
||||
}
|
||||
|
||||
// bindUnmarshaler attempts to unmarshal a reflect.Value into a BindUnmarshaler
|
||||
func bindUnmarshaler(field reflect.Value) (BindUnmarshaler, bool) {
|
||||
ptr := reflect.New(field.Type())
|
||||
if ptr.CanInterface() {
|
||||
iface := ptr.Interface()
|
||||
if unmarshaler, ok := iface.(BindUnmarshaler); ok {
|
||||
return unmarshaler, ok
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func unmarshalField(valueKind reflect.Kind, val string, field reflect.Value) (bool, error) {
|
||||
switch valueKind {
|
||||
case reflect.Ptr:
|
||||
return unmarshalFieldPtr(val, field)
|
||||
default:
|
||||
return unmarshalFieldNonPtr(val, field)
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalFieldNonPtr(value string, field reflect.Value) (bool, error) {
|
||||
if unmarshaler, ok := bindUnmarshaler(field); ok {
|
||||
err := unmarshaler.UnmarshalParam(value)
|
||||
field.Set(reflect.ValueOf(unmarshaler).Elem())
|
||||
return true, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func unmarshalFieldPtr(value string, field reflect.Value) (bool, error) {
|
||||
if field.IsNil() {
|
||||
// Initialize the pointer to a nil value
|
||||
field.Set(reflect.New(field.Type().Elem()))
|
||||
}
|
||||
return unmarshalFieldNonPtr(value, field.Elem())
|
||||
}
|
30
pkg/routes/crud/read_all.go
Normal file
30
pkg/routes/crud/read_all.go
Normal file
@ -0,0 +1,30 @@
|
||||
package crud
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ReadAllWeb is the webhandler to get all objects of a type
|
||||
func (c *WebHandler) ReadAllWeb(ctx echo.Context) error {
|
||||
// Get our model
|
||||
currentStruct := c.EmptyStruct()
|
||||
|
||||
currentUser, err := models.GetCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Could not determine the current user.")
|
||||
}
|
||||
|
||||
// Get the object & bind params to struct
|
||||
if err := ParamBinder(currentStruct, ctx); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No or invalid model provided.")
|
||||
}
|
||||
|
||||
lists, err := currentStruct.ReadAll(¤tUser)
|
||||
if err != nil {
|
||||
return HandleHTTPError(err)
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, lists)
|
||||
}
|
38
pkg/routes/crud/read_one.go
Normal file
38
pkg/routes/crud/read_one.go
Normal file
@ -0,0 +1,38 @@
|
||||
package crud
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ReadOneWeb is the webhandler to get one object
|
||||
func (c *WebHandler) ReadOneWeb(ctx echo.Context) error {
|
||||
// Get our model
|
||||
currentStruct := c.EmptyStruct()
|
||||
|
||||
// Get the object & bind params to struct
|
||||
if err := ParamBinder(currentStruct, ctx); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No or invalid model provided.")
|
||||
}
|
||||
|
||||
// Get our object
|
||||
err := currentStruct.ReadOne()
|
||||
if err != nil {
|
||||
return HandleHTTPError(err)
|
||||
}
|
||||
|
||||
// Check rights
|
||||
// We can only check the rights on a full object, which is why we need to check it afterwards
|
||||
currentUser, err := models.GetCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Could not determine the current user.")
|
||||
}
|
||||
if !currentStruct.CanRead(¤tUser) {
|
||||
log.Log.Noticef("%s [ID: %d] tried to read while not having the rights for it", currentUser.Username, currentUser.ID)
|
||||
return echo.NewHTTPError(http.StatusForbidden, "You don't have the right to see this")
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, currentStruct)
|
||||
}
|
38
pkg/routes/crud/update.go
Normal file
38
pkg/routes/crud/update.go
Normal file
@ -0,0 +1,38 @@
|
||||
package crud
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"github.com/labstack/echo"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// UpdateWeb is the webhandler to update an object
|
||||
func (c *WebHandler) UpdateWeb(ctx echo.Context) error {
|
||||
|
||||
// Get our model
|
||||
currentStruct := c.EmptyStruct()
|
||||
|
||||
// Get the object & bind params to struct
|
||||
if err := ParamBinder(currentStruct, ctx); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No or invalid model provided.")
|
||||
}
|
||||
|
||||
// Check if the user has the right to do that
|
||||
currentUser, err := models.GetCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Could not determine the current user.")
|
||||
}
|
||||
if !currentStruct.CanUpdate(¤tUser) {
|
||||
log.Log.Noticef("%s [ID: %d] tried to update while not having the rights for it", currentUser.Username, currentUser.ID)
|
||||
return echo.NewHTTPError(http.StatusForbidden)
|
||||
}
|
||||
|
||||
// Do the update
|
||||
err = currentStruct.Update()
|
||||
if err != nil {
|
||||
return HandleHTTPError(err)
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, currentStruct)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user