1
0

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:
konrad
2020-01-26 17:08:06 +00:00
parent 8c33e24e92
commit 7e9446ea07
108 changed files with 1506 additions and 1024 deletions

50
pkg/user/db.go Normal file
View 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
View 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
View 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
View 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
View 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
}

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

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

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