All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m41s
修改绑定邮箱接口返回登录凭证,优化用户数据迁移流程 添加用户缓存清理逻辑,确保设备绑定后数据一致性 完善邮箱验证和绑定逻辑的注释和错误处理
176 lines
6.5 KiB
Go
176 lines
6.5 KiB
Go
package user
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"time"
|
||
|
||
"github.com/perfect-panel/server/internal/logic/auth"
|
||
"github.com/perfect-panel/server/pkg/constant"
|
||
"github.com/perfect-panel/server/pkg/tool"
|
||
|
||
"github.com/perfect-panel/server/internal/config"
|
||
"github.com/perfect-panel/server/internal/model/user"
|
||
"github.com/perfect-panel/server/pkg/jwt"
|
||
"github.com/perfect-panel/server/pkg/uuidx"
|
||
"github.com/perfect-panel/server/pkg/xerr"
|
||
"github.com/pkg/errors"
|
||
"gorm.io/gorm"
|
||
|
||
"github.com/perfect-panel/server/internal/svc"
|
||
"github.com/perfect-panel/server/internal/types"
|
||
"github.com/perfect-panel/server/pkg/logger"
|
||
)
|
||
|
||
type BindEmailWithPasswordLogic struct {
|
||
logger.Logger
|
||
ctx context.Context
|
||
svcCtx *svc.ServiceContext
|
||
}
|
||
|
||
// NewBindEmailWithPasswordLogic Bind Email With Password
|
||
func NewBindEmailWithPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindEmailWithPasswordLogic {
|
||
return &BindEmailWithPasswordLogic{
|
||
Logger: logger.WithContext(ctx),
|
||
ctx: ctx,
|
||
svcCtx: svcCtx,
|
||
}
|
||
}
|
||
|
||
func (l *BindEmailWithPasswordLogic) BindEmailWithPassword(req *types.BindEmailWithPasswordRequest) (*types.LoginResponse, error) {
|
||
// 获取当前设备用户
|
||
currentUser, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||
if !ok {
|
||
logger.Error("current user is not found in context")
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||
}
|
||
|
||
// 验证邮箱和密码是否匹配现有用户
|
||
emailUser, err := l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email)
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "email not registered: %v", req.Email)
|
||
}
|
||
logger.WithContext(l.ctx).Error(err)
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user by email failed: %v", err.Error())
|
||
}
|
||
|
||
// 验证密码
|
||
if !tool.VerifyPassWord(req.Password, emailUser.Password) {
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "password incorrect")
|
||
}
|
||
|
||
// 检查当前用户是否已经绑定了邮箱
|
||
currentEmailMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByUserId(l.ctx, "email", currentUser.Id)
|
||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByUserId error")
|
||
}
|
||
|
||
// 最终用户ID(可能是当前用户或邮箱用户)
|
||
finalUserId := currentUser.Id
|
||
|
||
// 如果当前用户已经绑定了邮箱,检查是否是同一个邮箱
|
||
if currentEmailMethod.Id > 0 {
|
||
// 如果绑定的是同一个邮箱,直接生成Token返回
|
||
if currentEmailMethod.AuthIdentifier == req.Email {
|
||
l.Infow("user is binding the same email that is already bound",
|
||
logger.Field("user_id", currentUser.Id),
|
||
logger.Field("email", req.Email),
|
||
)
|
||
// 直接使用当前用户ID生成Token
|
||
} else {
|
||
// 如果是不同的邮箱,不允许重复绑定
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "current user already has email bound")
|
||
}
|
||
} else {
|
||
// 检查该邮箱是否已经被其他用户绑定
|
||
existingEmailMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email)
|
||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByOpenID error")
|
||
}
|
||
|
||
// 如果邮箱已经被其他用户绑定,需要进行数据迁移
|
||
if existingEmailMethod.Id > 0 && existingEmailMethod.UserId != currentUser.Id {
|
||
// 调用设备绑定逻辑,这会触发数据迁移
|
||
bindLogic := auth.NewBindDeviceLogic(l.ctx, l.svcCtx)
|
||
|
||
// 获取当前用户的设备标识符
|
||
deviceMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByUserId(l.ctx, "device", currentUser.Id)
|
||
if err != nil {
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByUserId device error")
|
||
}
|
||
|
||
if deviceMethod.Id == 0 {
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "current user has no device identifier")
|
||
}
|
||
|
||
// 执行设备重新绑定,这会触发数据迁移
|
||
if err := bindLogic.BindDeviceToUser(deviceMethod.AuthIdentifier, "", "", emailUser.Id); err != nil {
|
||
l.Errorw("failed to bind device to email user",
|
||
logger.Field("current_user_id", currentUser.Id),
|
||
logger.Field("email_user_id", emailUser.Id),
|
||
logger.Field("device_identifier", deviceMethod.AuthIdentifier),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "bind device to email user failed")
|
||
}
|
||
|
||
l.Infow("successfully bound device to email user with data migration",
|
||
logger.Field("current_user_id", currentUser.Id),
|
||
logger.Field("email_user_id", emailUser.Id),
|
||
logger.Field("device_identifier", deviceMethod.AuthIdentifier),
|
||
)
|
||
|
||
// 数据迁移后,使用邮箱用户的ID
|
||
finalUserId = emailUser.Id
|
||
} else {
|
||
// 邮箱未被绑定,直接为当前用户创建邮箱绑定
|
||
emailMethod := &user.AuthMethods{
|
||
UserId: currentUser.Id,
|
||
AuthType: "email",
|
||
AuthIdentifier: req.Email,
|
||
Verified: true, // 通过密码验证,直接设为已验证
|
||
}
|
||
|
||
if err := l.svcCtx.UserModel.InsertUserAuthMethods(l.ctx, emailMethod); err != nil {
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "InsertUserAuthMethods error")
|
||
}
|
||
|
||
l.Infow("successfully bound email to current user",
|
||
logger.Field("user_id", currentUser.Id),
|
||
logger.Field("email", req.Email),
|
||
)
|
||
}
|
||
}
|
||
|
||
// 生成新的Token
|
||
sessionId := uuidx.NewUUID().String()
|
||
loginType := "device"
|
||
if l.ctx.Value(constant.LoginType) != nil {
|
||
loginType = l.ctx.Value(constant.LoginType).(string)
|
||
}
|
||
|
||
token, err := jwt.NewJwtToken(
|
||
l.svcCtx.Config.JwtAuth.AccessSecret,
|
||
time.Now().Unix(),
|
||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||
jwt.WithOption("UserId", finalUserId),
|
||
jwt.WithOption("SessionId", sessionId),
|
||
jwt.WithOption("LoginType", loginType),
|
||
)
|
||
if err != nil {
|
||
l.Logger.Error("[BindEmailWithPassword] token generate error", logger.Field("error", err.Error()))
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error())
|
||
}
|
||
|
||
// 设置session缓存
|
||
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
||
if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, finalUserId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil {
|
||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error())
|
||
}
|
||
|
||
return &types.LoginResponse{
|
||
Token: token,
|
||
}, nil
|
||
}
|