diff --git a/internal/base/constant/cache_key.go b/internal/base/constant/cache_key.go index a3558aae0..4135b53c8 100644 --- a/internal/base/constant/cache_key.go +++ b/internal/base/constant/cache_key.go @@ -32,6 +32,9 @@ const ( AdminTokenCacheKey = "answer:admin:token:" AdminTokenCacheTime = 7 * 24 * time.Hour UserTokenMappingCacheKey = "answer:user-token:mapping:" + UserEmailCodeCacheKey = "answer:user:email-code:" + UserEmailCodeCacheTime = 10 * time.Minute + UserLatestEmailCodeCacheKey = "answer:user-id:email-code:" SiteInfoCacheKey = "answer:site-info:" SiteInfoCacheTime = 1 * time.Hour ConfigID2KEYCacheKeyPrefix = "answer:config:id:" diff --git a/internal/repo/export/email_repo.go b/internal/repo/export/email_repo.go index 9552c4ac6..59cba04b7 100644 --- a/internal/repo/export/email_repo.go +++ b/internal/repo/export/email_repo.go @@ -21,6 +21,8 @@ package export import ( "context" + "github.com/apache/incubator-answer/internal/base/constant" + "github.com/tidwall/gjson" "time" "github.com/apache/incubator-answer/internal/base/data" @@ -42,9 +44,15 @@ func NewEmailRepo(data *data.Data) export.EmailRepo { } // SetCode The email code is used to verify that the link in the message is out of date -func (e *emailRepo) SetCode(ctx context.Context, code, content string, duration time.Duration) error { - err := e.data.Cache.SetString(ctx, code, content, duration) - if err != nil { +func (e *emailRepo) SetCode(ctx context.Context, userID, code, content string, duration time.Duration) error { + // Setting the latest code is to help ensure that only one link is active at a time. + // Set userID -> latest code + if err := e.data.Cache.SetString(ctx, constant.UserLatestEmailCodeCacheKey+userID, code, duration); err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + // Set latest code -> content + if err := e.data.Cache.SetString(ctx, constant.UserEmailCodeCacheKey+code, content, duration); err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil @@ -52,12 +60,39 @@ func (e *emailRepo) SetCode(ctx context.Context, code, content string, duration // VerifyCode verify the code if out of date func (e *emailRepo) VerifyCode(ctx context.Context, code string) (content string, err error) { - content, exist, err := e.data.Cache.GetString(ctx, code) + // Get latest code -> content + codeCacheKey := constant.UserEmailCodeCacheKey + code + content, exist, err := e.data.Cache.GetString(ctx, codeCacheKey) if err != nil { return "", err } if !exist { return "", nil } + + // Delete the code after verification + _ = e.data.Cache.Del(ctx, codeCacheKey) + + // If some email content does not need to verify the latest code is the same as the code, skip it. + // For example, some unsubscribe email content does not need to verify the latest code. + // This link always works before the code is out of date. + if skipValidationLatestCode := gjson.Get(content, "skip_validation_latest_code").Bool(); skipValidationLatestCode { + return content, nil + } + userID := gjson.Get(content, "user_id").String() + + // Get userID -> latest code + latestCode, exist, err := e.data.Cache.GetString(ctx, constant.UserLatestEmailCodeCacheKey+userID) + if err != nil { + return "", err + } + if !exist { + return "", nil + } + + // Check if the latest code is the same as the code, if not, means the code is out of date + if latestCode != code { + return "", nil + } return content, nil } diff --git a/internal/schema/email_template.go b/internal/schema/email_template.go index 9a6be794f..56d3ee4e7 100644 --- a/internal/schema/email_template.go +++ b/internal/schema/email_template.go @@ -42,6 +42,8 @@ type EmailCodeContent struct { NotificationSources []constant.NotificationSource `json:"notification_source,omitempty"` // Used for third-party login account binding BindingKey string `json:"binding_key,omitempty"` + // Skip the validation of the latest code + SkipValidationLatestCode bool `json:"skip_validation_latest_code"` } func (r *EmailCodeContent) ToJSONString() string { diff --git a/internal/service/content/user_service.go b/internal/service/content/user_service.go index a2b330057..11f3bb63b 100644 --- a/internal/service/content/user_service.go +++ b/internal/service/content/user_service.go @@ -227,7 +227,7 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet if err != nil { return err } - go us.emailService.SendAndSaveCode(ctx, req.Email, title, body, code, data.ToJSONString()) + go us.emailService.SendAndSaveCode(ctx, userInfo.ID, req.Email, title, body, code, data.ToJSONString()) return nil } @@ -450,7 +450,7 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo if err != nil { return nil, nil, err } - go us.emailService.SendAndSaveCode(ctx, userInfo.EMail, title, body, code, data.ToJSONString()) + go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString()) roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID) if err != nil { @@ -500,7 +500,7 @@ func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) e if err != nil { return err } - go us.emailService.SendAndSaveCode(ctx, userInfo.EMail, title, body, code, data.ToJSONString()) + go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString()) return nil } @@ -621,7 +621,7 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema. } log.Infof("send email confirmation %s", verifyEmailURL) - go us.emailService.SendAndSaveCode(ctx, req.Email, title, body, code, data.ToJSONString()) + go us.emailService.SendAndSaveCode(ctx, userInfo.ID, req.Email, title, body, code, data.ToJSONString()) return nil, nil } diff --git a/internal/service/export/email_service.go b/internal/service/export/email_service.go index 03d9c182e..ae5d337a2 100644 --- a/internal/service/export/email_service.go +++ b/internal/service/export/email_service.go @@ -51,7 +51,7 @@ type EmailService struct { // EmailRepo email repository type EmailRepo interface { - SetCode(ctx context.Context, code, content string, duration time.Duration) error + SetCode(ctx context.Context, userID, code, content string, duration time.Duration) error VerifyCode(ctx context.Context, code string) (content string, err error) } @@ -89,30 +89,32 @@ func (e *EmailConfig) IsTLS() bool { } // SaveCode save code -func (es *EmailService) SaveCode(ctx context.Context, code, codeContent string) { - err := es.emailRepo.SetCode(ctx, code, codeContent, 10*time.Minute) +func (es *EmailService) SaveCode(ctx context.Context, userID, code, codeContent string) { + err := es.emailRepo.SetCode(ctx, userID, code, codeContent, constant.UserEmailCodeCacheTime) if err != nil { log.Error(err) } } // SendAndSaveCode send email and save code -func (es *EmailService) SendAndSaveCode(ctx context.Context, toEmailAddr, subject, body, code, codeContent string) { - es.Send(ctx, toEmailAddr, subject, body) - err := es.emailRepo.SetCode(ctx, code, codeContent, 10*time.Minute) +func (es *EmailService) SendAndSaveCode(ctx context.Context, userID, toEmailAddr, subject, body, code, codeContent string) { + err := es.emailRepo.SetCode(ctx, userID, code, codeContent, constant.UserEmailCodeCacheTime) if err != nil { log.Error(err) + return } + es.Send(ctx, toEmailAddr, subject, body) } // SendAndSaveCodeWithTime send email and save code func (es *EmailService) SendAndSaveCodeWithTime( - ctx context.Context, toEmailAddr, subject, body, code, codeContent string, duration time.Duration) { - es.Send(ctx, toEmailAddr, subject, body) - err := es.emailRepo.SetCode(ctx, code, codeContent, duration) + ctx context.Context, userID, toEmailAddr, subject, body, code, codeContent string, duration time.Duration) { + err := es.emailRepo.SetCode(ctx, userID, code, codeContent, duration) if err != nil { log.Error(err) + return } + es.Send(ctx, toEmailAddr, subject, body) } // Send email send diff --git a/internal/service/notification/invite_answer_notification.go b/internal/service/notification/invite_answer_notification.go index 1122d052d..503caa51f 100644 --- a/internal/service/notification/invite_answer_notification.go +++ b/internal/service/notification/invite_answer_notification.go @@ -59,8 +59,9 @@ func (ns *ExternalNotificationService) sendInviteAnswerNotificationEmail(ctx con NotificationSources: []constant.NotificationSource{ constant.InboxSource, }, - Email: email, - UserID: userID, + Email: email, + UserID: userID, + SkipValidationLatestCode: true, } // If receiver has set language, use it to send email. @@ -74,5 +75,5 @@ func (ns *ExternalNotificationService) sendInviteAnswerNotificationEmail(ctx con } ns.emailService.SendAndSaveCodeWithTime( - ctx, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour) + ctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour) } diff --git a/internal/service/notification/new_answer_notification.go b/internal/service/notification/new_answer_notification.go index 4a43192f8..1e08b455f 100644 --- a/internal/service/notification/new_answer_notification.go +++ b/internal/service/notification/new_answer_notification.go @@ -59,8 +59,9 @@ func (ns *ExternalNotificationService) sendNewAnswerNotificationEmail(ctx contex NotificationSources: []constant.NotificationSource{ constant.InboxSource, }, - Email: email, - UserID: userID, + Email: email, + UserID: userID, + SkipValidationLatestCode: true, } // If receiver has set language, use it to send email. @@ -74,5 +75,5 @@ func (ns *ExternalNotificationService) sendNewAnswerNotificationEmail(ctx contex } ns.emailService.SendAndSaveCodeWithTime( - ctx, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour) + ctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour) } diff --git a/internal/service/notification/new_comment_notification.go b/internal/service/notification/new_comment_notification.go index 048c66f1a..5e2dce7b6 100644 --- a/internal/service/notification/new_comment_notification.go +++ b/internal/service/notification/new_comment_notification.go @@ -59,8 +59,9 @@ func (ns *ExternalNotificationService) sendNewCommentNotificationEmail(ctx conte NotificationSources: []constant.NotificationSource{ constant.InboxSource, }, - Email: email, - UserID: userID, + Email: email, + UserID: userID, + SkipValidationLatestCode: true, } // If receiver has set language, use it to send email. if len(lang) > 0 { @@ -73,5 +74,5 @@ func (ns *ExternalNotificationService) sendNewCommentNotificationEmail(ctx conte } ns.emailService.SendAndSaveCodeWithTime( - ctx, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour) + ctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour) } diff --git a/internal/service/notification/new_question_notification.go b/internal/service/notification/new_question_notification.go index d41733974..08a774884 100644 --- a/internal/service/notification/new_question_notification.go +++ b/internal/service/notification/new_question_notification.go @@ -189,9 +189,10 @@ func (ns *ExternalNotificationService) sendNewQuestionNotificationEmail(ctx cont constant.AllNewQuestionSource, constant.AllNewQuestionForFollowingTagsSource, }, + SkipValidationLatestCode: true, } ns.emailService.SendAndSaveCodeWithTime( - ctx, userInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour) + ctx, userInfo.ID, userInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour) } func (ns *ExternalNotificationService) syncNewQuestionNotificationToPlugin(ctx context.Context, diff --git a/internal/service/siteinfo/siteinfo_service.go b/internal/service/siteinfo/siteinfo_service.go index f82a7c7fb..74027ec27 100644 --- a/internal/service/siteinfo/siteinfo_service.go +++ b/internal/service/siteinfo/siteinfo_service.go @@ -274,7 +274,7 @@ func (s *SiteInfoService) UpdateSMTPConfig(ctx context.Context, req *schema.Upda if err != nil { return err } - go s.emailService.SendAndSaveCode(ctx, req.TestEmailRecipient, title, body, "", "") + go s.emailService.Send(ctx, req.TestEmailRecipient, title, body) } return nil } diff --git a/internal/service/user_admin/user_backyard.go b/internal/service/user_admin/user_backyard.go index 6ce98f997..72b9c2555 100644 --- a/internal/service/user_admin/user_backyard.go +++ b/internal/service/user_admin/user_backyard.go @@ -513,7 +513,7 @@ func (us *UserAdminService) setUserRoleInfo(ctx context.Context, resp []*schema. func (us *UserAdminService) GetUserActivation(ctx context.Context, req *schema.GetUserActivationReq) ( resp *schema.GetUserActivationResp, err error) { - user, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) + userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) if err != nil { return nil, err } @@ -527,11 +527,11 @@ func (us *UserAdminService) GetUserActivation(ctx context.Context, req *schema.G } data := &schema.EmailCodeContent{ - Email: user.EMail, - UserID: user.ID, + Email: userInfo.EMail, + UserID: userInfo.ID, } code := uuid.NewString() - us.emailService.SaveCode(ctx, code, data.ToJSONString()) + us.emailService.SaveCode(ctx, userInfo.ID, code, data.ToJSONString()) resp = &schema.GetUserActivationResp{ ActivationURL: fmt.Sprintf("%s/users/account-activation?code=%s", general.SiteUrl, code), } @@ -540,7 +540,7 @@ func (us *UserAdminService) GetUserActivation(ctx context.Context, req *schema.G // SendUserActivation send user activation email func (us *UserAdminService) SendUserActivation(ctx context.Context, req *schema.SendUserActivationReq) (err error) { - user, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) + userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) if err != nil { return err } @@ -554,17 +554,16 @@ func (us *UserAdminService) SendUserActivation(ctx context.Context, req *schema. } data := &schema.EmailCodeContent{ - Email: user.EMail, - UserID: user.ID, + Email: userInfo.EMail, + UserID: userInfo.ID, } code := uuid.NewString() - us.emailService.SaveCode(ctx, code, data.ToJSONString()) verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", general.SiteUrl, code) title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) if err != nil { return err } - go us.emailService.SendAndSaveCode(ctx, user.EMail, title, body, code, data.ToJSONString()) + go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString()) return nil } diff --git a/internal/service/user_external_login/user_external_login_service.go b/internal/service/user_external_login/user_external_login_service.go index 0923908fb..3b70a535f 100644 --- a/internal/service/user_external_login/user_external_login_service.go +++ b/internal/service/user_external_login/user_external_login_service.go @@ -328,7 +328,7 @@ func (us *UserExternalLoginService) ExternalLoginBindingUserSendEmail( if err != nil { return nil, err } - go us.emailService.SendAndSaveCode(ctx, userInfo.EMail, title, body, code, data.ToJSONString()) + go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString()) return resp, nil }