package auth import ( "backend/internal/domain" "backend/internal/utils" "backend/internal/utils/email_template" errs "backend/pkg/library/errors" "backend/pkg/member/domain/member" "backend/pkg/member/domain/usecase" notificationUC "backend/pkg/notification/domain/usecase" "bytes" "context" "fmt" "html/template" "backend/internal/svc" "backend/internal/types" "github.com/zeromicro/go-zero/core/logx" ) type RequestPasswordResetLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewRequestPasswordResetLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RequestPasswordResetLogic { return &RequestPasswordResetLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } // RequestPasswordReset 請求發送密碼重設驗證碼 aka 忘記密碼 func (l *RequestPasswordResetLogic) RequestPasswordReset(req *types.RequestPasswordResetReq) (resp *types.RespOK, err error) { // 驗證並標準化帳號 acc, err := l.validateAndNormalizeAccount(req.AccountType, req.Identifier) if err != nil { return nil, err } // 檢查發送冷卻時間 rk := domain.GenerateVerifyCodeRedisKey.With(fmt.Sprintf("%s:%d", acc, member.GenerateCodeTypeForgetPassword)).ToString() if err := l.checkVerifyCodeCooldown(rk); err != nil { return nil, err } // 確認帳號是否註冊並檢查平台限制 if err := l.checkAccountAndPlatform(acc); err != nil { return nil, err } // 生成驗證碼 vcode, err := l.svcCtx.AccountUC.GenerateRefreshCode(l.ctx, usecase.GenerateRefreshCodeRequest{ LoginID: acc, CodeType: member.GenerateCodeTypeForgetPassword, }) if err != nil { return nil, err } // 獲取用戶資訊並確認綁定帳號 account, err := l.svcCtx.AccountUC.GetUIDByAccount(l.ctx, usecase.GetUIDByAccountRequest{Account: acc}) if err != nil { return nil, errs.ResNotFoundError(fmt.Sprintf("account not found:%s", acc)) } info, err := l.svcCtx.AccountUC.GetUserInfo(l.ctx, usecase.GetUserInfoRequest{UID: account.UID}) if err != nil { return nil, err } nickname := generateMsgName(&info) switch member.GetAccountTypeByCode(req.AccountType) { case member.AccountTypeMail: body, title, err := email_template.GetEmailTemplate(email_template.Language(info.PreferredLanguage), email_template.ForgetPasswordVerify) if err != nil { e := errs.ResNotFoundError("failed to get correct email template") return nil, e } tmpl, err := template.New("ForgetPasswordEmail").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, notificationUC.MailReq{ To: []string{req.Identifier}, 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 member.AccountTypePhone: //// 送出手機號碼 //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 member.AccountTypeNone: case member.AccountTypeDefine: default: return &types.RespOK{}, errs.InputInvalidRangeError("") } // 設置 Redis 鍵 l.setRedisKeyWithExpiry(rk, vcode.Data.VerifyCode, 60) return &types.RespOK{}, nil } // validateAndNormalizeAccount 驗證並標準化帳號 func (l *RequestPasswordResetLogic) validateAndNormalizeAccount(accountType, account string) (string, error) { switch member.GetAccountTypeByCode(accountType) { case member.AccountTypePhone: phone, isPhone := utils.NormalizeTaiwanMobile(account) if !isPhone { return "", errs.InputInvalidFormatError("phone number is invalid") } return phone, nil case member.AccountTypeMail: if !utils.IsValidEmail(account) { return "", errs.InputInvalidFormatError("email is invalid") } return account, nil case member.AccountTypeNone, member.AccountTypeDefine: default: } return "", errs.InputInvalidFormatError("unsupported account type") } // checkVerifyCodeCooldown 檢查是否已在限制時間內發送過驗證碼 func (l *RequestPasswordResetLogic) checkVerifyCodeCooldown(rk string) error { if cachedCode, err := l.svcCtx.Redis.GetCtx(l.ctx, rk); err != nil || cachedCode != "" { return errs.SysTooManyRequestError("verification code already sent, please wait 3min for system to send again") } return nil } // checkAccountAndPlatform 檢查帳號是否註冊及平台限制 func (l *RequestPasswordResetLogic) checkAccountAndPlatform(acc string) error { accountInfo, err := l.svcCtx.AccountUC.GetUserAccountInfo(l.ctx, usecase.GetUIDByAccountRequest{Account: acc}) if err != nil { return err } if accountInfo.Data.Platform != member.Digimon { return errs.InputInvalidFormatError( "failed to send verify code since platform not correct") } return nil } // setRedisKeyWithExpiry 設置 Redis 鍵 func (l *RequestPasswordResetLogic) setRedisKeyWithExpiry(rk, verifyCode string, expiry int) { if status, err := l.svcCtx.Redis.SetnxExCtx(l.ctx, rk, verifyCode, expiry); err != nil || !status { _ = errs.DBErrorErrorL(l.svcCtx.Logger, []errs.LogField{ {Key: "redisKey", Val: rk}, {Key: "error", Val: err.Error()}, }, "failed to set redis expire").Wrap(err) } } // generateMsgName 取得寄信用的名稱 func generateMsgName(info *usecase.UserInfo) string { if info.FullName != nil { return *info.FullName } if info.Nickname != nil { return *info.Nickname } return info.UID }