Skip to content

Commit 7a9844a

Browse files
authored
Merge pull request #271 from jinxiu89/master
代码提交:新增找回密码功能
2 parents 4e2451e + 3d6bebf commit 7a9844a

15 files changed

Lines changed: 1162 additions & 343 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package api
2+
3+
import (
4+
"bbs-go/internal/controllers/render"
5+
"bbs-go/internal/pkg/locales"
6+
"bbs-go/internal/pkg/validate"
7+
"strings"
8+
9+
"github.com/kataras/iris/v12"
10+
"github.com/mlogclub/simple/common/strs"
11+
"github.com/mlogclub/simple/web"
12+
13+
"bbs-go/internal/services"
14+
)
15+
16+
type ForgetPasswordController struct {
17+
Ctx iris.Context
18+
}
19+
20+
// 定义用于JSON请求的结构体
21+
type SendEmailRequest struct {
22+
Email string `json:"email"`
23+
}
24+
25+
type ResetPasswordRequest struct {
26+
Token string `json:"token"`
27+
Password string `json:"password"`
28+
RePassword string `json:"rePassword"`
29+
}
30+
31+
// 发送密码重置邮件
32+
func (c *ForgetPasswordController) PostSendEmail() *web.JsonResult {
33+
var email string
34+
35+
// 检查请求是否为JSON格式 (注意包含charset)
36+
contentType := c.Ctx.GetHeader("Content-Type")
37+
if strings.Contains(contentType, "application/json") {
38+
// JSON请求格式
39+
var req SendEmailRequest
40+
if err := c.Ctx.ReadJSON(&req); err != nil {
41+
return web.JsonError(err)
42+
}
43+
email = strings.TrimSpace(req.Email)
44+
} else {
45+
// 表单请求格式
46+
email = strings.TrimSpace(c.Ctx.PostValueTrim("email"))
47+
}
48+
49+
if strs.IsBlank(email) {
50+
return web.JsonErrorMsg(locales.Get("errors.email_empty"))
51+
}
52+
if err := validate.IsEmail(email); err != nil {
53+
// 将验证错误消息替换为多语言化的消息
54+
return web.JsonErrorMsg(locales.Get("errors.email_invalid"))
55+
}
56+
57+
err := services.UserService.SendPasswordResetEmail(email)
58+
if err != nil {
59+
return web.JsonError(err)
60+
}
61+
return web.JsonSuccess()
62+
}
63+
64+
// 重置密码
65+
func (c *ForgetPasswordController) PostResetPassword() *web.JsonResult {
66+
var (
67+
token string
68+
password string
69+
rePassword string
70+
)
71+
72+
// 检查请求是否为JSON格式 (注意包含charset)
73+
contentType := c.Ctx.GetHeader("Content-Type")
74+
if strings.Contains(contentType, "application/json") {
75+
// JSON请求格式
76+
var req ResetPasswordRequest
77+
if err := c.Ctx.ReadJSON(&req); err != nil {
78+
return web.JsonError(err)
79+
}
80+
token = req.Token
81+
password = req.Password
82+
rePassword = req.RePassword
83+
} else {
84+
// 表单请求格式
85+
token = c.Ctx.PostValueTrim("token")
86+
password = c.Ctx.PostValueTrim("password")
87+
rePassword = c.Ctx.PostValueTrim("rePassword")
88+
}
89+
90+
if strs.IsBlank(token) {
91+
return web.JsonErrorMsg(locales.Get("errors.reset_link_invalid"))
92+
}
93+
if strs.IsBlank(password) {
94+
return web.JsonErrorMsg(locales.Get("errors.password_empty"))
95+
}
96+
if strs.IsBlank(rePassword) {
97+
return web.JsonErrorMsg(locales.Get("errors.confirm_password_empty"))
98+
}
99+
100+
err := services.UserService.ResetPassword(token, password, rePassword)
101+
if err != nil {
102+
return web.JsonError(err)
103+
}
104+
105+
// 重置成功后,可能需要返回登录信息
106+
user, err := services.UserService.GetUserByPasswordResetToken(token)
107+
if err != nil {
108+
return web.JsonError(err)
109+
}
110+
111+
// 标记重置链接已使用
112+
err = services.UserService.MarkPasswordResetTokenUsed(token)
113+
if err != nil {
114+
return web.JsonError(err)
115+
}
116+
117+
return render.BuildLoginSuccess(c.Ctx, user, "")
118+
}

