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  }