新增 OIDC/SSO、邮件验证码与 Gitea 项目同步,并强化 Feed 与管理后台。
作为 OIDC Provider 对接 Gitea;注册支持邮件验证码/验证码;侧栏同步公开仓库;Feed 分页、文章大纲、标签云与站点品牌设置。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
128
service/email_code.go
Normal file
128
service/email_code.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.iioio.com/freefire/jiang13-forum/model"
|
||||
)
|
||||
|
||||
const (
|
||||
emailCodeLen = 6
|
||||
emailCodeTTL = 10 * time.Minute
|
||||
emailCodeCooldown = 60 * time.Second
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
subject := "注册验证码"
|
||||
body := fmt.Sprintf("您的注册验证码是:%s\n\n%d 分钟内有效,如非本人操作请忽略。", code, int(emailCodeTTL.Minutes()))
|
||||
if err := s.mail.Send(email, subject, body); 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
|
||||
}
|
||||
Reference in New Issue
Block a user