eintopf.info@v0.13.16/service/user/user.go (about)

     1  // Copyright (C) 2022 The Eintopf authors
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <https://www.gnu.org/licenses/>.
    15  
    16  package user
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"time"
    22  
    23  	"golang.org/x/crypto/bcrypt"
    24  
    25  	"eintopf.info/internal/crud"
    26  	"eintopf.info/internal/xerror"
    27  	"eintopf.info/service/auth"
    28  )
    29  
    30  type user interface {
    31  	getID() string
    32  	getEmail() string
    33  	getNickname() string
    34  	getPassword() string
    35  }
    36  
    37  // NewUser defines the data for a new user.
    38  type NewUser struct {
    39  	Email    string    `json:"email"`
    40  	Nickname string    `json:"nickname"`
    41  	Password string    `json:"password"`
    42  	Role     auth.Role `json:"role"`
    43  }
    44  
    45  func (n *NewUser) getID() string {
    46  	return ""
    47  }
    48  
    49  func (n *NewUser) getEmail() string {
    50  	return n.Email
    51  }
    52  
    53  func (n *NewUser) getNickname() string {
    54  	return n.Nickname
    55  }
    56  
    57  func (n *NewUser) getPassword() string {
    58  	return n.Password
    59  }
    60  
    61  // User defines the user model by embedding NewUser along with additional
    62  // fields, that can't be set by the user during creation.
    63  type User struct {
    64  	ID          string    `json:"id" db:"id"`
    65  	Deactivated bool      `json:"deactivated" db:"deactivated"`
    66  	CreatedAt   time.Time `json:"createdAt" db:"created_at"`
    67  	Email       string    `json:"email" db:"email"`
    68  	Nickname    string    `json:"nickname" db:"nickname"`
    69  	Password    string    `json:"password" db:"password"`
    70  	Role        auth.Role `json:"role" db:"role"`
    71  }
    72  
    73  // UserFromNewUser converts a NewUser to a User.
    74  func UserFromNewUser(newUser *NewUser, id string) *User {
    75  	return &User{
    76  		ID:          id,
    77  		Deactivated: false,
    78  		CreatedAt:   time.Now(),
    79  		Email:       newUser.Email,
    80  		Nickname:    newUser.Nickname,
    81  		Password:    newUser.Password,
    82  		Role:        newUser.Role,
    83  	}
    84  }
    85  
    86  func (u User) Identifier() string {
    87  	return u.ID
    88  }
    89  
    90  func (u *User) getID() string {
    91  	return u.ID
    92  }
    93  
    94  func (u *User) getEmail() string {
    95  	return u.Email
    96  }
    97  
    98  func (u *User) getNickname() string {
    99  	return u.Nickname
   100  }
   101  
   102  func (u *User) getPassword() string {
   103  	return u.Password
   104  }
   105  
   106  // SortOrder defines the order of sorting.
   107  type SortOrder string
   108  
   109  // Possible values for SortOrder.
   110  const (
   111  	OrderAsc  = SortOrder("ASC")
   112  	OrderDesc = SortOrder("DESC")
   113  )
   114  
   115  // FindParams defines parameters used by the Find method.
   116  type FindParams struct {
   117  	Offset int64
   118  	Limit  int64
   119  
   120  	Sort  string
   121  	Order SortOrder
   122  
   123  	Filters *FindFilters
   124  }
   125  
   126  // FindFilters defines the possible filters for the find method.
   127  type FindFilters struct {
   128  	ID           *string    `db:"id"`
   129  	NotID        *string    `db:"id"`
   130  	Deactivated  *bool      `db:"deactivated"`
   131  	Email        *string    `db:"email"`
   132  	Nickname     *string    `db:"nickname"`
   133  	LikeNickname *string    `db:"nickname"`
   134  	Password     *string    `db:"password"`
   135  	Role         *auth.Role `db:"role"`
   136  }
   137  
   138  // Storer defines a service for CRUD operations on the user model.
   139  type Storer = crud.Storer[NewUser, User, FindFilters]
   140  
   141  // Service defines a service to manage users.
   142  // generate go run github.com/petergtz/pegomock/pegomock generate eintopf.info/service/user Service --output=../../internal/mock/user_service.go --package=mock --mock-name=UserService
   143  type Service interface {
   144  	Storer
   145  	auth.Authenticator
   146  	auth.Authorizer
   147  }
   148  
   149  type service struct {
   150  	store Storer
   151  }
   152  
   153  // NewService returns a new user service.
   154  func NewService(store Storer) Service {
   155  	return &service{store}
   156  }
   157  
   158  func (s *service) Create(ctx context.Context, user *NewUser) (*User, error) {
   159  	if err := s.checkUser(ctx, user); err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	hashedPW, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  	user.Password = string(hashedPW)
   168  	return s.store.Create(ctx, user)
   169  }
   170  
   171  func (s *service) Update(ctx context.Context, user *User) (*User, error) {
   172  	if user.Password == "" {
   173  		// It should be able to update the user without providing a password. In
   174  		// this case get the password from the database.
   175  		oldUser, err := s.store.FindByID(auth.ContextWithRole(ctx, auth.RoleInternal), user.ID)
   176  		if err != nil {
   177  			return nil, err
   178  		}
   179  		if oldUser != nil {
   180  			user.Password = oldUser.Password
   181  		}
   182  	} else {
   183  		// Make sure the password gets hashed, when the user is updating the
   184  		// password.
   185  		hashedPW, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
   186  		if err != nil {
   187  			return nil, err
   188  		}
   189  		user.Password = string(hashedPW)
   190  	}
   191  	if err := s.checkUser(ctx, user); err != nil {
   192  		return nil, err
   193  	}
   194  	return s.store.Update(ctx, user)
   195  }
   196  func (s *service) Delete(ctx context.Context, id string) error {
   197  	return s.store.Delete(ctx, id)
   198  }
   199  func (s *service) FindByID(ctx context.Context, id string) (*User, error) {
   200  	return s.store.FindByID(ctx, id)
   201  }
   202  func (s *service) Find(ctx context.Context, params *crud.FindParams[FindFilters]) ([]*User, int, error) {
   203  	return s.store.Find(ctx, params)
   204  }
   205  
   206  func (s *service) Validate(ctx context.Context, email, password string) (string, error) {
   207  	users, _, err := s.store.Find(auth.ContextWithRole(ctx, auth.RoleInternal), &crud.FindParams[FindFilters]{
   208  		Filters: &FindFilters{Email: &email},
   209  	})
   210  	if err != nil {
   211  		return "", err
   212  	}
   213  	if len(users) != 1 {
   214  		return "", err
   215  	}
   216  	user := users[0]
   217  	if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
   218  		return "", nil
   219  	}
   220  	if user.Deactivated {
   221  		return "", auth.ErrDeactivated
   222  	}
   223  	return user.ID, nil
   224  }
   225  
   226  func (s *service) Role(ctx context.Context, id string) (auth.Role, error) {
   227  	user, err := s.FindByID(auth.ContextWithRole(ctx, auth.RoleInternal), id)
   228  	if err != nil {
   229  		return auth.RoleNormal, err
   230  	}
   231  	if user == nil {
   232  		return auth.RoleNormal, fmt.Errorf("user not found")
   233  	}
   234  	return user.Role, nil
   235  }
   236  
   237  var ErrEmailAlreadyExists = xerror.BadInputError{Err: fmt.Errorf("email already exists")}
   238  var ErrNicknameAlreadyExists = xerror.BadInputError{Err: fmt.Errorf("nickname already exists")}
   239  
   240  func (s *service) checkUser(ctx context.Context, u user) error {
   241  	if u.getEmail() == "" {
   242  		return xerror.BadInputError{Err: fmt.Errorf("empty email")}
   243  	}
   244  	if u.getPassword() == "" {
   245  		return xerror.BadInputError{Err: fmt.Errorf("empty password")}
   246  	}
   247  	if u.getNickname() == "" {
   248  		return xerror.BadInputError{Err: fmt.Errorf("empty nickname")}
   249  	}
   250  
   251  	id := u.getID()
   252  	email := u.getEmail()
   253  	existingUsers, _, err := s.store.Find(auth.ContextWithRole(ctx, auth.RoleInternal), &crud.FindParams[FindFilters]{
   254  		Filters: &FindFilters{Email: &email, NotID: &id},
   255  	})
   256  	if err != nil {
   257  		return err
   258  	}
   259  	if len(existingUsers) > 0 {
   260  		return ErrEmailAlreadyExists
   261  	}
   262  
   263  	nickname := u.getNickname()
   264  	existingUsers, _, err = s.store.Find(auth.ContextWithRole(ctx, auth.RoleInternal), &crud.FindParams[FindFilters]{
   265  		Filters: &FindFilters{Nickname: &nickname, NotID: &id},
   266  	})
   267  	if err != nil || len(existingUsers) > 0 {
   268  		return ErrNicknameAlreadyExists
   269  	}
   270  	return nil
   271  }