backend/internal/logic/user/request_verification_code_l...

212 lines
5.9 KiB
Go
Raw 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 user
import (
"backend/internal/domain"
"backend/internal/utils/email_template"
errs "backend/pkg/library/errors"
mbr "backend/pkg/member/domain/member"
member "backend/pkg/member/domain/usecase"
"backend/pkg/notification/domain/usecase"
"backend/pkg/permission/domain/token"
"bytes"
"context"
"fmt"
"html/template"
"regexp"
"strings"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type RequestVerificationCodeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewRequestVerificationCodeLogic 請求發送驗證碼 (用於驗證信箱/手機)
func NewRequestVerificationCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RequestVerificationCodeLogic {
return &RequestVerificationCodeLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *RequestVerificationCodeLogic) RequestVerificationCode(req *types.RequestVerificationCodeReq) (resp *types.RespOK, err error) {
acc := ""
ct := mbr.GenerateCodeTypeEmail
switch req.Purpose {
case "email_verification":
if !isValidEmail(req.Account) {
return nil, errs.InputInvalidFormatError("email is invalid")
}
acc = req.Account
// 1. TODO 討論 email 不可以再被使用
// 2. TODO 討論 email 跟我帳號是不是一樣(如果是用自己的信箱註冊的話)
case "phone_verification":
phone, isPhone := normalizeTaiwanMobile(req.Account)
if !isPhone {
return nil, errs.InputInvalidFormatError("phone number is invalid")
}
acc = phone
// TODO 討論號碼有被用過就不可以再被使用了
ct = mbr.GenerateCodeTypePhone
default:
return &types.RespOK{}, errs.InputInvalidRangeError("")
}
uid := token.UID(l.ctx)
// 限制三分鐘內只可以發送一次
rk := domain.GenerateVerifyCodeRedisKey.With(
fmt.Sprintf("%s-%s", uid, req.Purpose),
).ToString()
// 拿不到不會出錯DB 壞掉才會
get, err := l.svcCtx.Redis.GetCtx(l.ctx, rk)
if err != nil {
return nil, errs.DBErrorError("failed to connect to redis").Wrap(err)
}
if get != "" {
// 已經發送過驗證碼,返回提示
return nil, errs.SysTooManyRequestError("code already sent, please wait 3min for system to send again")
}
// 生成驗證碼
vcode, err := l.svcCtx.AccountUC.GenerateRefreshCode(l.ctx, member.GenerateRefreshCodeRequest{
LoginID: acc,
CodeType: ct,
})
if err != nil {
return nil, err
}
// 取得用戶資訊
info, err := l.svcCtx.AccountUC.GetUserInfo(l.ctx, member.GetUserInfoRequest{
UID: uid,
})
if err != nil {
return nil, err
}
nickname := generateMsgName(&info)
switch ct {
case mbr.GenerateCodeTypeEmail:
body, title, err := email_template.GetEmailTemplate(email_template.Language(info.PreferredLanguage), email_template.BindingEmail)
if err != nil {
e := errs.ResNotFoundError("failed to get correct email template")
return nil, e
}
tmpl, err := template.New("BindEmailBody").Parse(body)
if err != nil {
e := errs.ResInvalidFormatError("failed to get correct email template")
return nil, e
}
emailParams := email_template.ForgetPasswordEmailReq{
Username: nickname,
VerifyCode: vcode.Data.VerifyCode,
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, emailParams); err != nil {
e := errs.ResInvalidFormatError("failed to build data")
return nil, e
}
err = l.svcCtx.DeliveryUC.SendEmail(l.ctx, usecase.MailReq{
To: []string{req.Account},
From: l.svcCtx.Config.SMTPConfig.Sender,
SenderName: l.svcCtx.Config.SMTPConfig.SenderName,
Subject: title,
Body: buf.String(),
})
if err != nil {
e := errs.SvcThirdPartyError("failed to send email").Wrap(err)
return nil, e
}
case mbr.GenerateCodeTypePhone:
//// 送出手機號碼
//templateResp, err := l.svcCtx.NotificationUseCase.GetSMSTemplateByTypeID(
// l.ctx, notificationModule.Language(info.PreferredLanguage), notificationModule.BindingPhone)
//if err != nil {
// return nil, err
//}
//
//fmt.Println(fmt.Sprintf("%s:%s", templateResp.Body, vcode.Data.VerifyCode))
////err = l.svcCtx.NotificationUseCase.SendMessage(l.ctx, notificationModule.SMSMessageRequest{
//// PhoneNumber: acc,
//// RecipientName: nickname,
//// MessageContent: fmt.Sprintf("%s:%s", templateResp.Body, vcode),
////})
////if err != nil {
//// return nil, err
////}
case mbr.GenerateCodeTypeNone:
case mbr.GenerateCodeTypeForgetPassword:
default:
return &types.RespOK{}, errs.InputInvalidRangeError("")
}
// 設置 Redis 鍵,並設置 3 分鐘的過期時間
status, err := l.svcCtx.Redis.SetnxExCtx(l.ctx, rk, vcode.Data.VerifyCode, 60*3)
if err != nil || !status {
// 純記錄,前面都已經成功,就不報錯了
_ = errs.DBErrorErrorL(l.svcCtx.Logger,
[]errs.LogField{
{Key: "req", Val: req},
{Key: "func", Val: "Redis.SetnxExCtx"},
{Key: "err", Val: err.Error()},
}, "failed to set redis expire").Wrap(err)
}
return &types.RespOK{}, nil
}
// 標準化號碼並驗證是否為合法台灣手機號碼
func normalizeTaiwanMobile(phone string) (string, bool) {
// 移除空格
phone = strings.ReplaceAll(phone, " ", "")
// 移除 "+886" 並將剩餘部分標準化
if strings.HasPrefix(phone, "+886") {
phone = strings.TrimPrefix(phone, "+886")
if !strings.HasPrefix(phone, "0") {
phone = "0" + phone
}
}
// 正則表達式驗證標準化後的號碼
regex := regexp.MustCompile(`^(09\d{8})$`)
if regex.MatchString(phone) {
return phone, true
}
return "", false
}
// 驗證 Email 格式的函數
func isValidEmail(email string) bool {
// 定義正則表達式
regex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
return regex.MatchString(email)
}
// generateMsgName 取得寄信用的名稱
func generateMsgName(info *member.UserInfo) string {
if info.FullName != nil {
return *info.FullName
}
if info.Nickname != nil {
return *info.Nickname
}
return info.UID
}