|
|
|
@@ -1,7 +1,6 @@
|
|
|
|
|
package app
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"math/rand"
|
|
|
|
@@ -96,8 +95,8 @@ func handleAppSendEmailCode(c *gin.Context) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if req.Purpose == "register" {
|
|
|
|
|
if !app.EnableEmailVerify {
|
|
|
|
|
response.Error(c, 400, "该应用未启用邮箱验证")
|
|
|
|
|
if !app.EnableRegisterVerify {
|
|
|
|
|
response.Error(c, 400, "该应用未启用注册验证")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
} else if req.Purpose == "reset_password" {
|
|
|
|
@@ -203,6 +202,8 @@ func handleAppRegister(c *gin.Context) {
|
|
|
|
|
Username string `json:"username"`
|
|
|
|
|
Email string `json:"email"`
|
|
|
|
|
EmailCode string `json:"email_code"`
|
|
|
|
|
Phone string `json:"phone"`
|
|
|
|
|
SmsCode string `json:"sms_code"`
|
|
|
|
|
Password string `json:"password"`
|
|
|
|
|
DeviceID string `json:"device_id"`
|
|
|
|
|
DeviceName string `json:"device_name"`
|
|
|
|
@@ -229,59 +230,60 @@ func handleAppRegister(c *gin.Context) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var allowedMethods []string
|
|
|
|
|
if appModel.RegisterMethods != "" {
|
|
|
|
|
if err := json.Unmarshal([]byte(appModel.RegisterMethods), &allowedMethods); err != nil {
|
|
|
|
|
log.Printf("[DEBUG] Failed to parse register methods: %v", err)
|
|
|
|
|
allowedMethods = []string{"username"}
|
|
|
|
|
if appModel.EnableRegisterVerify {
|
|
|
|
|
if appModel.VerifyMethod == "email" {
|
|
|
|
|
if req.Email == "" {
|
|
|
|
|
response.Error(c, 400, "请输入邮箱地址")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if req.EmailCode == "" {
|
|
|
|
|
response.Error(c, 400, "请输入邮箱验证码")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var verifyCode model.EmailVerifyCode
|
|
|
|
|
if err := database.DB.Where(
|
|
|
|
|
"application_id = ? AND email = ? AND code = ? AND purpose = ? AND used = ?",
|
|
|
|
|
appModel.ID, req.Email, req.EmailCode, "register", false,
|
|
|
|
|
).First(&verifyCode).Error; err != nil {
|
|
|
|
|
response.Error(c, 400, "验证码错误或已过期")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if verifyCode.ExpiresAt.Before(time.Now()) {
|
|
|
|
|
response.Error(c, 400, "验证码已过期")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
verifyCode.Used = true
|
|
|
|
|
database.DB.Save(&verifyCode)
|
|
|
|
|
} else if appModel.VerifyMethod == "sms" {
|
|
|
|
|
if req.Phone == "" {
|
|
|
|
|
response.Error(c, 400, "请输入手机号")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if req.SmsCode == "" {
|
|
|
|
|
response.Error(c, 400, "请输入短信验证码")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var verifyCode model.SmsVerifyCode
|
|
|
|
|
if err := database.DB.Where(
|
|
|
|
|
"application_id = ? AND phone = ? AND code = ? AND purpose = ? AND used = ?",
|
|
|
|
|
appModel.ID, req.Phone, req.SmsCode, "register", false,
|
|
|
|
|
).First(&verifyCode).Error; err != nil {
|
|
|
|
|
response.Error(c, 400, "验证码错误或已过期")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if verifyCode.ExpiresAt.Before(time.Now()) {
|
|
|
|
|
response.Error(c, 400, "验证码已过期")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
verifyCode.Used = true
|
|
|
|
|
database.DB.Save(&verifyCode)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
allowedMethods = []string{"username"}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isEmailRegister := req.Email != "" && req.Username == ""
|
|
|
|
|
isUsernameRegister := req.Username != "" && req.Email == ""
|
|
|
|
|
isPhoneRegister := false
|
|
|
|
|
|
|
|
|
|
if isEmailRegister && !containsString(allowedMethods, "email") {
|
|
|
|
|
response.Error(c, 403, "邮箱注册方式未开放")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if isUsernameRegister && !containsString(allowedMethods, "username") {
|
|
|
|
|
response.Error(c, 403, "用户名注册方式未开放")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if isPhoneRegister && !containsString(allowedMethods, "phone") {
|
|
|
|
|
response.Error(c, 403, "手机号注册方式未开放")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if appModel.EnableEmailVerify && appModel.RequireEmailVerify {
|
|
|
|
|
if req.Email == "" {
|
|
|
|
|
response.Error(c, 400, "请输入邮箱地址")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if req.EmailCode == "" {
|
|
|
|
|
response.Error(c, 400, "请输入邮箱验证码")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var verifyCode model.EmailVerifyCode
|
|
|
|
|
if err := database.DB.Where(
|
|
|
|
|
"application_id = ? AND email = ? AND code = ? AND purpose = ? AND used = ?",
|
|
|
|
|
appModel.ID, req.Email, req.EmailCode, "register", false,
|
|
|
|
|
).First(&verifyCode).Error; err != nil {
|
|
|
|
|
response.Error(c, 400, "验证码错误或已过期")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if verifyCode.ExpiresAt.Before(time.Now()) {
|
|
|
|
|
response.Error(c, 400, "验证码已过期")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
verifyCode.Used = true
|
|
|
|
|
database.DB.Save(&verifyCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var user model.AppUser
|
|
|
|
@@ -609,40 +611,38 @@ func handleAppLogin(c *gin.Context) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if device.ID > 0 {
|
|
|
|
|
if appModel.MultiOpen && appModel.MaxInstances > 0 {
|
|
|
|
|
if appModel.MultiOpenMode != "unlimited" && appModel.MaxInstances > 0 {
|
|
|
|
|
var sessionCount int64
|
|
|
|
|
database.DB.Model(&model.DeviceSession{}).
|
|
|
|
|
Where("device_id = ? AND instance_id != ? AND last_heartbeat > ?", device.ID, instanceID, timeoutThreshold).
|
|
|
|
|
Count(&sessionCount)
|
|
|
|
|
|
|
|
|
|
if sessionCount >= int64(appModel.MaxInstances) {
|
|
|
|
|
if appModel.MultiOpenMode == "forbidden" {
|
|
|
|
|
log.Printf("[DEBUG] Multi-instance limit exceeded for user %d device %s: %d/%d", user.ID, req.DeviceID, sessionCount, appModel.MaxInstances)
|
|
|
|
|
|
|
|
|
|
var sessions []model.DeviceSession
|
|
|
|
|
database.DB.Where("device_id = ? AND instance_id != ?", device.ID, instanceID).Order("last_heartbeat DESC").Find(&sessions)
|
|
|
|
|
|
|
|
|
|
sessionList := make([]gin.H, 0)
|
|
|
|
|
for _, s := range sessions {
|
|
|
|
|
isOnline := s.LastHeartbeat != nil && s.LastHeartbeat.After(timeoutThreshold)
|
|
|
|
|
sessionList = append(sessionList, gin.H{
|
|
|
|
|
"id": s.ID,
|
|
|
|
|
"instance_id": s.InstanceID,
|
|
|
|
|
"is_online": isOnline,
|
|
|
|
|
"last_heartbeat": s.LastHeartbeat,
|
|
|
|
|
"created_at": s.CreatedAt,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response.ErrorWithData(c, 403, "多开数量已达上限", gin.H{
|
|
|
|
|
"error_code": "MULTI_INSTANCE_LIMIT_EXCEEDED",
|
|
|
|
|
"max_instances": appModel.MaxInstances,
|
|
|
|
|
"instance_count": sessionCount,
|
|
|
|
|
"instances": sessionList,
|
|
|
|
|
log.Printf("[DEBUG] Multi-instance limit exceeded for user %d device %s: %d/%d", user.ID, req.DeviceID, sessionCount, appModel.MaxInstances)
|
|
|
|
|
|
|
|
|
|
var sessions []model.DeviceSession
|
|
|
|
|
database.DB.Where("device_id = ? AND instance_id != ?", device.ID, instanceID).Order("last_heartbeat DESC").Find(&sessions)
|
|
|
|
|
|
|
|
|
|
sessionList := make([]gin.H, 0)
|
|
|
|
|
for _, s := range sessions {
|
|
|
|
|
isOnline := s.LastHeartbeat != nil && s.LastHeartbeat.After(timeoutThreshold)
|
|
|
|
|
sessionList = append(sessionList, gin.H{
|
|
|
|
|
"id": s.ID,
|
|
|
|
|
"instance_id": s.InstanceID,
|
|
|
|
|
"is_online": isOnline,
|
|
|
|
|
"last_heartbeat": s.LastHeartbeat,
|
|
|
|
|
"created_at": s.CreatedAt,
|
|
|
|
|
})
|
|
|
|
|
service.LogVerification(c, &appModel.ID, &user.ID, "login_failed", "登录失败: 多开数量已达上限 - "+req.Username, req.DeviceID, fmt.Errorf("多开数量已达上限"))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response.ErrorWithData(c, 403, "多开数量已达上限", gin.H{
|
|
|
|
|
"error_code": "MULTI_INSTANCE_LIMIT_EXCEEDED",
|
|
|
|
|
"max_instances": appModel.MaxInstances,
|
|
|
|
|
"instance_count": sessionCount,
|
|
|
|
|
"instances": sessionList,
|
|
|
|
|
})
|
|
|
|
|
service.LogVerification(c, &appModel.ID, &user.ID, "login_failed", "登录失败: 多开数量已达上限 - "+req.Username, req.DeviceID, fmt.Errorf("多开数量已达上限"))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|