434 lines
11 KiB
Go
434 lines
11 KiB
Go
// Copyright 2018 Lars Hoogestraat
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package models
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.hoogi.eu/snafu/go-blog/crypt"
|
|
"git.hoogi.eu/snafu/go-blog/httperror"
|
|
"git.hoogi.eu/snafu/go-blog/logger"
|
|
"git.hoogi.eu/snafu/go-blog/settings"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
//UserDatasourceService defines an interface for CRUD operations for users
|
|
type UserDatasourceService interface {
|
|
Create(u *User) (int, error)
|
|
List(p *Pagination) ([]User, error)
|
|
Get(userID int) (*User, error)
|
|
Update(u *User, changePassword bool) error
|
|
Count(ac AdminCriteria) (int, error)
|
|
GetByMail(mail string) (*User, error)
|
|
GetByUsername(username string) (*User, error)
|
|
Remove(userID int) error
|
|
}
|
|
|
|
//User represents a user
|
|
type User struct {
|
|
ID int
|
|
Username string
|
|
Email string
|
|
DisplayName string
|
|
Password []byte
|
|
PlainPassword []byte
|
|
Salt []byte
|
|
LastModified time.Time
|
|
Active bool
|
|
IsAdmin bool
|
|
}
|
|
|
|
const (
|
|
bcryptRounds = 12
|
|
)
|
|
|
|
//UserService containing the service to access users
|
|
type UserService struct {
|
|
Datasource UserDatasourceService
|
|
Config settings.User
|
|
UserInterceptor UserInterceptor
|
|
}
|
|
|
|
//UserInterceptor will be executed before and after updating/creating users
|
|
type UserInterceptor interface {
|
|
PreCreate(user *User) error
|
|
PostCreate(user *User) error
|
|
PreUpdate(oldUser *User, user *User) error
|
|
PostUpdate(oldUser *User, user *User) error
|
|
PreRemove(user *User) error
|
|
PostRemove(user *User) error
|
|
}
|
|
|
|
type Validations int
|
|
|
|
const (
|
|
VDupEmail = 1 << iota
|
|
VDupUsername
|
|
VPassword
|
|
)
|
|
|
|
func (u *User) validate(us UserService, minPasswordLength int, v Validations) error {
|
|
u.DisplayName = strings.TrimSpace(u.DisplayName)
|
|
u.Email = strings.TrimSpace(u.Email)
|
|
u.Username = strings.TrimSpace(u.Username)
|
|
|
|
if len(u.DisplayName) == 0 {
|
|
return httperror.ValueRequired("display name")
|
|
}
|
|
|
|
if len([]rune(u.DisplayName)) > 191 {
|
|
return httperror.ValueTooLong("display name", 191)
|
|
}
|
|
|
|
if len(u.Email) == 0 {
|
|
return httperror.ValueRequired("email")
|
|
}
|
|
|
|
if len(u.Email) > 191 {
|
|
return httperror.ValueTooLong("email", 191)
|
|
}
|
|
|
|
if len(u.Username) == 0 {
|
|
return httperror.ValueRequired("username")
|
|
}
|
|
|
|
if len([]rune(u.Username)) > 60 {
|
|
return httperror.ValueTooLong("username", 60)
|
|
}
|
|
|
|
if (v & VPassword) != 0 {
|
|
if len(u.PlainPassword) < minPasswordLength && len(u.PlainPassword) > 0 {
|
|
return httperror.New(http.StatusUnprocessableEntity,
|
|
fmt.Sprintf("The password is too short. It must be at least %d characters long.", minPasswordLength),
|
|
fmt.Errorf("the password is too short, it must be at least %d characters long", minPasswordLength),
|
|
)
|
|
}
|
|
}
|
|
|
|
if (v & VDupEmail) != 0 {
|
|
err := us.duplicateMail(u.Email)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if (v & VDupUsername) != 0 {
|
|
err := us.duplicateUsername(u.Username)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (us UserService) duplicateMail(mail string) error {
|
|
user, err := us.Datasource.GetByMail(mail)
|
|
if err != nil {
|
|
if err != sql.ErrNoRows {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if user != nil {
|
|
return httperror.New(http.StatusUnprocessableEntity, fmt.Sprintf("The mail %s already exists.", mail), fmt.Errorf("the mail %s already exits", mail))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (us UserService) duplicateUsername(username string) error {
|
|
user, err := us.Datasource.GetByUsername(username)
|
|
if err != nil {
|
|
if err != sql.ErrNoRows {
|
|
return err
|
|
}
|
|
}
|
|
if user != nil {
|
|
return httperror.New(http.StatusUnprocessableEntity,
|
|
fmt.Sprintf("The username %s already exists.", username),
|
|
fmt.Errorf("the username %s already exists", username))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
//Count returns the amount of users
|
|
func (us UserService) Count(a AdminCriteria) (int, error) {
|
|
return us.Datasource.Count(a)
|
|
}
|
|
|
|
//List returns a list of users. Limits the amount based on the defined pagination
|
|
func (us UserService) List(p *Pagination) ([]User, error) {
|
|
return us.Datasource.List(p)
|
|
}
|
|
|
|
//GetByID gets the user based on the given id; will not contain the user password
|
|
func (us UserService) GetByID(userID int) (*User, error) {
|
|
u, err := us.Datasource.Get(userID)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, httperror.NotFound("user", fmt.Errorf("the user with id %d was not found", userID))
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
return u, nil
|
|
}
|
|
|
|
//GetByUsername gets the user based on the given username; will contain the user password
|
|
func (us UserService) GetByUsername(username string) (*User, error) {
|
|
u, err := us.Datasource.GetByUsername(username)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, httperror.NotFound("user", err)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
return u, nil
|
|
}
|
|
|
|
//GetByMail gets the user based on the given mail; will contain the user password
|
|
func (us UserService) GetByMail(mail string) (*User, error) {
|
|
u, err := us.Datasource.GetByMail(mail)
|
|
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, httperror.NotFound("user", err)
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
//Create creates the user
|
|
//If an UserInterceptor is available the action PreCreate is executed before creating and PostCreate after creating the user
|
|
func (us UserService) Create(u *User) (int, error) {
|
|
if us.UserInterceptor != nil {
|
|
if err := us.UserInterceptor.PreCreate(u); err != nil {
|
|
return -1, httperror.InternalServerError(fmt.Errorf("error while executing user interceptor 'PreCreate' error %v", err))
|
|
}
|
|
}
|
|
|
|
if err := u.validate(us, us.Config.MinPasswordLength, VDupUsername|VDupEmail|VPassword); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
salt := crypt.GenerateSalt()
|
|
saltedPassword := append(u.PlainPassword[:], salt[:]...)
|
|
password, err := crypt.CryptPassword([]byte(saltedPassword), bcryptRounds)
|
|
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
u.Salt = salt
|
|
u.Password = password
|
|
|
|
userID, err := us.Datasource.Create(u)
|
|
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
if us.UserInterceptor != nil {
|
|
errUserInterceptor := us.UserInterceptor.PostCreate(u)
|
|
logger.Log.Errorf("error while executing PostCreate user interceptor method %v", errUserInterceptor)
|
|
}
|
|
|
|
salt = nil
|
|
saltedPassword = nil
|
|
u.PlainPassword = nil
|
|
|
|
return userID, nil
|
|
}
|
|
|
|
//Update updates the user
|
|
//If an UserInterceptor is available the action PreUpdate is executed before updating and PostUpdate after updating the user
|
|
func (us UserService) Update(u *User, changePassword bool) error {
|
|
oldUser, err := us.Datasource.Get(u.ID)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !oldUser.IsAdmin {
|
|
if oldUser.ID != u.ID {
|
|
return httperror.PermissionDenied("update", "user", fmt.Errorf("permission denied user %d is not granted to update user %d", oldUser.ID, u.ID))
|
|
}
|
|
}
|
|
|
|
if us.UserInterceptor != nil {
|
|
|
|
if err := us.UserInterceptor.PreUpdate(oldUser, u); err != nil {
|
|
return httperror.InternalServerError(fmt.Errorf("error while executing user interceptor 'PreUpdate' error %v", err))
|
|
}
|
|
}
|
|
|
|
var v Validations
|
|
|
|
if u.Email != oldUser.Email {
|
|
v |= VDupEmail
|
|
}
|
|
|
|
if u.Username != oldUser.Username {
|
|
v |= VDupUsername
|
|
}
|
|
|
|
if changePassword {
|
|
v |= VPassword
|
|
}
|
|
|
|
if err = u.validate(us, us.Config.MinPasswordLength, v); err != nil {
|
|
return err
|
|
}
|
|
|
|
oneAdmin, err := us.OneAdmin()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if oneAdmin {
|
|
if (oldUser.IsAdmin && !u.IsAdmin) || (oldUser.IsAdmin && !u.Active) {
|
|
return httperror.New(http.StatusUnprocessableEntity,
|
|
"Could not update user, because no administrator would remain",
|
|
fmt.Errorf("could not update user %s action, because no administrator would remain", oldUser.Username))
|
|
}
|
|
}
|
|
|
|
if changePassword {
|
|
salt := crypt.GenerateSalt()
|
|
saltedPassword := append(u.PlainPassword[:], salt[:]...)
|
|
password, err := crypt.CryptPassword([]byte(saltedPassword), bcryptRounds)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
u.Password = password
|
|
u.Salt = salt
|
|
}
|
|
|
|
err = us.Datasource.Update(u, changePassword)
|
|
|
|
u.Password = nil
|
|
|
|
if us.UserInterceptor != nil {
|
|
if err := us.UserInterceptor.PostUpdate(oldUser, u); err != nil {
|
|
logger.Log.Errorf("error while executing PostUpdate user interceptor method %v", err)
|
|
}
|
|
}
|
|
u.PlainPassword = nil
|
|
|
|
return err
|
|
}
|
|
|
|
// Authenticate authenticates the user by the given login method (email or username)
|
|
// if the user was found but the password is wrong the found user and an error will be returned
|
|
func (us UserService) Authenticate(u *User, loginMethod settings.LoginMethod) (*User, error) {
|
|
var err error
|
|
|
|
if len(u.Username) == 0 || len(u.PlainPassword) == 0 {
|
|
return nil, httperror.New(http.StatusUnauthorized, "Your username or password is invalid.", errors.New("no username or password were given"))
|
|
}
|
|
|
|
var password = u.PlainPassword
|
|
|
|
if loginMethod == settings.EMail {
|
|
u, err = us.Datasource.GetByMail(u.Email)
|
|
} else {
|
|
u, err = us.Datasource.GetByUsername(u.Username)
|
|
}
|
|
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
//Do some extra work
|
|
bcrypt.CompareHashAndPassword([]byte("$2a$12$bQlRnXTNZMp6kCyoAlnf3uZW5vtmSj9CHP7pYplRUVK2n0C5xBHBa"), password)
|
|
return nil, httperror.New(http.StatusUnauthorized, "Your username or password is invalid.", err)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
u.PlainPassword = password
|
|
|
|
if err := u.comparePassword(); err != nil {
|
|
return u, httperror.New(http.StatusUnauthorized, "Your username or password is invalid.", err)
|
|
}
|
|
|
|
if !u.Active {
|
|
return nil, httperror.New(http.StatusUnprocessableEntity,
|
|
"Your account is deactivated.",
|
|
fmt.Errorf("the user with id %d tried to logged in but the account is deactivated", u.ID))
|
|
}
|
|
|
|
u.PlainPassword = nil
|
|
u.Password = nil
|
|
u.Salt = nil
|
|
|
|
return u, nil
|
|
}
|
|
|
|
// Remove removes the user returns an error if no administrator would remain
|
|
func (us UserService) Remove(u *User) error {
|
|
if us.UserInterceptor != nil {
|
|
if err := us.UserInterceptor.PreRemove(u); err != nil {
|
|
return httperror.InternalServerError(fmt.Errorf("error while executing user interceptor 'PreRemove' error %v", err))
|
|
}
|
|
}
|
|
|
|
oneAdmin, err := us.OneAdmin()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if oneAdmin {
|
|
if u.IsAdmin {
|
|
return httperror.New(http.StatusUnprocessableEntity,
|
|
"Could not remove administrator. No Administrator would remain.",
|
|
fmt.Errorf("could not remove administrator %s no administrator would remain", u.Username))
|
|
}
|
|
}
|
|
|
|
err = us.Datasource.Remove(u.ID)
|
|
|
|
if us.UserInterceptor != nil {
|
|
if err := us.UserInterceptor.PostRemove(u); err != nil {
|
|
logger.Log.Errorf("error while executing PostRemove user interceptor method %v", err)
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
//OneAdmin returns true if there is only one admin
|
|
func (us UserService) OneAdmin() (bool, error) {
|
|
c, err := us.Datasource.Count(OnlyAdmins)
|
|
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
|
|
if c == 1 {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (u User) comparePassword() error {
|
|
return bcrypt.CompareHashAndPassword(u.Password, append(u.PlainPassword[:], u.Salt[:]...))
|
|
}
|