feat: 支持管理员自助修改密码 (#57)
This commit is contained in:
@@ -113,6 +113,39 @@ func (h *Handler) Logout(c *gin.Context) {
|
||||
c.Redirect(http.StatusSeeOther, "/login?msg="+url.QueryEscape("已退出登录"))
|
||||
}
|
||||
|
||||
// ChangePassword 修改当前管理员自己的密码;目标身份只取 Session,不读表单用户编号。
|
||||
func (h *Handler) ChangePassword(c *gin.Context) {
|
||||
returnPath := passwordReturnPath(c.PostForm("next"))
|
||||
err := service.ChangeAdminPassword(h.db, currentUser(c), c.PostForm("current_password"),
|
||||
c.PostForm("new_password"), c.PostForm("password_confirm"), time.Now())
|
||||
if service.IsValidationError(err) {
|
||||
query := url.Values{
|
||||
"change_password": {"1"},
|
||||
"password_error": {err.Error()},
|
||||
}
|
||||
if field := service.ValidationField(err); field != "" {
|
||||
query.Set("password_field", field)
|
||||
}
|
||||
separator := "?"
|
||||
if strings.Contains(returnPath, "?") {
|
||||
separator = "&"
|
||||
}
|
||||
c.Redirect(http.StatusSeeOther, returnPath+separator+query.Encode())
|
||||
return
|
||||
}
|
||||
if errors.Is(err, repository.ErrUserPasswordChanged) {
|
||||
clearAuthCookie(c)
|
||||
c.Redirect(http.StatusSeeOther, "/login?msg="+url.QueryEscape("密码已在其他操作中变更,请重新登录"))
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
fail(c, http.StatusInternalServerError, "修改密码失败,原密码和现有登录保持不变。刷新后重试。")
|
||||
return
|
||||
}
|
||||
clearAuthCookie(c)
|
||||
c.Redirect(http.StatusSeeOther, "/login?msg="+url.QueryEscape("密码修改成功,请使用新密码重新登录"))
|
||||
}
|
||||
|
||||
// AuthRequired 只挂在 Web 业务路由组。Client API 注册在另一个组,不能经过这里。
|
||||
func AuthRequired(db *sql.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
@@ -27,6 +27,16 @@ func TestSafeNext只允许本站绝对路径(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordReturnPath移除改密反馈参数(t *testing.T) {
|
||||
got := passwordReturnPath("/pdd?q=shoe&change_password=1&password_error=bad&password_field=new_password")
|
||||
if got != "/pdd?q=shoe" {
|
||||
t.Fatalf("passwordReturnPath = %q,期望保留业务筛选并移除改密参数", got)
|
||||
}
|
||||
if got := passwordReturnPath("https://example.com"); got != "/shopee" {
|
||||
t.Fatalf("外部返回地址应回退到 /shopee,实际 %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthCookie安全属性(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
for _, test := range []struct {
|
||||
|
||||
@@ -12,6 +12,7 @@ package web
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -44,6 +45,8 @@ func Register(r *gin.Engine, db *sql.DB, onlineThreshold time.Duration) {
|
||||
// 登录中间件只挂业务网页组,绝不能挂在整个 Engine。
|
||||
pages := r.Group("/", CSRFMiddleware(), AuthRequired(db))
|
||||
pages.POST("/logout", h.Logout)
|
||||
account := pages.Group("/account", AdminRequired())
|
||||
account.POST("/change-password", h.ChangePassword)
|
||||
|
||||
// 打开根路径直接进第一个模块
|
||||
pages.GET("/", func(c *gin.Context) {
|
||||
@@ -97,11 +100,16 @@ func Register(r *gin.Engine, db *sql.DB, onlineThreshold time.Duration) {
|
||||
|
||||
// page 组装每个页面都要的公共数据(导航高亮、标题、CSRF token)。
|
||||
func page(c *gin.Context, active, title string, extra gin.H) gin.H {
|
||||
actor := currentUser(c)
|
||||
data := gin.H{
|
||||
"Active": active,
|
||||
"Title": title,
|
||||
"CSRFToken": csrfToken(c),
|
||||
"CurrentUser": currentUser(c),
|
||||
"Active": active,
|
||||
"Title": title,
|
||||
"CSRFToken": csrfToken(c),
|
||||
"CurrentUser": actor,
|
||||
"PasswordChangeOpen": actor != nil && actor.IsAdmin() && c.Query("change_password") == "1",
|
||||
"PasswordChangeError": c.Query("password_error"),
|
||||
"PasswordChangeField": c.Query("password_field"),
|
||||
"PasswordReturnPath": passwordReturnPath(c.Request.URL.RequestURI()),
|
||||
}
|
||||
for k, v := range extra {
|
||||
data[k] = v
|
||||
@@ -109,6 +117,20 @@ func page(c *gin.Context, active, title string, extra gin.H) gin.H {
|
||||
return data
|
||||
}
|
||||
|
||||
// passwordReturnPath 保留用户所在业务页和原筛选条件,但移除改密弹窗自己的反馈参数。
|
||||
func passwordReturnPath(raw string) string {
|
||||
parsed, err := url.Parse(safeNext(raw))
|
||||
if err != nil {
|
||||
return "/shopee"
|
||||
}
|
||||
query := parsed.Query()
|
||||
query.Del("change_password")
|
||||
query.Del("password_error")
|
||||
query.Del("password_field")
|
||||
parsed.RawQuery = query.Encode()
|
||||
return parsed.RequestURI()
|
||||
}
|
||||
|
||||
// fail 渲染一个错误页。
|
||||
//
|
||||
// 错误信息要说清三件事:发生了什么、保住了什么、下一步做什么。
|
||||
|
||||
Reference in New Issue
Block a user