server/internal/server/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ func NewServer() {
7676
m.Party("/fans").Handle(new(api.FansController))
7777
m.Party("/user-report").Handle(new(api.UserReportController))
7878
m.Party("/install").Handle(new(api.InstallController))
79+
m.Party("/forget-password").Handle(new(api.ForgetPasswordController))
7980
})
8081

8182
// admin

server/internal/services/user_service.go

Lines changed: 155 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package services
22

33
import (
4+
"bbs-go/internal/models"
45
"bbs-go/internal/models/constants"
56
"bbs-go/internal/models/dto"
67
"bbs-go/internal/pkg/bbsurls"
@@ -24,14 +25,15 @@ import (
2425
"gorm.io/gorm"
2526

2627
"bbs-go/internal/cache"
27-
28-
"bbs-go/internal/models"
2928
"bbs-go/internal/repositories"
3029
)
3130

3231
// 邮箱验证邮件有效期(小时)
3332
const emailVerifyExpireHour = 24
3433

34+
// 密码重置邮件有效期(小时)
35+
const passwordResetExpireHour = 1
36+
3537
var UserService = newUserService()
3638

3739
func newUserService() *userService {
@@ -206,13 +208,24 @@ func (s *userService) SignUp(username, email, nickname, password, rePassword str
206208
// 验证密码
207209
err := validate.IsValidPassword(password, rePassword)
208210
if err != nil {
209-
return nil, err
211+
// 根据不同的验证错误返回对应的多语言化消息
212+
if strings.Contains(err.Error(), "密码过于简单") {
213+
return nil, errors.New(locales.Get("errors.password_simple"))
214+
} else if strings.Contains(err.Error(), "密码长度不能超过128") {
215+
return nil, errors.New(locales.Get("errors.password_length_invalid"))
216+
} else if strings.Contains(err.Error(), "两次输入密码不匹配") {
217+
return nil, errors.New(locales.Get("errors.password_not_match"))
218+
} else if strings.Contains(err.Error(), "请输入密码") {
219+
return nil, errors.New(locales.Get("errors.password_empty"))
220+
}
221+
// 默认返回密码无效的错误
222+
return nil, errors.New(locales.Get("errors.password_empty"))
210223
}
211224

212225
// 验证邮箱
213226
if len(email) > 0 {
214227
if err := validate.IsEmail(email); err != nil {
215-
return nil, err
228+
return nil, errors.New(locales.Get("errors.email_invalid"))
216229
}
217230
if s.GetByEmail(email) != nil {
218231
return nil, errors.New("邮箱:" + email + " 已被占用")
@@ -353,7 +366,7 @@ func (s *userService) SetUsername(userId int64, username string) error {
353366
func (s *userService) SetEmail(userId int64, email string) error {
354367
email = strings.TrimSpace(email)
355368
if err := validate.IsEmail(email); err != nil {
356-
return err
369+
return errors.New(locales.Get("errors.email_invalid"))
357370
}
358371
user := s.Get(userId)
359372
if user == nil {
@@ -366,16 +379,31 @@ func (s *userService) SetEmail(userId int64, email string) error {
366379
if s.isEmailExists(email) {
367380
return errors.New("邮箱:" + email + " 已被占用")
368381
}
382+
369383
return s.Updates(userId, map[string]interface{}{
370-
"email": email,
384+
385+
"email": email,
386+
371387
"email_verified": false,
372388
})
389+
373390
}
374391

375392
// SetPassword 设置密码
376393
func (s *userService) SetPassword(userId int64, password, rePassword string) error {
377394
if err := validate.IsValidPassword(password, rePassword); err != nil {
378-
return err
395+
// 根据不同的验证错误返回对应的多语言化消息
396+
if strings.Contains(err.Error(), "密码过于简单") {
397+
return errors.New(locales.Get("errors.password_simple"))
398+
} else if strings.Contains(err.Error(), "密码长度不能超过128") {
399+
return errors.New(locales.Get("errors.password_length_invalid"))
400+
} else if strings.Contains(err.Error(), "两次输入密码不匹配") {
401+
return errors.New(locales.Get("errors.password_not_match"))
402+
} else if strings.Contains(err.Error(), "请输入密码") {
403+
return errors.New(locales.Get("errors.password_empty"))
404+
}
405+
// 默认返回密码无效的错误
406+
return errors.New(locales.Get("errors.password_empty"))
379407
}
380408
user := s.Get(userId)
381409
if len(user.Password) > 0 {
@@ -388,7 +416,18 @@ func (s *userService) SetPassword(userId int64, password, rePassword string) err
388416
// UpdatePassword 修改密码
389417
func (s *userService) UpdatePassword(userId int64, oldPassword, password, rePassword string) error {
390418
if err := validate.IsValidPassword(password, rePassword); err != nil {
391-
return err
419+
// 根据不同的验证错误返回对应的多语言化消息
420+
if strings.Contains(err.Error(), "密码过于简单") {
421+
return errors.New(locales.Get("errors.password_simple"))
422+
} else if strings.Contains(err.Error(), "密码长度不能超过128") {
423+
return errors.New(locales.Get("errors.password_length_invalid"))
424+
} else if strings.Contains(err.Error(), "两次输入密码不匹配") {
425+
return errors.New(locales.Get("errors.password_not_match"))
426+
} else if strings.Contains(err.Error(), "请输入密码") {
427+
return errors.New(locales.Get("errors.password_empty"))
428+
}
429+
// 默认返回密码无效的错误
430+
return errors.New(locales.Get("errors.password_empty"))
392431
}
393432
user := s.Get(userId)
394433

@@ -400,7 +439,8 @@ func (s *userService) UpdatePassword(userId int64, oldPassword, password, rePass
400439
return errors.New("旧密码验证失败")
401440
}
402441

403-
return s.UpdateColumn(userId, "password", passwd.EncodePassword(password))
442+
password = passwd.EncodePassword(password)
443+
return s.UpdateColumn(userId, "password", password)
404444
}
405445

406446
// IncrTopicCount topic_count + 1
@@ -451,7 +491,7 @@ func (s *userService) SendEmailVerifyEmail(userId int64) error {
451491
return errors.New(locales.Get("user.email_verified"))
452492
}
453493
if err := validate.IsEmail(user.Email.String); err != nil {
454-
return err
494+
return errors.New(locales.Get("errors.email_invalid"))
455495
}
456496
// 如果设置了邮箱白名单
457497
if emailWhitelist := SysConfigService.GetEmailWhitelist(); len(emailWhitelist) > 0 {
@@ -633,3 +673,108 @@ func (s *userService) addScore(userId int64, score int, sourceType, sourceId, de
633673
}
634674
return err
635675
}
676+
677+
// SendPasswordResetEmail 发送密码重置邮件
678+
func (s *userService) SendPasswordResetEmail(emailStr string) error {
679+
user := s.GetByEmail(emailStr)
680+
if user == nil {
681+
return errors.New(locales.Get("errors.email_not_registered"))
682+
}
683+
684+
// 生成重置令牌
685+
token := strs.UUID()
686+
resetUrl := bbsurls.AbsUrl("/user/reset-password?token=" + token)
687+
688+
// 准备邮件内容
689+
siteTitle := cache.SysConfigCache.GetStr(constants.SysConfigSiteTitle)
690+
subject := locales.Getf("user.password_reset_title", siteTitle)
691+
title := locales.Getf("user.password_reset_title", siteTitle)
692+
content := locales.Getf("user.password_reset_content", siteTitle, passwordResetExpireHour, resetUrl)
693+
link := &dto.ActionLink{Title: locales.Get("user.password_reset_link"), Url: resetUrl}
694+
695+
return sqls.DB().Transaction(func(tx *gorm.DB) error {
696+
// 保存重置令牌到数据库
697+
emailCode := &models.EmailCode{
698+
UserId: user.Id,
699+
Email: emailStr,
700+
Token: token,
701+
Title: title,
702+
Content: content,
703+
Used: false,
704+
CreateTime: dates.NowTimestamp(),
705+
}
706+
if err := repositories.EmailCodeRepository.Create(tx, emailCode); err != nil {
707+
return err
708+
}
709+
710+
// 发送邮件
711+
if err := email.SendTemplateEmail(nil, emailStr, subject, title, content, "", link); err != nil {
712+
return err
713+
}
714+
715+
return nil
716+
})
717+
}
718+
719+
// ResetPassword 重置密码
720+
func (s *userService) ResetPassword(token, password, rePassword string) error {
721+
// 验证密码
722+
if err := validate.IsValidPassword(password, rePassword); err != nil {
723+
// 根据不同的验证错误返回对应的多语言化消息
724+
if strings.Contains(err.Error(), "密码过于简单") {
725+
return errors.New(locales.Get("errors.password_simple"))
726+
} else if strings.Contains(err.Error(), "密码长度不能超过128") {
727+
return errors.New(locales.Get("errors.password_length_invalid"))
728+
} else if strings.Contains(err.Error(), "两次输入密码不匹配") {
729+
return errors.New(locales.Get("errors.password_not_match"))
730+
} else if strings.Contains(err.Error(), "请输入密码") {
731+
return errors.New(locales.Get("errors.password_empty"))
732+
}
733+
// 默认返回密码无效的错误
734+
return errors.New(locales.Get("errors.password_empty"))
735+
}
736+
737+
// 查找重置令牌
738+
emailCode := EmailCodeService.FindOne(sqls.NewCnd().Eq("token", token))
739+
if emailCode == nil || emailCode.Used {
740+
return errors.New(locales.Get("errors.reset_link_invalid"))
741+
}
742+
743+
// 检查令牌是否过期
744+
if dates.FromTimestamp(emailCode.CreateTime).Add(time.Hour * time.Duration(passwordResetExpireHour)).Before(time.Now()) {
745+
return errors.New(locales.Get("errors.reset_link_expired"))
746+
}
747+
748+
// 更新用户密码
749+
encodedPassword := passwd.EncodePassword(password)
750+
if err := s.UpdateColumn(emailCode.UserId, "password", encodedPassword); err != nil {
751+
return err
752+
}
753+
754+
return nil
755+
}
756+
757+
// GetUserByPasswordResetToken 根据密码重置令牌获取用户
758+
func (s *userService) GetUserByPasswordResetToken(token string) (*models.User, error) {
759+
emailCode := EmailCodeService.FindOne(sqls.NewCnd().Eq("token", token))
760+
if emailCode == nil || emailCode.Used {
761+
return nil, errors.New(locales.Get("errors.reset_link_invalid"))
762+
}
763+
764+
user := s.Get(emailCode.UserId)
765+
if user == nil {
766+
return nil, errors.New(locales.Get("errors.user_not_found"))
767+
}
768+
769+
return user, nil
770+
}
771+
772+
// MarkPasswordResetTokenUsed 标记密码重置令牌已使用
773+
func (s *userService) MarkPasswordResetTokenUsed(token string) error {
774+
emailCode := EmailCodeService.FindOne(sqls.NewCnd().Eq("token", token))
775+
if emailCode == nil {
776+
return errors.New(locales.Get("errors.reset_link_invalid"))
777+
}
778+
779+
return repositories.EmailCodeRepository.UpdateColumn(sqls.DB(), emailCode.Id, "used", true)
780+
}

0 commit comments

Comments
 (0)