package auth import ( "context" "crypto/rand" "fmt" "math/big" "time" "haixun-backend/internal/library/clock" "haixun-backend/internal/model/member/domain/entity" "haixun-backend/internal/svc" "github.com/zeromicro/go-zero/core/logx" ) const emailVerificationTTL = 15 * time.Minute const emailVerificationResendCooldown = 60 * time.Second func newVerifyCode() (string, error) { n, err := rand.Int(rand.Reader, big.NewInt(1000000)) if err != nil { return "", err } return fmt.Sprintf("%06d", n.Int64()), nil } func requiresNativeEmailVerification(member *entity.Member) bool { if member == nil { return false } return member.Origin == "" || member.Origin == entity.OriginNative } func issueAndSendEmailVerification(ctx context.Context, svcCtx *svc.ServiceContext, member *entity.Member) error { verifyCode, err := newVerifyCode() if err != nil { return err } expiresAt := clock.NowUnixNano() + int64(emailVerificationTTL) if err := svcCtx.Member.SetEmailVerificationCode(ctx, member.TenantID, member.UID, verifyCode, expiresAt); err != nil { return err } if err := sendEmailVerification(ctx, svcCtx, member.Email, verifyCode); err != nil { logx.WithContext(ctx).Errorf("send verification email failed: uid=%s err=%v", member.UID, err) } return nil } func sendEmailVerification(ctx context.Context, svcCtx *svc.ServiceContext, email, verifyCode string) error { if svcCtx.Mailer == nil { return nil } body := fmt.Sprintf("你的巡樓 Console 驗證碼是:%s\n\n此驗證碼 15 分鐘內有效。", verifyCode) if err := svcCtx.Mailer.Send(ctx, email, "巡樓 Console 帳號驗證碼", body); err != nil { logx.WithContext(ctx).Errorf("send verification email failed: email=%s err=%v", email, err) return err } return nil } func maybeSendLoginVerificationEmail(ctx context.Context, svcCtx *svc.ServiceContext, member *entity.Member) { if member == nil || !requiresNativeEmailVerification(member) || member.EmailVerified { return } if member.EmailVerifyCode != "" && member.EmailVerifyExpiresAt > clock.NowUnixNano() { return } _ = issueAndSendEmailVerification(ctx, svcCtx, member) } func emailVerificationSentRecently(member *entity.Member) bool { if member == nil || member.EmailVerifyExpiresAt <= 0 { return false } sentAt := member.EmailVerifyExpiresAt - int64(emailVerificationTTL) return clock.NowUnixNano()-sentAt < int64(emailVerificationResendCooldown) }