package biz import ( "context" "net/http" "sort" "time" "github.com/LyricTian/captcha" "github.com/gin-gonic/gin" "github.com/guxuan/hailin_service/internal/config" "github.com/guxuan/hailin_service/internal/mods/rbac/dal" "github.com/guxuan/hailin_service/internal/mods/rbac/schema" "github.com/guxuan/hailin_service/pkg/cachex" "github.com/guxuan/hailin_service/pkg/crypto/hash" "github.com/guxuan/hailin_service/pkg/errors" "github.com/guxuan/hailin_service/pkg/jwtx" "github.com/guxuan/hailin_service/pkg/logging" "github.com/guxuan/hailin_service/pkg/util" "go.uber.org/zap" ) // Login management for RBAC type Login struct { Cache cachex.Cacher Auth jwtx.Auther UserDAL *dal.User UserRoleDAL *dal.UserRole MenuDAL *dal.Menu UserBIZ *User } func (a *Login) ParseUserID(c *gin.Context) (string, error) { rootID := config.C.General.Root.ID if config.C.Middleware.Auth.Disable { return rootID, nil } invalidToken := errors.Unauthorized(config.ErrInvalidTokenID, "Invalid access token") token := util.GetToken(c) if token == "" { return "", invalidToken } ctx := c.Request.Context() ctx = util.NewUserToken(ctx, token) userID, err := a.Auth.ParseSubject(ctx, token) if err != nil { if err == jwtx.ErrInvalidToken { return "", invalidToken } return "", err } else if userID == rootID { c.Request = c.Request.WithContext(util.NewIsRootUser(ctx)) return userID, nil } userCacheVal, ok, err := a.Cache.Get(ctx, config.CacheNSForUser, userID) if err != nil { return "", err } else if ok { userCache := util.ParseUserCache(userCacheVal) c.Request = c.Request.WithContext(util.NewUserCache(ctx, userCache)) return userID, nil } // Check user status, if not activated, force to logout user, err := a.UserDAL.Get(ctx, userID, schema.UserQueryOptions{ QueryOptions: util.QueryOptions{SelectFields: []string{"status"}}, }) if err != nil { return "", err } else if user == nil || user.Status != schema.UserStatusActivated { return "", invalidToken } roleIDs, err := a.UserBIZ.GetRoleIDs(ctx, userID) if err != nil { return "", err } userCache := util.UserCache{ RoleIDs: roleIDs, } err = a.Cache.Set(ctx, config.CacheNSForUser, userID, userCache.String()) if err != nil { return "", err } c.Request = c.Request.WithContext(util.NewUserCache(ctx, userCache)) return userID, nil } // This function generates a new captcha ID and returns it as a `schema.Captcha` struct. The length of // the captcha is determined by the `config.C.Util.Captcha.Length` configuration value. func (a *Login) GetCaptcha(ctx context.Context) (*schema.Captcha, error) { return &schema.Captcha{ CaptchaID: captcha.NewLen(config.C.Util.Captcha.Length), }, nil } // Response captcha image func (a *Login) ResponseCaptcha(ctx context.Context, w http.ResponseWriter, id string, reload bool) error { if reload && !captcha.Reload(id) { return errors.NotFound("", "Captcha id not found") } err := captcha.WriteImage(w, id, config.C.Util.Captcha.Width, config.C.Util.Captcha.Height) if err != nil { if err == captcha.ErrNotFound { return errors.NotFound("", "Captcha id not found") } return err } w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") w.Header().Set("Pragma", "no-cache") w.Header().Set("Expires", "0") w.Header().Set("Content-Type", "image/png") return nil } func (a *Login) genUserToken(ctx context.Context, userID string) (*schema.LoginToken, error) { token, err := a.Auth.GenerateToken(ctx, userID) if err != nil { return nil, err } tokenBuf, err := token.EncodeToJSON() if err != nil { return nil, err } logging.Context(ctx).Info("Generate user token", zap.Any("token", string(tokenBuf))) return &schema.LoginToken{ AccessToken: token.GetAccessToken(), TokenType: token.GetTokenType(), ExpiresAt: token.GetExpiresAt(), }, nil } func (a *Login) Login(ctx context.Context, formItem *schema.LoginForm) (*schema.LoginToken, error) { // verify captcha //if !captcha.VerifyString(formItem.CaptchaID, formItem.CaptchaCode) { // return nil, errors.BadRequest(config.ErrInvalidCaptchaID, "Incorrect captcha") //} ctx = logging.NewTag(ctx, logging.TagKeyLogin) // login by root if formItem.Username == config.C.General.Root.Username { if formItem.Password != config.C.General.Root.Password { return nil, errors.BadRequest(config.ErrInvalidUsernameOrPassword, "账号密码错误!") } userID := config.C.General.Root.ID ctx = logging.NewUserID(ctx, userID) logging.Context(ctx).Info("Login by root") return a.genUserToken(ctx, userID) } // get user info user, err := a.UserDAL.GetByUsername(ctx, formItem.Username, schema.UserQueryOptions{ QueryOptions: util.QueryOptions{ SelectFields: []string{"id", "password", "status"}, }, }) if err != nil { return nil, err } else if user == nil { return nil, errors.BadRequest(config.ErrInvalidUsernameOrPassword, "Incorrect username or password") } else if user.Status != schema.UserStatusActivated { return nil, errors.BadRequest("", "User status is not activated, please contact the administrator") } // check password if err := hash.CompareHashAndPassword(user.Password, formItem.Password); err != nil { return nil, errors.BadRequest(config.ErrInvalidUsernameOrPassword, "Incorrect username or password") } userID := user.ID ctx = logging.NewUserID(ctx, userID) // set user cache with role ids roleIDs, err := a.UserBIZ.GetRoleIDs(ctx, userID) if err != nil { return nil, err } userCache := util.UserCache{RoleIDs: roleIDs} err = a.Cache.Set(ctx, config.CacheNSForUser, userID, userCache.String(), time.Duration(config.C.Dictionary.UserCacheExp)*time.Hour) if err != nil { logging.Context(ctx).Error("Failed to set cache", zap.Error(err)) } logging.Context(ctx).Info("Login success", zap.String("username", formItem.Username)) // generate token return a.genUserToken(ctx, userID) } func (a *Login) RefreshToken(ctx context.Context) (*schema.LoginToken, error) { userID := util.FromUserID(ctx) user, err := a.UserDAL.Get(ctx, userID, schema.UserQueryOptions{ QueryOptions: util.QueryOptions{ SelectFields: []string{"status"}, }, }) if err != nil { return nil, err } else if user == nil { return nil, errors.BadRequest("", "Incorrect user") } else if user.Status != schema.UserStatusActivated { return nil, errors.BadRequest("", "User status is not activated, please contact the administrator") } return a.genUserToken(ctx, userID) } func (a *Login) Logout(ctx context.Context) error { userToken := util.FromUserToken(ctx) if userToken == "" { return nil } ctx = logging.NewTag(ctx, logging.TagKeyLogout) if err := a.Auth.DestroyToken(ctx, userToken); err != nil { return err } userID := util.FromUserID(ctx) err := a.Cache.Delete(ctx, config.CacheNSForUser, userID) if err != nil { logging.Context(ctx).Error("Failed to delete user cache", zap.Error(err)) } logging.Context(ctx).Info("Logout success") return nil } // Get user info func (a *Login) GetUserInfo(ctx context.Context) (*schema.User, error) { if util.FromIsRootUser(ctx) { return &schema.User{ ID: config.C.General.Root.ID, Username: config.C.General.Root.Username, Name: config.C.General.Root.Name, Status: schema.UserStatusActivated, }, nil } userID := util.FromUserID(ctx) user, err := a.UserDAL.Get(ctx, userID, schema.UserQueryOptions{ QueryOptions: util.QueryOptions{ OmitFields: []string{"password"}, }, }) if err != nil { return nil, err } else if user == nil { return nil, errors.NotFound("", "User not found") } userRoleResult, err := a.UserRoleDAL.Query(ctx, schema.UserRoleQueryParam{ UserID: userID, }, schema.UserRoleQueryOptions{ JoinRole: true, }) if err != nil { return nil, err } user.Roles = userRoleResult.Data return user, nil } // Change login password func (a *Login) UpdatePassword(ctx context.Context, updateItem *schema.UpdateLoginPassword) error { if util.FromIsRootUser(ctx) { return errors.BadRequest("", "Root user cannot change password") } userID := util.FromUserID(ctx) user, err := a.UserDAL.Get(ctx, userID, schema.UserQueryOptions{ QueryOptions: util.QueryOptions{ SelectFields: []string{"password"}, }, }) if err != nil { return err } else if user == nil { return errors.NotFound("", "User not found") } // check old password if err := hash.CompareHashAndPassword(user.Password, updateItem.OldPassword); err != nil { return errors.BadRequest("", "Incorrect old password") } // update password newPassword, err := hash.GeneratePassword(updateItem.NewPassword) if err != nil { return err } return a.UserDAL.UpdatePasswordByID(ctx, userID, newPassword) } // Query menus based on user permissions func (a *Login) QueryMenus(ctx context.Context) (schema.Menus, error) { menuQueryParams := schema.MenuQueryParam{ Status: schema.MenuStatusEnabled, } isRoot := util.FromIsRootUser(ctx) if !isRoot { menuQueryParams.UserID = util.FromUserID(ctx) } menuResult, err := a.MenuDAL.Query(ctx, menuQueryParams, schema.MenuQueryOptions{ QueryOptions: util.QueryOptions{ OrderFields: schema.MenusOrderParams, }, }) if err != nil { return nil, err } else if isRoot { return menuResult.Data.ToTree(), nil } // fill parent menus if parentIDs := menuResult.Data.SplitParentIDs(); len(parentIDs) > 0 { var missMenusIDs []string menuIDMapper := menuResult.Data.ToMap() for _, parentID := range parentIDs { if _, ok := menuIDMapper[parentID]; !ok { missMenusIDs = append(missMenusIDs, parentID) } } if len(missMenusIDs) > 0 { parentResult, err := a.MenuDAL.Query(ctx, schema.MenuQueryParam{ InIDs: missMenusIDs, }) if err != nil { return nil, err } menuResult.Data = append(menuResult.Data, parentResult.Data...) sort.Sort(menuResult.Data) } } return menuResult.Data.ToTree(), nil } // Update current user info func (a *Login) UpdateUser(ctx context.Context, updateItem *schema.UpdateCurrentUser) error { if util.FromIsRootUser(ctx) { return errors.BadRequest("", "Root user cannot update") } userID := util.FromUserID(ctx) user, err := a.UserDAL.Get(ctx, userID) if err != nil { return err } else if user == nil { return errors.NotFound("", "User not found") } user.Name = updateItem.Name user.Phone = updateItem.Phone user.Email = updateItem.Email user.Remark = updateItem.Remark return a.UserDAL.Update(ctx, user, "name", "phone", "email", "remark") }