hailin/internal/mods/rbac/biz/login.biz.go
2025-06-19 10:30:46 +08:00

375 lines
10 KiB
Go

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")
}