New structure (#7)
This commit is contained in:
10
pkg/models/crudable.go
Normal file
10
pkg/models/crudable.go
Normal file
@ -0,0 +1,10 @@
|
||||
package models
|
||||
|
||||
// CRUDable defines the crud methods
|
||||
type CRUDable interface {
|
||||
Create(*User) error
|
||||
ReadOne() error
|
||||
ReadAll(*User) (interface{}, error)
|
||||
Update() error
|
||||
Delete() error
|
||||
}
|
734
pkg/models/error.go
Normal file
734
pkg/models/error.go
Normal file
@ -0,0 +1,734 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HTTPErrorProcessor is executed when the defined error is thrown, it will make sure the user sees an appropriate error message and http status code
|
||||
type HTTPErrorProcessor interface {
|
||||
HTTPError() HTTPError
|
||||
}
|
||||
|
||||
// HTTPError holds informations about an http error
|
||||
type HTTPError struct {
|
||||
HTTPCode int `json:"-"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// =====================
|
||||
// User Operation Errors
|
||||
// =====================
|
||||
|
||||
// ErrUsernameExists represents a "UsernameAlreadyExists" kind of error.
|
||||
type ErrUsernameExists struct {
|
||||
UserID int64
|
||||
Username string
|
||||
}
|
||||
|
||||
// IsErrUsernameExists checks if an error is a ErrUsernameExists.
|
||||
func IsErrUsernameExists(err error) bool {
|
||||
_, ok := err.(ErrUsernameExists)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUsernameExists) Error() string {
|
||||
return fmt.Sprintf("User with that username already exists [user id: %d, username: %s]", err.UserID, err.Username)
|
||||
}
|
||||
|
||||
// ErrorCodeUsernameExists holds the unique world-error code of this error
|
||||
const ErrorCodeUsernameExists = 1001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUsernameExists) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrorCodeUsernameExists, Message: "A user with this username already exists."}
|
||||
}
|
||||
|
||||
// ErrUserEmailExists represents a "UserEmailExists" kind of error.
|
||||
type ErrUserEmailExists struct {
|
||||
UserID int64
|
||||
Email string
|
||||
}
|
||||
|
||||
// IsErrUserEmailExists checks if an error is a ErrUserEmailExists.
|
||||
func IsErrUserEmailExists(err error) bool {
|
||||
_, ok := err.(ErrUserEmailExists)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserEmailExists) Error() string {
|
||||
return fmt.Sprintf("User with that email already exists [user id: %d, email: %s]", err.UserID, err.Email)
|
||||
}
|
||||
|
||||
// ErrorCodeUserEmailExists holds the unique world-error code of this error
|
||||
const ErrorCodeUserEmailExists = 1002
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserEmailExists) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrorCodeUserEmailExists, Message: "A user with this email address already exists."}
|
||||
}
|
||||
|
||||
// ErrNoUsernamePassword represents a "NoUsernamePassword" kind of error.
|
||||
type ErrNoUsernamePassword struct{}
|
||||
|
||||
// IsErrNoUsernamePassword checks if an error is a ErrNoUsernamePassword.
|
||||
func IsErrNoUsernamePassword(err error) bool {
|
||||
_, ok := err.(ErrNoUsernamePassword)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNoUsernamePassword) Error() string {
|
||||
return fmt.Sprintf("No username and password provided")
|
||||
}
|
||||
|
||||
// ErrCodeNoUsernamePassword holds the unique world-error code of this error
|
||||
const ErrCodeNoUsernamePassword = 1004
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrNoUsernamePassword) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeNoUsernamePassword, Message: "Please specify a username and a password."}
|
||||
}
|
||||
|
||||
// ErrUserDoesNotExist represents a "UserDoesNotExist" kind of error.
|
||||
type ErrUserDoesNotExist struct {
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrUserDoesNotExist checks if an error is a ErrUserDoesNotExist.
|
||||
func IsErrUserDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrUserDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("User does not exist [user id: %d]", err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeUserDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeUserDoesNotExist = 1005
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserDoesNotExist) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeUserDoesNotExist, Message: "The user does not exist."}
|
||||
}
|
||||
|
||||
// ErrCouldNotGetUserID represents a "ErrCouldNotGetUserID" kind of error.
|
||||
type ErrCouldNotGetUserID struct{}
|
||||
|
||||
// IsErrCouldNotGetUserID checks if an error is a ErrCouldNotGetUserID.
|
||||
func IsErrCouldNotGetUserID(err error) bool {
|
||||
_, ok := err.(ErrCouldNotGetUserID)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrCouldNotGetUserID) Error() string {
|
||||
return fmt.Sprintf("Could not get user ID")
|
||||
}
|
||||
|
||||
// ErrCodeCouldNotGetUserID holds the unique world-error code of this error
|
||||
const ErrCodeCouldNotGetUserID = 1006
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrCouldNotGetUserID) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeCouldNotGetUserID, Message: "Could not get user id."}
|
||||
}
|
||||
|
||||
// ErrNoPasswordResetToken represents an error where no password reset token exists for that user
|
||||
type ErrNoPasswordResetToken struct {
|
||||
UserID int64
|
||||
}
|
||||
|
||||
func (err ErrNoPasswordResetToken) Error() string {
|
||||
return fmt.Sprintf("No token to reset a password [UserID: %d]", err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeNoPasswordResetToken holds the unique world-error code of this error
|
||||
const ErrCodeNoPasswordResetToken = 1008
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrNoPasswordResetToken) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeNoPasswordResetToken, Message: "No token to reset a user's password provided."}
|
||||
}
|
||||
|
||||
// ErrInvalidPasswordResetToken is an error where the password reset token is invalid
|
||||
type ErrInvalidPasswordResetToken struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
func (err ErrInvalidPasswordResetToken) Error() string {
|
||||
return fmt.Sprintf("Invalid token to reset a password [Token: %s]", err.Token)
|
||||
}
|
||||
|
||||
// ErrCodeInvalidPasswordResetToken holds the unique world-error code of this error
|
||||
const ErrCodeInvalidPasswordResetToken = 1009
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrInvalidPasswordResetToken) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeInvalidPasswordResetToken, Message: "Invalid token to reset a user's password."}
|
||||
}
|
||||
|
||||
// IsErrInvalidPasswordResetToken checks if an error is a ErrInvalidPasswordResetToken.
|
||||
func IsErrInvalidPasswordResetToken(err error) bool {
|
||||
_, ok := err.(ErrInvalidPasswordResetToken)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ErrInvalidEmailConfirmToken is an error where the email confirm token is invalid
|
||||
type ErrInvalidEmailConfirmToken struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
func (err ErrInvalidEmailConfirmToken) Error() string {
|
||||
return fmt.Sprintf("Invalid email confirm token [Token: %s]", err.Token)
|
||||
}
|
||||
|
||||
// ErrCodeInvalidEmailConfirmToken holds the unique world-error code of this error
|
||||
const ErrCodeInvalidEmailConfirmToken = 1010
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrInvalidEmailConfirmToken) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeInvalidEmailConfirmToken, Message: "Invalid email confirm token."}
|
||||
}
|
||||
|
||||
// IsErrInvalidEmailConfirmToken checks if an error is a ErrInvalidEmailConfirmToken.
|
||||
func IsErrInvalidEmailConfirmToken(err error) bool {
|
||||
_, ok := err.(ErrInvalidEmailConfirmToken)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ===================
|
||||
// Empty things errors
|
||||
// ===================
|
||||
|
||||
// ErrIDCannotBeZero represents a "IDCannotBeZero" kind of error. Used if an ID (of something, not defined) is 0 where it should not.
|
||||
type ErrIDCannotBeZero struct{}
|
||||
|
||||
// IsErrIDCannotBeZero checks if an error is a ErrIDCannotBeZero.
|
||||
func IsErrIDCannotBeZero(err error) bool {
|
||||
_, ok := err.(ErrIDCannotBeZero)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrIDCannotBeZero) Error() string {
|
||||
return fmt.Sprintf("ID cannot be empty or 0")
|
||||
}
|
||||
|
||||
// ErrCodeIDCannotBeZero holds the unique world-error code of this error
|
||||
const ErrCodeIDCannotBeZero = 2001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrIDCannotBeZero) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeIDCannotBeZero, Message: "The ID cannot be empty or 0."}
|
||||
}
|
||||
|
||||
// ===========
|
||||
// List errors
|
||||
// ===========
|
||||
|
||||
// ErrListDoesNotExist represents a "ErrListDoesNotExist" kind of error. Used if the list does not exist.
|
||||
type ErrListDoesNotExist struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
// IsErrListDoesNotExist checks if an error is a ErrListDoesNotExist.
|
||||
func IsErrListDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrListDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrListDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("List does not exist [ID: %d]", err.ID)
|
||||
}
|
||||
|
||||
// ErrCodeListDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeListDoesNotExist = 3001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrListDoesNotExist) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeListDoesNotExist, Message: "This list does not exist."}
|
||||
}
|
||||
|
||||
// ErrNeedToHaveListReadAccess represents an error, where the user dont has read access to that List
|
||||
type ErrNeedToHaveListReadAccess struct {
|
||||
ListID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrNeedToHaveListReadAccess checks if an error is a ErrListDoesNotExist.
|
||||
func IsErrNeedToHaveListReadAccess(err error) bool {
|
||||
_, ok := err.(ErrNeedToHaveListReadAccess)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNeedToHaveListReadAccess) Error() string {
|
||||
return fmt.Sprintf("User needs to have read access to that list [ListID: %d, UserID: %d]", err.ListID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeNeedToHaveListReadAccess holds the unique world-error code of this error
|
||||
const ErrCodeNeedToHaveListReadAccess = 3004
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrNeedToHaveListReadAccess) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeNeedToHaveListReadAccess, Message: "You need to have read access to this list."}
|
||||
}
|
||||
|
||||
// ErrListTitleCannotBeEmpty represents a "ErrListTitleCannotBeEmpty" kind of error. Used if the list does not exist.
|
||||
type ErrListTitleCannotBeEmpty struct{}
|
||||
|
||||
// IsErrListTitleCannotBeEmpty checks if an error is a ErrListTitleCannotBeEmpty.
|
||||
func IsErrListTitleCannotBeEmpty(err error) bool {
|
||||
_, ok := err.(ErrListTitleCannotBeEmpty)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrListTitleCannotBeEmpty) Error() string {
|
||||
return fmt.Sprintf("List title cannot be empty.")
|
||||
}
|
||||
|
||||
// ErrCodeListTitleCannotBeEmpty holds the unique world-error code of this error
|
||||
const ErrCodeListTitleCannotBeEmpty = 3005
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrListTitleCannotBeEmpty) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeListTitleCannotBeEmpty, Message: "You must provide at least a list title."}
|
||||
}
|
||||
|
||||
// ================
|
||||
// List task errors
|
||||
// ================
|
||||
|
||||
// ErrListTaskCannotBeEmpty represents a "ErrListDoesNotExist" kind of error. Used if the list does not exist.
|
||||
type ErrListTaskCannotBeEmpty struct{}
|
||||
|
||||
// IsErrListTaskCannotBeEmpty checks if an error is a ErrListDoesNotExist.
|
||||
func IsErrListTaskCannotBeEmpty(err error) bool {
|
||||
_, ok := err.(ErrListTaskCannotBeEmpty)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrListTaskCannotBeEmpty) Error() string {
|
||||
return fmt.Sprintf("List task text cannot be empty.")
|
||||
}
|
||||
|
||||
// ErrCodeListTaskCannotBeEmpty holds the unique world-error code of this error
|
||||
const ErrCodeListTaskCannotBeEmpty = 4001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrListTaskCannotBeEmpty) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeListTaskCannotBeEmpty, Message: "You must provide at least a list task text."}
|
||||
}
|
||||
|
||||
// ErrListTaskDoesNotExist represents a "ErrListDoesNotExist" kind of error. Used if the list does not exist.
|
||||
type ErrListTaskDoesNotExist struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
// IsErrListTaskDoesNotExist checks if an error is a ErrListDoesNotExist.
|
||||
func IsErrListTaskDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrListTaskDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrListTaskDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("List task does not exist. [ID: %d]", err.ID)
|
||||
}
|
||||
|
||||
// ErrCodeListTaskDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeListTaskDoesNotExist = 4002
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrListTaskDoesNotExist) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeListTaskDoesNotExist, Message: "This list task does not exist"}
|
||||
}
|
||||
|
||||
// =================
|
||||
// Namespace errors
|
||||
// =================
|
||||
|
||||
// ErrNamespaceDoesNotExist represents a "ErrNamespaceDoesNotExist" kind of error. Used if the namespace does not exist.
|
||||
type ErrNamespaceDoesNotExist struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
// IsErrNamespaceDoesNotExist checks if an error is a ErrNamespaceDoesNotExist.
|
||||
func IsErrNamespaceDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrNamespaceDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNamespaceDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("Namespace does not exist [ID: %d]", err.ID)
|
||||
}
|
||||
|
||||
// ErrCodeNamespaceDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeNamespaceDoesNotExist = 5001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrNamespaceDoesNotExist) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeNamespaceDoesNotExist, Message: "Namespace not found."}
|
||||
}
|
||||
|
||||
// ErrUserDoesNotHaveAccessToNamespace represents an error, where the user is not the owner of that namespace (used i.e. when deleting a namespace)
|
||||
type ErrUserDoesNotHaveAccessToNamespace struct {
|
||||
NamespaceID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrUserDoesNotHaveAccessToNamespace checks if an error is a ErrNamespaceDoesNotExist.
|
||||
func IsErrUserDoesNotHaveAccessToNamespace(err error) bool {
|
||||
_, ok := err.(ErrUserDoesNotHaveAccessToNamespace)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserDoesNotHaveAccessToNamespace) Error() string {
|
||||
return fmt.Sprintf("User does not have access to the namespace [NamespaceID: %d, UserID: %d]", err.NamespaceID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeUserDoesNotHaveAccessToNamespace holds the unique world-error code of this error
|
||||
const ErrCodeUserDoesNotHaveAccessToNamespace = 5003
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserDoesNotHaveAccessToNamespace) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeUserDoesNotHaveAccessToNamespace, Message: "This user does not have access to the namespace."}
|
||||
}
|
||||
|
||||
// ErrNamespaceNameCannotBeEmpty represents an error, where a namespace name is empty.
|
||||
type ErrNamespaceNameCannotBeEmpty struct {
|
||||
NamespaceID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrNamespaceNameCannotBeEmpty checks if an error is a ErrNamespaceDoesNotExist.
|
||||
func IsErrNamespaceNameCannotBeEmpty(err error) bool {
|
||||
_, ok := err.(ErrNamespaceNameCannotBeEmpty)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNamespaceNameCannotBeEmpty) Error() string {
|
||||
return fmt.Sprintf("Namespace name cannot be empty [NamespaceID: %d, UserID: %d]", err.NamespaceID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeNamespaceNameCannotBeEmpty holds the unique world-error code of this error
|
||||
const ErrCodeNamespaceNameCannotBeEmpty = 5006
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrNamespaceNameCannotBeEmpty) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeNamespaceNameCannotBeEmpty, Message: "The namespace name cannot be empty."}
|
||||
}
|
||||
|
||||
// ErrNeedToHaveNamespaceReadAccess represents an error, where the user is not the owner of that namespace (used i.e. when deleting a namespace)
|
||||
type ErrNeedToHaveNamespaceReadAccess struct {
|
||||
NamespaceID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrNeedToHaveNamespaceReadAccess checks if an error is a ErrNamespaceDoesNotExist.
|
||||
func IsErrNeedToHaveNamespaceReadAccess(err error) bool {
|
||||
_, ok := err.(ErrNeedToHaveNamespaceReadAccess)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrNeedToHaveNamespaceReadAccess) Error() string {
|
||||
return fmt.Sprintf("User does not have access to that namespace [NamespaceID: %d, UserID: %d]", err.NamespaceID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeNeedToHaveNamespaceReadAccess holds the unique world-error code of this error
|
||||
const ErrCodeNeedToHaveNamespaceReadAccess = 5009
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrNeedToHaveNamespaceReadAccess) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeNeedToHaveNamespaceReadAccess, Message: "You need to have namespace read access to do this."}
|
||||
}
|
||||
|
||||
// ErrTeamDoesNotHaveAccessToNamespace represents an error, where the Team is not the owner of that namespace (used i.e. when deleting a namespace)
|
||||
type ErrTeamDoesNotHaveAccessToNamespace struct {
|
||||
NamespaceID int64
|
||||
TeamID int64
|
||||
}
|
||||
|
||||
// IsErrTeamDoesNotHaveAccessToNamespace checks if an error is a ErrNamespaceDoesNotExist.
|
||||
func IsErrTeamDoesNotHaveAccessToNamespace(err error) bool {
|
||||
_, ok := err.(ErrTeamDoesNotHaveAccessToNamespace)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTeamDoesNotHaveAccessToNamespace) Error() string {
|
||||
return fmt.Sprintf("Team does not have access to that namespace [NamespaceID: %d, TeamID: %d]", err.NamespaceID, err.TeamID)
|
||||
}
|
||||
|
||||
// ErrCodeTeamDoesNotHaveAccessToNamespace holds the unique world-error code of this error
|
||||
const ErrCodeTeamDoesNotHaveAccessToNamespace = 5010
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrTeamDoesNotHaveAccessToNamespace) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeTeamDoesNotHaveAccessToNamespace, Message: "You need to have access to this namespace to do this."}
|
||||
}
|
||||
|
||||
// ErrUserAlreadyHasNamespaceAccess represents an error where a user already has access to a namespace
|
||||
type ErrUserAlreadyHasNamespaceAccess struct {
|
||||
UserID int64
|
||||
NamespaceID int64
|
||||
}
|
||||
|
||||
// IsErrUserAlreadyHasNamespaceAccess checks if an error is ErrUserAlreadyHasNamespaceAccess.
|
||||
func IsErrUserAlreadyHasNamespaceAccess(err error) bool {
|
||||
_, ok := err.(ErrUserAlreadyHasNamespaceAccess)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserAlreadyHasNamespaceAccess) Error() string {
|
||||
return fmt.Sprintf("User already has access to that namespace. [User ID: %d, Namespace ID: %d]", err.UserID, err.NamespaceID)
|
||||
}
|
||||
|
||||
// ErrCodeUserAlreadyHasNamespaceAccess holds the unique world-error code of this error
|
||||
const ErrCodeUserAlreadyHasNamespaceAccess = 5011
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserAlreadyHasNamespaceAccess) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusConflict, Code: ErrCodeUserAlreadyHasNamespaceAccess, Message: "This user already has access to this namespace."}
|
||||
}
|
||||
|
||||
// ============
|
||||
// Team errors
|
||||
// ============
|
||||
|
||||
// ErrTeamNameCannotBeEmpty represents an error where a team name is empty.
|
||||
type ErrTeamNameCannotBeEmpty struct {
|
||||
TeamID int64
|
||||
}
|
||||
|
||||
// IsErrTeamNameCannotBeEmpty checks if an error is a ErrNamespaceDoesNotExist.
|
||||
func IsErrTeamNameCannotBeEmpty(err error) bool {
|
||||
_, ok := err.(ErrTeamNameCannotBeEmpty)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTeamNameCannotBeEmpty) Error() string {
|
||||
return fmt.Sprintf("Team name cannot be empty [Team ID: %d]", err.TeamID)
|
||||
}
|
||||
|
||||
// ErrCodeTeamNameCannotBeEmpty holds the unique world-error code of this error
|
||||
const ErrCodeTeamNameCannotBeEmpty = 6001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrTeamNameCannotBeEmpty) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeTeamNameCannotBeEmpty, Message: "The team name cannot be empty"}
|
||||
}
|
||||
|
||||
// ErrTeamDoesNotExist represents an error where a team does not exist
|
||||
type ErrTeamDoesNotExist struct {
|
||||
TeamID int64
|
||||
}
|
||||
|
||||
// IsErrTeamDoesNotExist checks if an error is ErrTeamDoesNotExist.
|
||||
func IsErrTeamDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrTeamDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTeamDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("Team does not exist [Team ID: %d]", err.TeamID)
|
||||
}
|
||||
|
||||
// ErrCodeTeamDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeTeamDoesNotExist = 6002
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrTeamDoesNotExist) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "This team does not exist."}
|
||||
}
|
||||
|
||||
// ErrInvalidTeamRight represents an error where a team right is invalid
|
||||
type ErrInvalidTeamRight struct {
|
||||
Right TeamRight
|
||||
}
|
||||
|
||||
// IsErrInvalidTeamRight checks if an error is ErrInvalidTeamRight.
|
||||
func IsErrInvalidTeamRight(err error) bool {
|
||||
_, ok := err.(ErrInvalidTeamRight)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrInvalidTeamRight) Error() string {
|
||||
return fmt.Sprintf("Team right invalid [Right: %d]", err.Right)
|
||||
}
|
||||
|
||||
// ErrCodeInvalidTeamRight holds the unique world-error code of this error
|
||||
const ErrCodeInvalidTeamRight = 6003
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrInvalidTeamRight) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeInvalidTeamRight, Message: "The team right is invalid."}
|
||||
}
|
||||
|
||||
// ErrTeamAlreadyHasAccess represents an error where a team already has access to a list/namespace
|
||||
type ErrTeamAlreadyHasAccess struct {
|
||||
TeamID int64
|
||||
ID int64
|
||||
}
|
||||
|
||||
// IsErrTeamAlreadyHasAccess checks if an error is ErrTeamAlreadyHasAccess.
|
||||
func IsErrTeamAlreadyHasAccess(err error) bool {
|
||||
_, ok := err.(ErrTeamAlreadyHasAccess)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTeamAlreadyHasAccess) Error() string {
|
||||
return fmt.Sprintf("Team already has access. [Team ID: %d, ID: %d]", err.TeamID, err.ID)
|
||||
}
|
||||
|
||||
// ErrCodeTeamAlreadyHasAccess holds the unique world-error code of this error
|
||||
const ErrCodeTeamAlreadyHasAccess = 6004
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrTeamAlreadyHasAccess) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusConflict, Code: ErrCodeTeamAlreadyHasAccess, Message: "This team already has access."}
|
||||
}
|
||||
|
||||
// ErrUserIsMemberOfTeam represents an error where a user is already member of a team.
|
||||
type ErrUserIsMemberOfTeam struct {
|
||||
TeamID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrUserIsMemberOfTeam checks if an error is ErrUserIsMemberOfTeam.
|
||||
func IsErrUserIsMemberOfTeam(err error) bool {
|
||||
_, ok := err.(ErrUserIsMemberOfTeam)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserIsMemberOfTeam) Error() string {
|
||||
return fmt.Sprintf("User is already a member of that team. [Team ID: %d, User ID: %d]", err.TeamID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeUserIsMemberOfTeam holds the unique world-error code of this error
|
||||
const ErrCodeUserIsMemberOfTeam = 6005
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserIsMemberOfTeam) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusConflict, Code: ErrCodeUserIsMemberOfTeam, Message: "This user is already a member of that team."}
|
||||
}
|
||||
|
||||
// ErrCannotDeleteLastTeamMember represents an error where a user wants to delete the last member of a team (probably himself)
|
||||
type ErrCannotDeleteLastTeamMember struct {
|
||||
TeamID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrCannotDeleteLastTeamMember checks if an error is ErrCannotDeleteLastTeamMember.
|
||||
func IsErrCannotDeleteLastTeamMember(err error) bool {
|
||||
_, ok := err.(ErrCannotDeleteLastTeamMember)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrCannotDeleteLastTeamMember) Error() string {
|
||||
return fmt.Sprintf("Cannot delete last team member. [Team ID: %d, User ID: %d]", err.TeamID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeCannotDeleteLastTeamMember holds the unique world-error code of this error
|
||||
const ErrCodeCannotDeleteLastTeamMember = 6006
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrCannotDeleteLastTeamMember) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeCannotDeleteLastTeamMember, Message: "You cannot delete the last member of a team."}
|
||||
}
|
||||
|
||||
// ErrTeamDoesNotHaveAccessToList represents an error, where the Team is not the owner of that List (used i.e. when deleting a List)
|
||||
type ErrTeamDoesNotHaveAccessToList struct {
|
||||
ListID int64
|
||||
TeamID int64
|
||||
}
|
||||
|
||||
// IsErrTeamDoesNotHaveAccessToList checks if an error is a ErrListDoesNotExist.
|
||||
func IsErrTeamDoesNotHaveAccessToList(err error) bool {
|
||||
_, ok := err.(ErrTeamDoesNotHaveAccessToList)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTeamDoesNotHaveAccessToList) Error() string {
|
||||
return fmt.Sprintf("Team does not have access to the list [ListID: %d, TeamID: %d]", err.ListID, err.TeamID)
|
||||
}
|
||||
|
||||
// ErrCodeTeamDoesNotHaveAccessToList holds the unique world-error code of this error
|
||||
const ErrCodeTeamDoesNotHaveAccessToList = 6007
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrTeamDoesNotHaveAccessToList) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeTeamDoesNotHaveAccessToList, Message: "This team does not have access to the list."}
|
||||
}
|
||||
|
||||
// ====================
|
||||
// User <-> List errors
|
||||
// ====================
|
||||
|
||||
// ErrInvalidUserRight represents an error where a user right is invalid
|
||||
type ErrInvalidUserRight struct {
|
||||
Right UserRight
|
||||
}
|
||||
|
||||
// IsErrInvalidUserRight checks if an error is ErrInvalidUserRight.
|
||||
func IsErrInvalidUserRight(err error) bool {
|
||||
_, ok := err.(ErrInvalidUserRight)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrInvalidUserRight) Error() string {
|
||||
return fmt.Sprintf("User right is invalid [Right: %d]", err.Right)
|
||||
}
|
||||
|
||||
// ErrCodeInvalidUserRight holds the unique world-error code of this error
|
||||
const ErrCodeInvalidUserRight = 7001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrInvalidUserRight) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeInvalidUserRight, Message: "The user right is invalid."}
|
||||
}
|
||||
|
||||
// ErrUserAlreadyHasAccess represents an error where a user already has access to a list/namespace
|
||||
type ErrUserAlreadyHasAccess struct {
|
||||
UserID int64
|
||||
ListID int64
|
||||
}
|
||||
|
||||
// IsErrUserAlreadyHasAccess checks if an error is ErrUserAlreadyHasAccess.
|
||||
func IsErrUserAlreadyHasAccess(err error) bool {
|
||||
_, ok := err.(ErrUserAlreadyHasAccess)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserAlreadyHasAccess) Error() string {
|
||||
return fmt.Sprintf("User already has access to that list. [User ID: %d, List ID: %d]", err.UserID, err.ListID)
|
||||
}
|
||||
|
||||
// ErrCodeUserAlreadyHasAccess holds the unique world-error code of this error
|
||||
const ErrCodeUserAlreadyHasAccess = 7002
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserAlreadyHasAccess) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusConflict, Code: ErrCodeUserAlreadyHasAccess, Message: "This user already has access to this list."}
|
||||
}
|
||||
|
||||
// ErrUserDoesNotHaveAccessToList represents an error, where the user is not the owner of that List (used i.e. when deleting a List)
|
||||
type ErrUserDoesNotHaveAccessToList struct {
|
||||
ListID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrUserDoesNotHaveAccessToList checks if an error is a ErrListDoesNotExist.
|
||||
func IsErrUserDoesNotHaveAccessToList(err error) bool {
|
||||
_, ok := err.(ErrUserDoesNotHaveAccessToList)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserDoesNotHaveAccessToList) Error() string {
|
||||
return fmt.Sprintf("User does not have access to the list [ListID: %d, UserID: %d]", err.ListID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeUserDoesNotHaveAccessToList holds the unique world-error code of this error
|
||||
const ErrCodeUserDoesNotHaveAccessToList = 7003
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserDoesNotHaveAccessToList) HTTPError() HTTPError {
|
||||
return HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeUserDoesNotHaveAccessToList, Message: "This user does not have access to the list."}
|
||||
}
|
18
pkg/models/fixtures/list.yml
Normal file
18
pkg/models/fixtures/list.yml
Normal file
@ -0,0 +1,18 @@
|
||||
-
|
||||
id: 1
|
||||
title: Test1
|
||||
description: Lorem Ipsum
|
||||
owner_id: 1
|
||||
namespace_id: 1
|
||||
-
|
||||
id: 2
|
||||
title: Test2
|
||||
description: Lorem Ipsum
|
||||
owner_id: 3
|
||||
namespace_id: 1
|
||||
-
|
||||
id: 3
|
||||
title: Test3
|
||||
description: Lorem Ipsum
|
||||
owner_id: 3
|
||||
namespace_id: 2
|
10
pkg/models/fixtures/namespaces.yml
Normal file
10
pkg/models/fixtures/namespaces.yml
Normal file
@ -0,0 +1,10 @@
|
||||
-
|
||||
id: 1
|
||||
name: testnamespace
|
||||
description: Lorem Ipsum
|
||||
owner_id: 1
|
||||
-
|
||||
id: 2
|
||||
name: testnamespace2
|
||||
description: Lorem Ipsum
|
||||
owner_id: 2
|
7
pkg/models/fixtures/team_members.yml
Normal file
7
pkg/models/fixtures/team_members.yml
Normal file
@ -0,0 +1,7 @@
|
||||
-
|
||||
team_id: 1
|
||||
user_id: 1
|
||||
admin: true
|
||||
-
|
||||
team_id: 1
|
||||
user_id: 2
|
5
pkg/models/fixtures/teams.yml
Normal file
5
pkg/models/fixtures/teams.yml
Normal file
@ -0,0 +1,5 @@
|
||||
-
|
||||
id: 1
|
||||
name: testteam1
|
||||
description: Lorem Ipsum
|
||||
created_by_id: 1
|
15
pkg/models/fixtures/users.yml
Normal file
15
pkg/models/fixtures/users.yml
Normal file
@ -0,0 +1,15 @@
|
||||
-
|
||||
id: 1
|
||||
username: 'user1'
|
||||
password: '1234'
|
||||
email: 'johndoe@example.com'
|
||||
-
|
||||
id: 2
|
||||
username: 'user2'
|
||||
password: '1234'
|
||||
email: 'johndoe@example.com'
|
||||
-
|
||||
id: 3
|
||||
username: 'user3'
|
||||
password: '1234'
|
||||
email: 'johndoe@example.com'
|
143
pkg/models/list.go
Normal file
143
pkg/models/list.go
Normal file
@ -0,0 +1,143 @@
|
||||
package models
|
||||
|
||||
// List represents a list of tasks
|
||||
type List struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"list"`
|
||||
Title string `xorm:"varchar(250)" json:"title"`
|
||||
Description string `xorm:"varchar(1000)" json:"description"`
|
||||
OwnerID int64 `xorm:"int(11) INDEX" json:"-"`
|
||||
NamespaceID int64 `xorm:"int(11) INDEX" json:"-" param:"namespace"`
|
||||
|
||||
Owner User `xorm:"-" json:"owner"`
|
||||
Tasks []*ListTask `xorm:"-" json:"tasks"`
|
||||
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// GetListsByNamespaceID gets all lists in a namespace
|
||||
func GetListsByNamespaceID(nID int64) (lists []*List, err error) {
|
||||
err = x.Where("namespace_id = ?", nID).Find(&lists)
|
||||
return lists, err
|
||||
}
|
||||
|
||||
// ReadAll gets all lists a user has access to
|
||||
func (l *List) ReadAll(u *User) (interface{}, error) {
|
||||
lists := []*List{}
|
||||
fullUser, err := GetUserByID(u.ID)
|
||||
if err != nil {
|
||||
return lists, err
|
||||
}
|
||||
|
||||
// Gets all Lists where the user is either owner or in a team which has access to the list
|
||||
// Or in a team which has namespace read access
|
||||
err = x.Select("l.*").
|
||||
Table("list").
|
||||
Alias("l").
|
||||
Join("INNER", []string{"namespaces", "n"}, "l.namespace_id = n.id").
|
||||
Join("LEFT", []string{"team_namespaces", "tn"}, "tn.namespace_id = n.id").
|
||||
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tn.team_id").
|
||||
Join("LEFT", []string{"team_list", "tl"}, "l.id = tl.list_id").
|
||||
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
|
||||
Join("LEFT", []string{"users_list", "ul"}, "ul.list_id = l.id").
|
||||
Join("LEFT", []string{"users_namespace", "un"}, "un.namespace_id = l.namespace_id").
|
||||
Where("tm.user_id = ?", fullUser.ID).
|
||||
Or("tm2.user_id = ?", fullUser.ID).
|
||||
Or("l.owner_id = ?", fullUser.ID).
|
||||
Or("ul.user_id = ?", fullUser.ID).
|
||||
Or("un.user_id = ?", fullUser.ID).
|
||||
GroupBy("l.id").
|
||||
Find(&lists)
|
||||
|
||||
// Add more list details
|
||||
AddListDetails(lists)
|
||||
|
||||
return lists, err
|
||||
}
|
||||
|
||||
// ReadOne gets one list by its ID
|
||||
func (l *List) ReadOne() (err error) {
|
||||
err = l.GetSimpleByID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get list tasks
|
||||
l.Tasks, err = GetTasksByListID(l.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get list owner
|
||||
l.Owner, err = GetUserByID(l.OwnerID)
|
||||
return
|
||||
}
|
||||
|
||||
// GetSimpleByID gets a list with only the basic items, aka no tasks or user objects. Returns an error if the list does not exist.
|
||||
func (l *List) GetSimpleByID() (err error) {
|
||||
if l.ID < 1 {
|
||||
return ErrListDoesNotExist{ID: l.ID}
|
||||
}
|
||||
|
||||
// We need to re-init our list object, because otherwise xorm creates a "where for every item in that list object,
|
||||
// leading to not finding anything if the id is good, but for example the title is different.
|
||||
id := l.ID
|
||||
*l = List{}
|
||||
exists, err := x.Where("id = ?", id).Get(l)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return ErrListDoesNotExist{ID: l.ID}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// AddListDetails adds owner user objects and list tasks to all lists in the slice
|
||||
func AddListDetails(lists []*List) (err error) {
|
||||
var listIDs []int64
|
||||
var ownerIDs []int64
|
||||
for _, l := range lists {
|
||||
listIDs = append(listIDs, l.ID)
|
||||
ownerIDs = append(ownerIDs, l.OwnerID)
|
||||
}
|
||||
|
||||
// Get all tasks
|
||||
ts := []*ListTask{}
|
||||
err = x.In("list_id", listIDs).Find(&ts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get all list owners
|
||||
owners := []*User{}
|
||||
err = x.In("id", ownerIDs).Find(&owners)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Build it all into the lists slice
|
||||
for in, list := range lists {
|
||||
// Owner
|
||||
for _, owner := range owners {
|
||||
if list.OwnerID == owner.ID {
|
||||
lists[in].Owner = *owner
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Tasks
|
||||
for _, task := range ts {
|
||||
if task.ListID == list.ID {
|
||||
lists[in].Tasks = append(lists[in].Tasks, task)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
85
pkg/models/list_create_test.go
Normal file
85
pkg/models/list_create_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestList_Create(t *testing.T) {
|
||||
// Create test database
|
||||
//assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
// Get our doer
|
||||
doer, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Dummy list for testing
|
||||
dummylist := List{
|
||||
Title: "test",
|
||||
Description: "Lorem Ipsum",
|
||||
NamespaceID: 1,
|
||||
}
|
||||
|
||||
// Check if the user can create
|
||||
assert.True(t, dummylist.CanCreate(&doer))
|
||||
|
||||
// Create it
|
||||
err = dummylist.Create(&doer)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get the list
|
||||
newdummy := List{ID: dummylist.ID}
|
||||
err = newdummy.ReadOne()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dummylist.Title, newdummy.Title)
|
||||
assert.Equal(t, dummylist.Description, newdummy.Description)
|
||||
assert.Equal(t, dummylist.OwnerID, doer.ID)
|
||||
|
||||
// Check if the user can see it
|
||||
assert.True(t, dummylist.CanRead(&doer))
|
||||
|
||||
// Try updating a list
|
||||
assert.True(t, dummylist.CanUpdate(&doer))
|
||||
dummylist.Description = "Lorem Ipsum dolor sit amet."
|
||||
err = dummylist.Update()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Delete it
|
||||
assert.True(t, dummylist.CanDelete(&doer))
|
||||
|
||||
err = dummylist.Delete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try updating a nonexistant list
|
||||
err = dummylist.Update()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListDoesNotExist(err))
|
||||
|
||||
// Delete a nonexistant list
|
||||
err = dummylist.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListDoesNotExist(err))
|
||||
|
||||
// Check failing with no title
|
||||
list2 := List{}
|
||||
err = list2.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListTitleCannotBeEmpty(err))
|
||||
|
||||
// Check creation with a nonexistant namespace
|
||||
list3 := List{
|
||||
Title: "test",
|
||||
Description: "Lorem Ipsum",
|
||||
NamespaceID: 876694,
|
||||
}
|
||||
|
||||
err = list3.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
|
||||
// Try creating with a nonexistant owner
|
||||
nUser := &User{ID: 9482385}
|
||||
err = dummylist.Create(nUser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
}
|
57
pkg/models/list_create_update.go
Normal file
57
pkg/models/list_create_update.go
Normal file
@ -0,0 +1,57 @@
|
||||
package models
|
||||
|
||||
// CreateOrUpdateList updates a list or creates it if it doesn't exist
|
||||
func CreateOrUpdateList(list *List) (err error) {
|
||||
|
||||
// Check we have at least a title
|
||||
if list.Title == "" {
|
||||
return ErrListTitleCannotBeEmpty{}
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
if list.NamespaceID != 0 {
|
||||
_, err = GetNamespaceByID(list.NamespaceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if list.ID == 0 {
|
||||
_, err = x.Insert(list)
|
||||
} else {
|
||||
_, err = x.ID(list.ID).Update(list)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = list.ReadOne()
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// Update implements the update method of CRUDable
|
||||
func (l *List) Update() (err error) {
|
||||
// Check if it exists
|
||||
if err = l.GetSimpleByID(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return CreateOrUpdateList(l)
|
||||
}
|
||||
|
||||
// Create implements the create method of CRUDable
|
||||
func (l *List) Create(doer *User) (err error) {
|
||||
// Check rights
|
||||
u, err := GetUserByID(doer.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
l.OwnerID = u.ID
|
||||
l.Owner.ID = u.ID
|
||||
l.ID = 0 // Otherwise only the first time a new list would be created
|
||||
|
||||
return CreateOrUpdateList(l)
|
||||
}
|
19
pkg/models/list_delete.go
Normal file
19
pkg/models/list_delete.go
Normal file
@ -0,0 +1,19 @@
|
||||
package models
|
||||
|
||||
// Delete implements the delete method of CRUDable
|
||||
func (l *List) Delete() (err error) {
|
||||
// Check if the list exists
|
||||
if err = l.GetSimpleByID(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the list
|
||||
_, err = x.ID(l.ID).Delete(&List{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete all todotasks on that list
|
||||
_, err = x.Where("list_id = ?", l.ID).Delete(&ListTask{})
|
||||
return
|
||||
}
|
33
pkg/models/list_read_test.go
Normal file
33
pkg/models/list_read_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestList_ReadAll(t *testing.T) {
|
||||
// Create test database
|
||||
//assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
// Get all lists for our namespace
|
||||
lists, err := GetListsByNamespaceID(1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(lists), 2)
|
||||
|
||||
// Get all lists our user has access to
|
||||
u, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
lists2 := List{}
|
||||
lists3, err := lists2.ReadAll(&u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(lists3).Kind(), reflect.Slice)
|
||||
s := reflect.ValueOf(lists3)
|
||||
assert.Equal(t, s.Len(), 1)
|
||||
|
||||
// Try getting lists for a nonexistant user
|
||||
_, err = lists2.ReadAll(&User{ID: 984234})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
}
|
115
pkg/models/list_rights.go
Normal file
115
pkg/models/list_rights.go
Normal file
@ -0,0 +1,115 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// IsAdmin returns whether the user has admin rights on the list or not
|
||||
func (l *List) IsAdmin(u *User) bool {
|
||||
// Owners are always admins
|
||||
if l.Owner.ID == u.ID {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check individual rights
|
||||
if l.checkListUserRight(u, UserRightAdmin) {
|
||||
return true
|
||||
}
|
||||
|
||||
return l.checkListTeamRight(u, TeamRightAdmin)
|
||||
}
|
||||
|
||||
// CanWrite return whether the user can write on that list or not
|
||||
func (l *List) CanWrite(user *User) bool {
|
||||
// Admins always have write access
|
||||
if l.IsAdmin(user) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check individual rights
|
||||
if l.checkListUserRight(user, UserRightWrite) {
|
||||
return true
|
||||
}
|
||||
|
||||
return l.checkListTeamRight(user, TeamRightWrite)
|
||||
}
|
||||
|
||||
// CanRead checks if a user has read access to a list
|
||||
func (l *List) CanRead(user *User) bool {
|
||||
// Admins always have read access
|
||||
if l.IsAdmin(user) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check individual rights
|
||||
if l.checkListUserRight(user, UserRightRead) {
|
||||
return true
|
||||
}
|
||||
|
||||
return l.checkListTeamRight(user, TeamRightRead)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a list
|
||||
func (l *List) CanDelete(doer *User) bool {
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
log.Log.Error("Error occurred during CanDelete for List: %s", err)
|
||||
return false
|
||||
}
|
||||
return l.IsAdmin(doer)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update a list
|
||||
func (l *List) CanUpdate(doer *User) bool {
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for List: %s", err)
|
||||
return false
|
||||
}
|
||||
return l.CanWrite(doer)
|
||||
}
|
||||
|
||||
// CanCreate checks if the user can update a list
|
||||
func (l *List) CanCreate(doer *User) bool {
|
||||
// A user can create a list if he has write access to the namespace
|
||||
n, _ := GetNamespaceByID(l.NamespaceID)
|
||||
return n.CanWrite(doer)
|
||||
}
|
||||
|
||||
func (l *List) checkListTeamRight(user *User, r TeamRight) bool {
|
||||
exists, err := x.Select("l.*").
|
||||
Table("list").
|
||||
Alias("l").
|
||||
Join("LEFT", []string{"team_namespaces", "tn"}, " l.namespace_id = tn.namespace_id").
|
||||
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tn.team_id").
|
||||
Join("LEFT", []string{"team_list", "tl"}, "l.id = tl.list_id").
|
||||
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
|
||||
Where("((tm.user_id = ? AND tn.right = ?) OR (tm2.user_id = ? AND tl.right = ?)) AND l.id = ?",
|
||||
user.ID, r, user.ID, r, l.ID).
|
||||
Exist(&List{})
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during checkListTeamRight for List: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return exists
|
||||
}
|
||||
|
||||
func (l *List) checkListUserRight(user *User, r UserRight) bool {
|
||||
exists, err := x.Select("l.*").
|
||||
Table("list").
|
||||
Alias("l").
|
||||
Join("LEFT", []string{"users_namespace", "un"}, "un.namespace_id = l.namespace_id").
|
||||
Join("LEFT", []string{"users_list", "ul"}, "ul.list_id = l.id").
|
||||
Join("LEFT", []string{"namespaces", "n"}, "n.id = l.namespace_id").
|
||||
Where("((ul.user_id = ? AND ul.right = ?) "+
|
||||
"OR (un.user_id = ? AND un.right = ?) "+
|
||||
"OR n.owner_id = ?)"+
|
||||
"AND l.id = ?",
|
||||
user.ID, r, user.ID, r, user.ID, l.ID).
|
||||
Exist(&List{})
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during checkListUserRight for List: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return exists
|
||||
}
|
98
pkg/models/list_tasks.go
Normal file
98
pkg/models/list_tasks.go
Normal file
@ -0,0 +1,98 @@
|
||||
package models
|
||||
|
||||
// ListTask represents an task in a todolist
|
||||
type ListTask struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"listtask"`
|
||||
Text string `xorm:"varchar(250)" json:"text"`
|
||||
Description string `xorm:"varchar(250)" json:"description"`
|
||||
Done bool `xorm:"INDEX" json:"done"`
|
||||
DueDateUnix int64 `xorm:"int(11) INDEX" json:"dueDate"`
|
||||
ReminderUnix int64 `xorm:"int(11) INDEX" json:"reminderDate"`
|
||||
CreatedByID int64 `xorm:"int(11)" json:"-"` // ID of the user who put that task on the list
|
||||
ListID int64 `xorm:"int(11) INDEX" json:"listID" param:"list"`
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CreatedBy User `xorm:"-" json:"createdBy"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for listtasks
|
||||
func (ListTask) TableName() string {
|
||||
return "tasks"
|
||||
}
|
||||
|
||||
// GetTasksByListID gets all todotasks for a list
|
||||
func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
|
||||
err = x.Where("list_id = ?", listID).Find(&tasks)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// No need to iterate over users if the list doesn't has tasks
|
||||
if len(tasks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Get all users and put them into the array
|
||||
var userIDs []int64
|
||||
for _, i := range tasks {
|
||||
found := false
|
||||
for _, u := range userIDs {
|
||||
if i.CreatedByID == u {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
userIDs = append(userIDs, i.CreatedByID)
|
||||
}
|
||||
}
|
||||
|
||||
var users []User
|
||||
err = x.In("id", userIDs).Find(&users)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for in, task := range tasks {
|
||||
for _, u := range users {
|
||||
if task.CreatedByID == u.ID {
|
||||
tasks[in].CreatedBy = u
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// obsfucate the user password
|
||||
tasks[in].CreatedBy.Password = ""
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetListTaskByID returns all tasks a list has
|
||||
func GetListTaskByID(listTaskID int64) (listTask ListTask, err error) {
|
||||
if listTaskID < 1 {
|
||||
return ListTask{}, ErrListTaskDoesNotExist{listTaskID}
|
||||
}
|
||||
|
||||
exists, err := x.ID(listTaskID).Get(&listTask)
|
||||
if err != nil {
|
||||
return ListTask{}, err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return ListTask{}, ErrListTaskDoesNotExist{listTaskID}
|
||||
}
|
||||
|
||||
u, err := GetUserByID(listTask.CreatedByID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
listTask.CreatedBy = u
|
||||
|
||||
return
|
||||
}
|
56
pkg/models/list_tasks_create_update.go
Normal file
56
pkg/models/list_tasks_create_update.go
Normal file
@ -0,0 +1,56 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/imdario/mergo"
|
||||
)
|
||||
|
||||
// Create is the implementation to create a list task
|
||||
func (i *ListTask) Create(doer *User) (err error) {
|
||||
i.ID = 0
|
||||
|
||||
// Check if we have at least a text
|
||||
if i.Text == "" {
|
||||
return ErrListTaskCannotBeEmpty{}
|
||||
}
|
||||
|
||||
// Check if the list exists
|
||||
l := &List{ID: i.ListID}
|
||||
if err = l.GetSimpleByID(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
u, err := GetUserByID(doer.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i.CreatedByID = u.ID
|
||||
i.CreatedBy = u
|
||||
_, err = x.Cols("text", "description", "done", "due_date_unix", "reminder_unix", "created_by_id", "list_id", "created", "updated").Insert(i)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update updates a list task
|
||||
func (i *ListTask) Update() (err error) {
|
||||
// Check if the task exists
|
||||
ot, err := GetListTaskByID(i.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// For whatever reason, xorm dont detect if done is updated, so we need to update this every time by hand
|
||||
// Which is why we merge the actual task struct with the one we got from the
|
||||
// The user struct overrides values in the actual one.
|
||||
if err := mergo.Merge(&ot, i, mergo.WithOverride); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// And because a false is considered to be a null value, we need to explicitly check that case here.
|
||||
if i.Done == false {
|
||||
ot.Done = false
|
||||
}
|
||||
|
||||
_, err = x.ID(i.ID).Cols("text", "description", "done", "due_date_unix", "reminder_unix").Update(ot)
|
||||
*i = ot
|
||||
return
|
||||
}
|
14
pkg/models/list_tasks_delete.go
Normal file
14
pkg/models/list_tasks_delete.go
Normal file
@ -0,0 +1,14 @@
|
||||
package models
|
||||
|
||||
// Delete implements the delete method for listTask
|
||||
func (i *ListTask) Delete() (err error) {
|
||||
|
||||
// Check if it exists
|
||||
_, err = GetListTaskByID(i.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = x.ID(i.ID).Delete(ListTask{})
|
||||
return
|
||||
}
|
43
pkg/models/list_tasks_rights.go
Normal file
43
pkg/models/list_tasks_rights.go
Normal file
@ -0,0 +1,43 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// CanDelete checks if the user can delete an task
|
||||
func (i *ListTask) CanDelete(doer *User) bool {
|
||||
// Get the task
|
||||
lI, err := GetListTaskByID(i.ID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanDelete for ListTask: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// A user can delete an task if he has write acces to its list
|
||||
l := &List{ID: lI.ListID}
|
||||
l.ReadOne()
|
||||
return l.CanWrite(doer)
|
||||
}
|
||||
|
||||
// CanUpdate determines if a user has the right to update a list task
|
||||
func (i *ListTask) CanUpdate(doer *User) bool {
|
||||
// Get the task
|
||||
lI, err := GetListTaskByID(i.ID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanDelete for ListTask: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// A user can update an task if he has write acces to its list
|
||||
l := &List{ID: lI.ListID}
|
||||
l.ReadOne()
|
||||
return l.CanWrite(doer)
|
||||
}
|
||||
|
||||
// CanCreate determines if a user has the right to create a list task
|
||||
func (i *ListTask) CanCreate(doer *User) bool {
|
||||
// A user can create an task if he has write acces to its list
|
||||
l := &List{ID: i.ListID}
|
||||
l.ReadOne()
|
||||
return l.CanWrite(doer)
|
||||
}
|
74
pkg/models/list_tasks_test.go
Normal file
74
pkg/models/list_tasks_test.go
Normal file
@ -0,0 +1,74 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListTask_Create(t *testing.T) {
|
||||
//assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
// Fake list task
|
||||
listtask := ListTask{
|
||||
Text: "Lorem",
|
||||
Description: "Lorem Ipsum BACKERY",
|
||||
ListID: 1,
|
||||
}
|
||||
|
||||
// Add one point to a list
|
||||
doer, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.True(t, listtask.CanCreate(&doer))
|
||||
|
||||
err = listtask.Create(&doer)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Update it
|
||||
listtask.Text = "Test34"
|
||||
assert.True(t, listtask.CanUpdate(&doer))
|
||||
err = listtask.Update()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check if it was updated
|
||||
li, err := GetListTaskByID(listtask.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, li.Text, "Test34")
|
||||
|
||||
// Delete the task
|
||||
assert.True(t, listtask.CanDelete(&doer))
|
||||
err = listtask.Delete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Delete a nonexistant task
|
||||
listtask.ID = 0
|
||||
err = listtask.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListTaskDoesNotExist(err))
|
||||
|
||||
// Try adding a list task with an empty text
|
||||
listtask.Text = ""
|
||||
err = listtask.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListTaskCannotBeEmpty(err))
|
||||
|
||||
// Try adding one to a nonexistant list
|
||||
listtask.ListID = 99993939
|
||||
listtask.Text = "Lorem Ipsum"
|
||||
err = listtask.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListDoesNotExist(err))
|
||||
|
||||
// Try updating a nonexistant task
|
||||
listtask.ID = 94829352
|
||||
err = listtask.Update()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListTaskDoesNotExist(err))
|
||||
|
||||
// Try inserting an task with a nonexistant user
|
||||
nUser := &User{ID: 9482385}
|
||||
listtask.ListID = 1
|
||||
err = listtask.Create(nUser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
}
|
25
pkg/models/list_users.go
Normal file
25
pkg/models/list_users.go
Normal file
@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
// ListUser represents a list <-> user relation
|
||||
type ListUser struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"namespace"`
|
||||
UserID int64 `xorm:"int(11) not null INDEX" json:"user_id" param:"user"`
|
||||
ListID int64 `xorm:"int(11) not null INDEX" json:"list_id" param:"list"`
|
||||
Right UserRight `xorm:"int(11) INDEX" json:"right"`
|
||||
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName is the table name for ListUser
|
||||
func (ListUser) TableName() string {
|
||||
return "users_list"
|
||||
}
|
||||
|
||||
type userWithRight struct {
|
||||
User `xorm:"extends"`
|
||||
Right UserRight `json:"right"`
|
||||
}
|
40
pkg/models/list_users_create.go
Normal file
40
pkg/models/list_users_create.go
Normal file
@ -0,0 +1,40 @@
|
||||
package models
|
||||
|
||||
// Create creates a new list <-> user relation
|
||||
func (ul *ListUser) Create(u *User) (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := ul.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the list exists
|
||||
l := &List{ID: ul.ListID}
|
||||
if err = l.GetSimpleByID(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
if _, err = GetUserByID(ul.UserID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the user already has access or is owner of that list
|
||||
// We explicitly DONT check for teams here
|
||||
if l.OwnerID == ul.UserID {
|
||||
return ErrUserAlreadyHasAccess{UserID: ul.UserID, ListID: ul.ListID}
|
||||
}
|
||||
|
||||
exist, err := x.Where("list_id = ? AND user_id = ?", ul.ListID, ul.UserID).Get(&ListUser{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if exist {
|
||||
return ErrUserAlreadyHasAccess{UserID: ul.UserID, ListID: ul.ListID}
|
||||
}
|
||||
|
||||
// Insert user <-> list relation
|
||||
_, err = x.Insert(ul)
|
||||
|
||||
return
|
||||
}
|
25
pkg/models/list_users_delete.go
Normal file
25
pkg/models/list_users_delete.go
Normal file
@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
// Delete deletes a list <-> user relation
|
||||
func (lu *ListUser) Delete() (err error) {
|
||||
|
||||
// Check if the user exists
|
||||
_, err = GetUserByID(lu.UserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user has access to the list
|
||||
has, err := x.Where("user_id = ? AND list_id = ?", lu.UserID, lu.ListID).
|
||||
Get(&ListUser{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
return ErrUserDoesNotHaveAccessToList{ListID: lu.ListID, UserID: lu.UserID}
|
||||
}
|
||||
|
||||
_, err = x.Where("user_id = ? AND list_id = ?", lu.UserID, lu.ListID).
|
||||
Delete(&ListUser{})
|
||||
return
|
||||
}
|
22
pkg/models/list_users_readall.go
Normal file
22
pkg/models/list_users_readall.go
Normal file
@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
// ReadAll gets all users who have access to a list
|
||||
func (ul *ListUser) ReadAll(u *User) (interface{}, error) {
|
||||
// Check if the user has access to the list
|
||||
l := &List{ID: ul.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !l.CanRead(u) {
|
||||
return nil, ErrNeedToHaveListReadAccess{}
|
||||
}
|
||||
|
||||
// Get all users
|
||||
all := []*userWithRight{}
|
||||
err := x.
|
||||
Join("INNER", "users_list", "user_id = users.id").
|
||||
Where("users_list.list_id = ?", ul.ListID).
|
||||
Find(&all)
|
||||
|
||||
return all, err
|
||||
}
|
38
pkg/models/list_users_rights.go
Normal file
38
pkg/models/list_users_rights.go
Normal file
@ -0,0 +1,38 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// CanCreate checks if the user can create a new user <-> list relation
|
||||
func (lu *ListUser) CanCreate(doer *User) bool {
|
||||
// Get the list and check if the user has write access on it
|
||||
l := List{ID: lu.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
log.Log.Error("Error occurred during CanCreate for ListUser: %s", err)
|
||||
return false
|
||||
}
|
||||
return l.CanWrite(doer)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a user <-> list relation
|
||||
func (lu *ListUser) CanDelete(doer *User) bool {
|
||||
// Get the list and check if the user has write access on it
|
||||
l := List{ID: lu.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
log.Log.Error("Error occurred during CanDelete for ListUser: %s", err)
|
||||
return false
|
||||
}
|
||||
return l.CanWrite(doer)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update a user <-> list relation
|
||||
func (lu *ListUser) CanUpdate(doer *User) bool {
|
||||
// Get the list and check if the user has write access on it
|
||||
l := List{ID: lu.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for ListUser: %s", err)
|
||||
return false
|
||||
}
|
||||
return l.CanWrite(doer)
|
||||
}
|
16
pkg/models/list_users_update.go
Normal file
16
pkg/models/list_users_update.go
Normal file
@ -0,0 +1,16 @@
|
||||
package models
|
||||
|
||||
// Update updates a user <-> list relation
|
||||
func (lu *ListUser) Update() (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := lu.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = x.
|
||||
Where("list_id = ? AND user_id = ?", lu.ListID, lu.UserID).
|
||||
Cols("right").
|
||||
Update(lu)
|
||||
return
|
||||
}
|
7
pkg/models/main_test.go
Normal file
7
pkg/models/main_test.go
Normal file
@ -0,0 +1,7 @@
|
||||
package models
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
MainTest(m, "..")
|
||||
}
|
6
pkg/models/message.go
Normal file
6
pkg/models/message.go
Normal file
@ -0,0 +1,6 @@
|
||||
package models
|
||||
|
||||
// Message is a standard message
|
||||
type Message struct {
|
||||
Message string `json:"message"`
|
||||
}
|
92
pkg/models/models.go
Normal file
92
pkg/models/models.go
Normal file
@ -0,0 +1,92 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
_ "github.com/go-sql-driver/mysql" // Because.
|
||||
"github.com/go-xorm/core"
|
||||
"github.com/go-xorm/xorm"
|
||||
xrc "github.com/go-xorm/xorm-redis-cache"
|
||||
_ "github.com/mattn/go-sqlite3" // Because.
|
||||
|
||||
"encoding/gob"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
x *xorm.Engine
|
||||
|
||||
tables []interface{}
|
||||
)
|
||||
|
||||
func getEngine() (*xorm.Engine, error) {
|
||||
// Use Mysql if set
|
||||
if viper.GetString("database.type") == "mysql" {
|
||||
connStr := fmt.Sprintf(
|
||||
"%s:%s@tcp(%s)/%s?charset=utf8&parseTime=true",
|
||||
viper.GetString("database.user"),
|
||||
viper.GetString("database.password"),
|
||||
viper.GetString("database.host"),
|
||||
viper.GetString("database.database"))
|
||||
e, err := xorm.NewEngine("mysql", connStr)
|
||||
e.SetMaxOpenConns(viper.GetInt("database.openconnections"))
|
||||
return e, err
|
||||
}
|
||||
|
||||
// Otherwise use sqlite
|
||||
path := viper.GetString("database.path")
|
||||
if path == "" {
|
||||
path = "./db.db"
|
||||
}
|
||||
return xorm.NewEngine("sqlite3", path)
|
||||
}
|
||||
|
||||
func init() {
|
||||
tables = append(tables,
|
||||
new(User),
|
||||
new(List),
|
||||
new(ListTask),
|
||||
new(Team),
|
||||
new(TeamMember),
|
||||
new(TeamList),
|
||||
new(TeamNamespace),
|
||||
new(Namespace),
|
||||
new(ListUser),
|
||||
new(NamespaceUser),
|
||||
)
|
||||
}
|
||||
|
||||
// SetEngine sets the xorm.Engine
|
||||
func SetEngine() (err error) {
|
||||
x, err = getEngine()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
// Cache
|
||||
if viper.GetBool("cache.enabled") {
|
||||
switch viper.GetString("cache.type") {
|
||||
case "memory":
|
||||
cacher := xorm.NewLRUCacher(xorm.NewMemoryStore(), viper.GetInt("cache.maxelementsize"))
|
||||
x.SetDefaultCacher(cacher)
|
||||
break
|
||||
case "redis":
|
||||
cacher := xrc.NewRedisCacher(viper.GetString("cache.redishost"), viper.GetString("cache.redispassword"), xrc.DEFAULT_EXPIRATION, x.Logger())
|
||||
x.SetDefaultCacher(cacher)
|
||||
gob.Register(tables)
|
||||
break
|
||||
default:
|
||||
fmt.Println("Did not find a valid cache type. Caching disabled. Please refer to the docs for poosible cache types.")
|
||||
}
|
||||
}
|
||||
|
||||
x.SetMapper(core.GonicMapper{})
|
||||
|
||||
// Sync dat shit
|
||||
if err = x.StoreEngine("InnoDB").Sync2(tables...); err != nil {
|
||||
return fmt.Errorf("sync database struct error: %v", err)
|
||||
}
|
||||
|
||||
x.ShowSQL(viper.GetBool("database.showqueries"))
|
||||
|
||||
return nil
|
||||
}
|
13
pkg/models/models_test.go
Normal file
13
pkg/models/models_test.go
Normal file
@ -0,0 +1,13 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSetEngine(t *testing.T) {
|
||||
viper.Set("database.path", "file::memory:?cache=shared")
|
||||
err := SetEngine()
|
||||
assert.NoError(t, err)
|
||||
}
|
134
pkg/models/namespace.go
Normal file
134
pkg/models/namespace.go
Normal file
@ -0,0 +1,134 @@
|
||||
package models
|
||||
|
||||
// Namespace holds informations about a namespace
|
||||
type Namespace struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"namespace"`
|
||||
Name string `xorm:"varchar(250)" json:"name"`
|
||||
Description string `xorm:"varchar(1000)" json:"description"`
|
||||
OwnerID int64 `xorm:"int(11) not null INDEX" json:"-"`
|
||||
|
||||
Owner User `xorm:"-" json:"owner"`
|
||||
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName makes beautiful table names
|
||||
func (Namespace) TableName() string {
|
||||
return "namespaces"
|
||||
}
|
||||
|
||||
// GetNamespaceByID returns a namespace object by its ID
|
||||
func GetNamespaceByID(id int64) (namespace Namespace, err error) {
|
||||
if id < 1 {
|
||||
return namespace, ErrNamespaceDoesNotExist{ID: id}
|
||||
}
|
||||
|
||||
namespace.ID = id
|
||||
exists, err := x.Get(&namespace)
|
||||
if err != nil {
|
||||
return namespace, err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return namespace, ErrNamespaceDoesNotExist{ID: id}
|
||||
}
|
||||
|
||||
// Get the namespace Owner
|
||||
namespace.Owner, err = GetUserByID(namespace.OwnerID)
|
||||
if err != nil {
|
||||
return namespace, err
|
||||
}
|
||||
|
||||
return namespace, err
|
||||
}
|
||||
|
||||
// ReadOne gets one namespace
|
||||
func (n *Namespace) ReadOne() (err error) {
|
||||
*n, err = GetNamespaceByID(n.ID)
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll gets all namespaces a user has access to
|
||||
func (n *Namespace) ReadAll(doer *User) (interface{}, error) {
|
||||
|
||||
type namespaceWithLists struct {
|
||||
Namespace `xorm:"extends"`
|
||||
Lists []*List `xorm:"-" json:"lists"`
|
||||
}
|
||||
|
||||
all := []*namespaceWithLists{}
|
||||
|
||||
err := x.Select("namespaces.*").
|
||||
Table("namespaces").
|
||||
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
|
||||
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
|
||||
Join("LEFT", "users_namespace", "users_namespace.namespace_id = namespaces.id").
|
||||
Where("team_members.user_id = ?", doer.ID).
|
||||
Or("namespaces.owner_id = ?", doer.ID).
|
||||
Or("users_namespace.user_id = ?", doer.ID).
|
||||
GroupBy("namespaces.id").
|
||||
Find(&all)
|
||||
|
||||
if err != nil {
|
||||
return all, err
|
||||
}
|
||||
|
||||
// Get all users
|
||||
users := []*User{}
|
||||
err = x.Select("users.*").
|
||||
Table("namespaces").
|
||||
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
|
||||
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
|
||||
Join("INNER", "users", "users.id = namespaces.owner_id").
|
||||
Where("team_members.user_id = ?", doer.ID).
|
||||
Or("namespaces.owner_id = ?", doer.ID).
|
||||
GroupBy("users.id").
|
||||
Find(&users)
|
||||
|
||||
if err != nil {
|
||||
return all, err
|
||||
}
|
||||
|
||||
// Make a list of namespace ids
|
||||
var namespaceids []int64
|
||||
for _, nsp := range all {
|
||||
namespaceids = append(namespaceids, nsp.ID)
|
||||
}
|
||||
|
||||
// Get all lists
|
||||
lists := []*List{}
|
||||
err = x.Table(&lists).
|
||||
In("namespace_id", namespaceids).
|
||||
Find(&lists)
|
||||
if err != nil {
|
||||
return all, err
|
||||
}
|
||||
|
||||
// More details for the lists
|
||||
AddListDetails(lists)
|
||||
|
||||
// Put objects in our namespace list
|
||||
for i, n := range all {
|
||||
|
||||
// Users
|
||||
for _, u := range users {
|
||||
if n.OwnerID == u.ID {
|
||||
all[i].Owner = *u
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// List infos
|
||||
for _, l := range lists {
|
||||
if n.ID == l.NamespaceID {
|
||||
all[i].Lists = append(all[i].Lists, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return all, nil
|
||||
}
|
21
pkg/models/namespace_create.go
Normal file
21
pkg/models/namespace_create.go
Normal file
@ -0,0 +1,21 @@
|
||||
package models
|
||||
|
||||
// Create implements the creation method via the interface
|
||||
func (n *Namespace) Create(doer *User) (err error) {
|
||||
// Check if we have at least a name
|
||||
if n.Name == "" {
|
||||
return ErrNamespaceNameCannotBeEmpty{NamespaceID: 0, UserID: doer.ID}
|
||||
}
|
||||
n.ID = 0 // This would otherwise prevent the creation of new lists after one was created
|
||||
|
||||
// Check if the User exists
|
||||
n.Owner, err = GetUserByID(doer.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
n.OwnerID = n.Owner.ID
|
||||
|
||||
// Insert
|
||||
_, err = x.Insert(n)
|
||||
return
|
||||
}
|
41
pkg/models/namespace_delete.go
Normal file
41
pkg/models/namespace_delete.go
Normal file
@ -0,0 +1,41 @@
|
||||
package models
|
||||
|
||||
// Delete deletes a namespace
|
||||
func (n *Namespace) Delete() (err error) {
|
||||
|
||||
// Check if the namespace exists
|
||||
_, err = GetNamespaceByID(n.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the namespace
|
||||
_, err = x.ID(n.ID).Delete(&Namespace{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete all lists with their tasks
|
||||
lists, err := GetListsByNamespaceID(n.ID)
|
||||
var listIDs []int64
|
||||
// We need to do that for here because we need the list ids to delete two times:
|
||||
// 1) to delete the lists itself
|
||||
// 2) to delete the list tasks
|
||||
for _, l := range lists {
|
||||
listIDs = append(listIDs, l.ID)
|
||||
}
|
||||
|
||||
// Delete tasks
|
||||
_, err = x.In("list_id", listIDs).Delete(&ListTask{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the lists
|
||||
_, err = x.In("id", listIDs).Delete(&List{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
112
pkg/models/namespace_rights.go
Normal file
112
pkg/models/namespace_rights.go
Normal file
@ -0,0 +1,112 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// IsAdmin returns true or false if the user is admin on that namespace or not
|
||||
func (n *Namespace) IsAdmin(u *User) bool {
|
||||
// Owners always have admin rights
|
||||
if u.ID == n.Owner.ID {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check user rights
|
||||
if n.checkUserRights(u, UserRightAdmin) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if that user is in a team which has admin rights to that namespace
|
||||
return n.checkTeamRights(u, TeamRightAdmin)
|
||||
}
|
||||
|
||||
// CanWrite checks if a user has write access to a namespace
|
||||
func (n *Namespace) CanWrite(u *User) bool {
|
||||
// Admins always have write access
|
||||
if n.IsAdmin(u) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check user rights
|
||||
if n.checkUserRights(u, UserRightWrite) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if that user is in a team which has write rights to that namespace
|
||||
return n.checkTeamRights(u, TeamRightWrite)
|
||||
}
|
||||
|
||||
// CanRead checks if a user has read access to that namespace
|
||||
func (n *Namespace) CanRead(u *User) bool {
|
||||
// Admins always have read access
|
||||
if n.IsAdmin(u) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check user rights
|
||||
if n.checkUserRights(u, UserRightRead) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if the user is in a team which has access to the namespace
|
||||
return n.checkTeamRights(u, TeamRightRead)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update the namespace
|
||||
func (n *Namespace) CanUpdate(u *User) bool {
|
||||
nn, err := GetNamespaceByID(n.ID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for Namespace: %s", err)
|
||||
return false
|
||||
}
|
||||
return nn.IsAdmin(u)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a namespace
|
||||
func (n *Namespace) CanDelete(u *User) bool {
|
||||
nn, err := GetNamespaceByID(n.ID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanDelete for Namespace: %s", err)
|
||||
return false
|
||||
}
|
||||
return nn.IsAdmin(u)
|
||||
}
|
||||
|
||||
// CanCreate checks if the user can create a new namespace
|
||||
func (n *Namespace) CanCreate(u *User) bool {
|
||||
// This is currently a dummy function, later on we could imagine global limits etc.
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *Namespace) checkTeamRights(u *User, r TeamRight) bool {
|
||||
exists, err := x.Select("namespaces.*").
|
||||
Table("namespaces").
|
||||
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
|
||||
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
|
||||
Where("namespaces.id = ? "+
|
||||
"AND (team_members.user_id = ? AND team_namespaces.right = ?) "+
|
||||
"OR namespaces.owner_id = ? ", n.ID, u.ID, r, u.ID).
|
||||
Get(&Namespace{})
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during checkTeamRights for Namespace: %s, TeamRight: %d", err, r)
|
||||
return false
|
||||
}
|
||||
|
||||
return exists
|
||||
}
|
||||
|
||||
func (n *Namespace) checkUserRights(u *User, r UserRight) bool {
|
||||
exists, err := x.Select("namespaces.*").
|
||||
Table("namespaces").
|
||||
Join("LEFT", "users_namespace", "users_namespace.namespace_id = namespaces.id").
|
||||
Where("namespaces.id = ? AND ("+
|
||||
"namespaces.owner_id = ? "+
|
||||
"OR (users_namespace.user_id = ? AND users_namespace.right = ?))", n.ID, u.ID, u.ID, r).
|
||||
Get(&Namespace{})
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during checkUserRights for Namespace: %s, UserRight: %d", err, r)
|
||||
return false
|
||||
}
|
||||
|
||||
return exists
|
||||
}
|
93
pkg/models/namespace_test.go
Normal file
93
pkg/models/namespace_test.go
Normal file
@ -0,0 +1,93 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNamespace_Create(t *testing.T) {
|
||||
// Create test database
|
||||
//assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
// Dummy namespace
|
||||
dummynamespace := Namespace{
|
||||
Name: "Test",
|
||||
Description: "Lorem Ipsum",
|
||||
}
|
||||
|
||||
// Doer
|
||||
doer, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try creating it
|
||||
assert.True(t, dummynamespace.CanCreate(&doer))
|
||||
err = dummynamespace.Create(&doer)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check if it really exists
|
||||
assert.True(t, dummynamespace.CanRead(&doer))
|
||||
newOne := Namespace{ID: dummynamespace.ID}
|
||||
err = newOne.ReadOne()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, newOne.Name, "Test")
|
||||
|
||||
// Try creating one without a name
|
||||
n2 := Namespace{}
|
||||
err = n2.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceNameCannotBeEmpty(err))
|
||||
|
||||
// Try inserting one with a nonexistant user
|
||||
nUser := &User{ID: 9482385}
|
||||
dnsp2 := dummynamespace
|
||||
err = dnsp2.Create(nUser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
|
||||
// Update it
|
||||
assert.True(t, dummynamespace.CanUpdate(&doer))
|
||||
dummynamespace.Description = "Dolor sit amet."
|
||||
err = dummynamespace.Update()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try updating one with a nonexistant owner
|
||||
dummynamespace.Owner.ID = 94829838572
|
||||
err = dummynamespace.Update()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
|
||||
// Try updating without a name
|
||||
dummynamespace.Name = ""
|
||||
err = dummynamespace.Update()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceNameCannotBeEmpty(err))
|
||||
|
||||
// Try updating a nonexistant one
|
||||
n := Namespace{ID: 284729, Name: "Lorem"}
|
||||
err = n.Update()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
|
||||
// Delete it
|
||||
assert.True(t, dummynamespace.CanDelete(&doer))
|
||||
err = dummynamespace.Delete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try deleting a nonexistant one
|
||||
err = n.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
|
||||
// Check if it was successfully deleted
|
||||
err = dummynamespace.ReadOne()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
|
||||
// Get all namespaces of a user
|
||||
nsps, err := n.ReadAll(&doer)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(nsps).Kind(), reflect.Slice)
|
||||
s := reflect.ValueOf(nsps)
|
||||
assert.Equal(t, 1, s.Len())
|
||||
}
|
28
pkg/models/namespace_update.go
Normal file
28
pkg/models/namespace_update.go
Normal file
@ -0,0 +1,28 @@
|
||||
package models
|
||||
|
||||
// Update implements the update method via the interface
|
||||
func (n *Namespace) Update() (err error) {
|
||||
// Check if we have at least a name
|
||||
if n.Name == "" {
|
||||
return ErrNamespaceNameCannotBeEmpty{NamespaceID: n.ID}
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
currentNamespace, err := GetNamespaceByID(n.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the (new) owner exists
|
||||
n.OwnerID = n.Owner.ID
|
||||
if currentNamespace.OwnerID != n.OwnerID {
|
||||
n.Owner, err = GetUserByID(n.OwnerID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Do the actual update
|
||||
_, err = x.ID(currentNamespace.ID).Update(n)
|
||||
return
|
||||
}
|
20
pkg/models/namespace_users.go
Normal file
20
pkg/models/namespace_users.go
Normal file
@ -0,0 +1,20 @@
|
||||
package models
|
||||
|
||||
// NamespaceUser represents a namespace <-> user relation
|
||||
type NamespaceUser struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"namespace"`
|
||||
UserID int64 `xorm:"int(11) not null INDEX" json:"user_id" param:"user"`
|
||||
NamespaceID int64 `xorm:"int(11) not null INDEX" json:"namespace_id" param:"namespace"`
|
||||
Right UserRight `xorm:"int(11) INDEX" json:"right"`
|
||||
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName is the table name for NamespaceUser
|
||||
func (NamespaceUser) TableName() string {
|
||||
return "users_namespace"
|
||||
}
|
43
pkg/models/namespace_users_create.go
Normal file
43
pkg/models/namespace_users_create.go
Normal file
@ -0,0 +1,43 @@
|
||||
package models
|
||||
|
||||
// Create creates a new namespace <-> user relation
|
||||
func (un *NamespaceUser) Create(u *User) (err error) {
|
||||
|
||||
// Reset the id
|
||||
un.ID = 0
|
||||
|
||||
// Check if the right is valid
|
||||
if err := un.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
l, err := GetNamespaceByID(un.NamespaceID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
if _, err = GetUserByID(un.UserID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the user already has access or is owner of that namespace
|
||||
// We explicitly DO NOT check for teams here
|
||||
if l.OwnerID == un.UserID {
|
||||
return ErrUserAlreadyHasNamespaceAccess{UserID: un.UserID, NamespaceID: un.NamespaceID}
|
||||
}
|
||||
|
||||
exist, err := x.Where("namespace_id = ? AND user_id = ?", un.NamespaceID, un.UserID).Get(&NamespaceUser{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if exist {
|
||||
return ErrUserAlreadyHasNamespaceAccess{UserID: un.UserID, NamespaceID: un.NamespaceID}
|
||||
}
|
||||
|
||||
// Insert user <-> namespace relation
|
||||
_, err = x.Insert(un)
|
||||
|
||||
return
|
||||
}
|
25
pkg/models/namespace_users_delete.go
Normal file
25
pkg/models/namespace_users_delete.go
Normal file
@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
// Delete deletes a namespace <-> user relation
|
||||
func (nu *NamespaceUser) Delete() (err error) {
|
||||
|
||||
// Check if the user exists
|
||||
_, err = GetUserByID(nu.UserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user has access to the namespace
|
||||
has, err := x.Where("user_id = ? AND namespace_id = ?", nu.UserID, nu.NamespaceID).
|
||||
Get(&NamespaceUser{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
return ErrUserDoesNotHaveAccessToNamespace{NamespaceID: nu.NamespaceID, UserID: nu.UserID}
|
||||
}
|
||||
|
||||
_, err = x.Where("user_id = ? AND namespace_id = ?", nu.UserID, nu.NamespaceID).
|
||||
Delete(&NamespaceUser{})
|
||||
return
|
||||
}
|
22
pkg/models/namespace_users_readall.go
Normal file
22
pkg/models/namespace_users_readall.go
Normal file
@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
// ReadAll gets all users who have access to a namespace
|
||||
func (un *NamespaceUser) ReadAll(u *User) (interface{}, error) {
|
||||
// Check if the user has access to the namespace
|
||||
l, err := GetNamespaceByID(un.NamespaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !l.CanRead(u) {
|
||||
return nil, ErrNeedToHaveNamespaceReadAccess{}
|
||||
}
|
||||
|
||||
// Get all users
|
||||
all := []*userWithRight{}
|
||||
err = x.
|
||||
Join("INNER", "users_namespace", "user_id = users.id").
|
||||
Where("users_namespace.namespace_id = ?", un.NamespaceID).
|
||||
Find(&all)
|
||||
|
||||
return all, err
|
||||
}
|
38
pkg/models/namespace_users_rights.go
Normal file
38
pkg/models/namespace_users_rights.go
Normal file
@ -0,0 +1,38 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// CanCreate checks if the user can create a new user <-> namespace relation
|
||||
func (nu *NamespaceUser) CanCreate(doer *User) bool {
|
||||
// Get the namespace and check if the user has write access on it
|
||||
n, err := GetNamespaceByID(nu.NamespaceID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanCreate for NamespaceUser: %s", err)
|
||||
return false
|
||||
}
|
||||
return n.CanWrite(doer)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a user <-> namespace relation
|
||||
func (nu *NamespaceUser) CanDelete(doer *User) bool {
|
||||
// Get the namespace and check if the user has write access on it
|
||||
n, err := GetNamespaceByID(nu.NamespaceID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanDelete for NamespaceUser: %s", err)
|
||||
return false
|
||||
}
|
||||
return n.CanWrite(doer)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update a user <-> namespace relation
|
||||
func (nu *NamespaceUser) CanUpdate(doer *User) bool {
|
||||
// Get the namespace and check if the user has write access on it
|
||||
n, err := GetNamespaceByID(nu.NamespaceID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for NamespaceUser: %s", err)
|
||||
return false
|
||||
}
|
||||
return n.CanWrite(doer)
|
||||
}
|
16
pkg/models/namespace_users_update.go
Normal file
16
pkg/models/namespace_users_update.go
Normal file
@ -0,0 +1,16 @@
|
||||
package models
|
||||
|
||||
// Update updates a user <-> namespace relation
|
||||
func (nu *NamespaceUser) Update() (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := nu.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = x.
|
||||
Where("namespace_id = ? AND user_id = ?", nu.NamespaceID, nu.UserID).
|
||||
Cols("right").
|
||||
Update(nu)
|
||||
return
|
||||
}
|
11
pkg/models/rights.go
Normal file
11
pkg/models/rights.go
Normal file
@ -0,0 +1,11 @@
|
||||
package models
|
||||
|
||||
// Rights defines rights methods
|
||||
type Rights interface {
|
||||
IsAdmin(*User) bool
|
||||
CanWrite(*User) bool
|
||||
CanRead(*User) bool
|
||||
CanDelete(*User) bool
|
||||
CanUpdate(*User) bool
|
||||
CanCreate(*User) bool
|
||||
}
|
25
pkg/models/team_list.go
Normal file
25
pkg/models/team_list.go
Normal file
@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
// TeamList defines the relation between a team and a list
|
||||
type TeamList struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
|
||||
TeamID int64 `xorm:"int(11) not null INDEX" json:"team_id" param:"team"`
|
||||
ListID int64 `xorm:"int(11) not null INDEX" json:"list_id" param:"list"`
|
||||
Right TeamRight `xorm:"int(11) INDEX" json:"right"`
|
||||
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName makes beautiful table names
|
||||
func (TeamList) TableName() string {
|
||||
return "team_list"
|
||||
}
|
||||
|
||||
type teamWithRight struct {
|
||||
Team `xorm:"extends"`
|
||||
Right TeamRight `json:"right"`
|
||||
}
|
37
pkg/models/team_list_create.go
Normal file
37
pkg/models/team_list_create.go
Normal file
@ -0,0 +1,37 @@
|
||||
package models
|
||||
|
||||
// Create creates a new team <-> list relation
|
||||
func (tl *TeamList) Create(doer *User) (err error) {
|
||||
|
||||
// Check if the rights are valid
|
||||
if err = tl.Right.isValid(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(tl.TeamID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the list exists
|
||||
l := &List{ID: tl.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the team is already on the list
|
||||
exists, err := x.Where("team_id = ?", tl.TeamID).
|
||||
And("list_id = ?", tl.ListID).
|
||||
Get(&TeamList{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
return ErrTeamAlreadyHasAccess{tl.TeamID, tl.ListID}
|
||||
}
|
||||
|
||||
// Insert the new team
|
||||
_, err = x.Insert(tl)
|
||||
return
|
||||
}
|
28
pkg/models/team_list_delete.go
Normal file
28
pkg/models/team_list_delete.go
Normal file
@ -0,0 +1,28 @@
|
||||
package models
|
||||
|
||||
// Delete deletes a team <-> list relation based on the list & team id
|
||||
func (tl *TeamList) Delete() (err error) {
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(tl.TeamID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the team has access to the list
|
||||
has, err := x.Where("team_id = ? AND list_id = ?", tl.TeamID, tl.ListID).
|
||||
Get(&TeamList{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
return ErrTeamDoesNotHaveAccessToList{TeamID: tl.TeamID, ListID: tl.ListID}
|
||||
}
|
||||
|
||||
// Delete the relation
|
||||
_, err = x.Where("team_id = ?", tl.TeamID).
|
||||
And("list_id = ?", tl.ListID).
|
||||
Delete(TeamList{})
|
||||
|
||||
return
|
||||
}
|
23
pkg/models/team_list_readall.go
Normal file
23
pkg/models/team_list_readall.go
Normal file
@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
// ReadAll implements the method to read all teams of a list
|
||||
func (tl *TeamList) ReadAll(u *User) (interface{}, error) {
|
||||
// Check if the user can read the namespace
|
||||
l := &List{ID: tl.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !l.CanRead(u) {
|
||||
return nil, ErrNeedToHaveListReadAccess{ListID: tl.ListID, UserID: u.ID}
|
||||
}
|
||||
|
||||
// Get the teams
|
||||
all := []*teamWithRight{}
|
||||
err := x.
|
||||
Table("teams").
|
||||
Join("INNER", "team_list", "team_id = teams.id").
|
||||
Where("team_list.list_id = ?", tl.ListID).
|
||||
Find(&all)
|
||||
|
||||
return all, err
|
||||
}
|
35
pkg/models/team_list_rights.go
Normal file
35
pkg/models/team_list_rights.go
Normal file
@ -0,0 +1,35 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// CanCreate checks if the user can create a team <-> list relation
|
||||
func (tl *TeamList) CanCreate(u *User) bool {
|
||||
l := List{ID: tl.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
log.Log.Error("Error occurred during CanCreate for TeamList: %s", err)
|
||||
return false
|
||||
}
|
||||
return l.IsAdmin(u)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a team <-> list relation
|
||||
func (tl *TeamList) CanDelete(user *User) bool {
|
||||
l := List{ID: tl.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
log.Log.Error("Error occurred during CanDelete for TeamList: %s", err)
|
||||
return false
|
||||
}
|
||||
return l.IsAdmin(user)
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update a team <-> list relation
|
||||
func (tl *TeamList) CanUpdate(user *User) bool {
|
||||
l := List{ID: tl.ListID}
|
||||
if err := l.GetSimpleByID(); err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for TeamList: %s", err)
|
||||
return false
|
||||
}
|
||||
return l.IsAdmin(user)
|
||||
}
|
92
pkg/models/team_list_test.go
Normal file
92
pkg/models/team_list_test.go
Normal file
@ -0,0 +1,92 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTeamList(t *testing.T) {
|
||||
// Dummy relation
|
||||
tl := TeamList{
|
||||
TeamID: 1,
|
||||
ListID: 1,
|
||||
Right: TeamRightAdmin,
|
||||
}
|
||||
|
||||
// Dummyuser
|
||||
u, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check normal creation
|
||||
assert.True(t, tl.CanCreate(&u))
|
||||
err = tl.Create(&u)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check again
|
||||
err = tl.Create(&u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamAlreadyHasAccess(err))
|
||||
|
||||
// Check with wrong rights
|
||||
tl2 := tl
|
||||
tl2.Right = TeamRightUnknown
|
||||
err = tl2.Create(&u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrInvalidTeamRight(err))
|
||||
|
||||
// Check with inexistant team
|
||||
tl3 := tl
|
||||
tl3.TeamID = 3253
|
||||
err = tl3.Create(&u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
|
||||
// Check with inexistant list
|
||||
tl4 := tl
|
||||
tl4.ListID = 3252
|
||||
err = tl4.Create(&u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListDoesNotExist(err))
|
||||
|
||||
// Test Read all
|
||||
teams, err := tl.ReadAll(&u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
|
||||
s := reflect.ValueOf(teams)
|
||||
assert.Equal(t, s.Len(), 1)
|
||||
|
||||
// Test Read all for nonexistant list
|
||||
_, err = tl4.ReadAll(&u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListDoesNotExist(err))
|
||||
|
||||
// Test Read all for a list where the user is owner of the namespace this list belongs to
|
||||
tl5 := tl
|
||||
tl5.ListID = 2
|
||||
_, err = tl5.ReadAll(&u)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test read all for a list where the user not has access
|
||||
tl6 := tl
|
||||
tl6.ListID = 3
|
||||
_, err = tl6.ReadAll(&u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNeedToHaveListReadAccess(err))
|
||||
|
||||
// Delete
|
||||
assert.True(t, tl.CanDelete(&u))
|
||||
err = tl.Delete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Delete a nonexistant team
|
||||
err = tl3.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
|
||||
// Delete with a nonexistant list
|
||||
err = tl4.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotHaveAccessToList(err))
|
||||
|
||||
}
|
16
pkg/models/team_list_update.go
Normal file
16
pkg/models/team_list_update.go
Normal file
@ -0,0 +1,16 @@
|
||||
package models
|
||||
|
||||
// Update updates a team <-> list relation
|
||||
func (tl *TeamList) Update() (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := tl.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = x.
|
||||
Where("list_id = ? AND team_id = ?", tl.ListID, tl.TeamID).
|
||||
Cols("right").
|
||||
Update(tl)
|
||||
return
|
||||
}
|
27
pkg/models/team_members_create.go
Normal file
27
pkg/models/team_members_create.go
Normal file
@ -0,0 +1,27 @@
|
||||
package models
|
||||
|
||||
// Create implements the create method to assign a user to a team
|
||||
func (tm *TeamMember) Create(doer *User) (err error) {
|
||||
// Check if the team extst
|
||||
_, err = GetTeamByID(tm.TeamID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
_, err = GetUserByID(tm.UserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if that user is already part of the team
|
||||
exists, err := x.Where("team_id = ? AND user_id = ?", tm.TeamID, tm.UserID).
|
||||
Get(&TeamMember{})
|
||||
if exists {
|
||||
return ErrUserIsMemberOfTeam{tm.TeamID, tm.UserID}
|
||||
}
|
||||
|
||||
// Insert the user
|
||||
_, err = x.Insert(tm)
|
||||
return
|
||||
}
|
16
pkg/models/team_members_delete.go
Normal file
16
pkg/models/team_members_delete.go
Normal file
@ -0,0 +1,16 @@
|
||||
package models
|
||||
|
||||
// Delete deletes a user from a team
|
||||
func (tm *TeamMember) Delete() (err error) {
|
||||
|
||||
total, err := x.Where("team_id = ?", tm.TeamID).Count(&TeamMember{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if total == 1 {
|
||||
return ErrCannotDeleteLastTeamMember{tm.TeamID, tm.UserID}
|
||||
}
|
||||
|
||||
_, err = x.Where("team_id = ? AND user_id = ?", tm.TeamID, tm.UserID).Delete(&TeamMember{})
|
||||
return
|
||||
}
|
27
pkg/models/team_members_rights.go
Normal file
27
pkg/models/team_members_rights.go
Normal file
@ -0,0 +1,27 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// CanCreate checks if the user can add a new tem member
|
||||
func (tm *TeamMember) CanCreate(u *User) bool {
|
||||
return tm.IsAdmin(u)
|
||||
}
|
||||
|
||||
// CanDelete checks if the user can delete a new team member
|
||||
func (tm *TeamMember) CanDelete(u *User) bool {
|
||||
return tm.IsAdmin(u)
|
||||
}
|
||||
|
||||
// IsAdmin checks if the user is team admin
|
||||
func (tm *TeamMember) IsAdmin(u *User) bool {
|
||||
// A user can add a member to a team if he is admin of that team
|
||||
exists, err := x.Where("user_id = ? AND team_id = ? AND admin = ?", u.ID, tm.TeamID, true).
|
||||
Get(&TeamMember{})
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during IsAdmin for TeamMember: %s", err)
|
||||
return false
|
||||
}
|
||||
return exists
|
||||
}
|
63
pkg/models/team_members_test.go
Normal file
63
pkg/models/team_members_test.go
Normal file
@ -0,0 +1,63 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTeamMember_Create(t *testing.T) {
|
||||
|
||||
// Dummy team member
|
||||
dummyteammember := TeamMember{
|
||||
TeamID: 1,
|
||||
UserID: 3,
|
||||
}
|
||||
|
||||
// Doer
|
||||
doer, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Insert a new team member
|
||||
assert.True(t, dummyteammember.CanCreate(&doer))
|
||||
err = dummyteammember.Create(&doer)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check he's in there
|
||||
team := Team{ID: 1}
|
||||
err = team.ReadOne()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, len(team.Members))
|
||||
|
||||
// Try inserting a user twice
|
||||
err = dummyteammember.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserIsMemberOfTeam(err))
|
||||
|
||||
// Delete it
|
||||
assert.True(t, dummyteammember.CanDelete(&doer))
|
||||
err = dummyteammember.Delete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Delete the other one
|
||||
tm := TeamMember{TeamID: 1, UserID: 2}
|
||||
err = tm.Delete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try deleting the last one
|
||||
tm = TeamMember{TeamID: 1, UserID: 1}
|
||||
err = tm.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrCannotDeleteLastTeamMember(err))
|
||||
|
||||
// Try inserting a user which does not exist
|
||||
dummyteammember.UserID = 9484
|
||||
err = dummyteammember.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
|
||||
// Try adding a user to a team which does not exist
|
||||
tm = TeamMember{TeamID: 94824, UserID: 1}
|
||||
err = tm.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
}
|
20
pkg/models/team_namespace.go
Normal file
20
pkg/models/team_namespace.go
Normal file
@ -0,0 +1,20 @@
|
||||
package models
|
||||
|
||||
// TeamNamespace defines the relationship between a Team and a Namespace
|
||||
type TeamNamespace struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
|
||||
TeamID int64 `xorm:"int(11) not null INDEX" json:"team_id" param:"team"`
|
||||
NamespaceID int64 `xorm:"int(11) not null INDEX" json:"namespace_id" param:"namespace"`
|
||||
Right TeamRight `xorm:"int(11) INDEX" json:"right"`
|
||||
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName makes beautiful table names
|
||||
func (TeamNamespace) TableName() string {
|
||||
return "team_namespaces"
|
||||
}
|
37
pkg/models/team_namespace_create.go
Normal file
37
pkg/models/team_namespace_create.go
Normal file
@ -0,0 +1,37 @@
|
||||
package models
|
||||
|
||||
// Create creates a new team <-> namespace relation
|
||||
func (tn *TeamNamespace) Create(doer *User) (err error) {
|
||||
|
||||
// Check if the rights are valid
|
||||
if err = tn.Right.isValid(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(tn.TeamID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
_, err = GetNamespaceByID(tn.NamespaceID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the team already has access to the namespace
|
||||
exists, err := x.Where("team_id = ?", tn.TeamID).
|
||||
And("namespace_id = ?", tn.NamespaceID).
|
||||
Get(&TeamNamespace{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
return ErrTeamAlreadyHasAccess{tn.TeamID, tn.NamespaceID}
|
||||
}
|
||||
|
||||
// Insert the new team
|
||||
_, err = x.Insert(tn)
|
||||
return
|
||||
}
|
28
pkg/models/team_namespace_delete.go
Normal file
28
pkg/models/team_namespace_delete.go
Normal file
@ -0,0 +1,28 @@
|
||||
package models
|
||||
|
||||
// Delete deletes a team <-> namespace relation based on the namespace & team id
|
||||
func (tn *TeamNamespace) Delete() (err error) {
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(tn.TeamID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the team has access to the namespace
|
||||
has, err := x.Where("team_id = ? AND namespace_id = ?", tn.TeamID, tn.NamespaceID).
|
||||
Get(&TeamNamespace{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
return ErrTeamDoesNotHaveAccessToNamespace{TeamID: tn.TeamID, NamespaceID: tn.NamespaceID}
|
||||
}
|
||||
|
||||
// Delete the relation
|
||||
_, err = x.Where("team_id = ?", tn.TeamID).
|
||||
And("namespace_id = ?", tn.NamespaceID).
|
||||
Delete(TeamNamespace{})
|
||||
|
||||
return
|
||||
}
|
23
pkg/models/team_namespace_readall.go
Normal file
23
pkg/models/team_namespace_readall.go
Normal file
@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
// ReadAll implements the method to read all teams of a namespace
|
||||
func (tn *TeamNamespace) ReadAll(user *User) (interface{}, error) {
|
||||
// Check if the user can read the namespace
|
||||
n, err := GetNamespaceByID(tn.NamespaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !n.CanRead(user) {
|
||||
return nil, ErrNeedToHaveNamespaceReadAccess{NamespaceID: tn.NamespaceID, UserID: user.ID}
|
||||
}
|
||||
|
||||
// Get the teams
|
||||
all := []*teamWithRight{}
|
||||
|
||||
err = x.Table("teams").
|
||||
Join("INNER", "team_namespaces", "team_id = teams.id").
|
||||
Where("team_namespaces.namespace_id = ?", tn.NamespaceID).
|
||||
Find(&all)
|
||||
|
||||
return all, err
|
||||
}
|
35
pkg/models/team_namespace_rights.go
Normal file
35
pkg/models/team_namespace_rights.go
Normal file
@ -0,0 +1,35 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// CanCreate checks if one can create a new team <-> namespace relation
|
||||
func (tn *TeamNamespace) CanCreate(user *User) bool {
|
||||
n, err := GetNamespaceByID(tn.NamespaceID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanCreate for TeamNamespace: %s", err)
|
||||
return false
|
||||
}
|
||||
return n.IsAdmin(user)
|
||||
}
|
||||
|
||||
// CanDelete checks if a user can remove a team from a namespace. Only namespace admins can do that.
|
||||
func (tn *TeamNamespace) CanDelete(user *User) bool {
|
||||
n, err := GetNamespaceByID(tn.NamespaceID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanDelete for TeamNamespace: %s", err)
|
||||
return false
|
||||
}
|
||||
return n.IsAdmin(user)
|
||||
}
|
||||
|
||||
// CanUpdate checks if a user can update a team from a Only namespace admins can do that.
|
||||
func (tn *TeamNamespace) CanUpdate(user *User) bool {
|
||||
n, err := GetNamespaceByID(tn.NamespaceID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for TeamNamespace: %s", err)
|
||||
return false
|
||||
}
|
||||
return n.IsAdmin(user)
|
||||
}
|
84
pkg/models/team_namespace_test.go
Normal file
84
pkg/models/team_namespace_test.go
Normal file
@ -0,0 +1,84 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTeamNamespace(t *testing.T) {
|
||||
// Dummy team <-> namespace relation
|
||||
tn := TeamNamespace{
|
||||
TeamID: 1,
|
||||
NamespaceID: 1,
|
||||
Right: TeamRightAdmin,
|
||||
}
|
||||
|
||||
dummyuser, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test normal creation
|
||||
assert.True(t, tn.CanCreate(&dummyuser))
|
||||
err = tn.Create(&dummyuser)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test again (should fail)
|
||||
err = tn.Create(&dummyuser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamAlreadyHasAccess(err))
|
||||
|
||||
// Test with invalid team right
|
||||
tn2 := tn
|
||||
tn2.Right = TeamRightUnknown
|
||||
err = tn2.Create(&dummyuser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrInvalidTeamRight(err))
|
||||
|
||||
// Check with inexistant team
|
||||
tn3 := tn
|
||||
tn3.TeamID = 324
|
||||
err = tn3.Create(&dummyuser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
|
||||
// Check with a namespace which does not exist
|
||||
tn4 := tn
|
||||
tn4.NamespaceID = 423
|
||||
err = tn4.Create(&dummyuser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
|
||||
// Check readall
|
||||
teams, err := tn.ReadAll(&dummyuser)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
|
||||
s := reflect.ValueOf(teams)
|
||||
assert.Equal(t, s.Len(), 1)
|
||||
|
||||
// Check readall for a nonexistant namespace
|
||||
_, err = tn4.ReadAll(&dummyuser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
|
||||
// Check with no right to read the namespace
|
||||
nouser := &User{ID: 393}
|
||||
_, err = tn.ReadAll(nouser)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNeedToHaveNamespaceReadAccess(err))
|
||||
|
||||
// Delete it
|
||||
assert.True(t, tn.CanDelete(&dummyuser))
|
||||
err = tn.Delete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try deleting with a nonexisting team
|
||||
err = tn3.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
|
||||
// Try deleting with a nonexistant namespace
|
||||
err = tn4.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotHaveAccessToNamespace(err))
|
||||
|
||||
}
|
16
pkg/models/team_namespace_update.go
Normal file
16
pkg/models/team_namespace_update.go
Normal file
@ -0,0 +1,16 @@
|
||||
package models
|
||||
|
||||
// Update updates a team <-> namespace relation
|
||||
func (tl *TeamNamespace) Update() (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := tl.Right.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = x.
|
||||
Where("namespace_id = ? AND team_id = ?", tl.TeamID, tl.TeamID).
|
||||
Cols("right").
|
||||
Update(tl)
|
||||
return
|
||||
}
|
27
pkg/models/team_right.go
Normal file
27
pkg/models/team_right.go
Normal file
@ -0,0 +1,27 @@
|
||||
package models
|
||||
|
||||
// TeamRight defines the rights teams can have for lists/namespaces
|
||||
type TeamRight int
|
||||
|
||||
// define unknown team right
|
||||
const (
|
||||
TeamRightUnknown = -1
|
||||
)
|
||||
|
||||
// Enumerate all the team rights
|
||||
const (
|
||||
// Can read lists in a Team
|
||||
TeamRightRead TeamRight = iota
|
||||
// Can write tasks in a Team like lists and todo tasks. Cannot create new lists.
|
||||
TeamRightWrite
|
||||
// Can manage a list/namespace, can do everything
|
||||
TeamRightAdmin
|
||||
)
|
||||
|
||||
func (r TeamRight) isValid() error {
|
||||
if r != TeamRightAdmin && r != TeamRightRead && r != TeamRightWrite {
|
||||
return ErrInvalidTeamRight{r}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
96
pkg/models/teams.go
Normal file
96
pkg/models/teams.go
Normal file
@ -0,0 +1,96 @@
|
||||
package models
|
||||
|
||||
// Team holds a team object
|
||||
type Team struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"team"`
|
||||
Name string `xorm:"varchar(250) not null" json:"name"`
|
||||
Description string `xorm:"varchar(250)" json:"description"`
|
||||
CreatedByID int64 `xorm:"int(11) not null INDEX" json:"-"`
|
||||
|
||||
CreatedBy User `xorm:"-" json:"created_by"`
|
||||
Members []*TeamUser `xorm:"-" json:"members"`
|
||||
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName makes beautiful table names
|
||||
func (Team) TableName() string {
|
||||
return "teams"
|
||||
}
|
||||
|
||||
// AfterLoad gets the created by user object
|
||||
func (t *Team) AfterLoad() {
|
||||
// Get the owner
|
||||
t.CreatedBy, _ = GetUserByID(t.CreatedByID)
|
||||
|
||||
// Get all members
|
||||
x.Select("*").
|
||||
Table("users").
|
||||
Join("INNER", "team_members", "team_members.user_id = users.id").
|
||||
Where("team_id = ?", t.ID).
|
||||
Find(&t.Members)
|
||||
}
|
||||
|
||||
// TeamMember defines the relationship between a user and a team
|
||||
type TeamMember struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
|
||||
TeamID int64 `xorm:"int(11) not null INDEX" json:"team_id" param:"team"`
|
||||
UserID int64 `xorm:"int(11) not null INDEX" json:"user_id" param:"user"`
|
||||
Admin bool `xorm:"tinyint(1) INDEX" json:"admin"`
|
||||
|
||||
Created int64 `xorm:"created" json:"created"`
|
||||
Updated int64 `xorm:"updated" json:"updated"`
|
||||
|
||||
CRUDable `xorm:"-" json:"-"`
|
||||
Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName makes beautiful table names
|
||||
func (TeamMember) TableName() string {
|
||||
return "team_members"
|
||||
}
|
||||
|
||||
// TeamUser is the team member type
|
||||
type TeamUser struct {
|
||||
User `xorm:"extends"`
|
||||
Admin bool `json:"admin"`
|
||||
}
|
||||
|
||||
// GetTeamByID gets a team by its ID
|
||||
func GetTeamByID(id int64) (team Team, err error) {
|
||||
if id < 1 {
|
||||
return team, ErrTeamDoesNotExist{id}
|
||||
}
|
||||
|
||||
exists, err := x.Where("id = ?", id).Get(&team)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
return team, ErrTeamDoesNotExist{id}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ReadOne implements the CRUD method to get one team
|
||||
func (t *Team) ReadOne() (err error) {
|
||||
*t, err = GetTeamByID(t.ID)
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll gets all teams the user is part of
|
||||
func (t *Team) ReadAll(user *User) (teams interface{}, err error) {
|
||||
all := []*Team{}
|
||||
err = x.Select("teams.*").
|
||||
Table("teams").
|
||||
Join("INNER", "team_members", "team_members.team_id = teams.id").
|
||||
Where("team_members.user_id = ?", user.ID).
|
||||
Find(&all)
|
||||
|
||||
return all, err
|
||||
}
|
22
pkg/models/teams_create.go
Normal file
22
pkg/models/teams_create.go
Normal file
@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
// Create is the handler to create a team
|
||||
func (t *Team) Create(doer *User) (err error) {
|
||||
// Check if we have a name
|
||||
if t.Name == "" {
|
||||
return ErrTeamNameCannotBeEmpty{}
|
||||
}
|
||||
|
||||
t.CreatedByID = doer.ID
|
||||
t.CreatedBy = *doer
|
||||
|
||||
_, err = x.Insert(t)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Insert the current user as member and admin
|
||||
tm := TeamMember{TeamID: t.ID, UserID: doer.ID, Admin: true}
|
||||
err = tm.Create(doer)
|
||||
return
|
||||
}
|
33
pkg/models/teams_delete.go
Normal file
33
pkg/models/teams_delete.go
Normal file
@ -0,0 +1,33 @@
|
||||
package models
|
||||
|
||||
// Delete deletes a team
|
||||
func (t *Team) Delete() (err error) {
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(t.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the team
|
||||
_, err = x.ID(t.ID).Delete(&Team{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete team members
|
||||
_, err = x.Where("team_id = ?", t.ID).Delete(&TeamMember{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete team <-> namespace relations
|
||||
_, err = x.Where("team_id = ?", t.ID).Delete(&TeamNamespace{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete team <-> lists relations
|
||||
_, err = x.Where("team_id = ?", t.ID).Delete(&TeamList{})
|
||||
return
|
||||
}
|
58
pkg/models/teams_rights.go
Normal file
58
pkg/models/teams_rights.go
Normal file
@ -0,0 +1,58 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// CanCreate checks if the user can create a new team
|
||||
func (t *Team) CanCreate(u *User) bool {
|
||||
// This is currently a dummy function, later on we could imagine global limits etc.
|
||||
return true
|
||||
}
|
||||
|
||||
// CanUpdate checks if the user can update a team
|
||||
func (t *Team) CanUpdate(u *User) bool {
|
||||
|
||||
// Check if the current user is in the team and has admin rights in it
|
||||
exists, err := x.Where("team_id = ?", t.ID).
|
||||
And("user_id = ?", u.ID).
|
||||
And("admin = ?", true).
|
||||
Get(&TeamMember{})
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for Team: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return exists
|
||||
}
|
||||
|
||||
// CanDelete checks if a user can delete a team
|
||||
func (t *Team) CanDelete(u *User) bool {
|
||||
return t.IsAdmin(u)
|
||||
}
|
||||
|
||||
// IsAdmin returns true when the user is admin of a team
|
||||
func (t *Team) IsAdmin(u *User) bool {
|
||||
exists, err := x.Where("team_id = ?", t.ID).
|
||||
And("user_id = ?", u.ID).
|
||||
And("admin = ?", true).
|
||||
Get(&TeamMember{})
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for Team: %s", err)
|
||||
return false
|
||||
}
|
||||
return exists
|
||||
}
|
||||
|
||||
// CanRead returns true if the user has read access to the team
|
||||
func (t *Team) CanRead(user *User) bool {
|
||||
// Check if the user is in the team
|
||||
exists, err := x.Where("team_id = ?", t.ID).
|
||||
And("user_id = ?", user.ID).
|
||||
Get(&TeamMember{})
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for Team: %s", err)
|
||||
return false
|
||||
}
|
||||
return exists
|
||||
}
|
85
pkg/models/teams_test.go
Normal file
85
pkg/models/teams_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTeam_Create(t *testing.T) {
|
||||
//Dummyteam
|
||||
dummyteam := Team{
|
||||
Name: "Testteam293",
|
||||
Description: "Lorem Ispum",
|
||||
}
|
||||
|
||||
// Doer
|
||||
doer, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Insert it
|
||||
assert.True(t, dummyteam.CanCreate(&doer))
|
||||
err = dummyteam.Create(&doer)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check if it was inserted and we're admin
|
||||
tm := Team{ID: dummyteam.ID}
|
||||
err = tm.ReadOne()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(tm.Members))
|
||||
assert.Equal(t, doer.ID, tm.Members[0].User.ID)
|
||||
assert.True(t, tm.Members[0].Admin)
|
||||
assert.True(t, dummyteam.CanRead(&doer))
|
||||
|
||||
// Get all teams the user is part of
|
||||
ts, err := tm.ReadAll(&doer)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(ts).Kind(), reflect.Slice)
|
||||
s := reflect.ValueOf(ts)
|
||||
assert.Equal(t, 2, s.Len())
|
||||
|
||||
// Check inserting it with an empty name
|
||||
dummyteam.Name = ""
|
||||
err = dummyteam.Create(&doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamNameCannotBeEmpty(err))
|
||||
|
||||
// update it (still no name, should fail)
|
||||
assert.True(t, dummyteam.CanUpdate(&doer))
|
||||
err = dummyteam.Update()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamNameCannotBeEmpty(err))
|
||||
|
||||
// Update it, this time with a name
|
||||
dummyteam.Name = "Lorem"
|
||||
err = dummyteam.Update()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Delete it
|
||||
assert.True(t, dummyteam.CanDelete(&doer))
|
||||
err = dummyteam.Delete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try deleting a (now) nonexistant team
|
||||
err = dummyteam.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
|
||||
// Try updating the (now) nonexistant team
|
||||
err = dummyteam.Update()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
}
|
||||
|
||||
func TestIsErrInvalidTeamRight(t *testing.T) {
|
||||
assert.NoError(t, TeamRightAdmin.isValid())
|
||||
assert.NoError(t, TeamRightRead.isValid())
|
||||
assert.NoError(t, TeamRightWrite.isValid())
|
||||
|
||||
// Check invalid
|
||||
var tr TeamRight
|
||||
tr = 938
|
||||
err := tr.isValid()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrInvalidTeamRight(err))
|
||||
}
|
25
pkg/models/teams_update.go
Normal file
25
pkg/models/teams_update.go
Normal file
@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
// Update is the handler to create a team
|
||||
func (t *Team) Update() (err error) {
|
||||
// Check if we have a name
|
||||
if t.Name == "" {
|
||||
return ErrTeamNameCannotBeEmpty{}
|
||||
}
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(t.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = x.ID(t.ID).Update(t)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the newly updated team
|
||||
*t, err = GetTeamByID(t.ID)
|
||||
|
||||
return
|
||||
}
|
19
pkg/models/test_fixtures.go
Normal file
19
pkg/models/test_fixtures.go
Normal file
@ -0,0 +1,19 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"gopkg.in/testfixtures.v2"
|
||||
)
|
||||
|
||||
var fixtures *testfixtures.Context
|
||||
|
||||
// InitFixtures initialize test fixtures for a test database
|
||||
func InitFixtures(helper testfixtures.Helper, dir string) (err error) {
|
||||
testfixtures.SkipDatabaseNameCheck(true)
|
||||
fixtures, err = testfixtures.NewFolder(x.DB().DB, helper, dir)
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadFixtures load fixtures for a test database
|
||||
func LoadFixtures() error {
|
||||
return fixtures.Load()
|
||||
}
|
63
pkg/models/unit_tests.go
Normal file
63
pkg/models/unit_tests.go
Normal file
@ -0,0 +1,63 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/mail"
|
||||
"fmt"
|
||||
"github.com/go-xorm/core"
|
||||
"github.com/go-xorm/xorm"
|
||||
"gopkg.in/testfixtures.v2"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// IsTesting is set to true when we're running tests.
|
||||
// We don't have a good solution to test email sending yet, so we disable email sending when testing
|
||||
var IsTesting bool
|
||||
|
||||
// MainTest creates the test engine
|
||||
func MainTest(m *testing.M, pathToRoot string) {
|
||||
var err error
|
||||
fixturesDir := filepath.Join(pathToRoot, "models", "fixtures")
|
||||
if err = createTestEngine(fixturesDir); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating test engine: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
IsTesting = true
|
||||
|
||||
// Start the pseudo mail queue
|
||||
mail.StartMailDaemon()
|
||||
|
||||
// Create test database
|
||||
PrepareTestDatabase()
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func createTestEngine(fixturesDir string) error {
|
||||
var err error
|
||||
x, err = xorm.NewEngine("sqlite3", "file::memory:?cache=shared")
|
||||
//x, err = xorm.NewEngine("sqlite3", "db.db")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
x.SetMapper(core.GonicMapper{})
|
||||
|
||||
// Sync dat shit
|
||||
if err = x.StoreEngine("InnoDB").Sync2(tables...); err != nil {
|
||||
return fmt.Errorf("sync database struct error: %v", err)
|
||||
}
|
||||
|
||||
// Show SQL-Queries if necessary
|
||||
if os.Getenv("UNIT_TESTS_VERBOSE") == "1" {
|
||||
x.ShowSQL(true)
|
||||
}
|
||||
|
||||
return InitFixtures(&testfixtures.SQLite{}, fixturesDir)
|
||||
}
|
||||
|
||||
// PrepareTestDatabase load test fixtures into test database
|
||||
func PrepareTestDatabase() error {
|
||||
return LoadFixtures()
|
||||
}
|
109
pkg/models/user.go
Normal file
109
pkg/models/user.go
Normal file
@ -0,0 +1,109 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/labstack/echo"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// UserLogin Object to recive user credentials in JSON format
|
||||
type UserLogin struct {
|
||||
Username string `json:"username" form:"username"`
|
||||
Password string `json:"password" form:"password"`
|
||||
}
|
||||
|
||||
// User holds information about an user
|
||||
type User struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
|
||||
Username string `xorm:"varchar(250) not null unique" json:"username"`
|
||||
Password string `xorm:"varchar(250) not null" json:"-"`
|
||||
Email string `xorm:"varchar(250)" json:"email"`
|
||||
IsActive bool `json:"-"`
|
||||
|
||||
PasswordResetToken string `xorm:"varchar(450)" json:"-"`
|
||||
EmailConfirmToken string `xorm:"varchar(450)" json:"-"`
|
||||
|
||||
Created int64 `xorm:"created" json:"-"`
|
||||
Updated int64 `xorm:"updated" json:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for users
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
// APIUserPassword represents a user object without timestamps and a json password field.
|
||||
type APIUserPassword struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// APIFormat formats an API User into a normal user struct
|
||||
func (apiUser *APIUserPassword) APIFormat() User {
|
||||
return User{
|
||||
ID: apiUser.ID,
|
||||
Username: apiUser.Username,
|
||||
Password: apiUser.Password,
|
||||
Email: apiUser.Email,
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserByID gets informations about a user by its ID
|
||||
func GetUserByID(id int64) (user User, err error) {
|
||||
// Apparently xorm does otherwise look for all users but return only one, which leads to returing one even if the ID is 0
|
||||
if id < 1 {
|
||||
return User{}, ErrUserDoesNotExist{}
|
||||
}
|
||||
|
||||
return GetUser(User{ID: id})
|
||||
}
|
||||
|
||||
// GetUser gets a user object
|
||||
func GetUser(user User) (userOut User, err error) {
|
||||
userOut = user
|
||||
exists, err := x.Get(&userOut)
|
||||
|
||||
if !exists {
|
||||
return User{}, ErrUserDoesNotExist{UserID: user.ID}
|
||||
}
|
||||
|
||||
return userOut, err
|
||||
}
|
||||
|
||||
// CheckUserCredentials checks user credentials
|
||||
func CheckUserCredentials(u *UserLogin) (User, error) {
|
||||
|
||||
// Check if the user exists
|
||||
user, err := GetUser(User{Username: u.Username})
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Check the users password
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(u.Password))
|
||||
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetCurrentUser returns the current user based on its jwt token
|
||||
func GetCurrentUser(c echo.Context) (user User, err error) {
|
||||
jwtinf := c.Get("user").(*jwt.Token)
|
||||
claims := jwtinf.Claims.(jwt.MapClaims)
|
||||
userID, ok := claims["id"].(float64)
|
||||
if !ok {
|
||||
return user, ErrCouldNotGetUserID{}
|
||||
}
|
||||
user = User{
|
||||
ID: int64(userID),
|
||||
Email: claims["email"].(string),
|
||||
Username: claims["username"].(string),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
154
pkg/models/user_add_update.go
Normal file
154
pkg/models/user_add_update.go
Normal file
@ -0,0 +1,154 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/mail"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// CreateUser creates a new user and inserts it into the database
|
||||
func CreateUser(user User) (newUser User, err error) {
|
||||
|
||||
newUser = user
|
||||
|
||||
// Check if we have all needed informations
|
||||
if newUser.Password == "" || newUser.Username == "" {
|
||||
return User{}, ErrNoUsernamePassword{}
|
||||
}
|
||||
|
||||
// Check if the user already existst with that username
|
||||
exists := true
|
||||
existingUser, err := GetUser(User{Username: newUser.Username})
|
||||
if err != nil {
|
||||
if IsErrUserDoesNotExist(err) {
|
||||
exists = false
|
||||
} else {
|
||||
return User{}, err
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
return User{}, ErrUsernameExists{newUser.ID, newUser.Username}
|
||||
}
|
||||
|
||||
// Check if the user already existst with that email
|
||||
exists = true
|
||||
existingUser, err = GetUser(User{Email: newUser.Email})
|
||||
if err != nil {
|
||||
if IsErrUserDoesNotExist(err) {
|
||||
exists = false
|
||||
} else {
|
||||
return User{}, err
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
return User{}, ErrUserEmailExists{existingUser.ID, existingUser.Email}
|
||||
}
|
||||
|
||||
// Hash the password
|
||||
newUser.Password, err = hashPassword(user.Password)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Generate a confirm token
|
||||
newUser.EmailConfirmToken = utils.MakeRandomString(400)
|
||||
|
||||
// The new user should not be activated until it confirms his mail address
|
||||
newUser.IsActive = false
|
||||
|
||||
// Insert it
|
||||
_, err = x.Insert(newUser)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Get the full new User
|
||||
newUserOut, err := GetUser(newUser)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Create the user's namespace
|
||||
newN := &Namespace{Name: newUserOut.Username, Description: newUserOut.Username + "'s namespace.", Owner: newUserOut}
|
||||
err = newN.Create(&newUserOut)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Dont send a mail if we're testing
|
||||
if IsTesting {
|
||||
return newUserOut, err
|
||||
}
|
||||
|
||||
// Send the user a mail with a link to confirm the mail
|
||||
data := map[string]interface{}{
|
||||
"User": newUserOut,
|
||||
}
|
||||
|
||||
mail.SendMailWithTemplate(user.Email, newUserOut.Username+" + Vikunja = <3", "confirm-email", data)
|
||||
|
||||
return newUserOut, err
|
||||
}
|
||||
|
||||
// HashPassword hashes a password
|
||||
func hashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// UpdateUser updates a user
|
||||
func UpdateUser(user User) (updatedUser User, err error) {
|
||||
|
||||
// Check if it exists
|
||||
theUser, err := GetUserByID(user.ID)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Check if we have at least a username
|
||||
if user.Username == "" {
|
||||
//return User{}, ErrNoUsername{user.ID}
|
||||
user.Username = theUser.Username // Dont change the username if we dont have one
|
||||
}
|
||||
|
||||
user.Password = theUser.Password // set the password to the one in the database to not accedently resetting it
|
||||
|
||||
// Update it
|
||||
_, err = x.Id(user.ID).Update(user)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Get the newly updated user
|
||||
updatedUser, err = GetUserByID(user.ID)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
return updatedUser, err
|
||||
}
|
||||
|
||||
// UpdateUserPassword updates the password of a user
|
||||
func UpdateUserPassword(user *User, newPassword string) (err error) {
|
||||
|
||||
// Get all user details
|
||||
theUser, err := GetUserByID(user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Hash the new password and set it
|
||||
hashed, err := hashPassword(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
theUser.Password = hashed
|
||||
|
||||
// Update it
|
||||
_, err = x.Id(user.ID).Update(theUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
18
pkg/models/user_delete.go
Normal file
18
pkg/models/user_delete.go
Normal file
@ -0,0 +1,18 @@
|
||||
package models
|
||||
|
||||
// DeleteUserByID deletes a user by its ID
|
||||
func DeleteUserByID(id int64, doer *User) error {
|
||||
// Check if the id is 0
|
||||
if id == 0 {
|
||||
return ErrIDCannotBeZero{}
|
||||
}
|
||||
|
||||
// Delete the user
|
||||
_, err := x.Id(id).Delete(&User{})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
25
pkg/models/user_email_confirm.go
Normal file
25
pkg/models/user_email_confirm.go
Normal file
@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
// EmailConfirm holds the token to confirm a mail address
|
||||
type EmailConfirm struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// UserEmailConfirm handles the confirmation of an email address
|
||||
func UserEmailConfirm(c *EmailConfirm) (err error) {
|
||||
|
||||
user := User{}
|
||||
has, err := x.Where("email_confirm_token = ?", c.Token).Get(&user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !has {
|
||||
return ErrInvalidEmailConfirmToken{Token: c.Token}
|
||||
}
|
||||
|
||||
user.IsActive = true
|
||||
user.EmailConfirmToken = ""
|
||||
_, err = x.Where("id = ?", user.ID).Cols("is_active", "email_confirm_token").Update(&user)
|
||||
return
|
||||
}
|
94
pkg/models/user_password_reset.go
Normal file
94
pkg/models/user_password_reset.go
Normal file
@ -0,0 +1,94 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/mail"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
)
|
||||
|
||||
// PasswordReset holds the data to reset a password
|
||||
type PasswordReset struct {
|
||||
Token string `json:"token"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
// UserPasswordReset resets a users password
|
||||
func UserPasswordReset(reset *PasswordReset) (err error) {
|
||||
|
||||
// Check if the password is not empty
|
||||
if reset.NewPassword == "" {
|
||||
return ErrNoUsernamePassword{}
|
||||
}
|
||||
|
||||
// Check if we have a token
|
||||
var user User
|
||||
exists, err := x.Where("password_reset_token = ?", reset.Token).Get(&user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return ErrInvalidPasswordResetToken{Token: reset.Token}
|
||||
}
|
||||
|
||||
// Hash the password
|
||||
user.Password, err = hashPassword(reset.NewPassword)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Save it
|
||||
_, err = x.Where("id = ?", user.ID).Update(&user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Dont send a mail if we're testing
|
||||
if IsTesting {
|
||||
return
|
||||
}
|
||||
|
||||
// Send a mail to the user to notify it his password was changed.
|
||||
data := map[string]interface{}{
|
||||
"User": user,
|
||||
}
|
||||
|
||||
mail.SendMailWithTemplate(user.Email, "Your password on Vikunja was changed", "password-changed", data)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// PasswordTokenRequest defines the request format for password reset resqest
|
||||
type PasswordTokenRequest struct {
|
||||
Username string `json:"user_name"`
|
||||
}
|
||||
|
||||
// RequestUserPasswordResetToken inserts a random token to reset a users password into the databsse
|
||||
func RequestUserPasswordResetToken(tr *PasswordTokenRequest) (err error) {
|
||||
// Check if the user exists
|
||||
user, err := GetUser(User{Username: tr.Username})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a token and save it
|
||||
user.PasswordResetToken = utils.MakeRandomString(400)
|
||||
|
||||
// Save it
|
||||
_, err = x.Where("id = ?", user.ID).Update(&user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Dont send a mail if we're testing
|
||||
if IsTesting {
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"User": user,
|
||||
}
|
||||
|
||||
// Send the user a mail with the reset token
|
||||
mail.SendMailWithTemplate(user.Email, "Reset your password on Vikunja", "reset-password", data)
|
||||
return
|
||||
}
|
27
pkg/models/user_right.go
Normal file
27
pkg/models/user_right.go
Normal file
@ -0,0 +1,27 @@
|
||||
package models
|
||||
|
||||
// UserRight defines the rights users can have for lists/namespaces
|
||||
type UserRight int
|
||||
|
||||
// define unknown user right
|
||||
const (
|
||||
UserRightUnknown = -1
|
||||
)
|
||||
|
||||
// Enumerate all the user rights
|
||||
const (
|
||||
// Can read lists in a User
|
||||
UserRightRead UserRight = iota
|
||||
// Can write tasks in a User like lists and todo tasks. Cannot create new lists.
|
||||
UserRightWrite
|
||||
// Can manage a list/namespace, can do everything
|
||||
UserRightAdmin
|
||||
)
|
||||
|
||||
func (r UserRight) isValid() error {
|
||||
if r != UserRightAdmin && r != UserRightRead && r != UserRightWrite {
|
||||
return ErrInvalidUserRight{r}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
154
pkg/models/user_test.go
Normal file
154
pkg/models/user_test.go
Normal file
@ -0,0 +1,154 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateUser(t *testing.T) {
|
||||
// Create test database
|
||||
//assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
// Get our doer
|
||||
doer, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Our dummy user for testing
|
||||
dummyuser := User{
|
||||
Username: "testuu",
|
||||
Password: "1234",
|
||||
Email: "noone@example.com",
|
||||
}
|
||||
|
||||
// Create a new user
|
||||
createdUser, err := CreateUser(dummyuser)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create a second new user
|
||||
_, err = CreateUser(User{Username: dummyuser.Username + "2", Email: dummyuser.Email + "m", Password: dummyuser.Password})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check if it fails to create the same user again
|
||||
_, err = CreateUser(dummyuser)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Check if it fails to create a user with just the same username
|
||||
_, err = CreateUser(User{Username: dummyuser.Username, Password: "fsdf"})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUsernameExists(err))
|
||||
|
||||
// Check if it fails to create one with the same email
|
||||
_, err = CreateUser(User{Username: "noone", Password: "1234", Email: dummyuser.Email})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserEmailExists(err))
|
||||
|
||||
// Check if it fails to create a user without password and username
|
||||
_, err = CreateUser(User{})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNoUsernamePassword(err))
|
||||
|
||||
// Check if he exists
|
||||
theuser, err := GetUser(createdUser)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get by his ID
|
||||
_, err = GetUserByID(theuser.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Passing 0 as ID should return an error
|
||||
_, err = GetUserByID(0)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
|
||||
// Check the user credentials
|
||||
user, err := CheckUserCredentials(&UserLogin{"testuu", "1234"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "testuu", user.Username)
|
||||
|
||||
// Check wrong password (should also fail)
|
||||
_, err = CheckUserCredentials(&UserLogin{"testuu", "12345"})
|
||||
assert.Error(t, err)
|
||||
|
||||
// Check usercredentials for a nonexistent user (should fail)
|
||||
_, err = CheckUserCredentials(&UserLogin{"dfstestuu", "1234"})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
|
||||
// Update the user
|
||||
uuser, err := UpdateUser(User{ID: theuser.ID, Password: "444444"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, theuser.Password, uuser.Password) // Password should not change
|
||||
assert.Equal(t, theuser.Username, uuser.Username) // Username should not change either
|
||||
|
||||
// Try updating one which does not exist
|
||||
_, err = UpdateUser(User{ID: 99999, Username: "dg"})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
|
||||
// Update a users password
|
||||
newpassword := "55555"
|
||||
err = UpdateUserPassword(&theuser, newpassword)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check if it was changed
|
||||
user, err = CheckUserCredentials(&UserLogin{theuser.Username, newpassword})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check if the searchterm works
|
||||
all, err := ListUsers("test")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, len(all) > 0)
|
||||
|
||||
all, err = ListUsers("")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, len(all) > 0)
|
||||
|
||||
// Try updating the password of a nonexistent user (should fail)
|
||||
err = UpdateUserPassword(&User{ID: 9999}, newpassword)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
|
||||
// Delete it
|
||||
err = DeleteUserByID(theuser.ID, &doer)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try deleting one with ID = 0
|
||||
err = DeleteUserByID(0, &doer)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrIDCannotBeZero(err))
|
||||
}
|
||||
|
||||
func TestUserPasswordReset(t *testing.T) {
|
||||
// Request a new token
|
||||
tr := &PasswordTokenRequest{
|
||||
Username: "user1",
|
||||
}
|
||||
err := RequestUserPasswordResetToken(tr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get the token / inside the user object
|
||||
userWithToken, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try resetting it
|
||||
reset := &PasswordReset{
|
||||
Token: userWithToken.PasswordResetToken,
|
||||
}
|
||||
|
||||
// Try resetting it without a password
|
||||
reset.NewPassword = ""
|
||||
err = UserPasswordReset(reset)
|
||||
assert.True(t, IsErrNoUsernamePassword(err))
|
||||
|
||||
// Reset it
|
||||
reset.NewPassword = "1234"
|
||||
err = UserPasswordReset(reset)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try resetting it with a wrong token
|
||||
reset.Token = utils.MakeRandomString(400)
|
||||
err = UserPasswordReset(reset)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrInvalidPasswordResetToken(err))
|
||||
}
|
19
pkg/models/users_list.go
Normal file
19
pkg/models/users_list.go
Normal file
@ -0,0 +1,19 @@
|
||||
package models
|
||||
|
||||
// ListUsers returns a list with all users, filtered by an optional searchstring
|
||||
func ListUsers(searchterm string) (users []User, err error) {
|
||||
|
||||
if searchterm == "" {
|
||||
err = x.Find(&users)
|
||||
} else {
|
||||
err = x.
|
||||
Where("username LIKE ?", "%"+searchterm+"%").
|
||||
Find(&users)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return []User{}, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
Reference in New Issue
Block a user