212 lines
5.9 KiB
Go
212 lines
5.9 KiB
Go
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
|
||
}
|