// Copyright 2020 The Gogs Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package db import ( "fmt" "strings" "time" "github.com/jinzhu/gorm" "github.com/pkg/errors" "gogs.io/gogs/internal/cryptoutil" "gogs.io/gogs/internal/errutil" ) // UsersStore is the persistent interface for users. // // NOTE: All methods are sorted in alphabetical order. type UsersStore interface { // Authenticate validates username and password via given login source ID. // It returns ErrUserNotExist when the user was not found. // // When the "loginSourceID" is negative, it aborts the process and returns // ErrUserNotExist if the user was not found in the database. // // When the "loginSourceID" is non-negative, it returns ErrLoginSourceMismatch // if the user has different login source ID than the "loginSourceID". // // When the "loginSourceID" is positive, it tries to authenticate via given // login source and creates a new user when not yet exists in the database. Authenticate(username, password string, loginSourceID int64) (*User, error) // Create creates a new user and persist to database. // It returns ErrUserAlreadyExist when a user with same name already exists, // or ErrEmailAlreadyUsed if the email has been used by another user. Create(opts CreateUserOpts) (*User, error) // GetByEmail returns the user (not organization) with given email. // It ignores records with unverified emails and returns ErrUserNotExist when not found. GetByEmail(email string) (*User, error) // GetByID returns the user with given ID. It returns ErrUserNotExist when not found. GetByID(id int64) (*User, error) // GetByUsername returns the user with given username. It returns ErrUserNotExist when not found. GetByUsername(username string) (*User, error) } var Users UsersStore // NOTE: This is a GORM create hook. func (u *User) BeforeCreate() { u.CreatedUnix = gorm.NowFunc().Unix() u.UpdatedUnix = u.CreatedUnix } // NOTE: This is a GORM query hook. func (u *User) AfterFind() { u.Created = time.Unix(u.CreatedUnix, 0).Local() u.Updated = time.Unix(u.UpdatedUnix, 0).Local() } var _ UsersStore = (*users)(nil) type users struct { *gorm.DB } type ErrLoginSourceMismatch struct { args errutil.Args } func (err ErrLoginSourceMismatch) Error() string { return fmt.Sprintf("login source mismatch: %v", err.args) } func (db *users) Authenticate(login, password string, loginSourceID int64) (*User, error) { login = strings.ToLower(login) var query *gorm.DB if strings.Contains(login, "@") { query = db.Where("email = ?", login) } else { query = db.Where("lower_name = ?", login) } user := new(User) err := query.First(user).Error if err != nil && err != gorm.ErrRecordNotFound { return nil, errors.Wrap(err, "get user") } // User found in the database if err == nil { // Note: This check is unnecessary but to reduce user confusion at login page // and make it more consistent from user's perspective. if loginSourceID >= 0 && user.LoginSource != loginSourceID { return nil, ErrLoginSourceMismatch{args: errutil.Args{"expect": loginSourceID, "actual": user.LoginSource}} } // Validate password hash fetched from database for local accounts. if user.IsLocal() { if user.ValidatePassword(password) { return user, nil } return nil, ErrUserNotExist{args: map[string]interface{}{"userID": user.ID, "name": user.Name}} } source, err := LoginSources.GetByID(user.LoginSource) if err != nil { return nil, errors.Wrap(err, "get login source") } _, err = authenticateViaLoginSource(source, login, password, false) if err != nil { return nil, errors.Wrap(err, "authenticate via login source") } return user, nil } // Non-local login source is always greater than 0. if loginSourceID <= 0 { return nil, ErrUserNotExist{args: map[string]interface{}{"login": login}} } source, err := LoginSources.GetByID(loginSourceID) if err != nil { return nil, errors.Wrap(err, "get login source") } user, err = authenticateViaLoginSource(source, login, password, true) if err != nil { return nil, errors.Wrap(err, "authenticate via login source") } return user, nil } type CreateUserOpts struct { Name string Email string Password string LoginSource int64 Activated bool } type ErrUserAlreadyExist struct { args errutil.Args } func IsErrUserAlreadyExist(err error) bool { _, ok := err.(ErrUserAlreadyExist) return ok } func (err ErrUserAlreadyExist) Error() string { return fmt.Sprintf("user already exists: %v", err.args) } type ErrEmailAlreadyUsed struct { args errutil.Args } func IsErrEmailAlreadyUsed(err error) bool { _, ok := err.(ErrEmailAlreadyUsed) return ok } func (err ErrEmailAlreadyUsed) Email() string { email, ok := err.args["email"].(string) if ok { return email } return "" } func (err ErrEmailAlreadyUsed) Error() string { return fmt.Sprintf("email has been used: %v", err.args) } func (db *users) Create(opts CreateUserOpts) (*User, error) { err := isUsernameAllowed(opts.Name) if err != nil { return nil, err } _, err = db.GetByUsername(opts.Name) if err == nil { return nil, ErrUserAlreadyExist{args: errutil.Args{"name": opts.Name}} } else if !IsErrUserNotExist(err) { return nil, err } _, err = db.GetByEmail(opts.Email) if err == nil { return nil, ErrEmailAlreadyUsed{args: errutil.Args{"email": opts.Email}} } else if !IsErrUserNotExist(err) { return nil, err } user := &User{ LowerName: strings.ToLower(opts.Name), Name: opts.Name, Email: opts.Email, Passwd: opts.Password, LoginSource: opts.LoginSource, MaxRepoCreation: -1, IsActive: opts.Activated, Avatar: cryptoutil.MD5(opts.Email), AvatarEmail: opts.Email, } user.Rands, err = GetUserSalt() if err != nil { return nil, err } user.Salt, err = GetUserSalt() if err != nil { return nil, err } user.EncodePassword() return user, db.DB.Create(user).Error } var _ errutil.NotFound = (*ErrUserNotExist)(nil) type ErrUserNotExist struct { args errutil.Args } func IsErrUserNotExist(err error) bool { _, ok := err.(ErrUserNotExist) return ok } func (err ErrUserNotExist) Error() string { return fmt.Sprintf("user does not exist: %v", err.args) } func (ErrUserNotExist) NotFound() bool { return true } func (db *users) GetByEmail(email string) (*User, error) { email = strings.ToLower(email) if len(email) == 0 { return nil, ErrUserNotExist{args: errutil.Args{"email": email}} } // First try to find the user by primary email user := new(User) err := db.Where("email = ? AND type = ? AND is_active = ?", email, UserIndividual, true).First(user).Error if err == nil { return user, nil } else if !gorm.IsRecordNotFoundError(err) { return nil, err } // Otherwise, check activated email addresses emailAddress := new(EmailAddress) err = db.Where("email = ? AND is_activated = ?", email, true).First(emailAddress).Error if err != nil { if gorm.IsRecordNotFoundError(err) { return nil, ErrUserNotExist{args: errutil.Args{"email": email}} } return nil, err } return db.GetByID(emailAddress.UID) } func (db *users) GetByID(id int64) (*User, error) { user := new(User) err := db.Where("id = ?", id).First(user).Error if err != nil { if gorm.IsRecordNotFoundError(err) { return nil, ErrUserNotExist{args: errutil.Args{"userID": id}} } return nil, err } return user, nil } func (db *users) GetByUsername(username string) (*User, error) { user := new(User) err := db.Where("lower_name = ?", strings.ToLower(username)).First(user).Error if err != nil { if gorm.IsRecordNotFoundError(err) { return nil, ErrUserNotExist{args: errutil.Args{"name": username}} } return nil, err } return user, nil }