2025-10-02 06:43:57 +00:00
|
|
|
package auth
|
|
|
|
|
|
|
|
|
|
import (
|
2025-10-22 13:40:31 +00:00
|
|
|
"backend/internal/domain"
|
|
|
|
|
"backend/internal/utils"
|
2025-11-08 06:37:41 +00:00
|
|
|
"backend/internal/utils/email_template"
|
2025-11-04 09:47:36 +00:00
|
|
|
errs "backend/pkg/library/errors"
|
2025-10-22 13:40:31 +00:00
|
|
|
"backend/pkg/member/domain/member"
|
|
|
|
|
"backend/pkg/member/domain/usecase"
|
2025-11-08 06:37:41 +00:00
|
|
|
notificationUC "backend/pkg/notification/domain/usecase"
|
|
|
|
|
"bytes"
|
2025-10-02 06:43:57 +00:00
|
|
|
"context"
|
2025-10-22 13:40:31 +00:00
|
|
|
"fmt"
|
2025-11-08 06:37:41 +00:00
|
|
|
"html/template"
|
2025-10-02 06:43:57 +00:00
|
|
|
|
|
|
|
|
"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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 13:14:58 +00:00
|
|
|
// RequestPasswordReset 請求發送密碼重設驗證碼 aka 忘記密碼
|
2025-10-02 06:43:57 +00:00
|
|
|
func (l *RequestPasswordResetLogic) RequestPasswordReset(req *types.RequestPasswordResetReq) (resp *types.RespOK, err error) {
|
2025-10-22 13:40:31 +00:00
|
|
|
// 驗證並標準化帳號
|
|
|
|
|
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 {
|
2025-11-04 09:47:36 +00:00
|
|
|
return nil, errs.ResNotFoundError(fmt.Sprintf("account not found:%s", acc))
|
2025-10-22 13:40:31 +00:00
|
|
|
}
|
|
|
|
|
info, err := l.svcCtx.AccountUC.GetUserInfo(l.ctx, usecase.GetUserInfoRequest{UID: account.UID})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-08 06:37:41 +00:00
|
|
|
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
|
|
|
|
|
}
|
2025-10-22 13:40:31 +00:00
|
|
|
|
2025-11-08 06:37:41 +00:00
|
|
|
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("")
|
|
|
|
|
}
|
2025-10-22 13:40:31 +00:00
|
|
|
|
|
|
|
|
// 設置 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 {
|
2025-11-04 09:47:36 +00:00
|
|
|
return "", errs.InputInvalidFormatError("phone number is invalid")
|
2025-10-22 13:40:31 +00:00
|
|
|
}
|
2025-10-02 06:43:57 +00:00
|
|
|
|
2025-10-22 13:40:31 +00:00
|
|
|
return phone, nil
|
|
|
|
|
case member.AccountTypeMail:
|
|
|
|
|
if !utils.IsValidEmail(account) {
|
2025-11-04 09:47:36 +00:00
|
|
|
return "", errs.InputInvalidFormatError("email is invalid")
|
2025-10-22 13:40:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return account, nil
|
|
|
|
|
case member.AccountTypeNone, member.AccountTypeDefine:
|
|
|
|
|
default:
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-04 09:47:36 +00:00
|
|
|
return "", errs.InputInvalidFormatError("unsupported account type")
|
2025-10-22 13:40:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// checkVerifyCodeCooldown 檢查是否已在限制時間內發送過驗證碼
|
|
|
|
|
func (l *RequestPasswordResetLogic) checkVerifyCodeCooldown(rk string) error {
|
|
|
|
|
if cachedCode, err := l.svcCtx.Redis.GetCtx(l.ctx, rk); err != nil || cachedCode != "" {
|
2025-11-04 09:47:36 +00:00
|
|
|
return errs.SysTooManyRequestError("verification code already sent, please wait 3min for system to send again")
|
2025-10-22 13:40:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2025-11-04 09:47:36 +00:00
|
|
|
return errs.InputInvalidFormatError(
|
2025-10-22 13:40:31 +00:00
|
|
|
"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 {
|
2025-11-04 09:47:36 +00:00
|
|
|
_ = errs.DBErrorErrorL(l.svcCtx.Logger, []errs.LogField{
|
|
|
|
|
{Key: "redisKey", Val: rk},
|
|
|
|
|
{Key: "error", Val: err.Error()},
|
2025-10-22 13:40:31 +00:00
|
|
|
}, "failed to set redis expire").Wrap(err)
|
|
|
|
|
}
|
2025-10-02 06:43:57 +00:00
|
|
|
}
|
2025-11-08 06:37:41 +00:00
|
|
|
|
|
|
|
|
// generateMsgName 取得寄信用的名稱
|
|
|
|
|
func generateMsgName(info *usecase.UserInfo) string {
|
|
|
|
|
if info.FullName != nil {
|
|
|
|
|
return *info.FullName
|
|
|
|
|
}
|
|
|
|
|
if info.Nickname != nil {
|
|
|
|
|
return *info.Nickname
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return info.UID
|
|
|
|
|
}
|