diff --git a/apis/public/user.api b/apis/public/user.api index 08a4334..6ffc69b 100644 --- a/apis/public/user.api +++ b/apis/public/user.api @@ -209,7 +209,7 @@ service ppanel { @doc "Bind Email With Password" @handler BindEmailWithPassword - post /bind_email_with_password (BindEmailWithPasswordRequest) + post /bind_email_with_password (BindEmailWithPasswordRequest) returns (LoginResponse) @doc "Bind Invite Code" @handler BindInviteCode diff --git a/internal/handler/public/user/bindEmailWithPasswordHandler.go b/internal/handler/public/user/bindEmailWithPasswordHandler.go index dec1d9b..333a98c 100644 --- a/internal/handler/public/user/bindEmailWithPasswordHandler.go +++ b/internal/handler/public/user/bindEmailWithPasswordHandler.go @@ -20,7 +20,7 @@ func BindEmailWithPasswordHandler(svcCtx *svc.ServiceContext) func(c *gin.Contex } l := user.NewBindEmailWithPasswordLogic(c.Request.Context(), svcCtx) - err := l.BindEmailWithPassword(&req) - result.HttpResult(c, nil, err) + resp, err := l.BindEmailWithPassword(&req) + result.HttpResult(c, resp, err) } } diff --git a/internal/logic/auth/bindDeviceLogic.go b/internal/logic/auth/bindDeviceLogic.go index 8be97b4..94b5de6 100644 --- a/internal/logic/auth/bindDeviceLogic.go +++ b/internal/logic/auth/bindDeviceLogic.go @@ -187,6 +187,28 @@ func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string, return err } + // 清理用户缓存,确保新设备能正确显示在用户的设备列表中 + userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, userId) + if err != nil { + l.Errorw("failed to find user for cache clearing", + logger.Field("user_id", userId), + logger.Field("error", err.Error()), + ) + // 不因为缓存清理失败而返回错误,因为设备创建已经成功 + } else { + if err := l.svcCtx.UserModel.ClearUserCache(l.ctx, userInfo); err != nil { + l.Errorw("failed to clear user cache", + logger.Field("user_id", userId), + logger.Field("error", err.Error()), + ) + // 不因为缓存清理失败而返回错误,因为设备创建已经成功 + } else { + l.Infow("cleared user cache after device creation", + logger.Field("user_id", userId), + ) + } + } + // 记录设备创建成功的日志 l.Infow("device created successfully", logger.Field("identifier", identifier), @@ -321,6 +343,51 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use return err } + // 清理新用户的缓存,确保用户信息能正确更新 + // 这是关键步骤:设备迁移后必须清理缓存,否则用户看到的还是旧的设备列表 + newUser, err := l.svcCtx.UserModel.FindOne(l.ctx, newUserId) + if err != nil { + l.Errorw("failed to find new user for cache clearing", + logger.Field("new_user_id", newUserId), + logger.Field("error", err.Error()), + ) + // 不因为缓存清理失败而返回错误,因为数据迁移已经成功 + } else { + if err := l.svcCtx.UserModel.ClearUserCache(l.ctx, newUser); err != nil { + l.Errorw("failed to clear new user cache", + logger.Field("new_user_id", newUserId), + logger.Field("error", err.Error()), + ) + // 不因为缓存清理失败而返回错误,因为数据迁移已经成功 + } else { + l.Infow("cleared new user cache after device rebinding", + logger.Field("new_user_id", newUserId), + ) + } + } + + // 清理原用户的缓存(如果原用户没有被禁用的话) + oldUser, err := l.svcCtx.UserModel.FindOne(l.ctx, oldUserId) + if err != nil { + l.Errorw("failed to find old user for cache clearing", + logger.Field("old_user_id", oldUserId), + logger.Field("error", err.Error()), + ) + // 不因为缓存清理失败而返回错误 + } else { + if err := l.svcCtx.UserModel.ClearUserCache(l.ctx, oldUser); err != nil { + l.Errorw("failed to clear old user cache", + logger.Field("old_user_id", oldUserId), + logger.Field("error", err.Error()), + ) + // 不因为缓存清理失败而返回错误 + } else { + l.Infow("cleared old user cache after device rebinding", + logger.Field("old_user_id", oldUserId), + ) + } + } + // 记录设备重新绑定成功的日志 l.Infow("device rebound successfully", logger.Field("identifier", deviceInfo.Identifier), diff --git a/internal/logic/public/user/bindEmailWithPasswordLogic.go b/internal/logic/public/user/bindEmailWithPasswordLogic.go index 513578f..20ff2e0 100644 --- a/internal/logic/public/user/bindEmailWithPasswordLogic.go +++ b/internal/logic/public/user/bindEmailWithPasswordLogic.go @@ -2,12 +2,17 @@ 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" @@ -32,104 +37,139 @@ func NewBindEmailWithPasswordLogic(ctx context.Context, svcCtx *svc.ServiceConte } } -func (l *BindEmailWithPasswordLogic) BindEmailWithPassword(req *types.BindEmailWithPasswordRequest) error { +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 errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + 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 errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "email not registered: %v", req.Email) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "email not registered: %v", req.Email) } logger.WithContext(l.ctx).Error(err) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user by email failed: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user by email failed: %v", err.Error()) } // 验证密码 if !tool.VerifyPassWord(req.Password, emailUser.Password) { - return errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "password incorrect") + 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 errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByUserId error") + 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), ) - return nil + // 直接使用当前用户ID生成Token + } else { + // 如果是不同的邮箱,不允许重复绑定 + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "current user already has email bound") } - // 如果是不同的邮箱,不允许重复绑定 - return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "current user already has email bound") - } - - // 检查该邮箱是否已经被其他用户绑定 - existingEmailMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return 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 errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByUserId device error") + } 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 deviceMethod.Id == 0 { - return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "current user has no device identifier") - } + // 如果邮箱已经被其他用户绑定,需要进行数据迁移 + if existingEmailMethod.Id > 0 && existingEmailMethod.UserId != currentUser.Id { + // 调用设备绑定逻辑,这会触发数据迁移 + bindLogic := auth.NewBindDeviceLogic(l.ctx, l.svcCtx) - // 执行设备重新绑定,这会触发数据迁移 - if err := bindLogic.BindDeviceToUser(deviceMethod.AuthIdentifier, "", "", emailUser.Id); err != nil { - l.Errorw("failed to bind device to email user", + // 获取当前用户的设备标识符 + 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), - logger.Field("error", err.Error()), ) - return 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), - ) - } else { - // 邮箱未被绑定,直接为当前用户创建邮箱绑定 - emailMethod := &user.AuthMethods{ - UserId: currentUser.Id, - AuthType: "email", - AuthIdentifier: req.Email, - Verified: true, // 通过密码验证,直接设为已验证 - } + // 数据迁移后,使用邮箱用户的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 errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "InsertUserAuthMethods error") - } + 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), - ) + l.Infow("successfully bound email to current user", + logger.Field("user_id", currentUser.Id), + logger.Field("email", req.Email), + ) + } } - return nil + // 生成新的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 } diff --git a/internal/logic/public/user/updateBindEmailLogic.go b/internal/logic/public/user/updateBindEmailLogic.go index f56ff8c..b789d05 100644 --- a/internal/logic/public/user/updateBindEmailLogic.go +++ b/internal/logic/public/user/updateBindEmailLogic.go @@ -30,37 +30,50 @@ func NewUpdateBindEmailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *U } } +// UpdateBindEmail 更新用户绑定的邮箱地址 +// 该方法用于用户更新或绑定新的邮箱地址,支持首次绑定和修改已绑定邮箱 func (l *UpdateBindEmailLogic) UpdateBindEmail(req *types.UpdateBindEmailRequest) error { + // 从上下文中获取当前用户信息 u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) if !ok { logger.Error("current user is not found in context") return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } + + // 查询当前用户是否已有邮箱认证方式 method, err := l.svcCtx.UserModel.FindUserAuthMethodByUserId(l.ctx, "email", u.Id) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByOpenID error") } + + // 检查要绑定的邮箱是否已被其他用户使用 m, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByOpenID error") } - // email already bind + + // 如果邮箱已被绑定,返回错误 if m.Id > 0 { return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "email already bind") } + + // 如果用户还没有邮箱认证方式,创建新的认证记录 if method.Id == 0 { method = &user.AuthMethods{ - UserId: u.Id, - AuthType: "email", - AuthIdentifier: req.Email, - Verified: false, + UserId: u.Id, // 用户ID + AuthType: "email", // 认证类型为邮箱 + AuthIdentifier: req.Email, // 邮箱地址 + Verified: false, // 初始状态为未验证 } + // 插入新的认证方式记录 if err := l.svcCtx.UserModel.InsertUserAuthMethods(l.ctx, method); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "InsertUserAuthMethods error") } } else { - method.Verified = false - method.AuthIdentifier = req.Email + // 如果用户已有邮箱认证方式,更新邮箱地址 + method.Verified = false // 重置验证状态 + method.AuthIdentifier = req.Email // 更新邮箱地址 + // 更新认证方式记录 if err := l.svcCtx.UserModel.UpdateUserAuthMethods(l.ctx, method); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateUserAuthMethods error") } diff --git a/internal/logic/public/user/verifyEmailLogic.go b/internal/logic/public/user/verifyEmailLogic.go index 4d48df1..b2fc3f6 100644 --- a/internal/logic/public/user/verifyEmailLogic.go +++ b/internal/logic/public/user/verifyEmailLogic.go @@ -15,13 +15,16 @@ import ( "github.com/pkg/errors" ) +// VerifyEmailLogic 邮箱验证逻辑结构体 +// 用于处理用户邮箱验证码验证的业务逻辑 type VerifyEmailLogic struct { logger.Logger ctx context.Context svcCtx *svc.ServiceContext } -// Verify Email +// NewVerifyEmailLogic 创建邮箱验证逻辑实例 +// 用于初始化邮箱验证处理器 func NewVerifyEmailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *VerifyEmailLogic { return &VerifyEmailLogic{ Logger: logger.WithContext(ctx), @@ -30,46 +33,68 @@ func NewVerifyEmailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Verif } } +// CacheKeyPayload Redis缓存中验证码的数据结构 +// 用于存储验证码和最后发送时间 type CacheKeyPayload struct { - Code string `json:"code"` - LastAt int64 `json:"lastAt"` + Code string `json:"code"` // 验证码 + LastAt int64 `json:"lastAt"` // 最后发送时间戳 } +// VerifyEmail 验证邮箱验证码 +// 该方法用于验证用户输入的邮箱验证码是否正确,并将邮箱标记为已验证状态 func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error { + // 构建Redis缓存键,格式:认证码缓存前缀:安全标识:邮箱地址 cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security, req.Email) + + // 从Redis中获取验证码缓存数据 value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() if err != nil { l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") } + // 解析缓存中的验证码数据 var payload CacheKeyPayload err = json.Unmarshal([]byte(value), &payload) if err != nil { l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") } + + // 验证用户输入的验证码是否与缓存中的验证码匹配 if payload.Code != req.Code { return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") } + + // 验证成功后删除Redis中的验证码缓存(一次性使用) l.svcCtx.Redis.Del(l.ctx, cacheKey) + // 从上下文中获取当前用户信息 u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) if !ok { logger.Error("current user is not found in context") return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } + + // 根据邮箱地址查找用户的邮箱认证方式记录 method, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByOpenID error") } + + // 验证邮箱认证记录是否属于当前用户(安全检查) if method.UserId != u.Id { return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "invalid access") } + + // 将邮箱标记为已验证状态 method.Verified = true + + // 更新数据库中的认证方式记录 err = l.svcCtx.UserModel.UpdateUserAuthMethods(l.ctx, method) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateUserAuthMethods error") } + return nil } diff --git a/script/test_device_login.go b/script/test_device_login.go index a4d0a84..23c1105 100644 --- a/script/test_device_login.go +++ b/script/test_device_login.go @@ -172,8 +172,8 @@ func bindEmailWithPassword(serverURL, secret, token, email, password, userAgent func main() { secret := "c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx" - serverURL := "http://localhost:8080" - identifier := "AP4A.241205.013" + serverURL := "http://127.0.0.1:8080" + identifier := "AP4A.241205.A17" userAgent := "ppanel-go-test/1.0" plain := map[string]interface{}{ diff --git a/server b/server new file mode 100755 index 0000000..31da9de Binary files /dev/null and b/server differ