feat: SSR 忘记密码与 Admin SMTP 设置
邮箱验证码重置密码(吊销 session);后台可配 SMTP 并发送测试信。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -102,6 +102,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
||||
Limiter: limiter, EmailCode: emailCodeSvc, Store: uploadStore,
|
||||
Points: services.NewPointsService(),
|
||||
FriendLink: friendLinkApplySvc,
|
||||
Mail: mailSvc,
|
||||
}, authMW)
|
||||
|
||||
r.GET("/media/thumb/*filepath", h.ServeImageThumb)
|
||||
|
||||
@@ -408,6 +408,9 @@ type adminSettingsData struct {
|
||||
RateWindow int
|
||||
FilterWords string
|
||||
FilterCount int
|
||||
Mail services.MailConfig
|
||||
MailReady bool
|
||||
TestTo string
|
||||
}
|
||||
|
||||
// AdminSettingsGet 设置页
|
||||
@@ -419,6 +422,7 @@ func (d Deps) AdminSettingsGet(c *gin.Context) {
|
||||
func (d Deps) renderAdminSettings(ctx *webctx.Context, errMsg string) {
|
||||
words := d.Settings.FilterWordsContent()
|
||||
lim := d.Settings.Limits()
|
||||
mail := d.Settings.MailConfigPublic()
|
||||
data := adminSettingsData{
|
||||
AdminChrome: d.adminChrome(ctx, "站点设置", "settings"),
|
||||
Brand: d.Settings.SiteBranding(),
|
||||
@@ -429,6 +433,8 @@ func (d Deps) renderAdminSettings(ctx *webctx.Context, errMsg string) {
|
||||
RateWindow: lim.RateLimitWindowSec,
|
||||
FilterWords: words,
|
||||
FilterCount: services.CountFilterWords(words),
|
||||
Mail: mail,
|
||||
MailReady: d.Settings.MailReady(),
|
||||
}
|
||||
data.Error = errMsg
|
||||
ctx.HTML(http.StatusOK, "admin/settings", data)
|
||||
@@ -498,3 +504,60 @@ func (d Deps) AdminSettingsFilterWordsPost(c *gin.Context) {
|
||||
ctx.SetFlash(fmt.Sprintf("敏感词已更新(有效词 %d 个)· %s", services.CountFilterWords(content), time.Now().Format("15:04:05")))
|
||||
ctx.Redirect("/admin/settings")
|
||||
}
|
||||
|
||||
// AdminSettingsMailPost 保存 SMTP
|
||||
func (d Deps) AdminSettingsMailPost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
if !ctx.CheckCSRF() {
|
||||
d.renderAdminSettings(ctx, "无效请求,请重试")
|
||||
return
|
||||
}
|
||||
port, _ := strconv.Atoi(c.PostForm("port"))
|
||||
cfg := services.MailConfig{
|
||||
Enabled: c.PostForm("enabled") == "1" || c.PostForm("enabled") == "on",
|
||||
Host: strings.TrimSpace(c.PostForm("host")),
|
||||
Port: port,
|
||||
Username: strings.TrimSpace(c.PostForm("username")),
|
||||
Password: c.PostForm("password"),
|
||||
From: strings.TrimSpace(c.PostForm("from")),
|
||||
FromName: strings.TrimSpace(c.PostForm("from_name")),
|
||||
Encryption: strings.TrimSpace(c.PostForm("encryption")),
|
||||
}
|
||||
if err := d.Settings.UpdateMailConfig(cfg); err != nil {
|
||||
d.renderAdminSettings(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
ctx.SetFlash("邮件设置已保存")
|
||||
ctx.Redirect("/admin/settings")
|
||||
}
|
||||
|
||||
// AdminSettingsMailTestPost 发送测试信
|
||||
func (d Deps) AdminSettingsMailTestPost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
to := strings.TrimSpace(c.PostForm("test_to"))
|
||||
if !ctx.CheckCSRF() {
|
||||
d.renderAdminSettings(ctx, "无效请求,请重试")
|
||||
return
|
||||
}
|
||||
if err := services.ValidateEmail(to); err != nil {
|
||||
d.renderAdminSettings(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
if !d.Settings.MailReady() {
|
||||
d.renderAdminSettings(ctx, services.ErrMailNotConfigured.Error())
|
||||
return
|
||||
}
|
||||
if d.Mail == nil {
|
||||
d.renderAdminSettings(ctx, "邮件服务未就绪")
|
||||
return
|
||||
}
|
||||
siteName := d.Settings.SiteBranding().Name
|
||||
err := d.Mail.Send(services.NormalizeEmail(to), "邮件配置测试",
|
||||
fmt.Sprintf("这是一封来自%s的测试邮件,说明 SMTP 配置正常。", siteName))
|
||||
if err != nil {
|
||||
d.renderAdminSettings(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
ctx.SetFlash("测试邮件已发送至 " + to)
|
||||
ctx.Redirect("/admin/settings")
|
||||
}
|
||||
|
||||
@@ -206,3 +206,101 @@ func (d Deps) renderRegisterWithChrome(ctx *webctx.Context, chrome PageChrome, e
|
||||
RequireEmailCode: mailReady,
|
||||
})
|
||||
}
|
||||
|
||||
type forgotPasswordData struct {
|
||||
PageChrome
|
||||
Email string
|
||||
MailReady bool
|
||||
}
|
||||
|
||||
// ForgotPasswordGet 忘记密码页
|
||||
func (d Deps) ForgotPasswordGet(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
if ctx.IsSigned() {
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
}
|
||||
d.renderForgotPassword(ctx, "", "")
|
||||
}
|
||||
|
||||
// ForgotPasswordSendCode 发送重置验证码
|
||||
func (d Deps) ForgotPasswordSendCode(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
if ctx.IsSigned() {
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
}
|
||||
email := strings.TrimSpace(c.PostForm("email"))
|
||||
if !ctx.CheckCSRF() {
|
||||
d.renderForgotPassword(ctx, "无效请求,请重试", email)
|
||||
return
|
||||
}
|
||||
if d.Limiter != nil && !d.Limiter.Allow("login", c.ClientIP()) {
|
||||
d.renderForgotPassword(ctx, "操作过于频繁,请稍后再试", email)
|
||||
return
|
||||
}
|
||||
if d.EmailCode == nil || !d.Settings.MailReady() {
|
||||
d.renderForgotPassword(ctx, services.ErrMailNotConfigured.Error(), email)
|
||||
return
|
||||
}
|
||||
if err := d.EmailCode.SendResetCode(email); err != nil {
|
||||
d.renderForgotPassword(ctx, err.Error(), email)
|
||||
return
|
||||
}
|
||||
chrome := d.chrome(ctx, "忘记密码 · "+d.Settings.SiteBranding().Name, "", "")
|
||||
chrome.Flash = "若该邮箱已注册,验证码将发送到邮箱"
|
||||
d.renderForgotPasswordWithChrome(ctx, chrome, "", email)
|
||||
}
|
||||
|
||||
// ForgotPasswordPost 提交重置
|
||||
func (d Deps) ForgotPasswordPost(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
if ctx.IsSigned() {
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
}
|
||||
email := strings.TrimSpace(c.PostForm("email"))
|
||||
if !ctx.CheckCSRF() {
|
||||
d.renderForgotPassword(ctx, "无效请求,请重试", email)
|
||||
return
|
||||
}
|
||||
if d.Limiter != nil && !d.Limiter.Allow("login", c.ClientIP()) {
|
||||
d.renderForgotPassword(ctx, "操作过于频繁,请稍后再试", email)
|
||||
return
|
||||
}
|
||||
if !d.Settings.MailReady() || d.EmailCode == nil {
|
||||
d.renderForgotPassword(ctx, services.ErrMailNotConfigured.Error(), email)
|
||||
return
|
||||
}
|
||||
code := strings.TrimSpace(c.PostForm("email_code"))
|
||||
if !d.EmailCode.VerifyPurpose(services.EmailCodePurposeReset, email, code) {
|
||||
d.renderForgotPassword(ctx, services.ErrEmailCodeInvalid.Error(), email)
|
||||
return
|
||||
}
|
||||
pass := c.PostForm("new_password")
|
||||
pass2 := c.PostForm("new_password2")
|
||||
if pass != pass2 {
|
||||
d.renderForgotPassword(ctx, "两次输入的新密码不一致", email)
|
||||
return
|
||||
}
|
||||
if err := d.User.ResetPasswordByEmail(email, pass); err != nil {
|
||||
d.renderForgotPassword(ctx, err.Error(), email)
|
||||
return
|
||||
}
|
||||
ctx.SetFlash("密码已重置,请使用新密码登录")
|
||||
ctx.Redirect("/login")
|
||||
}
|
||||
|
||||
func (d Deps) renderForgotPassword(ctx *webctx.Context, errMsg, email string) {
|
||||
chrome := d.chrome(ctx, "忘记密码 · "+d.Settings.SiteBranding().Name, "", "")
|
||||
d.renderForgotPasswordWithChrome(ctx, chrome, errMsg, email)
|
||||
}
|
||||
|
||||
func (d Deps) renderForgotPasswordWithChrome(ctx *webctx.Context, chrome PageChrome, errMsg, email string) {
|
||||
chrome.Error = errMsg
|
||||
ctx.HTML(http.StatusOK, "auth/forgot_password", forgotPasswordData{
|
||||
PageChrome: chrome,
|
||||
Email: email,
|
||||
MailReady: d.Settings.MailReady(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ type Deps struct {
|
||||
Store *services.UploadStore
|
||||
Points *services.PointsService
|
||||
FriendLink *services.FriendLinkApplyService
|
||||
Mail *services.MailService
|
||||
}
|
||||
|
||||
// BoardView 侧栏
|
||||
|
||||
@@ -29,6 +29,9 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
||||
g.GET("/register", deps.RegisterGet)
|
||||
g.POST("/register", deps.RegisterPost)
|
||||
g.POST("/register/send-code", deps.RegisterSendCode)
|
||||
g.GET("/forgot-password", deps.ForgotPasswordGet)
|
||||
g.POST("/forgot-password", deps.ForgotPasswordPost)
|
||||
g.POST("/forgot-password/send-code", deps.ForgotPasswordSendCode)
|
||||
g.GET("/compose", authMW.RequireAuth(), deps.ComposeGet)
|
||||
g.POST("/compose", authMW.RequireAuth(), deps.ComposePost)
|
||||
g.POST("/compose/upload", authMW.RequireAuth(), deps.ComposeUpload)
|
||||
@@ -51,6 +54,8 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
||||
admin.POST("/settings/brand", deps.AdminSettingsBrandPost)
|
||||
admin.POST("/settings/limits", deps.AdminSettingsLimitsPost)
|
||||
admin.POST("/settings/filter-words", deps.AdminSettingsFilterWordsPost)
|
||||
admin.POST("/settings/mail", deps.AdminSettingsMailPost)
|
||||
admin.POST("/settings/mail/test", deps.AdminSettingsMailTestPost)
|
||||
admin.GET("/friend-links", deps.AdminFriendLinksGet)
|
||||
admin.POST("/friend-links/settings", deps.AdminFriendLinksSettingsPost)
|
||||
admin.POST("/friend-links/brand", deps.AdminFriendLinksBrandAddPost)
|
||||
|
||||
Reference in New Issue
Block a user