diff --git a/apis/auth/auth.api b/apis/auth/auth.api index cb7225d..9ee085e 100644 --- a/apis/auth/auth.api +++ b/apis/auth/auth.api @@ -11,11 +11,12 @@ info ( type ( // User login request UserLoginRequest { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + CfToken string `json:"cf_token,optional"` } // Check user is exist request CheckUserRequest { @@ -27,13 +28,14 @@ type ( } // User login response UserRegisterRequest { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - Invite string `json:"invite,optional"` - Code string `json:"code,optional"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Invite string `json:"invite,optional"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + CfToken string `json:"cf_token,optional"` } // User login response ResetPasswordRequest { @@ -60,6 +62,7 @@ type ( } // login request TelephoneLoginRequest { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneCode string `json:"telephone_code"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` @@ -79,6 +82,7 @@ type ( } // User login response TelephoneRegisterRequest { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password" validate:"required"` @@ -107,6 +111,12 @@ type ( Code string `form:"code"` State string `form:"state"` } + DeviceLoginRequest { + Identifier string `json:"identifier" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `json:"user_agent" validate:"required"` + CfToken string `json:"cf_token,optional"` + } ) @server ( @@ -145,6 +155,10 @@ service ppanel { @doc "Reset password" @handler TelephoneResetPassword post /reset/telephone (TelephoneResetPasswordRequest) returns (LoginResponse) + + @doc "Device Login" + @handler DeviceLogin + post /login/device (DeviceLoginRequest) returns (LoginResponse) } @server ( diff --git a/internal/handler/auth/deviceLoginHandler.go b/internal/handler/auth/deviceLoginHandler.go new file mode 100644 index 0000000..6a772bf --- /dev/null +++ b/internal/handler/auth/deviceLoginHandler.go @@ -0,0 +1,26 @@ +package auth + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Device Login +func DeviceLoginHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeviceLoginRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := auth.NewDeviceLoginLogic(c.Request.Context(), svcCtx) + resp, err := l.DeviceLogin(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index e97fddd..f386b79 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -589,6 +589,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // User login authGroupRouter.POST("/login", auth.UserLoginHandler(serverCtx)) + // Device Login + authGroupRouter.POST("/login/device", auth.DeviceLoginHandler(serverCtx)) + // User Telephone login authGroupRouter.POST("/login/telephone", auth.TelephoneLoginHandler(serverCtx)) diff --git a/internal/logic/auth/bindDeviceLogic.go b/internal/logic/auth/bindDeviceLogic.go new file mode 100644 index 0000000..19b0666 --- /dev/null +++ b/internal/logic/auth/bindDeviceLogic.go @@ -0,0 +1,234 @@ +package auth + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type BindDeviceLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewBindDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindDeviceLogic { + return &BindDeviceLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// BindDeviceToUser binds a device to a user +// If the device is already bound to another user, it will disable that user and bind the device to the current user +func (l *BindDeviceLogic) BindDeviceToUser(identifier, ip, userAgent string, currentUserId int64) error { + if identifier == "" { + // No device identifier provided, skip binding + return nil + } + + l.Infow("binding device to user", + logger.Field("identifier", identifier), + logger.Field("user_id", currentUserId), + logger.Field("ip", ip), + ) + + // Check if device exists + deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, identifier) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // Device not found, create new device record + return l.createDeviceForUser(identifier, ip, userAgent, currentUserId) + } + l.Errorw("failed to query device", + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device failed: %v", err.Error()) + } + + // Device exists, check if it's bound to current user + if deviceInfo.UserId == currentUserId { + // Already bound to current user, just update IP and UserAgent + l.Infow("device already bound to current user, updating info", + logger.Field("identifier", identifier), + logger.Field("user_id", currentUserId), + ) + deviceInfo.Ip = ip + deviceInfo.UserAgent = userAgent + if err := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); err != nil { + l.Errorw("failed to update device", + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err.Error()) + } + return nil + } + + // Device is bound to another user, need to disable old user and rebind + l.Infow("device bound to another user, rebinding", + logger.Field("identifier", identifier), + logger.Field("old_user_id", deviceInfo.UserId), + logger.Field("new_user_id", currentUserId), + ) + + return l.rebindDeviceToNewUser(deviceInfo, ip, userAgent, currentUserId) +} + +func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string, userId int64) error { + l.Infow("creating new device for user", + logger.Field("identifier", identifier), + logger.Field("user_id", userId), + ) + + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + // Create device auth method + authMethod := &user.AuthMethods{ + UserId: userId, + AuthType: "device", + AuthIdentifier: identifier, + Verified: true, + } + if err := db.Create(authMethod).Error; err != nil { + l.Errorw("failed to create device auth method", + logger.Field("user_id", userId), + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device auth method failed: %v", err) + } + + // Create device record + deviceInfo := &user.Device{ + Ip: ip, + UserId: userId, + UserAgent: userAgent, + Identifier: identifier, + Enabled: true, + Online: false, + } + if err := db.Create(deviceInfo).Error; err != nil { + l.Errorw("failed to create device", + logger.Field("user_id", userId), + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device failed: %v", err) + } + + return nil + }) + + if err != nil { + l.Errorw("device creation failed", + logger.Field("identifier", identifier), + logger.Field("user_id", userId), + logger.Field("error", err.Error()), + ) + return err + } + + l.Infow("device created successfully", + logger.Field("identifier", identifier), + logger.Field("user_id", userId), + ) + + return nil +} + +func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, userAgent string, newUserId int64) error { + oldUserId := deviceInfo.UserId + + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + // Check if old user has other auth methods besides device + var authMethods []user.AuthMethods + if err := db.Where("user_id = ?", oldUserId).Find(&authMethods).Error; err != nil { + l.Errorw("failed to query auth methods for old user", + logger.Field("old_user_id", oldUserId), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query auth methods failed: %v", err) + } + + // Count non-device auth methods + nonDeviceAuthCount := 0 + for _, auth := range authMethods { + if auth.AuthType != "device" { + nonDeviceAuthCount++ + } + } + + // Only disable old user if they have no other auth methods + if nonDeviceAuthCount == 0 { + falseVal := false + if err := db.Model(&user.User{}).Where("id = ?", oldUserId).Update("enable", &falseVal).Error; err != nil { + l.Errorw("failed to disable old user", + logger.Field("old_user_id", oldUserId), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "disable old user failed: %v", err) + } + + l.Infow("disabled old user (no other auth methods)", + logger.Field("old_user_id", oldUserId), + ) + } else { + l.Infow("old user has other auth methods, not disabling", + logger.Field("old_user_id", oldUserId), + logger.Field("non_device_auth_count", nonDeviceAuthCount), + ) + } + + // Update device auth method to new user + if err := db.Model(&user.AuthMethods{}). + Where("auth_type = ? AND auth_identifier = ?", "device", deviceInfo.Identifier). + Update("user_id", newUserId).Error; err != nil { + l.Errorw("failed to update device auth method", + logger.Field("identifier", deviceInfo.Identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device auth method failed: %v", err) + } + + // Update device record + deviceInfo.UserId = newUserId + deviceInfo.Ip = ip + deviceInfo.UserAgent = userAgent + deviceInfo.Enabled = true + + if err := db.Save(deviceInfo).Error; err != nil { + l.Errorw("failed to update device", + logger.Field("identifier", deviceInfo.Identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err) + } + + return nil + }) + + if err != nil { + l.Errorw("device rebinding failed", + logger.Field("identifier", deviceInfo.Identifier), + logger.Field("old_user_id", oldUserId), + logger.Field("new_user_id", newUserId), + logger.Field("error", err.Error()), + ) + return err + } + + l.Infow("device rebound successfully", + logger.Field("identifier", deviceInfo.Identifier), + logger.Field("old_user_id", oldUserId), + logger.Field("new_user_id", newUserId), + ) + + return nil +} diff --git a/internal/logic/auth/deviceLoginLogic.go b/internal/logic/auth/deviceLoginLogic.go new file mode 100644 index 0000000..1901e78 --- /dev/null +++ b/internal/logic/auth/deviceLoginLogic.go @@ -0,0 +1,294 @@ +package auth + +import ( + "context" + "fmt" + "time" + + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/jwt" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type DeviceLoginLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Device Login +func NewDeviceLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeviceLoginLogic { + return &DeviceLoginLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *types.LoginResponse, err error) { + //TODO : check device login rate limit + // Check if device login is enabled + //if !l.svcCtx.Config.Register.EnableDevice { + // return nil, xerr.NewErrMsg("Device login is disabled") + //} + + loginStatus := false + var userInfo *user.User + // Record login status + defer func() { + if userInfo != nil && userInfo.Id != 0 { + loginLog := log.Login{ + Method: "device", + LoginIP: req.IP, + UserAgent: req.UserAgent, + Success: loginStatus, + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }); err != nil { + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) + } + } + }() + + // Check if device exists by identifier + deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // Device not found, create new user and device + userInfo, err = l.registerUserAndDevice(req) + if err != nil { + return nil, err + } + } else { + l.Errorw("query device failed", + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device failed: %v", err.Error()) + } + } else { + // Device found, get user info + userInfo, err = l.svcCtx.UserModel.FindOne(l.ctx, deviceInfo.UserId) + if err != nil { + l.Errorw("query user failed", + logger.Field("user_id", deviceInfo.UserId), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user failed: %v", err.Error()) + } + } + + // Generate session id + sessionId := uuidx.NewUUID().String() + + // Generate token + token, err := jwt.NewJwtToken( + l.svcCtx.Config.JwtAuth.AccessSecret, + time.Now().Unix(), + l.svcCtx.Config.JwtAuth.AccessExpire, + jwt.WithOption("UserId", userInfo.Id), + jwt.WithOption("SessionId", sessionId), + ) + if err != nil { + l.Errorw("token generate error", + logger.Field("user_id", userInfo.Id), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + } + + // Store session id in redis + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { + l.Errorw("set session id error", + logger.Field("user_id", userInfo.Id), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) + } + + loginStatus = true + return &types.LoginResponse{ + Token: token, + }, nil +} + +func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest) (*user.User, error) { + l.Infow("device not found, creating new user and device", + logger.Field("identifier", req.Identifier), + logger.Field("ip", req.IP), + ) + + var userInfo *user.User + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + // Create new user + userInfo = &user.User{ + OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase, + } + if err := db.Create(userInfo).Error; err != nil { + l.Errorw("failed to create user", + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create user failed: %v", err) + } + + // Update refer code + userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) + if err := db.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { + l.Errorw("failed to update refer code", + logger.Field("user_id", userInfo.Id), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update refer code failed: %v", err) + } + + // Create device auth method + authMethod := &user.AuthMethods{ + UserId: userInfo.Id, + AuthType: "device", + AuthIdentifier: req.Identifier, + Verified: true, + } + if err := db.Create(authMethod).Error; err != nil { + l.Errorw("failed to create device auth method", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device auth method failed: %v", err) + } + + // Insert device record + deviceInfo := &user.Device{ + Ip: req.IP, + UserId: userInfo.Id, + UserAgent: req.UserAgent, + Identifier: req.Identifier, + Enabled: true, + Online: false, + } + if err := db.Create(deviceInfo).Error; err != nil { + l.Errorw("failed to insert device", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert device failed: %v", err) + } + + // Activate trial if enabled + if l.svcCtx.Config.Register.EnableTrial { + if err := l.activeTrial(userInfo.Id, db); err != nil { + return err + } + } + + return nil + }) + + if err != nil { + l.Errorw("device registration failed", + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return nil, err + } + + l.Infow("device registration completed successfully", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("refer_code", userInfo.ReferCode), + ) + + // Register log + registerLog := log.Register{ + AuthMethod: "device", + Identifier: req.Identifier, + RegisterIP: req.IP, + UserAgent: req.UserAgent, + Timestamp: time.Now().UnixMilli(), + } + content, _ := registerLog.Marshal() + + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeRegister.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }); err != nil { + l.Errorw("failed to insert register log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) + } + + return userInfo, nil +} + +func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) error { + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe) + if err != nil { + l.Errorw("failed to find trial subscription template", + logger.Field("user_id", userId), + logger.Field("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe), + logger.Field("error", err.Error()), + ) + return err + } + + startTime := time.Now() + expireTime := tool.AddTime(l.svcCtx.Config.Register.TrialTimeUnit, l.svcCtx.Config.Register.TrialTime, startTime) + subscribeToken := uuidx.SubscribeToken(fmt.Sprintf("Trial-%v", userId)) + subscribeUUID := uuidx.NewUUID().String() + + userSub := &user.Subscribe{ + UserId: userId, + OrderId: 0, + SubscribeId: sub.Id, + StartTime: startTime, + ExpireTime: expireTime, + Traffic: sub.Traffic, + Download: 0, + Upload: 0, + Token: subscribeToken, + UUID: subscribeUUID, + Status: 1, + } + + if err := db.Create(userSub).Error; err != nil { + l.Errorw("failed to insert trial subscription", + logger.Field("user_id", userId), + logger.Field("error", err.Error()), + ) + return err + } + + l.Infow("trial subscription activated successfully", + logger.Field("user_id", userId), + logger.Field("subscribe_id", sub.Id), + logger.Field("expire_time", expireTime), + logger.Field("traffic", sub.Traffic), + ) + + return nil +} diff --git a/internal/logic/auth/telephoneLoginLogic.go b/internal/logic/auth/telephoneLoginLogic.go index 54e4d8e..4a0fc48 100644 --- a/internal/logic/auth/telephoneLoginLogic.go +++ b/internal/logic/auth/telephoneLoginLogic.go @@ -124,6 +124,19 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r l.svcCtx.Redis.Del(l.ctx, cacheKey) } + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail login if device binding fails, just log the error + } + } + // Generate session id sessionId := uuidx.NewUUID().String() // Generate token diff --git a/internal/logic/auth/telephoneUserRegisterLogic.go b/internal/logic/auth/telephoneUserRegisterLogic.go index 6db07ee..c28552e 100644 --- a/internal/logic/auth/telephoneUserRegisterLogic.go +++ b/internal/logic/auth/telephoneUserRegisterLogic.go @@ -138,6 +138,20 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR } return nil }) + + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail register if device binding fails, just log the error + } + } + // Generate session id sessionId := uuidx.NewUUID().String() // Generate token diff --git a/internal/logic/auth/userLoginLogic.go b/internal/logic/auth/userLoginLogic.go index d328924..f5e0f9d 100644 --- a/internal/logic/auth/userLoginLogic.go +++ b/internal/logic/auth/userLoginLogic.go @@ -79,6 +79,20 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log if !tool.VerifyPassWord(req.Password, userInfo.Password) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") } + + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail login if device binding fails, just log the error + } + } + // Generate session id sessionId := uuidx.NewUUID().String() // Generate token diff --git a/internal/logic/auth/userRegisterLogic.go b/internal/logic/auth/userRegisterLogic.go index e4e01e4..8147362 100644 --- a/internal/logic/auth/userRegisterLogic.go +++ b/internal/logic/auth/userRegisterLogic.go @@ -125,6 +125,20 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp * } return nil }) + + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail register if device binding fails, just log the error + } + } + // Generate session id sessionId := uuidx.NewUUID().String() // Generate token diff --git a/internal/model/user/device.go b/internal/model/user/device.go index ed57df7..4d3edd1 100644 --- a/internal/model/user/device.go +++ b/internal/model/user/device.go @@ -76,3 +76,18 @@ func (m *customUserModel) DeleteDevice(ctx context.Context, id int64, tx ...*gor }, data.GetCacheKeys()...) return err } + +func (m *customUserModel) InsertDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error { + defer func() { + if clearErr := m.ClearDeviceCache(ctx, data); clearErr != nil { + // log cache clear error + } + }() + + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Create(data).Error + }) +} diff --git a/internal/model/user/model.go b/internal/model/user/model.go index 3c5dff9..52a900f 100644 --- a/internal/model/user/model.go +++ b/internal/model/user/model.go @@ -99,6 +99,7 @@ type customUserLogicModel interface { UpdateDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error FindOneDeviceByIdentifier(ctx context.Context, id string) (*Device, error) DeleteDevice(ctx context.Context, id int64, tx ...*gorm.DB) error + InsertDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error ClearSubscribeCache(ctx context.Context, data ...*Subscribe) error ClearUserCache(ctx context.Context, data ...*User) error diff --git a/internal/types/types.go b/internal/types/types.go index 87a4887..fa586ea 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -517,6 +517,13 @@ type DeleteUserSubscribeRequest struct { UserSubscribeId int64 `json:"user_subscribe_id"` } +type DeviceLoginRequest struct { + Identifier string `json:"identifier" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `json:"user_agent" validate:"required"` + CfToken string `json:"cf_token,optional"` +} + type Document struct { Id int64 `json:"id"` Title string `json:"title"` @@ -2090,6 +2097,7 @@ type TelephoneCheckUserResponse struct { } type TelephoneLoginRequest struct { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneCode string `json:"telephone_code"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` @@ -2100,6 +2108,7 @@ type TelephoneLoginRequest struct { } type TelephoneRegisterRequest struct { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password" validate:"required"` @@ -2490,21 +2499,23 @@ type UserLoginLog struct { } type UserLoginRequest struct { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + CfToken string `json:"cf_token,optional"` } type UserRegisterRequest struct { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - Invite string `json:"invite,optional"` - Code string `json:"code,optional"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Invite string `json:"invite,optional"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + CfToken string `json:"cf_token,optional"` } type UserStatistics struct {