419 lines
11 KiB
Go
419 lines
11 KiB
Go
package biz
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"ifms/internal/config"
|
||
"ifms/internal/consts"
|
||
"ifms/internal/mods/rbac/dal"
|
||
"ifms/internal/mods/rbac/schema"
|
||
"ifms/pkg/cachex"
|
||
"ifms/pkg/convert"
|
||
"ifms/pkg/crypto/hash"
|
||
"ifms/pkg/errors"
|
||
"ifms/pkg/jwtx"
|
||
"ifms/pkg/logging"
|
||
"ifms/pkg/util"
|
||
"net/http"
|
||
"sort"
|
||
"strconv"
|
||
"time"
|
||
|
||
"github.com/LyricTian/captcha"
|
||
"github.com/gin-gonic/gin"
|
||
"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
|
||
}
|
||
|
||
// TODO 登录逻辑,解析token
|
||
func (a *Login) ParseUserID(c *gin.Context) (*config.LoginUser, error) {
|
||
//return &config.LoginUser{ID: 1, TenantID: "888"}, nil
|
||
|
||
u := &config.LoginUser{}
|
||
//rID := config.C.General.Root.ID
|
||
//rootID := strconv.FormatInt(rID, 10)
|
||
//if config.C.Middleware.Auth.Disable {
|
||
// return rootID, nil
|
||
//}
|
||
|
||
invalidToken := errors.Unauthorized(consts.ErrInvalidTokenID, "登录信息不正确,请重新登录")
|
||
token := util.GetToken(c)
|
||
if token == "" {
|
||
return nil, invalidToken
|
||
}
|
||
|
||
ctx := c.Request.Context()
|
||
ctx = util.NewUserToken(ctx, token)
|
||
|
||
strU, err := a.Auth.ParseSubject(ctx, token)
|
||
if err != nil {
|
||
if err == jwtx.ErrInvalidToken {
|
||
return nil, invalidToken
|
||
}
|
||
return nil, err
|
||
}
|
||
|
||
err = json.Unmarshal([]byte(strU), u)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
userId := convert.ToString(u.ID)
|
||
userCacheVal, ok, err := a.Cache.Get(ctx, consts.CacheNSForUser, userId)
|
||
if err != nil {
|
||
return nil, err
|
||
} else if ok {
|
||
userCache := util.ParseUserCache(userCacheVal)
|
||
c.Request = c.Request.WithContext(util.NewUserCache(ctx, userCache))
|
||
|
||
//u.r = userCache.RoleIDs
|
||
//return , 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 u, 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
|
||
//TODO 验证码暂时注释
|
||
//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, "Incorrect username or password")
|
||
// }
|
||
//
|
||
// userID := config.C.General.Root.ID
|
||
// ctx = logging.NewUserID(ctx, strconv.FormatInt(userID, 10))
|
||
// logging.Context(ctx).Info("Login by root")
|
||
// return a.genUserToken(ctx, strconv.FormatInt(userID, 10))
|
||
//}
|
||
//
|
||
//// get user info
|
||
user, err := a.UserDAL.GetByUsername(ctx, formItem.Username)
|
||
if err != nil {
|
||
return nil, err
|
||
} else if user == nil {
|
||
return nil, errors.BadRequest(consts.ErrInvalidUsernameOrPassword, "用户名错误")
|
||
} else if user.Status != schema.UserStatusActivated {
|
||
return nil, errors.BadRequest("", "用户已被禁用")
|
||
}
|
||
//pass := util.EncryptToMD5(formItem.Password)
|
||
if formItem.Password != user.Password {
|
||
return nil, errors.BadRequest(consts.ErrInvalidUsernameOrPassword, "用户密码错误")
|
||
}
|
||
// check password
|
||
//if err := hash.CompareHashAndPassword(user.Password, pass); err != nil {
|
||
// return nil, errors.BadRequest(config.ErrInvalidUsernameOrPassword, "Incorrect username or password")
|
||
//}
|
||
u := fillUserInfo(user)
|
||
userID := convert.ToString(user.ID)
|
||
ctx = logging.NewUserID(ctx, userID)
|
||
|
||
// set user cache with role ids
|
||
roleIDs, err := a.UserBIZ.GetRoleIDs(ctx, user.ID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
u.RoleIDs = roleIDs
|
||
auth, flag, err := a.Cache.Get(ctx, consts.CacheNSForUserToken, userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if flag {
|
||
logging.Context(ctx).Info("Login success", zap.String("username", formItem.Username))
|
||
return &schema.LoginToken{AccessToken: auth, RoleIds: roleIDs}, nil
|
||
}
|
||
//TODO 角色列表
|
||
//roleIDs := []string{"admin"}
|
||
//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
|
||
j, err := json.Marshal(u)
|
||
strU := string(j)
|
||
token, err := a.genUserToken(ctx, strU)
|
||
token.RoleIds = roleIDs
|
||
err = a.Cache.Set(ctx, consts.CacheNSForUserToken, userID, token.AccessToken,
|
||
time.Duration(config.C.Dictionary.UserCacheExp)*time.Hour)
|
||
return token, err
|
||
}
|
||
|
||
func fillUserInfo(u *schema.User) *config.LoginUser {
|
||
target := &config.LoginUser{}
|
||
target.ID = u.ID
|
||
target.Username = u.Username
|
||
target.TenantID = u.TenantID
|
||
return target
|
||
}
|
||
|
||
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")
|
||
}
|
||
uId := strconv.Itoa(int(userID))
|
||
return a.genUserToken(ctx, uId)
|
||
}
|
||
|
||
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, consts.CacheNSForUser, convert.ToString(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
|
||
}
|
||
uId := strconv.Itoa(int(userID))
|
||
return a.UserDAL.UpdatePasswordByID(ctx, uId, 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")
|
||
}
|
||
|
||
// Update current user password
|