github.com/resonatecoop/id@v1.1.0-43/oauth/user.go (about) 1 package oauth 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 "time" 9 10 "github.com/mailgun/mailgun-go/v4" 11 "github.com/pariz/gountries" 12 "github.com/resonatecoop/id/log" 13 pass "github.com/resonatecoop/id/util/password" 14 "github.com/resonatecoop/user-api/model" 15 "github.com/uptrace/bun" 16 ) 17 18 var ( 19 // MinPasswordLength defines minimum password length 20 MaxLoginLength = 50 21 MinLoginLength = 3 22 23 // ErrLoginTooShort ... 24 ErrLoginTooShort = fmt.Errorf( 25 "Login must be at least %d characters long", 26 MinLoginLength, 27 ) 28 29 // ErrLoginTooShort ... 30 ErrLoginTooLong = fmt.Errorf( 31 "Login must be at maximum %d characters long", 32 MaxLoginLength, 33 ) 34 35 // ErrLoginRequired ... 36 ErrLoginRequired = errors.New("Login is required") 37 // ErrDisplayNameRequired ... 38 ErrDisplayNameRequired = errors.New("Display Name is required") 39 // ErrUsernameRequired ... 40 ErrUsernameRequired = errors.New("Email is required") 41 // ErrUserNotFound ... 42 ErrUserNotFound = errors.New("User not found") 43 // ErrInvalidUserPassword ... 44 ErrInvalidUserPassword = errors.New("Invalid user password") 45 // ErrCannotSetEmptyUsername ... 46 ErrCannotSetEmptyUsername = errors.New("Cannot set empty username") 47 // ErrUserPasswordNotSet ... 48 ErrUserPasswordNotSet = errors.New("User password not set") 49 // ErrUsernameTaken ... 50 ErrUsernameTaken = errors.New("Email is not available") 51 // ErrEmailInvalid 52 ErrEmailInvalid = errors.New("Not a valid email") 53 // ErrEmailNotFound 54 ErrEmailNotFound = errors.New("We can't find an account registered with that address or username") 55 // ErrAccountDeletionFailed 56 ErrAccountDeletionFailed = errors.New("Account could not be deleted. Please reach to us now") 57 // ErrEmailAsLogin 58 ErrEmailAsLogin = errors.New("Username cannot be an email address") 59 // ErrCountryNotFound 60 ErrCountryNotFound = errors.New("Country cannot be found") 61 // ErrEmailNotConfirmed 62 ErrEmailNotConfirmed = errors.New("Please confirm your email address") 63 ) 64 65 // UserExists returns true if user exists 66 func (s *Service) UserExists(username string) bool { 67 _, err := s.FindUserByUsername(username) 68 return err == nil 69 } 70 71 // FindUserByUsername looks up a user by username (email) 72 func (s *Service) FindUserByUsername(username string) (*model.User, error) { 73 ctx := context.Background() 74 // Usernames are case insensitive 75 user := new(model.User) 76 err := s.db.NewSelect(). 77 Model(user). 78 Where("username = LOWER(?)", username). 79 Limit(1). 80 Scan(ctx) 81 82 if err != nil { 83 return nil, ErrUserNotFound 84 } 85 86 return user, nil 87 } 88 89 func (s *Service) FindUserByEmail(email string) (*model.User, error) { 90 ctx := context.Background() 91 user := new(model.User) 92 err := s.db.NewSelect(). 93 Model(user). 94 Where("user_email = ?", email). 95 Limit(1). 96 Scan(ctx) 97 98 // Not found 99 if err != nil { 100 return nil, ErrUserNotFound 101 } 102 103 return user, nil 104 } 105 106 // SetPassword sets a user password 107 func (s *Service) SetPassword(user *model.User, password string) error { 108 return s.setPasswordCommon(s.db, user, password) 109 } 110 111 // SetPasswordTx sets a user password in a transaction 112 func (s *Service) SetPasswordTx(tx *bun.DB, user *model.User, password string) error { 113 return s.setPasswordCommon(tx, user, password) 114 } 115 116 // AuthUser authenticates user 117 func (s *Service) AuthUser(username, password string) (*model.User, error) { 118 // Fetch the user 119 user, err := s.FindUserByUsername(username) 120 if err != nil { 121 return nil, err 122 } 123 124 // Check that the password is set 125 if !user.Password.Valid { 126 return nil, ErrUserPasswordNotSet 127 } 128 129 // Verify the password 130 if pass.VerifyPassword(user.Password.String, password) != nil { 131 return nil, ErrInvalidUserPassword 132 } 133 134 return user, nil 135 } 136 137 // UpdateUsername ... 138 func (s *Service) UpdateUsername(user *model.User, username, password string) error { 139 return s.updateUsernameCommon(s.db, user, username, password) 140 } 141 142 // UpdateUsernameTx ... 143 func (s *Service) UpdateUsernameTx(tx *bun.DB, user *model.User, username, password string) error { 144 return s.updateUsernameCommon(tx, user, username, password) 145 } 146 147 func (s *Service) ConfirmUserEmail(email string) error { 148 ctx := context.Background() 149 user, err := s.FindUserByUsername(email) 150 151 if err != nil { 152 return err 153 } 154 155 _, err = s.db.NewUpdate(). 156 Model(user). 157 Set("email_confirmed = ?", true). 158 WherePK(). 159 Exec(ctx) 160 161 return err 162 } 163 164 // UpdateUser ... 165 func (s *Service) UpdateUser(user *model.User, fullName, firstName, lastName, country string, newsletter bool) error { 166 return s.updateUserCommon(s.db, user, fullName, firstName, lastName, country, newsletter) 167 } 168 169 // SetUserCountry ... 170 func (s *Service) SetUserCountry(user *model.User, country string) error { 171 return s.setUserCountryCommon(s.db, user, country) 172 } 173 174 // SetUserCountryTx 175 func (s *Service) SetUserCountryTx(tx *bun.DB, user *model.User, country string) error { 176 return s.setUserCountryCommon(tx, user, country) 177 } 178 179 // Delete user will soft delete user 180 func (s *Service) DeleteUser(user *model.User, password string) error { 181 return s.deleteUserCommon(s.db, user, password) 182 } 183 184 // DeleteUserTx deletes a user in a transaction 185 func (s *Service) DeleteUserTx(tx *bun.DB, user *model.User, password string) error { 186 return s.deleteUserCommon(tx, user, password) 187 } 188 189 func (s *Service) deleteUserCommon(db *bun.DB, user *model.User, password string) error { 190 ctx := context.Background() 191 192 // Check that the password is set 193 if !user.Password.Valid { 194 return ErrUserPasswordNotSet 195 } 196 197 // Verify the password 198 if pass.VerifyPassword(user.Password.String, password) != nil { 199 return ErrInvalidUserPassword 200 } 201 202 // will set deleted_at to current time using soft delete 203 _, err := db.NewDelete(). 204 Model(user). 205 WherePK(). 206 Exec(ctx) 207 208 if err != nil { 209 return ErrAccountDeletionFailed 210 } 211 212 // Inform user account is scheduled for deletion 213 mg := mailgun.NewMailgun(s.cnf.Mailgun.Domain, s.cnf.Mailgun.Key) 214 sender := s.cnf.Mailgun.Sender 215 body := "" 216 email := model.NewOauthEmail( 217 user.Username, 218 "Account deleted", 219 "account-deleted", 220 ) 221 subject := email.Subject 222 recipient := email.Recipient 223 message := mg.NewMessage(sender, subject, body, recipient) 224 message.SetTemplate(email.Template) // set mailgun template 225 err = message.AddTemplateVariable("email", recipient) 226 227 if err != nil { 228 log.ERROR.Print(err) 229 } 230 231 ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 232 defer cancel() 233 234 // Send the message with a 10 second timeout 235 _, _, err = mg.Send(ctx, message) 236 237 if err != nil { 238 log.ERROR.Print(err) 239 } 240 241 return nil 242 } 243 244 func (s *Service) setPasswordCommon(db *bun.DB, user *model.User, password string) error { 245 ctx := context.Background() 246 247 // Create a bcrypt hash 248 passwordHash, err := pass.HashPassword(password) 249 if err != nil { 250 return err 251 } 252 253 // Set the password on the user object 254 _, err = db.NewUpdate(). 255 Model(user). 256 Set("updated_at = ?", time.Now().UTC()). 257 Set("last_password_change = ?", time.Now().UTC()). 258 Set("password = ?", string(passwordHash)). 259 Where("id = ?", user.IDRecord.ID). 260 Exec(ctx) 261 262 if err != nil { 263 return err 264 } 265 266 return nil 267 } 268 269 // updateUserCommon ... 270 func (s *Service) updateUserCommon(db *bun.DB, user *model.User, fullName, firstName, lastName, country string, newsletter bool) error { 271 ctx := context.Background() 272 273 update := db.NewUpdate().Model(user) 274 275 if country != "" { 276 // validate country code 277 query := gountries.New() 278 _, err := query.FindCountryByAlpha(strings.ToLower(country)) 279 280 if err != nil { 281 // fallback to name 282 result, err := query.FindCountryByName(strings.ToLower(country)) 283 if err != nil { 284 return ErrCountryNotFound 285 } 286 country = result.Codes.Alpha2 287 } 288 289 if country != user.Country { 290 update.Set("country = ?", country) 291 } 292 } 293 294 if newsletter != user.NewsletterNotification { 295 update.Set("newsletter_notification = ?", newsletter) 296 } 297 298 if fullName != user.FullName && fullName != "" { 299 update.Set("full_name = ?", fullName) 300 } 301 302 if firstName != user.FirstName && firstName != "" { 303 update.Set("first_name = ?", firstName) 304 } 305 306 if lastName != user.LastName && lastName != "" { 307 update.Set("last_name = ?", lastName) 308 } 309 310 _, err := update.Where("id = ?", user.ID). 311 Exec(ctx) 312 313 return err 314 } 315 316 // updateUserCountryCommon Update wp user country (resolve from alpha2 or alpha3 code, fallback to common name otherwise) 317 func (s *Service) setUserCountryCommon(db *bun.DB, user *model.User, country string) error { 318 ctx := context.Background() 319 320 // validate country code 321 query := gountries.New() 322 gountry, err := query.FindCountryByAlpha(strings.ToLower(country)) 323 324 if err != nil { 325 // fallback to name 326 gountry, err = query.FindCountryByName(strings.ToLower(country)) 327 if err != nil { 328 return ErrCountryNotFound 329 } 330 } 331 332 countryCode := gountry.Codes.Alpha2 333 334 _, err = db.NewUpdate(). 335 Model(user). 336 Set("country = ?", countryCode). 337 Where("id = ?", user.ID). 338 Exec(ctx) 339 340 return err 341 } 342 343 // updateUsernameCommon ... 344 func (s *Service) updateUsernameCommon(db *bun.DB, user *model.User, username, password string) error { 345 ctx := context.Background() 346 347 if username == "" { 348 return ErrCannotSetEmptyUsername 349 } 350 351 // Check the email/username is available 352 if s.UserExists(username) { 353 return ErrUsernameTaken 354 } 355 356 // Check that the password is set 357 if !user.Password.Valid { 358 return ErrUserPasswordNotSet 359 } 360 361 // Verify the password 362 if pass.VerifyPassword(user.Password.String, password) != nil { 363 return ErrInvalidUserPassword 364 } 365 366 _, err := db.NewUpdate(). 367 Model(user). 368 Set("username = ?", strings.ToLower(username)). 369 Set("email_confirmed = ?", false). 370 WherePK(). 371 Exec(ctx) 372 373 // sends email with token for verification 374 email := model.NewOauthEmail( 375 username, 376 "Confirm email change", 377 "email-change-confirmation", 378 ) 379 380 _, err = s.SendEmailToken( 381 email, 382 fmt.Sprintf( 383 "https://%s/email-confirmation", 384 s.cnf.Hostname, 385 ), 386 ) 387 388 if err != nil { 389 log.ERROR.Print(err) 390 } 391 392 // notify current email address 393 mg := mailgun.NewMailgun(s.cnf.Mailgun.Domain, s.cnf.Mailgun.Key) 394 sender := s.cnf.Mailgun.Sender 395 body := "" 396 email = model.NewOauthEmail( 397 user.Username, 398 "Email change notification", 399 "email-change-notification", 400 ) 401 subject := email.Subject 402 recipient := email.Recipient 403 message := mg.NewMessage(sender, subject, body, recipient) 404 message.SetTemplate(email.Template) // set mailgun template 405 err = message.AddTemplateVariable("email", recipient) 406 407 if err != nil { 408 log.ERROR.Print(err) 409 } 410 411 ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 412 defer cancel() 413 414 // Send the message with a 10 second timeout 415 _, _, err = mg.Send(ctx, message) 416 417 if err != nil { 418 log.ERROR.Print(err) 419 } 420 421 return nil 422 }