mirror of https://github.com/veypi/OneAuth.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
195 lines
5.1 KiB
Go
195 lines
5.1 KiB
Go
//
|
|
// Copyright (C) 2024 veypi <i@veypi.com>
|
|
// 2025-03-04 16:08:06
|
|
// Distributed under terms of the MIT license.
|
|
//
|
|
|
|
package verification
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"time"
|
|
|
|
"github.com/veypi/vbase/cfg"
|
|
"github.com/veypi/vbase/libs/email"
|
|
"github.com/veypi/vbase/libs/sms"
|
|
"github.com/veypi/vbase/models"
|
|
"github.com/veypi/vigo"
|
|
)
|
|
|
|
// SendRequest 发送验证码请求
|
|
type SendRequest struct {
|
|
Type string `json:"type" src:"json" desc:"类型: email/sms"`
|
|
Target string `json:"target" src:"json" desc:"邮箱或手机号"`
|
|
Purpose string `json:"purpose" src:"json" desc:"用途: register/login/reset_password/bind"`
|
|
}
|
|
|
|
// SendResponse 发送响应
|
|
type SendResponse struct {
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// sendCode 统一发送验证码
|
|
func sendCode(x *vigo.X, req *SendRequest) (*SendResponse, error) {
|
|
// 参数校验
|
|
if req.Type != "email" && req.Type != "sms" {
|
|
return nil, vigo.ErrInvalidArg.WithString("invalid type, must be email or sms")
|
|
}
|
|
if req.Target == "" {
|
|
return nil, vigo.ErrInvalidArg.WithString("target is required")
|
|
}
|
|
if req.Purpose == "" {
|
|
req.Purpose = models.CodePurposeLogin
|
|
}
|
|
|
|
db := cfg.DB()
|
|
|
|
// 检查发送频率限制
|
|
interval, _ := models.GetSettingInt(models.SettingCodeSendInterval)
|
|
if interval == 0 {
|
|
interval = 60
|
|
}
|
|
|
|
var lastCode models.VerificationCode
|
|
if err := db.Where("target = ? AND type = ?", req.Target, req.Type).
|
|
Order("created_at DESC").First(&lastCode).Error; err == nil {
|
|
// 检查是否在间隔期内
|
|
if time.Since(lastCode.CreatedAt) < time.Duration(interval)*time.Second {
|
|
return nil, vigo.ErrTooManyRequests.WithString(fmt.Sprintf("please wait %d seconds", interval))
|
|
}
|
|
}
|
|
|
|
// 检查每日发送次数限制
|
|
// 配置说明: 0=禁用验证码功能, -1=不限制, >0=限制次数
|
|
maxDaily, _ := models.GetSettingInt(models.SettingCodeMaxDailyCount)
|
|
if maxDaily == 0 {
|
|
return nil, vigo.ErrForbidden.WithString("verification code service is disabled")
|
|
}
|
|
if maxDaily > 0 {
|
|
startOfDay := time.Now().Truncate(24 * time.Hour)
|
|
var dailyCount int64
|
|
db.Model(&models.VerificationCode{}).
|
|
Where("target = ? AND type = ? AND created_at >= ?", req.Target, req.Type, startOfDay).
|
|
Count(&dailyCount)
|
|
if int(dailyCount) >= maxDaily {
|
|
return nil, vigo.ErrTooManyRequests.WithString("daily limit exceeded")
|
|
}
|
|
}
|
|
|
|
// 生成验证码
|
|
codeLength, _ := models.GetSettingInt(models.SettingCodeLength)
|
|
if codeLength == 0 {
|
|
codeLength = 6
|
|
}
|
|
code := generateCode(codeLength)
|
|
|
|
// 过期时间
|
|
expiryMinutes, _ := models.GetSettingInt(models.SettingCodeExpiry)
|
|
if expiryMinutes == 0 {
|
|
expiryMinutes = 5
|
|
}
|
|
expiresAt := time.Now().Add(time.Duration(expiryMinutes) * time.Minute)
|
|
|
|
// 保存验证码到数据库
|
|
verification := &models.VerificationCode{
|
|
Target: req.Target,
|
|
Type: req.Type,
|
|
Code: code,
|
|
Purpose: req.Purpose,
|
|
Status: models.CodeStatusPending,
|
|
ExpiresAt: expiresAt,
|
|
RemoteIP: x.GetRemoteIP(),
|
|
}
|
|
|
|
if err := db.Create(verification).Error; err != nil {
|
|
return nil, vigo.ErrInternalServer.WithError(err)
|
|
}
|
|
|
|
// 发送验证码
|
|
var sendErr error
|
|
switch req.Type {
|
|
case "email":
|
|
sendErr = sendEmailCode(req.Target, code)
|
|
case "sms":
|
|
sendErr = sendSMSCode(req.Target, code)
|
|
}
|
|
|
|
if sendErr != nil {
|
|
// 更新状态为失败
|
|
db.Model(verification).Update("status", models.CodeStatusFailed)
|
|
return nil, vigo.ErrInternalServer.WithError(sendErr)
|
|
}
|
|
|
|
return &SendResponse{Message: "verification code sent"}, nil
|
|
}
|
|
|
|
// sendEmailCode 发送邮件验证码
|
|
func sendEmailCode(target, code string) error {
|
|
// 检查邮件服务是否启用
|
|
enabled, err := models.GetSettingBool(models.SettingEmailEnabled)
|
|
if err != nil || !enabled {
|
|
return fmt.Errorf("email service not enabled")
|
|
}
|
|
|
|
// 发送邮件
|
|
if err := email.SendVerificationCode(target, code); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 记录日志
|
|
log := &models.EmailLog{
|
|
To: target,
|
|
Subject: "验证码",
|
|
Provider: "smtp",
|
|
Status: "sent",
|
|
}
|
|
cfg.DB().Create(log)
|
|
|
|
return nil
|
|
}
|
|
|
|
// sendSMSCode 发送短信验证码
|
|
func sendSMSCode(phone, code string) error {
|
|
// 检查短信服务是否启用
|
|
enabled, err := models.GetSettingBool(models.SettingSMSEnabled)
|
|
if err != nil || !enabled {
|
|
return fmt.Errorf("sms service not enabled")
|
|
}
|
|
|
|
// 获取短信配置
|
|
provider, _ := models.GetSetting(models.SettingSMSProvider)
|
|
accessKey, _ := models.GetSetting(models.SettingSMSAccessKey)
|
|
accessSecret, _ := models.GetSetting(models.SettingSMSAccessSecret)
|
|
signName, _ := models.GetSetting(models.SettingSMSSignName)
|
|
templateCode, _ := models.GetSetting(models.SettingSMSTemplateCode)
|
|
|
|
if accessKey == "" || accessSecret == "" {
|
|
return fmt.Errorf("sms not configured")
|
|
}
|
|
|
|
// 创建短信提供商
|
|
smsProvider, err := sms.NewProvider(provider, accessKey, accessSecret, signName, templateCode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 发送短信
|
|
if err := smsProvider.Send(phone, code); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// generateCode 生成随机验证码
|
|
func generateCode(length int) string {
|
|
const digits = "0123456789"
|
|
code := make([]byte, length)
|
|
for i := range code {
|
|
code[i] = digits[rand.Intn(len(digits))]
|
|
}
|
|
return string(code)
|
|
}
|
|
|