98 lines
3.7 KiB
Go
98 lines
3.7 KiB
Go
package controllers
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.pcux.cn/xuans/Smail/db"
|
|
"git.pcux.cn/xuans/Smail/dto/response"
|
|
"git.pcux.cn/xuans/Smail/models"
|
|
"git.pcux.cn/xuans/Smail/services/access"
|
|
"git.pcux.cn/xuans/Smail/services/verification"
|
|
"git.pcux.cn/xuans/Smail/utils/context"
|
|
)
|
|
|
|
var errPasswordChangeVerification = errors.New("password change verification unavailable")
|
|
|
|
// 从数据库读取当前身份和已验证安全邮箱,不允许客户端指定验证码接收人。
|
|
func passwordChangeIdentity(ctx *context.Context, runtime securityRuntimeState) (*models.User, *models.ExternalEmail, string, error) {
|
|
if ctx == nil || db.Instance == nil || runtime.external == nil || runtime.verification == nil {
|
|
return nil, nil, "", errPasswordChangeVerification
|
|
}
|
|
user := &models.User{}
|
|
found, err := db.Instance.ID(ctx.UserID).Get(user)
|
|
if err != nil || !found || user.Disabled != 0 || user.EffectiveRole() != access.RoleSuperAdmin || user.Account != ctx.UserAccount {
|
|
return nil, nil, "", errPasswordChangeVerification
|
|
}
|
|
bindings, err := runtime.external.List(user.ID)
|
|
if err != nil {
|
|
return nil, nil, "", errPasswordChangeVerification
|
|
}
|
|
var binding *models.ExternalEmail
|
|
for i := range bindings {
|
|
item := &bindings[i]
|
|
if item.Status != models.ExternalEmailStatusVerified {
|
|
continue
|
|
}
|
|
if item.EmailType == models.ExternalEmailTypePrimary {
|
|
binding = item
|
|
break
|
|
}
|
|
if binding == nil && item.EmailType == models.ExternalEmailTypeBackup {
|
|
binding = item
|
|
}
|
|
}
|
|
if binding == nil {
|
|
return nil, nil, "", errPasswordChangeVerification
|
|
}
|
|
email, err := runtime.external.Decrypt(*binding)
|
|
if err != nil || strings.TrimSpace(email) == "" {
|
|
return nil, nil, "", errPasswordChangeVerification
|
|
}
|
|
return user, binding, email, nil
|
|
}
|
|
|
|
func requestPasswordChangeVerification(ctx *context.Context, w http.ResponseWriter, req *http.Request, input verificationRequest, runtime securityRuntimeState) {
|
|
user, binding, email, err := passwordChangeIdentity(ctx, runtime)
|
|
if err != nil {
|
|
writeSecurityError(w, response.ParamsError, "请确认超级管理员已绑定并验证外部安全邮箱")
|
|
return
|
|
}
|
|
if input.Email != "" && !strings.EqualFold(strings.TrimSpace(input.Email), email) {
|
|
writeSecurityError(w, response.ParamsError, "邮箱地址与当前验证邮箱不匹配")
|
|
return
|
|
}
|
|
challenge, err := runtime.verification.Issue(verification.IssueRequest{
|
|
Purpose: verification.PurposePasswordChange, UserID: user.ID,
|
|
Account: user.Account, Email: email, ResourceID: binding.ID,
|
|
BrowserTransaction: strconv.Itoa(user.SecurityVersion),
|
|
}, remoteIP(req))
|
|
if err != nil {
|
|
writeSecurityError(w, response.ParamsError, "验证码发送失败或请求过于频繁,请稍后重试")
|
|
return
|
|
}
|
|
response.NewSuccessResponse(map[string]any{
|
|
"id": challenge.ID, "expires_at": challenge.ExpiresAt.UTC(), "email": maskEmail(email),
|
|
}).FPrint(w)
|
|
}
|
|
|
|
func verifyPasswordChange(ctx *context.Context, req *http.Request, input modifyPasswordRequest) (*models.User, error) {
|
|
runtime := currentSecurityRuntime()
|
|
user, binding, email, err := passwordChangeIdentity(ctx, runtime)
|
|
if err != nil || strings.TrimSpace(input.Code) == "" || strings.TrimSpace(input.ChallengeID) == "" {
|
|
return nil, errPasswordChangeVerification
|
|
}
|
|
if input.Email != "" && !strings.EqualFold(strings.TrimSpace(input.Email), email) {
|
|
return nil, errPasswordChangeVerification
|
|
}
|
|
// 验证码同时绑定用途、账号、邮箱绑定记录及安全版本,不能跨流程复用。
|
|
if err := runtime.verification.VerifyBoundWithContext(input.ChallengeID, user.Account, email,
|
|
strings.TrimSpace(input.Code), verification.PurposePasswordChange, binding.ID,
|
|
remoteIP(req), strconv.Itoa(user.SecurityVersion)); err != nil {
|
|
return nil, err
|
|
}
|
|
return user, nil
|
|
}
|