86 lines
3.2 KiB
Go
86 lines
3.2 KiB
Go
package controllers
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"net/http"
|
||
"strings"
|
||
|
||
"git.pcux.cn/xuans/Smail/services/recovery"
|
||
"git.pcux.cn/xuans/Smail/services/verification"
|
||
"git.pcux.cn/xuans/Smail/utils/password"
|
||
)
|
||
|
||
// 新恢复入口不依赖登录 Cookie,每一步由短期一次性令牌关联。
|
||
func PublicPasswordRecoveryFlow(w http.ResponseWriter, req *http.Request) {
|
||
fail := func(status int, message string) {
|
||
writePublicRecoveryJSON(w, status, map[string]string{"message": message})
|
||
}
|
||
if !PublicMailRecoveryEnabled() || req == nil || req.Method != http.MethodPost {
|
||
fail(http.StatusBadRequest, "密码重置功能未启用")
|
||
return
|
||
}
|
||
limiter := emailRecoveryBeginIPAttempts
|
||
key := limiter.ipKey(req)
|
||
if limiter.blocked(key) || emailRecoveryFailureIPAttempts.blocked(emailRecoveryFailureIPAttempts.ipKey(req)) {
|
||
fail(http.StatusTooManyRequests, "请求过于频繁,请稍后重试")
|
||
return
|
||
}
|
||
limiter.recordFailure(key)
|
||
var input struct {
|
||
Identifier string `json:"identifier"`
|
||
Token string `json:"token"`
|
||
Method string `json:"method"`
|
||
Code string `json:"code"`
|
||
NewPassword string `json:"new_password"`
|
||
}
|
||
decoder := json.NewDecoder(req.Body)
|
||
decoder.DisallowUnknownFields()
|
||
if err := decoder.Decode(&input); err != nil {
|
||
fail(http.StatusBadRequest, "请求格式无效")
|
||
return
|
||
}
|
||
service := currentSecurityRuntime().recovery
|
||
if service == nil {
|
||
fail(http.StatusServiceUnavailable, "重置服务暂时不可用")
|
||
return
|
||
}
|
||
var result any
|
||
var err error
|
||
switch strings.TrimSuffix(req.URL.Path, "/") {
|
||
case "/rest/api/recovery/begin":
|
||
result, err = service.IdentifyPublicFlow(input.Identifier)
|
||
case "/rest/api/recovery/send-code":
|
||
err = service.SendPublicFlowCode(input.Token, remoteIP(req))
|
||
result = map[string]any{"sent": true, "retry_after": 60}
|
||
case "/rest/api/recovery/verify":
|
||
var token string
|
||
token, err = service.VerifyPublicFlow(input.Token, input.Method, input.Code, remoteIP(req))
|
||
result = map[string]string{"token": token}
|
||
case "/rest/api/recovery/reset":
|
||
err = service.CompletePublicFlow(input.Token, input.NewPassword)
|
||
result = map[string]bool{"reset": true}
|
||
default:
|
||
http.NotFound(w, req)
|
||
return
|
||
}
|
||
if err != nil {
|
||
if errors.Is(err, password.ErrBusy) || errors.Is(err, recovery.ErrUnavailable) {
|
||
fail(http.StatusServiceUnavailable, "重置服务暂时不可用,请稍后重试")
|
||
} else if errors.Is(err, verification.ErrRateLimited) {
|
||
fail(http.StatusTooManyRequests, "验证码发送过于频繁,请稍后重试")
|
||
} else if strings.HasSuffix(req.URL.Path, "/send-code") {
|
||
fail(http.StatusBadRequest, "验证码未发送,请检查邮箱绑定或稍后重试")
|
||
} else if strings.HasSuffix(req.URL.Path, "/begin") {
|
||
fail(http.StatusBadRequest, "账号不存在、已停用或无法使用此通道")
|
||
} else if errors.Is(err, recovery.ErrInvalidNewPassword) {
|
||
fail(http.StatusBadRequest, "新密码长度必须为 8-12 字节")
|
||
} else {
|
||
emailRecoveryFailureIPAttempts.recordFailure(emailRecoveryFailureIPAttempts.ipKey(req))
|
||
fail(http.StatusBadRequest, "验证未通过或流程已失效,请核对信息或重新开始")
|
||
}
|
||
return
|
||
}
|
||
writePublicRecoveryJSON(w, http.StatusOK, result)
|
||
}
|