feat: 分离密码重置和修改密码功能

- 密码重置:用户忘记密码时通过邮箱/短信验证(需开启开关)
- 修改密码:已登录用户通过旧密码修改(固定功能,无需配置)
- 添加 EnablePasswordReset 开关控制密码重置功能
- PasswordResetMethod 只支持 email/sms 两种验证方式
- 添加短信配置管理功能
- 移除独立的应用邮箱设置页面
This commit is contained in:
2026-05-03 01:18:27 +08:00
parent 00067b1bff
commit 1f7dfd17a7
31 changed files with 2435 additions and 1477 deletions
+81 -81
View File
@@ -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
}
}