feat: SSR 忘记密码与 Admin SMTP 设置
邮箱验证码重置密码(吊销 session);后台可配 SMTP 并发送测试信。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -32,7 +32,7 @@
|
|||||||
- [x] 注册(用户名、密码、昵称、邮箱;可选邮箱验证码)— SSR `/register`
|
- [x] 注册(用户名、密码、昵称、邮箱;可选邮箱验证码)— SSR `/register`
|
||||||
- [ ] 图形验证码接口(注册流程)
|
- [ ] 图形验证码接口(注册流程)
|
||||||
- [x] 登录 / 登出(opaque session Cookie `jiang13_session`)— SSR;SameSite=Lax;登出/禁言/改密吊销
|
- [x] 登录 / 登出(opaque session Cookie `jiang13_session`)— SSR;SameSite=Lax;登出/禁言/改密吊销
|
||||||
- [ ] 忘记密码:邮箱验证码 + 重置
|
- [x] 忘记密码:邮箱验证码 + 重置 — SSR `/forgot-password`(依赖 SMTP 就绪)
|
||||||
- [x] 注册配置:邮件就绪时强制验证码;安装后开放注册(不依赖 SMTP)
|
- [x] 注册配置:邮件就绪时强制验证码;安装后开放注册(不依赖 SMTP)
|
||||||
- [x] ~~首个用户自动成为管理员~~ → 改为仅 `/install` 创建管理员
|
- [x] ~~首个用户自动成为管理员~~ → 改为仅 `/install` 创建管理员
|
||||||
- [x] 个人中心:改昵称、签名、密码、上传头像、积分钱包/签到/抽奖 — SSR `/profile`(裁剪未做)
|
- [x] 个人中心:改昵称、签名、密码、上传头像、积分钱包/签到/抽奖 — SSR `/profile`(裁剪未做)
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
- [x] 仪表盘:用户/帖/板块计数 + 待审帖/评 — SSR `/admin/dashboard`(举报/友链待迁)
|
- [x] 仪表盘:用户/帖/板块计数 + 待审帖/评 — SSR `/admin/dashboard`(举报/友链待迁)
|
||||||
- [x] 敏感词:`forum_settings.filter_words` 读写 + 热更 — SSR `/admin/settings`
|
- [x] 敏感词:`forum_settings.filter_words` 读写 + 热更 — SSR `/admin/settings`
|
||||||
- [x] 基础限流(post/comment/register/login/window)— SSR;完整 Limits 字数等未迁
|
- [x] 基础限流(post/comment/register/login/window)— SSR;完整 Limits 字数等未迁
|
||||||
- [ ] SMTP 配置与测试信
|
- [x] SMTP 配置与测试信 — SSR `/admin/settings` 邮件区
|
||||||
- [x] 站点品牌文案:名称、标语、简介、keywords、Logo 字标、ICP — SSR(Logo/Favicon/OG 上传未迁)
|
- [x] 站点品牌文案:名称、标语、简介、keywords、Logo 字标、ICP — SSR(Logo/Favicon/OG 上传未迁)
|
||||||
- [ ] SQLite 一键备份与下载
|
- [ ] SQLite 一键备份与下载
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|------|------|-----|
|
|------|------|-----|
|
||||||
| `/login` | 登录 / 登出 | 已迁 |
|
| `/login` | 登录 / 登出 | 已迁 |
|
||||||
| `/register` | 注册(邮件就绪时要验证码) | 已迁 |
|
| `/register` | 注册(邮件就绪时要验证码) | 已迁 |
|
||||||
| `/forgot-password` | 忘记密码 | 未迁 |
|
| `/forgot-password` | 忘记密码 | 已迁 |
|
||||||
|
|
||||||
### 1.2 前台
|
### 1.2 前台
|
||||||
|
|
||||||
@@ -47,11 +47,11 @@
|
|||||||
| `/admin/dashboard` | 概览计数 | 已迁 |
|
| `/admin/dashboard` | 概览计数 | 已迁 |
|
||||||
| `/admin/boards` | 板块 CRUD | 已迁 |
|
| `/admin/boards` | 板块 CRUD | 已迁 |
|
||||||
| `/admin/moderation` | 待审帖/评 通过/拒绝 | 已迁 |
|
| `/admin/moderation` | 待审帖/评 通过/拒绝 | 已迁 |
|
||||||
| `/admin/settings` | 品牌 + 基础限流 + 敏感词 | 已迁 |
|
| `/admin/settings` | 品牌 + 基础限流 + 敏感词 + SMTP | 已迁 |
|
||||||
| `/admin/friend-links` | 品牌友链、申请审核、入口开关 | 已迁 |
|
| `/admin/friend-links` | 品牌友链、申请审核、入口开关 | 已迁 |
|
||||||
| `/admin/login` | 重定向前台登录 | 已迁 |
|
| `/admin/login` | 重定向前台登录 | 已迁 |
|
||||||
|
|
||||||
未迁(原 SPA):reports / users / badges / media / pages / SMTP / 完整 Limits 等。
|
未迁(原 SPA):reports / users / badges / media / pages / 完整 Limits 等。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ web_src/ → public/assets/
|
|||||||
|
|
||||||
### 已迁路径(摘要)
|
### 已迁路径(摘要)
|
||||||
|
|
||||||
公开写:`/install`、`/login`、`/logout`、`/register`、`/compose`(含门控插入)、`/post/:id/edit`、帖详情评论/赞/藏、积分解锁 `POST /post/:id/unlock`。
|
公开写:`/install`、`/login`、`/logout`、`/register`、`/forgot-password`(发码/重置)、`/compose`(含门控插入)、`/post/:id/edit`、帖详情评论/赞/藏、积分解锁 `POST /post/:id/unlock`。
|
||||||
|
|
||||||
个人闭环:`/profile`(昵称/签名/密码/头像、积分钱包/签到/抽奖)、`/user/:id`、`/favorites`。
|
个人闭环:`/profile`(昵称/签名/密码/头像、积分钱包/签到/抽奖)、`/user/:id`、`/favorites`。
|
||||||
`POST /profile/checkin`、`POST /profile/lottery`(CSRF + PRG)。
|
`POST /profile/checkin`、`POST /profile/lottery`(CSRF + PRG)。
|
||||||
@@ -90,4 +90,4 @@ web_src/ → public/assets/
|
|||||||
|
|
||||||
友链:`/links`(列表、登录申请/取消、Logo 上传);Admin `/admin/friend-links`(品牌增删、审核、入口开关)。
|
友链:`/links`(列表、登录申请/取消、Logo 上传);Admin `/admin/friend-links`(品牌增删、审核、入口开关)。
|
||||||
|
|
||||||
Admin:`/admin/dashboard`、`/admin/boards`、`/admin/moderation`、`/admin/settings`(品牌/限流/敏感词)、`/admin/friend-links`。
|
Admin:`/admin/dashboard`、`/admin/boards`、`/admin/moderation`、`/admin/settings`(品牌/限流/敏感词/SMTP)、`/admin/friend-links`。
|
||||||
@@ -102,6 +102,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
|||||||
Limiter: limiter, EmailCode: emailCodeSvc, Store: uploadStore,
|
Limiter: limiter, EmailCode: emailCodeSvc, Store: uploadStore,
|
||||||
Points: services.NewPointsService(),
|
Points: services.NewPointsService(),
|
||||||
FriendLink: friendLinkApplySvc,
|
FriendLink: friendLinkApplySvc,
|
||||||
|
Mail: mailSvc,
|
||||||
}, authMW)
|
}, authMW)
|
||||||
|
|
||||||
r.GET("/media/thumb/*filepath", h.ServeImageThumb)
|
r.GET("/media/thumb/*filepath", h.ServeImageThumb)
|
||||||
|
|||||||
@@ -408,6 +408,9 @@ type adminSettingsData struct {
|
|||||||
RateWindow int
|
RateWindow int
|
||||||
FilterWords string
|
FilterWords string
|
||||||
FilterCount int
|
FilterCount int
|
||||||
|
Mail services.MailConfig
|
||||||
|
MailReady bool
|
||||||
|
TestTo string
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminSettingsGet 设置页
|
// AdminSettingsGet 设置页
|
||||||
@@ -419,6 +422,7 @@ func (d Deps) AdminSettingsGet(c *gin.Context) {
|
|||||||
func (d Deps) renderAdminSettings(ctx *webctx.Context, errMsg string) {
|
func (d Deps) renderAdminSettings(ctx *webctx.Context, errMsg string) {
|
||||||
words := d.Settings.FilterWordsContent()
|
words := d.Settings.FilterWordsContent()
|
||||||
lim := d.Settings.Limits()
|
lim := d.Settings.Limits()
|
||||||
|
mail := d.Settings.MailConfigPublic()
|
||||||
data := adminSettingsData{
|
data := adminSettingsData{
|
||||||
AdminChrome: d.adminChrome(ctx, "站点设置", "settings"),
|
AdminChrome: d.adminChrome(ctx, "站点设置", "settings"),
|
||||||
Brand: d.Settings.SiteBranding(),
|
Brand: d.Settings.SiteBranding(),
|
||||||
@@ -429,6 +433,8 @@ func (d Deps) renderAdminSettings(ctx *webctx.Context, errMsg string) {
|
|||||||
RateWindow: lim.RateLimitWindowSec,
|
RateWindow: lim.RateLimitWindowSec,
|
||||||
FilterWords: words,
|
FilterWords: words,
|
||||||
FilterCount: services.CountFilterWords(words),
|
FilterCount: services.CountFilterWords(words),
|
||||||
|
Mail: mail,
|
||||||
|
MailReady: d.Settings.MailReady(),
|
||||||
}
|
}
|
||||||
data.Error = errMsg
|
data.Error = errMsg
|
||||||
ctx.HTML(http.StatusOK, "admin/settings", data)
|
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.SetFlash(fmt.Sprintf("敏感词已更新(有效词 %d 个)· %s", services.CountFilterWords(content), time.Now().Format("15:04:05")))
|
||||||
ctx.Redirect("/admin/settings")
|
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,
|
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
|
Store *services.UploadStore
|
||||||
Points *services.PointsService
|
Points *services.PointsService
|
||||||
FriendLink *services.FriendLinkApplyService
|
FriendLink *services.FriendLinkApplyService
|
||||||
|
Mail *services.MailService
|
||||||
}
|
}
|
||||||
|
|
||||||
// BoardView 侧栏
|
// BoardView 侧栏
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
|||||||
g.GET("/register", deps.RegisterGet)
|
g.GET("/register", deps.RegisterGet)
|
||||||
g.POST("/register", deps.RegisterPost)
|
g.POST("/register", deps.RegisterPost)
|
||||||
g.POST("/register/send-code", deps.RegisterSendCode)
|
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.GET("/compose", authMW.RequireAuth(), deps.ComposeGet)
|
||||||
g.POST("/compose", authMW.RequireAuth(), deps.ComposePost)
|
g.POST("/compose", authMW.RequireAuth(), deps.ComposePost)
|
||||||
g.POST("/compose/upload", authMW.RequireAuth(), deps.ComposeUpload)
|
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/brand", deps.AdminSettingsBrandPost)
|
||||||
admin.POST("/settings/limits", deps.AdminSettingsLimitsPost)
|
admin.POST("/settings/limits", deps.AdminSettingsLimitsPost)
|
||||||
admin.POST("/settings/filter-words", deps.AdminSettingsFilterWordsPost)
|
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.GET("/friend-links", deps.AdminFriendLinksGet)
|
||||||
admin.POST("/friend-links/settings", deps.AdminFriendLinksSettingsPost)
|
admin.POST("/friend-links/settings", deps.AdminFriendLinksSettingsPost)
|
||||||
admin.POST("/friend-links/brand", deps.AdminFriendLinksBrandAddPost)
|
admin.POST("/friend-links/brand", deps.AdminFriendLinksBrandAddPost)
|
||||||
|
|||||||
@@ -38,6 +38,32 @@
|
|||||||
<label>词表 <textarea name="filter_words" rows="12">{{.FilterWords}}</textarea></label>
|
<label>词表 <textarea name="filter_words" rows="12">{{.FilterWords}}</textarea></label>
|
||||||
<button type="submit">保存敏感词</button>
|
<button type="submit">保存敏感词</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<h2>邮件 SMTP</h2>
|
||||||
|
<p class="j13-muted">启用后可用于注册验证码、忘记密码重置与通知。{{if .MailReady}}当前:已就绪。{{else}}当前:未就绪。{{end}}{{if .Mail.HasPassword}} 已保存密码。{{end}}</p>
|
||||||
|
<form method="post" action="/admin/settings/mail" class="j13-form j13-admin-form">
|
||||||
|
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
|
||||||
|
<label class="j13-check"><input type="checkbox" name="enabled" value="1"{{if .Mail.Enabled}} checked{{end}}/> 启用邮件</label>
|
||||||
|
<label>SMTP Host <input name="host" value="{{.Mail.Host}}" placeholder="smtp.example.com"/></label>
|
||||||
|
<label>端口 <input name="port" type="number" min="1" value="{{.Mail.Port}}"/></label>
|
||||||
|
<label>加密
|
||||||
|
<select name="encryption">
|
||||||
|
<option value="ssl"{{if eq .Mail.Encryption "ssl"}} selected{{end}}>SSL</option>
|
||||||
|
<option value="starttls"{{if eq .Mail.Encryption "starttls"}} selected{{end}}>STARTTLS</option>
|
||||||
|
<option value="none"{{if eq .Mail.Encryption "none"}} selected{{end}}>无</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>用户名 <input name="username" value="{{.Mail.Username}}" autocomplete="off"/></label>
|
||||||
|
<label>密码 <input name="password" type="password" value="" autocomplete="new-password" placeholder="留空表示不修改"/></label>
|
||||||
|
<label>发件人 From <input name="from" type="email" value="{{.Mail.From}}"/></label>
|
||||||
|
<label>发件人显示名 <input name="from_name" value="{{.Mail.FromName}}"/></label>
|
||||||
|
<button type="submit">保存邮件设置</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/admin/settings/mail/test" class="j13-form j13-admin-form">
|
||||||
|
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
|
||||||
|
<label>测试收件邮箱 <input name="test_to" type="email" required placeholder="you@example.com"/></label>
|
||||||
|
<button type="submit" class="j13-btn-secondary">发送测试信</button>
|
||||||
|
</form>
|
||||||
</main>
|
</main>
|
||||||
{{template "base/footer" .}}
|
{{template "base/footer" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
33
templates/auth/forgot_password.tmpl
Normal file
33
templates/auth/forgot_password.tmpl
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{{define "auth/forgot_password"}}
|
||||||
|
{{template "base/head" .}}
|
||||||
|
<main class="j13-main j13-main--solo j13-auth">
|
||||||
|
<h1>忘记密码</h1>
|
||||||
|
{{template "base/alert" .}}
|
||||||
|
{{if .MailReady}}
|
||||||
|
<form method="post" action="/forgot-password" class="j13-form" id="forgot-form">
|
||||||
|
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
|
||||||
|
<label>注册邮箱
|
||||||
|
<input type="email" name="email" required autocomplete="email" value="{{.Email}}"/>
|
||||||
|
</label>
|
||||||
|
<label>邮箱验证码
|
||||||
|
<div class="j13-form__row">
|
||||||
|
<input name="email_code" required inputmode="numeric" autocomplete="one-time-code" maxlength="8"/>
|
||||||
|
<button type="submit" formaction="/forgot-password/send-code" formnovalidate class="j13-btn-secondary">发送验证码</button>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label>新密码
|
||||||
|
<input type="password" name="new_password" required autocomplete="new-password" minlength="6"/>
|
||||||
|
</label>
|
||||||
|
<label>确认新密码
|
||||||
|
<input type="password" name="new_password2" required autocomplete="new-password" minlength="6"/>
|
||||||
|
</label>
|
||||||
|
<button type="submit">重置密码</button>
|
||||||
|
</form>
|
||||||
|
<p class="j13-muted">若该邮箱已注册,验证码将发送到邮箱。重置后其它设备会话将失效。</p>
|
||||||
|
{{else}}
|
||||||
|
<p class="j13-muted">邮件服务未配置,无法通过邮箱重置密码。请联系站长在后台配置 SMTP,或使用其它方式找回。</p>
|
||||||
|
{{end}}
|
||||||
|
<p><a href="/login">返回登录</a> · <a href="/">返回首页</a></p>
|
||||||
|
</main>
|
||||||
|
{{template "base/footer" .}}
|
||||||
|
{{end}}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<button type="submit">登录</button>
|
<button type="submit">登录</button>
|
||||||
</form>
|
</form>
|
||||||
<p><a href="/register">没有账号?注册</a> · <a href="/">返回首页</a></p>
|
<p><a href="/register">没有账号?注册</a> · <a href="/forgot-password">忘记密码</a> · <a href="/">返回首页</a></p>
|
||||||
</main>
|
</main>
|
||||||
{{template "base/footer" .}}
|
{{template "base/footer" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user