diff --git a/pkg/email/template.go b/pkg/email/template.go index ab327a7..451e27d 100644 --- a/pkg/email/template.go +++ b/pkg/email/template.go @@ -33,3 +33,17 @@ func NewActivationEmail(userName, activateURL string) (string, string) { return fmt.Sprintf("【%s】注册激活", options["siteName"]), util.Replace(replace, options["mail_activation_template"]) } + +// NewResetEmail 新建重设密码邮件 +func NewResetEmail(userName, resetURL string) (string, string) { + options := model.GetSettingByNames("siteName", "siteURL", "siteTitle", "mail_reset_pwd_template") + replace := map[string]string{ + "{siteTitle}": options["siteName"], + "{userName}": userName, + "{resetUrl}": resetURL, + "{siteUrl}": options["siteURL"], + "{siteSecTitle}": options["siteTitle"], + } + return fmt.Sprintf("【%s】密码重置", options["siteName"]), + util.Replace(replace, options["mail_reset_pwd_template"]) +} diff --git a/routers/controllers/user.go b/routers/controllers/user.go index 8300018..f378653 100644 --- a/routers/controllers/user.go +++ b/routers/controllers/user.go @@ -151,6 +151,28 @@ func User2FALogin(c *gin.Context) { } } +// UserSendReset 发送密码重设邮件 +func UserSendReset(c *gin.Context) { + var service user.UserResetEmailService + if err := c.ShouldBindJSON(&service); err == nil { + res := service.Reset(c) + c.JSON(200, res) + } else { + c.JSON(200, ErrorResponse(err)) + } +} + +// UserReset 重设密码 +func UserReset(c *gin.Context) { + var service user.UserResetService + if err := c.ShouldBindJSON(&service); err == nil { + res := service.Reset(c) + c.JSON(200, res) + } else { + c.JSON(200, ErrorResponse(err)) + } +} + // UserActivate 用户激活 func UserActivate(c *gin.Context) { var service user.SettingService diff --git a/routers/router.go b/routers/router.go index ffc7691..55f9e45 100644 --- a/routers/router.go +++ b/routers/router.go @@ -104,9 +104,16 @@ func InitMasterRouter() *gin.Engine { // 用户登录 user.POST("session", controllers.UserLogin) // 用户注册 - user.POST("", middleware.IsFunctionEnabled("register_enabled"), controllers.UserRegister) + user.POST("", + middleware.IsFunctionEnabled("register_enabled"), + controllers.UserRegister, + ) // 用二步验证户登录 user.POST("2fa", controllers.User2FALogin) + // 发送密码重设邮件 + user.POST("reset", controllers.UserSendReset) + // 通过邮件里的链接重设密码 + user.PATCH("reset", controllers.UserReset) // 邮件激活 user.GET("activate/:id", middleware.SignRequired(), @@ -118,11 +125,13 @@ func InitMasterRouter() *gin.Engine { // WebAuthn登陆初始化 user.GET("authn/:username", middleware.IsFunctionEnabled("authn_enabled"), - controllers.StartLoginAuthn) + controllers.StartLoginAuthn, + ) // WebAuthn登陆 user.POST("authn/finish/:username", middleware.IsFunctionEnabled("authn_enabled"), - controllers.FinishLoginAuthn) + controllers.FinishLoginAuthn, + ) // 获取用户主页展示用分享 user.GET("profile/:id", middleware.HashID(hashid.UserID), diff --git a/service/user/login.go b/service/user/login.go index 42eaa9a..f3c38b5 100644 --- a/service/user/login.go +++ b/service/user/login.go @@ -1,12 +1,18 @@ package user import ( + "fmt" "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/cache" + "github.com/HFO4/cloudreve/pkg/email" + "github.com/HFO4/cloudreve/pkg/hashid" "github.com/HFO4/cloudreve/pkg/serializer" "github.com/HFO4/cloudreve/pkg/util" "github.com/gin-gonic/gin" "github.com/mojocn/base64Captcha" "github.com/pquerna/otp/totp" + "net/url" + "strings" ) // UserLoginService 管理用户登录的服务 @@ -17,6 +23,86 @@ type UserLoginService struct { CaptchaCode string `form:"captchaCode" json:"captchaCode"` } +// UserResetEmailService 发送密码重设邮件服务 +type UserResetEmailService struct { + UserName string `form:"userName" json:"userName" binding:"required,email"` + CaptchaCode string `form:"captchaCode" json:"captchaCode"` +} + +// UserResetService 密码重设服务 +type UserResetService struct { + Password string `form:"Password" json:"Password" binding:"required,min=4,max=64"` + ID string `json:"id" binding:"required"` + Secret string `json:"secret" binding:"required"` +} + +// Reset 重设密码 +func (service *UserResetService) Reset(c *gin.Context) serializer.Response { + // 取得原始用户ID + uid, err := hashid.DecodeHashID(service.ID, hashid.UserID) + if err != nil { + return serializer.Err(serializer.CodeNotFound, "重设链接无效", err) + } + + // 检查重设会话 + resetSession, exist := cache.Get(fmt.Sprintf("user_reset_%d", uid)) + if !exist || resetSession.(string) != service.Secret { + return serializer.Err(serializer.CodeNotFound, "链接已过期", err) + } + + // 重设用户密码 + user, err := model.GetActiveUserByID(uid) + if err != nil { + return serializer.Err(serializer.CodeNotFound, "用户不存在", err) + } + + user.SetPassword(service.Password) + if err := user.Update(map[string]interface{}{"password": user.Password}); err != nil { + return serializer.DBErr("无法重设密码", err) + } + + cache.Deletes([]string{fmt.Sprintf("%d", uid)}, "user_reset_") + return serializer.Response{} +} + +// Reset 发送密码重设邮件 +func (service *UserResetEmailService) Reset(c *gin.Context) serializer.Response { + // 检查验证码 + isCaptchaRequired := model.IsTrueVal(model.GetSettingByName("forget_captcha")) + if isCaptchaRequired { + captchaID := util.GetSession(c, "captchaID") + util.DeleteSession(c, "captchaID") + if captchaID == nil || !base64Captcha.VerifyCaptcha(captchaID.(string), service.CaptchaCode) { + return serializer.ParamErr("验证码错误", nil) + } + } + + // 查找用户 + if user, err := model.GetUserByEmail(service.UserName); err == nil { + + // 创建密码重设会话 + secret := util.RandStringRunes(32) + cache.Set(fmt.Sprintf("user_reset_%d", user.ID), secret, 3600) + + // 生成用户访问的重设链接 + controller, _ := url.Parse("/reset") + finalURL := model.GetSiteURL().ResolveReference(controller) + queries := finalURL.Query() + queries.Add("id", hashid.HashID(user.ID, hashid.UserID)) + queries.Add("sign", secret) + finalURL.RawQuery = queries.Encode() + + // 发送密码重设邮件 + title, body := email.NewResetEmail(user.Nick, strings.ReplaceAll(finalURL.String(), "/reset", "/#/reset")) + if err := email.Send(user.Email, title, body); err != nil { + return serializer.Err(serializer.CodeInternalSetting, "无法发送密码重设邮件", err) + } + + } + + return serializer.Response{} +} + // Login 二步验证继续登录 func (service *Enable2FA) Login(c *gin.Context) serializer.Response { if uid, ok := util.GetSession(c, "2fa_user_id").(uint); ok {