hi-server/internal/logic/public/user/bindEmailWithPasswordLogic.go
shanshanzhong fde3210a88
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m41s
feat(用户): 实现邮箱绑定功能并返回登录凭证
修改绑定邮箱接口返回登录凭证,优化用户数据迁移流程
添加用户缓存清理逻辑,确保设备绑定后数据一致性
完善邮箱验证和绑定逻辑的注释和错误处理
2025-10-23 10:07:59 -07:00

176 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}