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>
68 lines
1.5 KiB
Go
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
|
|
}
|