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 }