vikunja-api/pkg/models/user.go

393 lines
10 KiB
Go
Raw Normal View History

2018-11-26 21:17:33 +01:00
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2018 Vikunja and contributors. All rights reserved.
//
// This program 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.
//
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
2018-06-10 11:11:41 +02:00
package models
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"
2018-12-01 00:26:56 +01:00
"code.vikunja.io/web"
"fmt"
2018-06-10 11:11:41 +02:00
"github.com/dgrijalva/jwt-go"
2019-05-07 21:42:24 +02:00
"github.com/labstack/echo/v4"
2018-06-10 11:11:41 +02:00
"golang.org/x/crypto/bcrypt"
2018-12-01 00:26:56 +01:00
"reflect"
"time"
2018-06-10 11:11:41 +02:00
)
// UserLogin Object to recive user credentials in JSON format
type UserLogin struct {
2019-01-03 23:22:06 +01:00
// The username used to log in.
Username string `json:"username"`
2019-01-03 23:22:06 +01:00
// The password for the user.
Password string `json:"password"`
2018-06-10 11:11:41 +02:00
}
// User holds information about an user
type User struct {
2019-01-03 23:22:06 +01:00
// 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.
2019-01-03 23:22:06 +01:00
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.
2019-04-23 10:34:06 +02:00
Email string `xorm:"varchar(250) null" json:"email,omitempty" valid:"email,length(0|250)" maxLength:"250"`
2019-03-29 18:54:35 +01:00
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"`
2018-10-27 11:33:28 +02:00
2019-03-29 18:54:35 +01:00
PasswordResetToken string `xorm:"varchar(450) null" json:"-"`
EmailConfirmToken string `xorm:"varchar(450) null" json:"-"`
2018-10-27 11:33:28 +02:00
2019-01-03 23:22:06 +01:00
// A unix timestamp when this task was created. You cannot change this value.
2019-03-29 18:54:35 +01:00
Created int64 `xorm:"created not null" json:"created"`
2019-01-03 23:22:06 +01:00
// A unix timestamp when this task was last updated. You cannot change this value.
2019-03-29 18:54:35 +01:00
Updated int64 `xorm:"updated not null" json:"updated"`
2018-12-01 00:26:56 +01:00
web.Auth `xorm:"-" json:"-"`
2018-06-10 11:11:41 +02:00
}
2019-04-01 20:19:55 +02:00
// 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)
2019-04-01 20:19:55 +02:00
u.Email = ""
}
// GetID implements the Auth interface
func (u *User) GetID() int64 {
return u.ID
}
2018-12-01 00:26:56 +01:00
2018-06-10 11:11:41 +02:00
// TableName returns the table name for users
func (User) TableName() string {
return "users"
}
2018-12-01 00:26:56 +01:00
func getUserWithError(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
}
2018-07-10 14:02:23 +02:00
// APIUserPassword represents a user object without timestamps and a json password field.
type APIUserPassword struct {
2019-01-03 23:22:06 +01:00
// 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"`
2018-06-13 13:45:22 +02:00
}
2018-07-10 14:02:23 +02:00
// APIFormat formats an API User into a normal user struct
func (apiUser *APIUserPassword) APIFormat() User {
2018-06-13 13:45:22 +02:00
return User{
ID: apiUser.ID,
Username: apiUser.Username,
Password: apiUser.Password,
Email: apiUser.Email,
}
}
2018-06-10 11:11:41 +02:00
// GetUserByID gets informations about a user by its ID
2018-08-30 19:14:02 +02:00
func GetUserByID(id int64) (user User, err error) {
2018-06-10 11:11:41 +02:00
// 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{}
2018-06-10 11:11:41 +02:00
}
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})
}
2018-06-10 11:11:41 +02:00
// GetUser gets a user object
2018-08-30 19:14:02 +02:00
func GetUser(user User) (userOut User, err error) {
2018-06-10 11:11:41 +02:00
userOut = user
2018-08-30 19:14:02 +02:00
exists, err := x.Get(&userOut)
2018-06-10 11:11:41 +02:00
if !exists {
2018-10-27 11:33:28 +02:00
return User{}, ErrUserDoesNotExist{UserID: user.ID}
2018-06-10 11:11:41 +02:00
}
2018-08-30 19:14:02 +02:00
return userOut, err
2018-06-10 11:11:41 +02:00
}
// CheckUserCredentials checks user credentials
func CheckUserCredentials(u *UserLogin) (User, error) {
// Check if we have any credentials
if u.Password == "" || u.Username == "" {
return User{}, ErrNoUsernamePassword{}
}
2018-06-10 11:11:41 +02:00
// Check if the user exists
user, err := GetUserByUsername(u.Username)
2018-06-10 11:11:41 +02:00
if err != nil {
2018-12-19 22:05:25 +01:00
// 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{}
2018-06-10 11:11:41 +02:00
}
// User is invalid if it needs to verify its email address
if !user.IsActive {
return User{}, ErrEmailNotConfirmed{UserID: user.ID}
}
2018-06-10 11:11:41 +02:00
// Check the users password
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(u.Password))
if err != nil {
if err == bcrypt.ErrMismatchedHashAndPassword {
return User{}, ErrWrongUsernameOrPassword{}
}
2018-06-10 11:11:41 +02:00
return User{}, err
}
return user, nil
}
// GetCurrentUser returns the current user based on its jwt token
2018-12-01 00:26:56 +01:00
func GetCurrentUser(c echo.Context) (user *User, err error) {
2018-06-10 11:11:41 +02:00
jwtinf := c.Get("user").(*jwt.Token)
claims := jwtinf.Claims.(jwt.MapClaims)
userID, ok := claims["id"].(float64)
if !ok {
return user, ErrCouldNotGetUserID{}
}
2018-12-01 00:26:56 +01:00
user = &User{
2018-06-10 11:11:41 +02:00
ID: int64(userID),
Email: claims["email"].(string),
Username: claims["username"].(string),
}
return
}
// UpdateActiveUsersFromContext updates the currently active users in redis
func UpdateActiveUsersFromContext(c echo.Context) (err error) {
user, err := GetCurrentUser(c)
if err != nil {
return err
}
allActiveUsers, err := metrics.GetActiveUsers()
if err != nil {
return
}
var uupdated bool
for in, u := range allActiveUsers {
if u.UserID == user.ID {
allActiveUsers[in].LastSeen = time.Now()
uupdated = true
}
}
if !uupdated {
allActiveUsers = append(allActiveUsers, &metrics.ActiveUser{UserID: user.ID, LastSeen: time.Now()})
}
return metrics.SetActiveUsers(allActiveUsers)
}
// 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
existingUser, 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
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
}
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
}
// 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 !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
}
// 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
}
// Update the metrics
metrics.UpdateCount(-1, metrics.ActiveUsersKey)
return err
}