1
0

New structure (#7)

This commit is contained in:
konrad
2018-10-31 12:42:38 +00:00
committed by Gitea
parent 3f9fad0e2a
commit 301a4eedda
104 changed files with 326 additions and 280 deletions

73
pkg/config/config.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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."}
}

View 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

View 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

View File

@ -0,0 +1,7 @@
-
team_id: 1
user_id: 1
admin: true
-
team_id: 1
user_id: 2

View File

@ -0,0 +1,5 @@
-
id: 1
name: testteam1
description: Lorem Ipsum
created_by_id: 1

View 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
View 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
}

View 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))
}

View 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
View 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
}

View 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
View 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
View 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
}

View 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
}

View 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
}

View 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)
}

View 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
View 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"`
}

View 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
}

View 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
}

View 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
}

View 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)
}

View 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
View File

@ -0,0 +1,7 @@
package models
import "testing"
func TestMain(m *testing.M) {
MainTest(m, "..")
}

6
pkg/models/message.go Normal file
View 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
View 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
View 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
View 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
}

View 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
}

View 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
}

View 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
}

View 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())
}

View 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
}

View 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"
}

View 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
}

View 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
}

View 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
}

View 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)
}

View 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
View 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
View 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"`
}

View 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
}

View 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
}

View 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
}

View 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)
}

View 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))
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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))
}

View 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"
}

View 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
}

View 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
}

View 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
}

View 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)
}

View 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))
}

View 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
View 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
View 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
}

View 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
}

View 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
}

View 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
View 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))
}

View 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
}

View 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
View 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
View 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
}

View 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
View 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
}

View 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
}

View 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
View 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
View 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
View 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
}

View 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
}

View 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,
})
}

View 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
}

View 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"`
}

View 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"

View 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{"🍵"})
}

View 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)
}

View 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."})
}

View 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"})
}

View 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)
}

View 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."})
}

View 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)
}

View 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
View 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(&currentUser) {
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(&currentUser)
if err != nil {
return HandleHTTPError(err)
}
return ctx.JSON(http.StatusCreated, currentStruct)
}

37
pkg/routes/crud/delete.go Normal file
View 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(&currentUser) {
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
View 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)
}

View 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())
}

View 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(&currentUser)
if err != nil {
return HandleHTTPError(err)
}
return ctx.JSON(http.StatusOK, lists)
}

View 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(&currentUser) {
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
View 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(&currentUser) {
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