Skip to content

Commit

Permalink
feat(user): ensure that only one link is active at a time
Browse files Browse the repository at this point in the history
  • Loading branch information
LinkinStars committed Jul 22, 2024
1 parent f014602 commit 2820efc
Show file tree
Hide file tree
Showing 12 changed files with 83 additions and 38 deletions.
3 changes: 3 additions & 0 deletions internal/base/constant/cache_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"
Expand Down
43 changes: 39 additions & 4 deletions internal/repo/export/email_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -42,22 +44,55 @@ 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
}

// 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
}
2 changes: 2 additions & 0 deletions internal/schema/email_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 4 additions & 4 deletions internal/service/content/user_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down
20 changes: 11 additions & 9 deletions internal/service/export/email_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions internal/service/notification/invite_answer_notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
}
7 changes: 4 additions & 3 deletions internal/service/notification/new_answer_notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
}
7 changes: 4 additions & 3 deletions internal/service/notification/new_comment_notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
3 changes: 2 additions & 1 deletion internal/service/notification/new_question_notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion internal/service/siteinfo/siteinfo_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
17 changes: 8 additions & 9 deletions internal/service/user_admin/user_backyard.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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),
}
Expand All @@ -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
}
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down

0 comments on commit 2820efc

Please sign in to comment.