144 lines
5.6 KiB
Go
144 lines
5.6 KiB
Go
package controllers
|
|
|
|
import (
|
|
"errors"
|
|
"git.pcux.cn/xuans/Smail/config"
|
|
"git.pcux.cn/xuans/Smail/db"
|
|
"git.pcux.cn/xuans/Smail/dto/response"
|
|
"git.pcux.cn/xuans/Smail/i18n"
|
|
"git.pcux.cn/xuans/Smail/models"
|
|
"git.pcux.cn/xuans/Smail/services/access"
|
|
"git.pcux.cn/xuans/Smail/services/auth"
|
|
"git.pcux.cn/xuans/Smail/session"
|
|
"git.pcux.cn/xuans/Smail/utils/context"
|
|
"git.pcux.cn/xuans/Smail/utils/csrf"
|
|
"git.pcux.cn/xuans/Smail/utils/password"
|
|
log "github.com/sirupsen/logrus"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type modifyPasswordRequest struct {
|
|
CurrentPassword string `json:"current_password"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
func ModifyPassword(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
|
if ctx == nil || ctx.UserID <= 0 || strings.TrimSpace(ctx.UserAccount) == "" {
|
|
response.NewErrorResponse(response.NoAccessPrivileges, "未登录用户不能修改密码", "").FPrint(w)
|
|
return
|
|
}
|
|
if ctx.EffectiveRole() == access.RoleSuperAdmin {
|
|
response.NewErrorResponse(response.NoAccessPrivileges, "\u8d85\u7ea7\u7ba1\u7406\u5458\u5fc5\u987b\u901a\u8fc7\u90e8\u7f72\u7aef\u4e13\u7528\u901a\u9053\u91cd\u7f6e\u5bc6\u7801", "").FPrint(w)
|
|
return
|
|
}
|
|
var retData modifyPasswordRequest
|
|
if err := decodeJSONRequest(w, req, &retData); err != nil {
|
|
log.WithContext(ctx).Debug("invalid password change request")
|
|
response.NewErrorResponse(response.ParamsError, i18n.GetText(ctx.Lang, "unknowError"), "").FPrint(w)
|
|
return
|
|
}
|
|
if retData.CurrentPassword == "" || retData.Password == "" {
|
|
response.NewErrorResponse(response.ParamsError, i18n.GetText(ctx.Lang, "unknowError"), "").FPrint(w)
|
|
return
|
|
}
|
|
pairKey := loginPairAttempts.key(req, ctx.UserAccount)
|
|
accountKey := loginAccountAttempts.accountKey(ctx.UserAccount)
|
|
ipKey := loginIPAttempts.ipKey(req)
|
|
if loginPairAttempts.blocked(pairKey) || loginAccountAttempts.blocked(accountKey) || loginIPAttempts.blocked(ipKey) {
|
|
response.NewErrorResponse(response.ParamsError, i18n.GetText(ctx.Lang, "aperror"), "").FPrint(w)
|
|
return
|
|
}
|
|
currentUser, err := auth.Authenticate(ctx.UserAccount, retData.CurrentPassword)
|
|
if err != nil || currentUser.ID != ctx.UserID {
|
|
loginPairAttempts.recordFailure(pairKey)
|
|
loginAccountAttempts.recordFailure(accountKey)
|
|
loginIPAttempts.recordFailure(ipKey)
|
|
log.WithContext(ctx).Info("password change authentication failed")
|
|
response.NewErrorResponse(response.ParamsError, i18n.GetText(ctx.Lang, "aperror"), "").FPrint(w)
|
|
return
|
|
}
|
|
loginPairAttempts.clear(pairKey)
|
|
loginAccountAttempts.clear(accountKey)
|
|
|
|
securityVersion := currentUser.SecurityVersion + 1
|
|
if securityVersion <= 0 {
|
|
securityVersion = 1
|
|
}
|
|
now := time.Now().UTC()
|
|
user := models.User{
|
|
ID: ctx.UserID,
|
|
SessionVersion: currentUser.SessionVersion + 1,
|
|
SecurityVersion: securityVersion,
|
|
MustChangePassword: 0,
|
|
PasswordChangedAt: &now,
|
|
}
|
|
if err := auth.SetPassword(&user, retData.Password); err != nil {
|
|
if errors.Is(err, password.ErrInvalidLength) {
|
|
response.NewErrorResponse(response.ParamsError, "\u5bc6\u7801\u957f\u5ea6\u5fc5\u987b\u4e3a 8-256 \u5b57\u8282", "").FPrint(w)
|
|
return
|
|
}
|
|
log.WithContext(ctx).WithError(err).Error("password hash failed")
|
|
response.NewErrorResponse(response.ServerError, i18n.GetText(ctx.Lang, "unknowError"), "").FPrint(w)
|
|
return
|
|
}
|
|
credentialService := credentialRuntime()
|
|
if credentialService == nil {
|
|
log.WithContext(ctx).Error("credential service unavailable during password change")
|
|
response.NewErrorResponse(response.ServerError, i18n.GetText(ctx.Lang, "unknowError"), "").FPrint(w)
|
|
return
|
|
}
|
|
tx := db.Instance.NewSession()
|
|
if tx == nil {
|
|
response.NewErrorResponse(response.ServerError, i18n.GetText(ctx.Lang, "unknowError"), "").FPrint(w)
|
|
return
|
|
}
|
|
defer tx.Close()
|
|
if err := tx.Begin(); err != nil {
|
|
log.WithContext(ctx).WithError(err).Error("begin password change transaction failed")
|
|
response.NewErrorResponse(response.ServerError, i18n.GetText(ctx.Lang, "unknowError"), "").FPrint(w)
|
|
return
|
|
}
|
|
committed := false
|
|
defer func() {
|
|
if !committed {
|
|
_ = tx.Rollback()
|
|
}
|
|
}()
|
|
|
|
affected, err := tx.Where("id = ? and password = ? and session_version = ?", ctx.UserID, currentUser.Password, currentUser.SessionVersion).Cols("password", "session_version", "security_version", "must_change_password", "password_changed_at").Update(&user)
|
|
if err != nil {
|
|
log.WithContext(ctx).WithError(err).Error("update password failed")
|
|
response.NewErrorResponse(response.ServerError, i18n.GetText(ctx.Lang, "unknowError"), "").FPrint(w)
|
|
return
|
|
}
|
|
if affected != 1 {
|
|
log.WithContext(ctx).Warn("password changed concurrently")
|
|
response.NewErrorResponse(response.ParamsError, i18n.GetText(ctx.Lang, "unknowError"), "").FPrint(w)
|
|
return
|
|
}
|
|
if err := credentialService.RevokeAllWithSession(tx, ctx.UserID); err != nil {
|
|
log.WithContext(ctx).WithError(err).Error("revoke credentials after password change failed")
|
|
response.NewErrorResponse(response.ServerError, i18n.GetText(ctx.Lang, "unknowError"), "").FPrint(w)
|
|
return
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
log.WithContext(ctx).WithError(err).Error("commit password change failed")
|
|
response.NewErrorResponse(response.ServerError, i18n.GetText(ctx.Lang, "unknowError"), "").FPrint(w)
|
|
return
|
|
}
|
|
committed = true
|
|
if err := session.Instance.Destroy(req.Context()); err != nil {
|
|
log.WithContext(ctx).WithError(err).Error("session destruction after password change failed")
|
|
response.NewErrorResponse(response.ServerError, i18n.GetText(ctx.Lang, "unknowError"), "").FPrint(w)
|
|
return
|
|
}
|
|
csrf.Clear(w, req, !config.DevelopmentMode)
|
|
|
|
response.NewSuccessResponse(map[string]any{
|
|
"message": i18n.GetText(ctx.Lang, "succ"),
|
|
"reauth_required": true,
|
|
}).FPrint(w)
|
|
}
|