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.
OneAuth/api/verification/send.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)
}