Smail/backend/controllers/settings.go
smail 6de4a1cae3
Some checks are pending
Security Checks / security (push) Waiting to run
统一修复未知收件人超管限定与全部控制器Unicode转义
2026-09-10 22:27:27 +08:00

144 lines
5.5 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, "超级管理员必须通过部署端专用通道重置密码", "").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, "密码长度必须为 8-12 字节", "").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)
}