Files

386 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package warningconfig
import (
"context"
"crypto/rand"
"encoding/json"
"fmt"
"math/big"
"mime"
"net"
"net/mail"
"net/smtp"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
const (
warnMethodSMS = "0"
warnMethodEmail = "1"
warnMethodPhone = "2"
warnMethodPlatform = "3"
warnEmailCodeTTLSeconds = 300
warnEmailCodeRedisKeyScope = "iotlcm:warning:email-code"
)
func SendEmailCode(ctx context.Context, in *model.TSimWarningConfigEmailCodeParam) error {
if in == nil {
return gerror.New("邮箱验证码参数不能为空")
}
email, err := normalizeSingleWarnEmail(in.Email)
if err != nil {
return err
}
if strings.TrimSpace(in.CaptchaID) == "" || strings.TrimSpace(in.CaptchaValue) == "" {
return gerror.New("图片验证码不能为空")
}
if err = service.SysCaptcha().VerifyCaptcha(ctx, in.CaptchaID, in.CaptchaValue); err != nil {
return err
}
userID := contexts.GetUserId(ctx)
tenantID := contexts.GetTenantId(ctx)
if userID <= 0 || strings.TrimSpace(tenantID) == "" {
return gerror.New("登录信息已过期")
}
code, err := generateWarnEmailCode()
if err != nil {
return gerror.Wrap(err, "生成邮箱验证码失败")
}
if err = setWarnEmailCode(ctx, tenantID, userID, email, code); err != nil {
return gerror.Wrap(err, "保存邮箱验证码失败,请稍后重试")
}
return sendWarnEmailCode(ctx, email, code)
}
func ValidateEmailCode(ctx context.Context, options Options) error {
return validateEmailCode(ctx, options, false)
}
func ConsumeEmailCode(ctx context.Context, options Options) error {
return validateEmailCode(ctx, options, true)
}
func validateEmailCode(ctx context.Context, options Options, consume bool) error {
if !hasEmailMethod(options.WarnMethods) {
return nil
}
email, err := normalizeSingleWarnEmail(options.WarnEmails)
if err != nil {
return err
}
code := strings.TrimSpace(options.WarnEmailCode)
if code == "" {
return gerror.New("请选择邮箱提醒时必须填写邮箱验证码")
}
userID := contexts.GetUserId(ctx)
tenantID := contexts.GetTenantId(ctx)
if userID <= 0 || strings.TrimSpace(tenantID) == "" {
return gerror.New("登录信息已过期")
}
cacheCode, err := getWarnEmailCode(ctx, tenantID, userID, email)
if err != nil {
return gerror.Wrap(err, "获取邮箱验证码失败,请稍后重试")
}
if cacheCode == "" {
return gerror.New("邮箱验证码已失效")
}
if cacheCode != code {
return gerror.New("邮箱验证码错误")
}
if consume {
return deleteWarnEmailCode(ctx, tenantID, userID, email)
}
return nil
}
func validateNotificationOptions(options *Options) error {
if options == nil {
return gerror.New("预警配置参数不能为空")
}
if hasSMSMethod(options.WarnMethods) {
phones, err := normalizeWarnPhones(options.WarnPhones)
if err != nil {
return err
}
options.WarnPhones = phones
}
if hasEmailMethod(options.WarnMethods) {
emails, err := normalizeWarnEmails(options.WarnEmails)
if err != nil {
return err
}
options.WarnEmails = emails
}
return nil
}
func hasSMSMethod(methods string) bool {
for _, method := range splitWarnList(methods) {
if method == warnMethodSMS {
return true
}
}
return false
}
func hasEmailMethod(methods string) bool {
for _, method := range splitWarnList(methods) {
if method == warnMethodEmail {
return true
}
}
return false
}
func normalizeSingleWarnEmail(value string) (string, error) {
emails := splitWarnList(value)
if len(emails) == 0 {
return "", gerror.New("请选择邮箱提醒时必须填写邮箱地址")
}
if len(emails) > 1 {
return "", gerror.New("邮箱提醒验证码第一版仅支持一个邮箱地址")
}
address, err := mail.ParseAddress(emails[0])
if err != nil || address == nil || strings.TrimSpace(address.Address) == "" {
return "", gerror.New("邮箱格式不正确")
}
return strings.ToLower(strings.TrimSpace(address.Address)), nil
}
func normalizeWarnEmails(value string) (string, error) {
emails := splitWarnList(value)
if len(emails) == 0 {
return "", gerror.New("请选择邮箱提醒时必须填写邮箱地址")
}
result := make([]string, 0, len(emails))
for _, email := range emails {
address, err := mail.ParseAddress(email)
if err != nil || address == nil || strings.TrimSpace(address.Address) == "" {
return "", gerror.Newf("邮箱格式不正确:%s", email)
}
result = append(result, strings.ToLower(strings.TrimSpace(address.Address)))
}
return strings.Join(result, ","), nil
}
func normalizeWarnPhones(value string) (string, error) {
phones := splitWarnList(value)
if len(phones) == 0 {
return "", gerror.New("请选择短信提醒时必须填写短信号码")
}
for _, phone := range phones {
if !isValidWarnPhone(phone) {
return "", gerror.Newf("手机号格式不正确:%s", phone)
}
}
return strings.Join(phones, ","), nil
}
func isValidWarnPhone(phone string) bool {
if len(phone) != 11 || phone[0] != '1' {
return false
}
for _, r := range phone {
if r < '0' || r > '9' {
return false
}
}
return phone[1] >= '3' && phone[1] <= '9'
}
func splitWarnList(value string) []string {
parts := strings.FieldsFunc(value, func(r rune) bool {
switch r {
case ',', ';', ',', ';', '|', '\n', '\r', '\t', ' ':
return true
default:
return false
}
})
seen := make(map[string]struct{}, len(parts))
result := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
key := strings.ToLower(part)
if _, exists := seen[key]; exists {
continue
}
seen[key] = struct{}{}
result = append(result, part)
}
return result
}
func warnEmailCodeKey(tenantID string, userID int64, email string) string {
return fmt.Sprintf("%s:%s:%d:%s", warnEmailCodeRedisKeyScope, strings.TrimSpace(tenantID), userID, strings.ToLower(strings.TrimSpace(email)))
}
func setWarnEmailCode(ctx context.Context, tenantID string, userID int64, email string, code string) error {
_, err := g.Redis().Do(ctx, "SETEX", warnEmailCodeKey(tenantID, userID, email), warnEmailCodeTTLSeconds, code)
return err
}
func getWarnEmailCode(ctx context.Context, tenantID string, userID int64, email string) (string, error) {
value, err := g.Redis().Get(ctx, warnEmailCodeKey(tenantID, userID, email))
if err != nil {
return "", err
}
if value.IsNil() {
return "", nil
}
return value.String(), nil
}
func deleteWarnEmailCode(ctx context.Context, tenantID string, userID int64, email string) error {
_, err := g.Redis().Del(ctx, warnEmailCodeKey(tenantID, userID, email))
return err
}
func generateWarnEmailCode() (string, error) {
n, err := rand.Int(rand.Reader, big.NewInt(1000000))
if err != nil {
return "", err
}
return fmt.Sprintf("%06d", n.Int64()), nil
}
func sendWarnEmailCode(ctx context.Context, email string, code string) error {
cfg, ok := loadWarnEmailCodeConfig(ctx)
if !ok || strings.TrimSpace(cfg.Host) == "" {
if strings.TrimSpace(cfg.Url) != "" {
return callWarnEmailCodeHTTP(ctx, cfg.UserCodeHttpSenderConfig, email, code)
}
g.Log().Infof(ctx, "warning email config code, email:%s, code:%s", email, code)
return nil
}
if cfg.Port <= 0 {
cfg.Port = 25
}
if strings.TrimSpace(cfg.From) == "" {
cfg.From = cfg.Username
}
if strings.TrimSpace(cfg.From) == "" {
return gerror.New("预警邮箱验证码发件人为空")
}
addr := net.JoinHostPort(cfg.Host, gconv.String(cfg.Port))
auth := smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host)
msg := buildWarnEmailCodeMessage(cfg, email, code)
err := utility.SendSMTP(ctx, utility.SMTPOptions{
Address: addr,
Host: cfg.Host,
Auth: auth,
From: cfg.From,
To: []string{email},
Data: msg,
SSL: cfg.SSL,
Timeout: utility.ExternalRequestTimeout(cfg.Timeout),
})
if err != nil {
g.Log().Errorf(ctx, "send warning email config code failed, email:%s, err:%+v", email, err)
return gerror.New("发送邮箱验证码失败,请稍后重试")
}
return nil
}
func loadWarnEmailCodeConfig(ctx context.Context) (model.UserCodeEmailSenderConfig, bool) {
var cfg model.UserCodeEmailSenderConfig
for _, key := range []string{"warning.email", "register.email"} {
v, err := g.Cfg().Get(ctx, key)
if err != nil || v.IsNil() {
continue
}
_ = v.Scan(&cfg)
if strings.TrimSpace(cfg.Url) != "" || strings.TrimSpace(cfg.Host) != "" {
return cfg, true
}
}
return cfg, false
}
func buildWarnEmailCodeMessage(cfg model.UserCodeEmailSenderConfig, email string, code string) []byte {
subject := strings.TrimSpace(cfg.Subject)
if subject == "" || subject == "注册验证码" {
subject = "预警邮箱验证码"
}
body := renderWarnEmailCodeTemplate(cfg.BodyTemplate, email, code)
if body == "" || strings.Contains(body, "注册验证码") {
body = fmt.Sprintf("您的预警邮箱验证码是:%s,5分钟内有效。", code)
}
from := strings.TrimSpace(cfg.From)
if strings.TrimSpace(cfg.FromName) != "" {
from = mime.QEncoding.Encode("UTF-8", cfg.FromName) + " <" + from + ">"
}
message := strings.Builder{}
message.WriteString("From: " + from + "\r\n")
message.WriteString("To: " + email + "\r\n")
message.WriteString("Subject: " + mime.QEncoding.Encode("UTF-8", subject) + "\r\n")
message.WriteString("MIME-Version: 1.0\r\n")
message.WriteString("Content-Type: text/html; charset=UTF-8\r\n")
message.WriteString("\r\n")
message.WriteString(body)
return []byte(message.String())
}
func renderWarnEmailCodeTemplate(template string, contact string, code string) string {
if strings.TrimSpace(template) == "" {
return ""
}
replacer := strings.NewReplacer(
"{contact}", contact,
"{{contact}}", contact,
"{email}", contact,
"{{email}}", contact,
"{code}", code,
"{{code}}", code,
)
return replacer.Replace(template)
}
func callWarnEmailCodeHTTP(ctx context.Context, cfg model.UserCodeHttpSenderConfig, email string, code string) error {
method := strings.ToUpper(strings.TrimSpace(cfg.Method))
if method == "" {
method = "POST"
}
successStatus := cfg.SuccessStatus
if successStatus <= 0 {
successStatus = 200
}
body := renderWarnEmailCodeTemplate(cfg.BodyTemplate, email, code)
if body == "" {
bodyBytes, _ := json.Marshal(g.Map{
"contact": email,
"email": email,
"code": code,
})
body = string(bodyBytes)
}
headers := cfg.Headers
if headers == nil {
headers = map[string]string{}
}
if _, ok := headers["Content-Type"]; !ok {
headers["Content-Type"] = "application/json"
}
resp, err := g.Client().Timeout(utility.ExternalRequestTimeout(cfg.Timeout)).SetHeaderMap(headers).DoRequest(ctx, method, cfg.Url, body)
if err != nil {
return gerror.Wrap(err, "发送邮箱验证码失败,请稍后重试")
}
defer resp.Close()
if resp.StatusCode != successStatus {
return gerror.Newf("发送邮箱验证码失败,服务响应状态码:%d", resp.StatusCode)
}
return nil
}