Skip to content

Commit 2820efc

Browse files
committed
feat(user): ensure that only one link is active at a time
1 parent f014602 commit 2820efc

12 files changed

+83
-38
lines changed

internal/base/constant/cache_key.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ const (
3232
AdminTokenCacheKey = "answer:admin:token:"
3333
AdminTokenCacheTime = 7 * 24 * time.Hour
3434
UserTokenMappingCacheKey = "answer:user-token:mapping:"
35+
UserEmailCodeCacheKey = "answer:user:email-code:"
36+
UserEmailCodeCacheTime = 10 * time.Minute
37+
UserLatestEmailCodeCacheKey = "answer:user-id:email-code:"
3538
SiteInfoCacheKey = "answer:site-info:"
3639
SiteInfoCacheTime = 1 * time.Hour
3740
ConfigID2KEYCacheKeyPrefix = "answer:config:id:"

internal/repo/export/email_repo.go

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ package export
2121

2222
import (
2323
"context"
24+
"github.com/apache/incubator-answer/internal/base/constant"
25+
"github.com/tidwall/gjson"
2426
"time"
2527

2628
"github.com/apache/incubator-answer/internal/base/data"
@@ -42,22 +44,55 @@ func NewEmailRepo(data *data.Data) export.EmailRepo {
4244
}
4345

4446
// SetCode The email code is used to verify that the link in the message is out of date
45-
func (e *emailRepo) SetCode(ctx context.Context, code, content string, duration time.Duration) error {
46-
err := e.data.Cache.SetString(ctx, code, content, duration)
47-
if err != nil {
47+
func (e *emailRepo) SetCode(ctx context.Context, userID, code, content string, duration time.Duration) error {
48+
// Setting the latest code is to help ensure that only one link is active at a time.
49+
// Set userID -> latest code
50+
if err := e.data.Cache.SetString(ctx, constant.UserLatestEmailCodeCacheKey+userID, code, duration); err != nil {
51+
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
52+
}
53+
54+
// Set latest code -> content
55+
if err := e.data.Cache.SetString(ctx, constant.UserEmailCodeCacheKey+code, content, duration); err != nil {
4856
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
4957
}
5058
return nil
5159
}
5260

5361
// VerifyCode verify the code if out of date
5462
func (e *emailRepo) VerifyCode(ctx context.Context, code string) (content string, err error) {
55-
content, exist, err := e.data.Cache.GetString(ctx, code)
63+
// Get latest code -> content
64+
codeCacheKey := constant.UserEmailCodeCacheKey + code
65+
content, exist, err := e.data.Cache.GetString(ctx, codeCacheKey)
5666
if err != nil {
5767
return "", err
5868
}
5969
if !exist {
6070
return "", nil
6171
}
72+
73+
// Delete the code after verification
74+
_ = e.data.Cache.Del(ctx, codeCacheKey)
75+
76+
// If some email content does not need to verify the latest code is the same as the code, skip it.
77+
// For example, some unsubscribe email content does not need to verify the latest code.
78+
// This link always works before the code is out of date.
79+
if skipValidationLatestCode := gjson.Get(content, "skip_validation_latest_code").Bool(); skipValidationLatestCode {
80+
return content, nil
81+
}
82+
userID := gjson.Get(content, "user_id").String()
83+
84+
// Get userID -> latest code
85+
latestCode, exist, err := e.data.Cache.GetString(ctx, constant.UserLatestEmailCodeCacheKey+userID)
86+
if err != nil {
87+
return "", err
88+
}
89+
if !exist {
90+
return "", nil
91+
}
92+
93+
// Check if the latest code is the same as the code, if not, means the code is out of date
94+
if latestCode != code {
95+
return "", nil
96+
}
6297
return content, nil
6398
}

internal/schema/email_template.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ type EmailCodeContent struct {
4242
NotificationSources []constant.NotificationSource `json:"notification_source,omitempty"`
4343
// Used for third-party login account binding
4444
BindingKey string `json:"binding_key,omitempty"`
45+
// Skip the validation of the latest code
46+
SkipValidationLatestCode bool `json:"skip_validation_latest_code"`
4547
}
4648

4749
func (r *EmailCodeContent) ToJSONString() string {

internal/service/content/user_service.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet
227227
if err != nil {
228228
return err
229229
}
230-
go us.emailService.SendAndSaveCode(ctx, req.Email, title, body, code, data.ToJSONString())
230+
go us.emailService.SendAndSaveCode(ctx, userInfo.ID, req.Email, title, body, code, data.ToJSONString())
231231
return nil
232232
}
233233

@@ -450,7 +450,7 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
450450
if err != nil {
451451
return nil, nil, err
452452
}
453-
go us.emailService.SendAndSaveCode(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
453+
go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString())
454454

455455
roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
456456
if err != nil {
@@ -500,7 +500,7 @@ func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) e
500500
if err != nil {
501501
return err
502502
}
503-
go us.emailService.SendAndSaveCode(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
503+
go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString())
504504
return nil
505505
}
506506

@@ -621,7 +621,7 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
621621
}
622622
log.Infof("send email confirmation %s", verifyEmailURL)
623623

624-
go us.emailService.SendAndSaveCode(ctx, req.Email, title, body, code, data.ToJSONString())
624+
go us.emailService.SendAndSaveCode(ctx, userInfo.ID, req.Email, title, body, code, data.ToJSONString())
625625
return nil, nil
626626
}
627627

internal/service/export/email_service.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ type EmailService struct {
5151

5252
// EmailRepo email repository
5353
type EmailRepo interface {
54-
SetCode(ctx context.Context, code, content string, duration time.Duration) error
54+
SetCode(ctx context.Context, userID, code, content string, duration time.Duration) error
5555
VerifyCode(ctx context.Context, code string) (content string, err error)
5656
}
5757

@@ -89,30 +89,32 @@ func (e *EmailConfig) IsTLS() bool {
8989
}
9090

9191
// SaveCode save code
92-
func (es *EmailService) SaveCode(ctx context.Context, code, codeContent string) {
93-
err := es.emailRepo.SetCode(ctx, code, codeContent, 10*time.Minute)
92+
func (es *EmailService) SaveCode(ctx context.Context, userID, code, codeContent string) {
93+
err := es.emailRepo.SetCode(ctx, userID, code, codeContent, constant.UserEmailCodeCacheTime)
9494
if err != nil {
9595
log.Error(err)
9696
}
9797
}
9898

9999
// SendAndSaveCode send email and save code
100-
func (es *EmailService) SendAndSaveCode(ctx context.Context, toEmailAddr, subject, body, code, codeContent string) {
101-
es.Send(ctx, toEmailAddr, subject, body)
102-
err := es.emailRepo.SetCode(ctx, code, codeContent, 10*time.Minute)
100+
func (es *EmailService) SendAndSaveCode(ctx context.Context, userID, toEmailAddr, subject, body, code, codeContent string) {
101+
err := es.emailRepo.SetCode(ctx, userID, code, codeContent, constant.UserEmailCodeCacheTime)
103102
if err != nil {
104103
log.Error(err)
104+
return
105105
}
106+
es.Send(ctx, toEmailAddr, subject, body)
106107
}
107108

108109
// SendAndSaveCodeWithTime send email and save code
109110
func (es *EmailService) SendAndSaveCodeWithTime(
110-
ctx context.Context, toEmailAddr, subject, body, code, codeContent string, duration time.Duration) {
111-
es.Send(ctx, toEmailAddr, subject, body)
112-
err := es.emailRepo.SetCode(ctx, code, codeContent, duration)
111+
ctx context.Context, userID, toEmailAddr, subject, body, code, codeContent string, duration time.Duration) {
112+
err := es.emailRepo.SetCode(ctx, userID, code, codeContent, duration)
113113
if err != nil {
114114
log.Error(err)
115+
return
115116
}
117+
es.Send(ctx, toEmailAddr, subject, body)
116118
}
117119

118120
// Send email send

internal/service/notification/invite_answer_notification.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ func (ns *ExternalNotificationService) sendInviteAnswerNotificationEmail(ctx con
5959
NotificationSources: []constant.NotificationSource{
6060
constant.InboxSource,
6161
},
62-
Email: email,
63-
UserID: userID,
62+
Email: email,
63+
UserID: userID,
64+
SkipValidationLatestCode: true,
6465
}
6566

6667
// If receiver has set language, use it to send email.
@@ -74,5 +75,5 @@ func (ns *ExternalNotificationService) sendInviteAnswerNotificationEmail(ctx con
7475
}
7576

7677
ns.emailService.SendAndSaveCodeWithTime(
77-
ctx, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
78+
ctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
7879
}

internal/service/notification/new_answer_notification.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ func (ns *ExternalNotificationService) sendNewAnswerNotificationEmail(ctx contex
5959
NotificationSources: []constant.NotificationSource{
6060
constant.InboxSource,
6161
},
62-
Email: email,
63-
UserID: userID,
62+
Email: email,
63+
UserID: userID,
64+
SkipValidationLatestCode: true,
6465
}
6566

6667
// If receiver has set language, use it to send email.
@@ -74,5 +75,5 @@ func (ns *ExternalNotificationService) sendNewAnswerNotificationEmail(ctx contex
7475
}
7576

7677
ns.emailService.SendAndSaveCodeWithTime(
77-
ctx, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
78+
ctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
7879
}

internal/service/notification/new_comment_notification.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ func (ns *ExternalNotificationService) sendNewCommentNotificationEmail(ctx conte
5959
NotificationSources: []constant.NotificationSource{
6060
constant.InboxSource,
6161
},
62-
Email: email,
63-
UserID: userID,
62+
Email: email,
63+
UserID: userID,
64+
SkipValidationLatestCode: true,
6465
}
6566
// If receiver has set language, use it to send email.
6667
if len(lang) > 0 {
@@ -73,5 +74,5 @@ func (ns *ExternalNotificationService) sendNewCommentNotificationEmail(ctx conte
7374
}
7475

7576
ns.emailService.SendAndSaveCodeWithTime(
76-
ctx, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
77+
ctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
7778
}

internal/service/notification/new_question_notification.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,10 @@ func (ns *ExternalNotificationService) sendNewQuestionNotificationEmail(ctx cont
189189
constant.AllNewQuestionSource,
190190
constant.AllNewQuestionForFollowingTagsSource,
191191
},
192+
SkipValidationLatestCode: true,
192193
}
193194
ns.emailService.SendAndSaveCodeWithTime(
194-
ctx, userInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
195+
ctx, userInfo.ID, userInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
195196
}
196197

197198
func (ns *ExternalNotificationService) syncNewQuestionNotificationToPlugin(ctx context.Context,

internal/service/siteinfo/siteinfo_service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ func (s *SiteInfoService) UpdateSMTPConfig(ctx context.Context, req *schema.Upda
274274
if err != nil {
275275
return err
276276
}
277-
go s.emailService.SendAndSaveCode(ctx, req.TestEmailRecipient, title, body, "", "")
277+
go s.emailService.Send(ctx, req.TestEmailRecipient, title, body)
278278
}
279279
return nil
280280
}

internal/service/user_admin/user_backyard.go

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ func (us *UserAdminService) setUserRoleInfo(ctx context.Context, resp []*schema.
513513

514514
func (us *UserAdminService) GetUserActivation(ctx context.Context, req *schema.GetUserActivationReq) (
515515
resp *schema.GetUserActivationResp, err error) {
516-
user, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)
516+
userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)
517517
if err != nil {
518518
return nil, err
519519
}
@@ -527,11 +527,11 @@ func (us *UserAdminService) GetUserActivation(ctx context.Context, req *schema.G
527527
}
528528

529529
data := &schema.EmailCodeContent{
530-
Email: user.EMail,
531-
UserID: user.ID,
530+
Email: userInfo.EMail,
531+
UserID: userInfo.ID,
532532
}
533533
code := uuid.NewString()
534-
us.emailService.SaveCode(ctx, code, data.ToJSONString())
534+
us.emailService.SaveCode(ctx, userInfo.ID, code, data.ToJSONString())
535535
resp = &schema.GetUserActivationResp{
536536
ActivationURL: fmt.Sprintf("%s/users/account-activation?code=%s", general.SiteUrl, code),
537537
}
@@ -540,7 +540,7 @@ func (us *UserAdminService) GetUserActivation(ctx context.Context, req *schema.G
540540

541541
// SendUserActivation send user activation email
542542
func (us *UserAdminService) SendUserActivation(ctx context.Context, req *schema.SendUserActivationReq) (err error) {
543-
user, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)
543+
userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)
544544
if err != nil {
545545
return err
546546
}
@@ -554,17 +554,16 @@ func (us *UserAdminService) SendUserActivation(ctx context.Context, req *schema.
554554
}
555555

556556
data := &schema.EmailCodeContent{
557-
Email: user.EMail,
558-
UserID: user.ID,
557+
Email: userInfo.EMail,
558+
UserID: userInfo.ID,
559559
}
560560
code := uuid.NewString()
561-
us.emailService.SaveCode(ctx, code, data.ToJSONString())
562561

563562
verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", general.SiteUrl, code)
564563
title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL)
565564
if err != nil {
566565
return err
567566
}
568-
go us.emailService.SendAndSaveCode(ctx, user.EMail, title, body, code, data.ToJSONString())
567+
go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString())
569568
return nil
570569
}

internal/service/user_external_login/user_external_login_service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ func (us *UserExternalLoginService) ExternalLoginBindingUserSendEmail(
328328
if err != nil {
329329
return nil, err
330330
}
331-
go us.emailService.SendAndSaveCode(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
331+
go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString())
332332
return resp, nil
333333
}
334334

0 commit comments

Comments
 (0)