Refactor User and DB handling (#123)
fix copyright date Add more user tests More user tests More user tests Start refactoring user tests Docs Fix lint Fix db fixtures init in tests Fix models test Fix loading fixtures Fix ineffasign Fix lint Fix integration tests Fix init of test engine creation Fix user related tests Better handling of creating test enging Moved all fixtures to db package Moved all fixtures to db package Moved user related stuff to seperate package Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/api/pulls/123
This commit is contained in:
50
pkg/user/db.go
Normal file
50
pkg/user/db.go
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright 2018-2020 Vikunja and contriubtors. All rights reserved.
|
||||
//
|
||||
// This file is part of Vikunja.
|
||||
//
|
||||
// Vikunja is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Vikunja is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Vikunja. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
var x *xorm.Engine
|
||||
|
||||
// InitDB sets up the database connection to use in this module
|
||||
func InitDB() (err error) {
|
||||
x, err = db.CreateDBEngine()
|
||||
if err != nil {
|
||||
log.Criticalf("Could not connect to db: %v", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Cache
|
||||
if config.CacheEnabled.GetBool() && config.CacheType.GetString() == "redis" {
|
||||
db.RegisterTableStructsForCache(GetTables())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTables returns all structs which are also a table.
|
||||
func GetTables() []interface{} {
|
||||
return []interface{}{
|
||||
&User{},
|
||||
}
|
||||
}
|
291
pkg/user/error.go
Normal file
291
pkg/user/error.go
Normal file
@ -0,0 +1,291 @@
|
||||
// Copyright2018-2020 Vikunja and contriubtors. All rights reserved.
|
||||
//
|
||||
// This file is part of Vikunja.
|
||||
//
|
||||
// Vikunja is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Vikunja is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Vikunja. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"code.vikunja.io/web"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// =====================
|
||||
// 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() web.HTTPError {
|
||||
return web.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() web.HTTPError {
|
||||
return web.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() web.HTTPError {
|
||||
return web.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() web.HTTPError {
|
||||
return web.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() web.HTTPError {
|
||||
return web.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() web.HTTPError {
|
||||
return web.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() web.HTTPError {
|
||||
return web.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() web.HTTPError {
|
||||
return web.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
|
||||
}
|
||||
|
||||
// ErrWrongUsernameOrPassword is an error where the email was not confirmed
|
||||
type ErrWrongUsernameOrPassword struct {
|
||||
}
|
||||
|
||||
func (err ErrWrongUsernameOrPassword) Error() string {
|
||||
return fmt.Sprintf("Wrong username or password")
|
||||
}
|
||||
|
||||
// ErrCodeWrongUsernameOrPassword holds the unique world-error code of this error
|
||||
const ErrCodeWrongUsernameOrPassword = 1011
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrWrongUsernameOrPassword) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeWrongUsernameOrPassword, Message: "Wrong username or password."}
|
||||
}
|
||||
|
||||
// IsErrWrongUsernameOrPassword checks if an error is a IsErrEmailNotConfirmed.
|
||||
func IsErrWrongUsernameOrPassword(err error) bool {
|
||||
_, ok := err.(ErrWrongUsernameOrPassword)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ErrEmailNotConfirmed is an error where the email was not confirmed
|
||||
type ErrEmailNotConfirmed struct {
|
||||
UserID int64
|
||||
}
|
||||
|
||||
func (err ErrEmailNotConfirmed) Error() string {
|
||||
return fmt.Sprintf("Email is not confirmed [UserID: %d]", err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeEmailNotConfirmed holds the unique world-error code of this error
|
||||
const ErrCodeEmailNotConfirmed = 1012
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrEmailNotConfirmed) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeEmailNotConfirmed, Message: "Please confirm your email address."}
|
||||
}
|
||||
|
||||
// IsErrEmailNotConfirmed checks if an error is a IsErrEmailNotConfirmed.
|
||||
func IsErrEmailNotConfirmed(err error) bool {
|
||||
_, ok := err.(ErrEmailNotConfirmed)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ErrEmptyNewPassword represents a "EmptyNewPassword" kind of error.
|
||||
type ErrEmptyNewPassword struct{}
|
||||
|
||||
// IsErrEmptyNewPassword checks if an error is a ErrEmptyNewPassword.
|
||||
func IsErrEmptyNewPassword(err error) bool {
|
||||
_, ok := err.(ErrEmptyNewPassword)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrEmptyNewPassword) Error() string {
|
||||
return fmt.Sprintf("New password is empty")
|
||||
}
|
||||
|
||||
// ErrCodeEmptyNewPassword holds the unique world-error code of this error
|
||||
const ErrCodeEmptyNewPassword = 1013
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrEmptyNewPassword) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeEmptyNewPassword, Message: "Please specify new password."}
|
||||
}
|
||||
|
||||
// ErrEmptyOldPassword represents a "EmptyOldPassword" kind of error.
|
||||
type ErrEmptyOldPassword struct{}
|
||||
|
||||
// IsErrEmptyOldPassword checks if an error is a ErrEmptyOldPassword.
|
||||
func IsErrEmptyOldPassword(err error) bool {
|
||||
_, ok := err.(ErrEmptyOldPassword)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrEmptyOldPassword) Error() string {
|
||||
return fmt.Sprintf("Old password is empty")
|
||||
}
|
||||
|
||||
// ErrCodeEmptyOldPassword holds the unique world-error code of this error
|
||||
const ErrCodeEmptyOldPassword = 1014
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrEmptyOldPassword) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeEmptyOldPassword, Message: "Please specify old password."}
|
||||
}
|
29
pkg/user/main_test.go
Normal file
29
pkg/user/main_test.go
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright 2018-2020 Vikunja and contriubtors. All rights reserved.
|
||||
//
|
||||
// This file is part of Vikunja.
|
||||
//
|
||||
// Vikunja is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Vikunja is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Vikunja. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestMain is the main test function used to bootstrap the test env
|
||||
func TestMain(m *testing.M) {
|
||||
InitTests()
|
||||
os.Exit(m.Run())
|
||||
}
|
42
pkg/user/test.go
Normal file
42
pkg/user/test.go
Normal file
@ -0,0 +1,42 @@
|
||||
// Copyright2018-2020 Vikunja and contriubtors. All rights reserved.
|
||||
//
|
||||
// This file is part of Vikunja.
|
||||
//
|
||||
// Vikunja is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Vikunja is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Vikunja. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// InitTests handles the actual bootstrapping of the test env
|
||||
func InitTests() {
|
||||
var err error
|
||||
x, err = db.CreateTestEngine()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = x.Sync2(GetTables()...)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.InitTestFixtures("users")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
359
pkg/user/user.go
Normal file
359
pkg/user/user.go
Normal file
@ -0,0 +1,359 @@
|
||||
// Copyright2018-2020 Vikunja and contriubtors. All rights reserved.
|
||||
//
|
||||
// This file is part of Vikunja.
|
||||
//
|
||||
// Vikunja is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Vikunja is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Vikunja. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/mail"
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"code.vikunja.io/web"
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/labstack/echo/v4"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Login Object to recive user credentials in JSON format
|
||||
type Login struct {
|
||||
// The username used to log in.
|
||||
Username string `json:"username"`
|
||||
// The password for the user.
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// User holds information about an user
|
||||
type User struct {
|
||||
// The unique, numeric id of this user.
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
|
||||
// The username of the user. Is always unique.
|
||||
Username string `xorm:"varchar(250) not null unique" json:"username" valid:"length(3|250)" minLength:"3" maxLength:"250"`
|
||||
Password string `xorm:"varchar(250) not null" json:"-"`
|
||||
// The user's email address.
|
||||
Email string `xorm:"varchar(250) null" json:"email,omitempty" valid:"email,length(0|250)" maxLength:"250"`
|
||||
IsActive bool `xorm:"null" json:"-"`
|
||||
// The users md5-hashed email address, used to get the avatar from gravatar and the likes.
|
||||
AvatarURL string `xorm:"-" json:"avatarUrl"`
|
||||
|
||||
PasswordResetToken string `xorm:"varchar(450) null" json:"-"`
|
||||
EmailConfirmToken string `xorm:"varchar(450) null" json:"-"`
|
||||
|
||||
// A unix timestamp when this task was created. You cannot change this value.
|
||||
Created int64 `xorm:"created not null" json:"created"`
|
||||
// A unix timestamp when this task was last updated. You cannot change this value.
|
||||
Updated int64 `xorm:"updated not null" json:"updated"`
|
||||
|
||||
web.Auth `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// AfterLoad is used to delete all emails to not have them leaked to the user
|
||||
func (u *User) AfterLoad() {
|
||||
u.AvatarURL = utils.Md5String(u.Email)
|
||||
}
|
||||
|
||||
// GetID implements the Auth interface
|
||||
func (u *User) GetID() int64 {
|
||||
return u.ID
|
||||
}
|
||||
|
||||
// TableName returns the table name for users
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
// GetFromAuth returns a user object from a web.Auth object and returns an error if the underlying type
|
||||
// is not a user object
|
||||
func GetFromAuth(a web.Auth) (*User, error) {
|
||||
u, is := a.(*User)
|
||||
if !is {
|
||||
return &User{}, fmt.Errorf("user is not user element, is %s", reflect.TypeOf(a))
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// APIUserPassword represents a user object without timestamps and a json password field.
|
||||
type APIUserPassword struct {
|
||||
// The unique, numeric id of this user.
|
||||
ID int64 `json:"id"`
|
||||
// The username of the username. Is always unique.
|
||||
Username string `json:"username" valid:"length(3|250)" minLength:"3" maxLength:"250"`
|
||||
// The user's password in clear text. Only used when registering the user.
|
||||
Password string `json:"password" valid:"length(8|250)" minLength:"8" maxLength:"250"`
|
||||
// The user's email address
|
||||
Email string `json:"email" valid:"email,length(0|250)" maxLength:"250"`
|
||||
}
|
||||
|
||||
// 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})
|
||||
}
|
||||
|
||||
// GetUserByUsername gets a user from its user name. This is an extra function to be able to add an extra error check.
|
||||
func GetUserByUsername(username string) (user *User, err error) {
|
||||
if username == "" {
|
||||
return &User{}, ErrUserDoesNotExist{}
|
||||
}
|
||||
|
||||
return GetUser(&User{Username: username})
|
||||
}
|
||||
|
||||
// GetUser gets a user object
|
||||
func GetUser(user *User) (userOut *User, err error) {
|
||||
return getUser(user, false)
|
||||
}
|
||||
|
||||
// GetUserWithEmail returns a user object with email
|
||||
func GetUserWithEmail(user *User) (userOut *User, err error) {
|
||||
return getUser(user, true)
|
||||
}
|
||||
|
||||
// getUser is a small helper function to avoid having duplicated code for almost the same use case
|
||||
func getUser(user *User, withEmail bool) (userOut *User, err error) {
|
||||
userOut = &User{} // To prevent a panic if user is nil
|
||||
*userOut = *user
|
||||
exists, err := x.Get(userOut)
|
||||
|
||||
if !exists {
|
||||
return &User{}, ErrUserDoesNotExist{UserID: user.ID}
|
||||
}
|
||||
|
||||
if !withEmail {
|
||||
userOut.Email = ""
|
||||
}
|
||||
|
||||
return userOut, err
|
||||
}
|
||||
|
||||
// CheckUserCredentials checks user credentials
|
||||
func CheckUserCredentials(u *Login) (*User, error) {
|
||||
// Check if we have any credentials
|
||||
if u.Password == "" || u.Username == "" {
|
||||
return &User{}, ErrNoUsernamePassword{}
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
user, err := GetUserByUsername(u.Username)
|
||||
if err != nil {
|
||||
// hashing the password takes a long time, so we hash something to not make it clear if the username was wrong
|
||||
bcrypt.GenerateFromPassword([]byte(u.Username), 14)
|
||||
return &User{}, ErrWrongUsernameOrPassword{}
|
||||
}
|
||||
|
||||
// User is invalid if it needs to verify its email address
|
||||
if !user.IsActive {
|
||||
return &User{}, ErrEmailNotConfirmed{UserID: user.ID}
|
||||
}
|
||||
|
||||
// Check the users password
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(u.Password))
|
||||
if err != nil {
|
||||
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||
return &User{}, ErrWrongUsernameOrPassword{}
|
||||
}
|
||||
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)
|
||||
return GetUserFromClaims(claims)
|
||||
}
|
||||
|
||||
// GetUserFromClaims Returns a new user from jwt claims
|
||||
func GetUserFromClaims(claims jwt.MapClaims) (user *User, err error) {
|
||||
userID, ok := claims["id"].(float64)
|
||||
if !ok {
|
||||
return user, ErrCouldNotGetUserID{}
|
||||
}
|
||||
user = &User{
|
||||
ID: int64(userID),
|
||||
Email: claims["email"].(string),
|
||||
Username: claims["username"].(string),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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 == "" || newUser.Email == "" {
|
||||
return &User{}, ErrNoUsernamePassword{}
|
||||
}
|
||||
|
||||
// Check if the user already existst with that username
|
||||
exists := true
|
||||
_, err = GetUserByUsername(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
|
||||
_, err = GetUser(&User{Email: newUser.Email})
|
||||
if err != nil {
|
||||
if IsErrUserDoesNotExist(err) {
|
||||
exists = false
|
||||
} else {
|
||||
return &User{}, err
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
return &User{}, ErrUserEmailExists{newUser.ID, newUser.Email}
|
||||
}
|
||||
|
||||
// Hash the password
|
||||
newUser.Password, err = hashPassword(user.Password)
|
||||
if err != nil {
|
||||
return &User{}, err
|
||||
}
|
||||
|
||||
newUser.IsActive = true
|
||||
if config.MailerEnabled.GetBool() {
|
||||
// The new user should not be activated until it confirms his mail address
|
||||
newUser.IsActive = false
|
||||
// Generate a confirm token
|
||||
newUser.EmailConfirmToken = utils.MakeRandomString(400)
|
||||
}
|
||||
|
||||
// Insert it
|
||||
_, err = x.Insert(newUser)
|
||||
if err != nil {
|
||||
return &User{}, err
|
||||
}
|
||||
|
||||
// Update the metrics
|
||||
metrics.UpdateCount(1, metrics.ActiveUsersKey)
|
||||
|
||||
// Get the full new User
|
||||
newUserOut, err := GetUser(newUser)
|
||||
if err != nil {
|
||||
return &User{}, err
|
||||
}
|
||||
|
||||
// Dont send a mail if we're testing
|
||||
if !config.MailerEnabled.GetBool() {
|
||||
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), 11)
|
||||
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) {
|
||||
|
||||
if newPassword == "" {
|
||||
return ErrEmptyNewPassword{}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
49
pkg/user/user_email_confirm.go
Normal file
49
pkg/user/user_email_confirm.go
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright2018-2020 Vikunja and contriubtors. All rights reserved.
|
||||
//
|
||||
// This file is part of Vikunja.
|
||||
//
|
||||
// Vikunja is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Vikunja is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Vikunja. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package user
|
||||
|
||||
// EmailConfirm holds the token to confirm a mail address
|
||||
type EmailConfirm struct {
|
||||
// The email confirm token sent via email.
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// ConfirmEmail handles the confirmation of an email address
|
||||
func ConfirmEmail(c *EmailConfirm) (err error) {
|
||||
|
||||
// Check if we have an email confirm token
|
||||
if c.Token == "" {
|
||||
return ErrInvalidEmailConfirmToken{}
|
||||
}
|
||||
|
||||
// Check if the token is valid
|
||||
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
|
||||
}
|
72
pkg/user/user_email_confirm_test.go
Normal file
72
pkg/user/user_email_confirm_test.go
Normal file
@ -0,0 +1,72 @@
|
||||
// Copyright2018-2020 Vikunja and contriubtors. All rights reserved.
|
||||
//
|
||||
// This file is part of Vikunja.
|
||||
//
|
||||
// Vikunja is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Vikunja is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Vikunja. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUserEmailConfirm(t *testing.T) {
|
||||
type args struct {
|
||||
c *EmailConfirm
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
errType func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "Test Empty token",
|
||||
args: args{
|
||||
c: &EmailConfirm{
|
||||
Token: "",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrInvalidEmailConfirmToken,
|
||||
},
|
||||
{
|
||||
name: "Test invalid token",
|
||||
args: args{
|
||||
c: &EmailConfirm{
|
||||
Token: "invalid",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errType: IsErrInvalidEmailConfirmToken,
|
||||
},
|
||||
{
|
||||
name: "Test valid token",
|
||||
args: args{
|
||||
c: &EmailConfirm{
|
||||
Token: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
if err := ConfirmEmail(tt.args.c); (err != nil) != tt.wantErr {
|
||||
t.Errorf("ConfirmEmail() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
118
pkg/user/user_password_reset.go
Normal file
118
pkg/user/user_password_reset.go
Normal file
@ -0,0 +1,118 @@
|
||||
// Copyright2018-2020 Vikunja and contriubtors. All rights reserved.
|
||||
//
|
||||
// This file is part of Vikunja.
|
||||
//
|
||||
// Vikunja is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Vikunja is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Vikunja. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/mail"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
)
|
||||
|
||||
// PasswordReset holds the data to reset a password
|
||||
type PasswordReset struct {
|
||||
// The previously issued reset token.
|
||||
Token string `json:"token"`
|
||||
// The new password for this user.
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
// ResetPassword resets a users password
|
||||
func ResetPassword(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 !config.MailerEnabled.GetBool() {
|
||||
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 {
|
||||
Email string `json:"email" valid:"email,length(0|250)" maxLength:"250"`
|
||||
}
|
||||
|
||||
// RequestUserPasswordResetToken inserts a random token to reset a users password into the databsse
|
||||
func RequestUserPasswordResetToken(tr *PasswordTokenRequest) (err error) {
|
||||
if tr.Email == "" {
|
||||
return ErrNoUsernamePassword{}
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
user, err := GetUserWithEmail(&User{Email: tr.Email})
|
||||
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 !config.MailerEnabled.GetBool() {
|
||||
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
|
||||
}
|
301
pkg/user/user_test.go
Normal file
301
pkg/user/user_test.go
Normal file
@ -0,0 +1,301 @@
|
||||
// Copyright 2018-2020 Vikunja and contriubtors. All rights reserved.
|
||||
//
|
||||
// This file is part of Vikunja.
|
||||
//
|
||||
// Vikunja is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Vikunja is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Vikunja. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateUser(t *testing.T) {
|
||||
// Our dummy user for testing
|
||||
dummyuser := &User{
|
||||
Username: "testuser",
|
||||
Password: "1234",
|
||||
Email: "noone@example.com",
|
||||
}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
createdUser, err := CreateUser(dummyuser)
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, createdUser.Created)
|
||||
})
|
||||
t.Run("already existing", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
_, err := CreateUser(&User{
|
||||
Username: "user1",
|
||||
Password: "12345",
|
||||
Email: "email@example.com",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUsernameExists(err))
|
||||
})
|
||||
t.Run("same email", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
_, err := CreateUser(&User{
|
||||
Username: "testuser",
|
||||
Password: "12345",
|
||||
Email: "user1@example.com",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserEmailExists(err))
|
||||
})
|
||||
t.Run("no username", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
_, err := CreateUser(&User{
|
||||
Username: "",
|
||||
Password: "12345",
|
||||
Email: "user1@example.com",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNoUsernamePassword(err))
|
||||
})
|
||||
t.Run("no password", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
_, err := CreateUser(&User{
|
||||
Username: "testuser",
|
||||
Password: "",
|
||||
Email: "user1@example.com",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNoUsernamePassword(err))
|
||||
})
|
||||
t.Run("no email", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
_, err := CreateUser(&User{
|
||||
Username: "testuser",
|
||||
Password: "12345",
|
||||
Email: "",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNoUsernamePassword(err))
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetUser(t *testing.T) {
|
||||
t.Run("by name", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
theuser, err := GetUser(&User{
|
||||
Username: "user1",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, theuser.ID, int64(1))
|
||||
assert.Empty(t, theuser.Email)
|
||||
})
|
||||
t.Run("by email", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
theuser, err := GetUser(&User{
|
||||
Email: "user1@example.com",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, theuser.ID, int64(1))
|
||||
assert.Empty(t, theuser.Email)
|
||||
})
|
||||
t.Run("by id", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
theuser, err := GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, theuser.ID, int64(1))
|
||||
assert.Equal(t, theuser.Username, "user1")
|
||||
assert.Empty(t, theuser.Email)
|
||||
})
|
||||
t.Run("invalid id", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
_, err := GetUserByID(99999)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
})
|
||||
t.Run("nonexistant", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
_, err := GetUserByID(0)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
})
|
||||
t.Run("empty name", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
_, err := GetUserByUsername("")
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
})
|
||||
t.Run("with email", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
theuser, err := GetUserWithEmail(&User{ID: 1})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, theuser.ID, int64(1))
|
||||
assert.Equal(t, theuser.Username, "user1")
|
||||
assert.NotEmpty(t, theuser.Email)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckUserCredentials(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
_, err := CheckUserCredentials(&Login{"user1", "1234"})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("unverified email", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
_, err := CheckUserCredentials(&Login{"user5", "1234"})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrEmailNotConfirmed(err))
|
||||
})
|
||||
t.Run("wrong password", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
_, err := CheckUserCredentials(&Login{"user1", "12345"})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrWrongUsernameOrPassword(err))
|
||||
})
|
||||
t.Run("nonexistant user", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
_, err := CheckUserCredentials(&Login{"dfstestuu", "1234"})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrWrongUsernameOrPassword(err))
|
||||
})
|
||||
t.Run("empty password", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
_, err := CheckUserCredentials(&Login{"user1", ""})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNoUsernamePassword(err))
|
||||
})
|
||||
t.Run("empty username", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
_, err := CheckUserCredentials(&Login{"", "1234"})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNoUsernamePassword(err))
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateUser(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
uuser, err := UpdateUser(&User{
|
||||
ID: 1,
|
||||
Password: "LoremIpsum",
|
||||
Email: "testing@example.com",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", uuser.Password) // Password should not change
|
||||
assert.Equal(t, "user1", uuser.Username) // Username should not change either
|
||||
})
|
||||
t.Run("change username", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
uuser, err := UpdateUser(&User{
|
||||
ID: 1,
|
||||
Username: "changedname",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", uuser.Password) // Password should not change
|
||||
assert.Equal(t, "changedname", uuser.Username)
|
||||
})
|
||||
t.Run("nonexistant", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
_, err := UpdateUser(&User{
|
||||
ID: 99999,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateUserPassword(t *testing.T) {
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
err := UpdateUserPassword(&User{
|
||||
ID: 1,
|
||||
}, "12345",
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("nonexistant user", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
err := UpdateUserPassword(&User{
|
||||
ID: 9999,
|
||||
}, "12345")
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
})
|
||||
t.Run("empty password", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
err := UpdateUserPassword(&User{
|
||||
ID: 1,
|
||||
}, "",
|
||||
)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrEmptyNewPassword(err))
|
||||
})
|
||||
}
|
||||
|
||||
func TestListUsers(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
all, err := ListUsers("user1")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, len(all) > 0)
|
||||
assert.Equal(t, all[0].Username, "user1")
|
||||
})
|
||||
t.Run("all users", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
all, err := ListUsers("")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, all, 13)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserPasswordReset(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
reset := &PasswordReset{
|
||||
Token: "passwordresettesttoken",
|
||||
NewPassword: "12345",
|
||||
}
|
||||
err := ResetPassword(reset)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("without password", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
reset := &PasswordReset{
|
||||
Token: "passwordresettesttoken",
|
||||
}
|
||||
err := ResetPassword(reset)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNoUsernamePassword(err))
|
||||
})
|
||||
t.Run("empty token", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
reset := &PasswordReset{
|
||||
Token: "somethingsomething",
|
||||
NewPassword: "12345",
|
||||
}
|
||||
err := ResetPassword(reset)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrInvalidPasswordResetToken(err))
|
||||
})
|
||||
t.Run("wrong token", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
reset := &PasswordReset{
|
||||
Token: "somethingsomething",
|
||||
NewPassword: "12345",
|
||||
}
|
||||
err := ResetPassword(reset)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrInvalidPasswordResetToken(err))
|
||||
})
|
||||
}
|
36
pkg/user/users_list.go
Normal file
36
pkg/user/users_list.go
Normal file
@ -0,0 +1,36 @@
|
||||
// Copyright2018-2020 Vikunja and contriubtors. All rights reserved.
|
||||
//
|
||||
// This file is part of Vikunja.
|
||||
//
|
||||
// Vikunja is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Vikunja is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Vikunja. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package user
|
||||
|
||||
// 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