spark-server/internal/module/user/logic/email_code.go
zhr ae9a71034d feat: implement Spark IM V3 modular monolith
Add HTTP /v1 MVC, TCP send path, groups with server-side member-avatar collage, object-storage upload, and friends/favorites around the architecture-v3 layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 19:52:11 +08:00

68 lines
1.5 KiB
Go

package logic
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"spark-server/internal/module/user/entity"
"spark-server/internal/pkg/errs"
)
const (
codeTTLSeconds = 300
codeCooldownSeconds = 60
)
// EmailCodeTTLSeconds is the OTP lifetime returned to clients.
const EmailCodeTTLSeconds = codeTTLSeconds
// CodeStore is the email OTP cache (Redis).
type CodeStore interface {
Put(ctx context.Context, email, code string, ttlSeconds int) error
Get(ctx context.Context, email string) (string, error)
Del(ctx context.Context, email string) error
SetCooldown(ctx context.Context, email string, ttlSeconds int) (bool, error)
}
func (s *Service) SendEmailCode(ctx context.Context, email string) error {
if s == nil || s.codes == nil {
return errs.ErrAvailable
}
email = entity.NormalizeEmail(email)
if email == "" {
return errs.ErrInvalidArgument
}
if entity.IsTestEmail(email) {
return nil
}
ok, err := s.codes.SetCooldown(ctx, email, codeCooldownSeconds)
if err != nil {
return err
}
if !ok {
return errs.ErrRateLimited
}
code, err := randomDigits(6)
if err != nil {
return errs.ErrInner.WithError("email-code", err)
}
return s.codes.Put(ctx, email, code, codeTTLSeconds)
}
func randomDigits(n int) (string, error) {
if n <= 0 {
return "", fmt.Errorf("digits")
}
var buf [8]byte
out := make([]byte, n)
for i := 0; i < n; i++ {
if _, err := rand.Read(buf[:4]); err != nil {
return "", err
}
out[i] = '0' + byte(binary.BigEndian.Uint32(buf[:4])%10)
}
return string(out), nil
}