refactor: use POST for account binding endpoints and normalize reset responses

- Switch /api/oauth/email/bind and /api/oauth/wechat/bind from GET to
  POST with JSON body for better REST semantics
- Normalize password reset endpoint to return consistent responses
- Apply url.QueryEscape to WeChat code parameter for robustness
This commit is contained in:
CaIon
2026-03-31 18:43:23 +08:00
parent 310d618a16
commit e099117c61
5 changed files with 50 additions and 33 deletions
+4 -11
View File
@@ -8,6 +8,7 @@ import (
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/middleware"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/oauth"
@@ -116,7 +117,6 @@ func GetStatus(c *gin.Context) {
"user_agreement_enabled": legalSetting.UserAgreement != "",
"privacy_policy_enabled": legalSetting.PrivacyPolicy != "",
"checkin_enabled": operation_setting.GetCheckinSetting().Enabled,
"_qn": "new-api",
}
// 根据启用状态注入可选内容
@@ -308,13 +308,7 @@ func SendPasswordResetEmail(c *gin.Context) {
})
return
}
if !model.IsEmailAlreadyTaken(email) {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "该邮箱地址未注册",
})
return
}
if model.IsEmailAlreadyTaken(email) {
code := common.GenerateVerificationCode(0)
common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose)
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", system_setting.ServerAddress, email, code)
@@ -325,14 +319,13 @@ func SendPasswordResetEmail(c *gin.Context) {
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, link, common.VerificationValidMinutes)
err := common.SendEmail(subject, email, content)
if err != nil {
common.ApiError(c, err)
return
logger.LogError(c.Request.Context(), fmt.Sprintf("failed to send password reset email to %s: %s", email, err.Error()))
}
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
})
return
}
type PasswordResetRequest struct {
+12 -2
View File
@@ -925,9 +925,19 @@ func ManageUser(c *gin.Context) {
return
}
type emailBindRequest struct {
Email string `json:"email"`
Code string `json:"code"`
}
func EmailBind(c *gin.Context) {
email := c.Query("email")
code := c.Query("code")
var req emailBindRequest
if err := common.DecodeJson(c.Request.Body, &req); err != nil {
common.ApiError(c, errors.New("invalid request body"))
return
}
email := req.Email
code := req.Code
if !common.VerifyCodeWithKey(email, code, common.EmailVerificationPurpose) {
common.ApiErrorI18n(c, i18n.MsgUserVerificationCodeError)
return
+15 -2
View File
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
@@ -25,7 +26,7 @@ func getWeChatIdByCode(code string) (string, error) {
if code == "" {
return "", errors.New("无效的参数")
}
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/user?code=%s", common.WeChatServerAddress, code), nil)
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/user?code=%s", common.WeChatServerAddress, url.QueryEscape(code)), nil)
if err != nil {
return "", err
}
@@ -121,6 +122,10 @@ func WeChatAuth(c *gin.Context) {
setupLogin(&user, c)
}
type wechatBindRequest struct {
Code string `json:"code"`
}
func WeChatBind(c *gin.Context) {
if !common.WeChatAuthEnabled {
c.JSON(http.StatusOK, gin.H{
@@ -129,7 +134,15 @@ func WeChatBind(c *gin.Context) {
})
return
}
code := c.Query("code")
var req wechatBindRequest
if err := common.DecodeJson(c.Request.Body, &req); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的请求",
})
return
}
code := req.Code
wechatId, err := getWeChatIdByCode(code)
if err != nil {
c.JSON(http.StatusOK, gin.H{
+2 -2
View File
@@ -36,10 +36,10 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
// OAuth routes - specific routes must come before :provider wildcard
apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode)
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), controller.EmailBind)
apiRouter.POST("/oauth/email/bind", middleware.CriticalRateLimit(), controller.EmailBind)
// Non-standard OAuth (WeChat, Telegram) - keep original routes
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), controller.WeChatBind)
apiRouter.POST("/oauth/wechat/bind", middleware.CriticalRateLimit(), controller.WeChatBind)
apiRouter.GET("/oauth/telegram/login", middleware.CriticalRateLimit(), controller.TelegramLogin)
apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), controller.TelegramBind)
// Standard OAuth providers (GitHub, Discord, OIDC, LinuxDO) - unified route
@@ -306,9 +306,9 @@ const PersonalSetting = () => {
const bindWeChat = async () => {
if (inputs.wechat_verification_code === '') return;
const res = await API.get(
`/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`,
);
const res = await API.post('/api/oauth/wechat/bind', {
code: inputs.wechat_verification_code,
});
const { success, message } = res.data;
if (success) {
showSuccess(t('微信账户绑定成功!'));
@@ -378,9 +378,10 @@ const PersonalSetting = () => {
return;
}
setLoading(true);
const res = await API.get(
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`,
);
const res = await API.post('/api/oauth/email/bind', {
email: inputs.email,
code: inputs.email_verification_code,
});
const { success, message } = res.data;
if (success) {
showSuccess(t('邮箱账户绑定成功!'));