diff --git a/cmd/database.go b/cmd/database.go new file mode 100644 index 00000000..a6866632 --- /dev/null +++ b/cmd/database.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "github.com/urfave/cli" + + "github.com/mayswind/lab/pkg/datastore" + "github.com/mayswind/lab/pkg/log" + "github.com/mayswind/lab/pkg/models" +) + +var Database = cli.Command{ + Name: "database", + Usage: "lab database maintenance", + Subcommands: []cli.Command{ + { + Name: "update", + Usage: "Update database structure", + Action: updateDatabaseStructure, + }, + }, +} + +func updateDatabaseStructure(c *cli.Context) error { + _, err := initializeSystem(c) + + if err != nil { + return err + } + + log.BootInfof("[database.updateDatabaseStructure] starting maintaining") + + _ = datastore.Container.UserStore.SyncStructs(new(models.User)) + + log.BootInfof("[database.updateDatabaseStructure] maintained successfully") + + return nil +} diff --git a/cmd/initializer.go b/cmd/initializer.go new file mode 100644 index 00000000..15eb6489 --- /dev/null +++ b/cmd/initializer.go @@ -0,0 +1,71 @@ +package cmd + +import ( + "encoding/json" + "os" + + "github.com/urfave/cli" + + "github.com/mayswind/lab/pkg/datastore" + "github.com/mayswind/lab/pkg/log" + "github.com/mayswind/lab/pkg/settings" + "github.com/mayswind/lab/pkg/uuid" +) + +func initializeSystem(c *cli.Context) (*settings.Config, error) { + var err error + configFilePath := c.GlobalString("conf-path") + + if configFilePath != "" { + if _, err = os.Stat(configFilePath); err != nil { + log.BootErrorf("[initializer.initializeSystem] cannot load configuration from custom config path %s, because file not exists", configFilePath) + return nil, err + } + + log.BootInfof("[initializer.initializeSystem] will loading configuration from custom config path %s", configFilePath) + } else { + configFilePath, err = settings.GetDefaultConfigFilePath() + + if err != nil { + log.BootErrorf("[initializer.initializeSystem] cannot get default configuration path, because %s", err.Error()) + return nil, err + } + + log.BootInfof("[initializer.initializeSystem] will load configuration from default config path %s", configFilePath) + } + + config, err := settings.LoadConfiguration(configFilePath) + + if err != nil { + log.BootErrorf("[initializer.initializeSystem] cannot load configuration, because %s", err.Error()) + return nil, err + } + + settings.SetCurrentConfig(config) + + err = datastore.InitializeDataStore(config) + + if err != nil { + log.BootErrorf("[initializer.initializeSystem] initializes data store failed, because %s", err.Error()) + return nil, err + } + + err = log.SetLoggerConfiguration(config) + + if err != nil { + log.BootErrorf("[initializer.initializeSystem] sets logger configuration failed, because %s", err.Error()) + return nil, err + } + + err = uuid.InitializeUuidGenerator(config) + + if err != nil { + log.BootErrorf("[initializer.initializeSystem] initializes uuid generator failed, because %s", err.Error()) + return nil, err + } + + cfgJson, _ := json.Marshal(config) + log.BootInfof("[initializer.initializeSystem] has loaded configuration %s", cfgJson) + + return config, nil +} diff --git a/lab.go b/lab.go new file mode 100644 index 00000000..1fde0011 --- /dev/null +++ b/lab.go @@ -0,0 +1 @@ +package lab diff --git a/pkg/errs/user.go b/pkg/errs/user.go index 4728b206..d647b474 100644 --- a/pkg/errs/user.go +++ b/pkg/errs/user.go @@ -13,6 +13,7 @@ var ( ErrUserPasswordWrong = NewNormalError(NORMAL_SUBCATEGORY_USER, 5, http.StatusBadRequest, "password is wrong") ErrUsernameAlreadyExists = NewNormalError(NORMAL_SUBCATEGORY_USER, 6, http.StatusBadRequest, "username already exists") ErrUserEmailAlreadyExists = NewNormalError(NORMAL_SUBCATEGORY_USER, 7, http.StatusBadRequest, "email already exists") - ErrLoginNameOrPasswordInvalid = NewNormalError(NORMAL_SUBCATEGORY_USER, 8, http.StatusUnauthorized, "login name or password is invalid") - ErrLoginNameOrPasswordWrong = NewNormalError(NORMAL_SUBCATEGORY_USER, 9, http.StatusUnauthorized, "login name or password is wrong") + ErrLoginNameInvalid = NewNormalError(NORMAL_SUBCATEGORY_USER, 8, http.StatusUnauthorized, "login name is invalid") + ErrLoginNameOrPasswordInvalid = NewNormalError(NORMAL_SUBCATEGORY_USER, 9, http.StatusUnauthorized, "login name or password is invalid") + ErrLoginNameOrPasswordWrong = NewNormalError(NORMAL_SUBCATEGORY_USER, 10, http.StatusUnauthorized, "login name or password is wrong") ) diff --git a/pkg/models/user.go b/pkg/models/user.go new file mode 100644 index 00000000..6088ed3c --- /dev/null +++ b/pkg/models/user.go @@ -0,0 +1,56 @@ +package models + +type UserType byte + +const ( + USER_TYPE_NORMAL UserType = 0 + USER_TYPE_ADMIN UserType = 63 + USER_TYPE_SUPER_ADMIN UserType = 127 +) + +type User struct { + Uid int64 `xorm:"PK"` + Username string `xorm:"VARCHAR(32) UNIQUE NOT NULL"` + Email string `xorm:"VARCHAR(100) UNIQUE NOT NULL"` + Nickname string `xorm:"VARCHAR(64) NOT NULL"` + Password string `xorm:"VARCHAR(64) NOT NULL"` + Salt string `xorm:"VARCHAR(10) NOT NULL"` + Rands string `xorm:"VARCHAR(10) NOT NULL"` + Type UserType `xorm:"TINYINT NOT NULL"` + IsAdmin bool `xorm:"NOT NULL"` + Deleted bool `xorm:"NOT NULL"` + EmailVerified bool `xorm:"NOT NULL"` + CreatedUnixTime int64 + UpdatedUnixTime int64 + DeletedUnixTime int64 + LastLoginUnixTime int64 +} + +type UserLoginRequest struct { + LoginName string `json:"loginName" binding:"required,notBlank,max=100,validUsername|validEmail"` + Password string `json:"password" binding:"required,min=6,max=128"` +} + +type UserRegisterRequest struct { + Username string `json:"username" binding:"required,notBlank,max=32,validUsername"` + Email string `json:"email" binding:"required,notBlank,max=100,validEmail"` + Nickname string `json:"nickname" binding:"required,notBlank,max=64"` + Password string `json:"password" binding:"required,min=6,max=128"` +} + +type UserProfileUpdateRequest struct { + Email string `json:"email" binding:"omitempty,notBlank,max=100,validEmail"` + Nickname string `json:"nickname" binding:"omitempty,notBlank,max=64"` + Password string `json:"password" binding:"omitempty,min=6,max=128"` +} + +type UserProfileResponse struct { + Uid string `json:"uid"` + Username string `json:"username"` + Email string `json:"email"` + Nickname string `json:"nickname"` + Type UserType `json:"type"` + CreatedAt int64 `json:"createdAt"` + UpdatedAt int64 `json:"updatedAt"` + LastLoginAt int64 `json:"lastLoginAt"` +} diff --git a/pkg/services/base.go b/pkg/services/base.go new file mode 100644 index 00000000..79ce892d --- /dev/null +++ b/pkg/services/base.go @@ -0,0 +1,39 @@ +package services + +import ( + "github.com/mayswind/lab/pkg/datastore" + "github.com/mayswind/lab/pkg/settings" + "github.com/mayswind/lab/pkg/uuid" +) + +type ServiceUsingDB struct { + container *datastore.DataStoreContainer +} + +func (s *ServiceUsingDB) UserDB() *datastore.Database { + return s.container.UserStore.Choose(0) +} + +func (s *ServiceUsingDB) TokenDB(uid int64) *datastore.Database { + return s.container.TokenStore.Choose(uid) +} + +func (s *ServiceUsingDB) UserDataDB(uid int64) *datastore.Database { + return s.container.UserDataStore.Choose(uid) +} + +type ServiceUsingConfig struct { + container *settings.ConfigContainer +} + +func (s *ServiceUsingConfig) CurrentConfig() *settings.Config { + return s.container.Current +} + +type ServiceUsingUuid struct { + container *uuid.UuidContainer +} + +func (s *ServiceUsingUuid) GenerateUuid(uuidType uuid.UuidType) int64 { + return s.container.GenerateUuid(uuidType) +} diff --git a/pkg/services/users.go b/pkg/services/users.go new file mode 100644 index 00000000..bed66f18 --- /dev/null +++ b/pkg/services/users.go @@ -0,0 +1,226 @@ +package services + +import ( + "time" + + "xorm.io/xorm" + + "github.com/mayswind/lab/pkg/datastore" + "github.com/mayswind/lab/pkg/errs" + "github.com/mayswind/lab/pkg/models" + "github.com/mayswind/lab/pkg/utils" + "github.com/mayswind/lab/pkg/uuid" +) + +type UserService struct { + ServiceUsingDB + ServiceUsingUuid +} + +var ( + Users = &UserService{ + ServiceUsingDB: ServiceUsingDB{ + container: datastore.Container, + }, + ServiceUsingUuid: ServiceUsingUuid{ + container: uuid.Container, + }, + } +) + +func (s *UserService) GetUserByUsernameOrEmailAndPassword(loginname string, password string) (*models.User, error) { + var user *models.User + var err error + + if utils.IsValidUsername(loginname) { + user, err = s.GetUserByUsername(loginname) + } else if utils.IsValidEmail(loginname) { + user, err = s.GetUserByEmail(loginname) + } else { + err = errs.ErrLoginNameInvalid + } + + if err != nil { + return nil, err + } + + if !s.IsPasswordEqualsUserPassword(password, user) { + return nil, errs.ErrUserPasswordWrong + } + + return user, nil +} + +func (s *UserService) GetUserById(uid int64) (*models.User, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + user := &models.User{} + has, err := s.UserDB().ID(uid).Where("deleted=?", false).Get(user) + + if err != nil { + return nil, err + } else if !has { + return nil, errs.ErrUserNotFound + } + + return user, nil +} + +func (s *UserService) GetUserByUsername(username string) (*models.User, error) { + if username == "" { + return nil, errs.ErrUsernameIsEmpty + } + + user := &models.User{} + has, err := s.UserDB().Where("username=? AND deleted=?", username, false).Get(user) + + if err != nil { + return nil, err + } else if !has { + return nil, errs.ErrUserNotFound + } + + return user, nil +} + +func (s *UserService) GetUserByEmail(email string) (*models.User, error) { + if email == "" { + return nil, errs.ErrEmailIsEmpty + } + + user := &models.User{} + has, err := s.UserDB().Where("email=? AND deleted=?", email, false).Get(user) + + if err != nil { + return nil, err + } else if !has { + return nil, errs.ErrUserNotFound + } + + return user, nil +} + +func (s *UserService) CreateUser(user *models.User) error { + exists, err := s.ExistsUsername(user.Username) + + if err != nil { + return err + } else if exists { + return errs.ErrUsernameAlreadyExists + } + + exists, err = s.ExistsEmail(user.Email) + + if err != nil { + return err + } else if exists { + return errs.ErrUserEmailAlreadyExists + } + + if user.Password == "" { + return errs.ErrPasswordIsEmpty + } + + if user.Salt, err = utils.GetRandomString(10); err != nil { + return err + } + + if user.Rands, err = utils.GetRandomString(10); err != nil { + return err + } + + user.Uid = s.GenerateUuid(uuid.UUID_TYPE_USER) + user.Password = utils.EncodePassword(user.Password, user.Salt) + + user.Deleted = false + + user.CreatedUnixTime = time.Now().Unix() + user.UpdatedUnixTime = time.Now().Unix() + user.LastLoginUnixTime = time.Now().Unix() + + return s.UserDB().DoTranscation(func(sess *xorm.Session) error { + _, err := sess.Insert(user) + return err + }) +} + +func (s *UserService) UpdateUser(user *models.User) (keyProfileUpdated bool, err error) { + if user.Uid <= 0 { + return false, errs.ErrUserIdInvalid + } + + var updateCols []string + + now := time.Now().Unix() + keyProfileUpdated = false + + if user.Email != "" { + user.EmailVerified = false + + updateCols = append(updateCols, "email") + updateCols = append(updateCols, "email_verified") + } + + if user.Password != "" { + user.Password = utils.EncodePassword(user.Password, user.Salt) + + keyProfileUpdated = true + updateCols = append(updateCols, "password") + } + + if user.Nickname != "" { + updateCols = append(updateCols, "nickname") + } + + user.UpdatedUnixTime = now + updateCols = append(updateCols, "updated_unix_time") + + err = s.UserDB().DoTranscation(func(sess *xorm.Session) error { + updatedRows, err := sess.ID(user.Uid).Where("deleted=?", false).Cols(updateCols...).Update(user) + + if updatedRows < 1 { + return errs.ErrUserNotFound + } + + return err + }) + + if err != nil { + return false, err + } + + return keyProfileUpdated, nil +} + +func (s *UserService) UpdateUserLastLoginTime(uid int64) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + return s.UserDB().DoTranscation(func(sess *xorm.Session) error { + _, err := sess.ID(uid).Where("deleted=?", false).Cols("last_login_unix_time").Update(&models.User{LastLoginUnixTime: time.Now().Unix()}) + return err + }) +} + +func (s *UserService) ExistsUsername(username string) (bool, error) { + if username == "" { + return false, errs.ErrUsernameIsEmpty + } + + return s.UserDB().Cols("username").Where("username=? AND deleted=?", username, false).Exist(&models.User{}) +} + +func (s *UserService) ExistsEmail(email string) (bool, error) { + if email == "" { + return false, errs.ErrEmailIsEmpty + } + + return s.UserDB().Cols("email").Where("email=? AND deleted=?", email, false).Exist(&models.User{}) +} + +func (s *UserService) IsPasswordEqualsUserPassword(password string, user *models.User) bool { + return user.Password == utils.EncodePassword(password, user.Salt) +}