Files
jiang13-forum/service/email_code.go

134 lines
2.7 KiB
Go

package service
import (
"crypto/rand"
"math/big"
"strings"
"sync"
"time"
"git.iioio.com/freefire/jiang13-forum/model"
)
const (
emailCodeLen = 6
emailCodeTTL = 10 * time.Minute
emailCodeCooldown = 60 * time.Second
)
// EmailCodeLen 注册邮箱验证码位数(供 API 告知前端)
const EmailCodeLen = emailCodeLen
type emailCodeEntry struct {
code string
expiresAt time.Time
sentAt time.Time
}
// EmailCodeService 注册邮箱验证码
type EmailCodeService struct {
mu sync.Mutex
entries map[string]emailCodeEntry
mail *MailService
}
func NewEmailCodeService(mail *MailService) *EmailCodeService {
s := &EmailCodeService{
entries: make(map[string]emailCodeEntry),
mail: mail,
}
go s.cleanup()
return s
}
// SendRegisterCode 向邮箱发送注册验证码
func (s *EmailCodeService) SendRegisterCode(email string) error {
email = NormalizeEmail(email)
if err := ValidateEmail(email); err != nil {
return err
}
var exist model.User
if err := model.DB.Where("email = ?", email).First(&exist).Error; err == nil {
return ErrEmailExists
}
s.mu.Lock()
if prev, ok := s.entries[email]; ok && time.Since(prev.sentAt) < emailCodeCooldown {
s.mu.Unlock()
return ErrEmailCodeCooldown
}
s.mu.Unlock()
code, err := randomDigits(emailCodeLen)
if err != nil {
return err
}
siteName := "姜十三论坛"
if s.mail != nil && s.mail.settings != nil {
siteName = s.mail.settings.SiteBranding().Name
}
subject, textBody, htmlBody := BuildRegisterCodeMail(siteName, code, int(emailCodeTTL.Minutes()))
if err := s.mail.SendHTML(email, subject, textBody, htmlBody); err != nil {
return err
}
s.mu.Lock()
s.entries[email] = emailCodeEntry{
code: code,
expiresAt: time.Now().Add(emailCodeTTL),
sentAt: time.Now(),
}
s.mu.Unlock()
return nil
}
// Verify 校验邮箱验证码(一次性)
func (s *EmailCodeService) Verify(email, code string) bool {
email = NormalizeEmail(email)
code = strings.TrimSpace(code)
if email == "" || code == "" {
return false
}
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.entries[email]
if !ok {
return false
}
delete(s.entries, email)
if time.Now().After(entry.expiresAt) {
return false
}
return entry.code == code
}
func (s *EmailCodeService) cleanup() {
ticker := time.NewTicker(time.Minute)
for range ticker.C {
now := time.Now()
s.mu.Lock()
for email, entry := range s.entries {
if now.After(entry.expiresAt) {
delete(s.entries, email)
}
}
s.mu.Unlock()
}
}
func randomDigits(n int) (string, error) {
var b strings.Builder
b.Grow(n)
max := big.NewInt(10)
for i := 0; i < n; i++ {
v, err := rand.Int(rand.Reader, max)
if err != nil {
return "", err
}
b.WriteByte(byte('0' + v.Int64()))
}
return b.String(), nil
}