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
+14 -21
View File
@@ -8,6 +8,7 @@ import (
"github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/middleware" "github.com/QuantumNous/new-api/middleware"
"github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/oauth" "github.com/QuantumNous/new-api/oauth"
@@ -116,7 +117,6 @@ func GetStatus(c *gin.Context) {
"user_agreement_enabled": legalSetting.UserAgreement != "", "user_agreement_enabled": legalSetting.UserAgreement != "",
"privacy_policy_enabled": legalSetting.PrivacyPolicy != "", "privacy_policy_enabled": legalSetting.PrivacyPolicy != "",
"checkin_enabled": operation_setting.GetCheckinSetting().Enabled, "checkin_enabled": operation_setting.GetCheckinSetting().Enabled,
"_qn": "new-api",
} }
// 根据启用状态注入可选内容 // 根据启用状态注入可选内容
@@ -308,31 +308,24 @@ func SendPasswordResetEmail(c *gin.Context) {
}) })
return return
} }
if !model.IsEmailAlreadyTaken(email) { if model.IsEmailAlreadyTaken(email) {
c.JSON(http.StatusOK, gin.H{ code := common.GenerateVerificationCode(0)
"success": false, common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose)
"message": "该邮箱地址未注册", link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", system_setting.ServerAddress, email, code)
}) subject := fmt.Sprintf("%s密码重置", common.SystemName)
return content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
} "<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+
code := common.GenerateVerificationCode(0) "<p>如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s </p>"+
common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose) "<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, link, common.VerificationValidMinutes)
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", system_setting.ServerAddress, email, code) err := common.SendEmail(subject, email, content)
subject := fmt.Sprintf("%s密码重置", common.SystemName) if err != nil {
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+ logger.LogError(c.Request.Context(), fmt.Sprintf("failed to send password reset email to %s: %s", email, err.Error()))
"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+ }
"<p>如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s </p>"+
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, link, common.VerificationValidMinutes)
err := common.SendEmail(subject, email, content)
if err != nil {
common.ApiError(c, err)
return
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",
}) })
return
} }
type PasswordResetRequest struct { type PasswordResetRequest struct {
+12 -2
View File
@@ -925,9 +925,19 @@ func ManageUser(c *gin.Context) {
return return
} }
type emailBindRequest struct {
Email string `json:"email"`
Code string `json:"code"`
}
func EmailBind(c *gin.Context) { func EmailBind(c *gin.Context) {
email := c.Query("email") var req emailBindRequest
code := c.Query("code") 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) { if !common.VerifyCodeWithKey(email, code, common.EmailVerificationPurpose) {
common.ApiErrorI18n(c, i18n.MsgUserVerificationCodeError) common.ApiErrorI18n(c, i18n.MsgUserVerificationCodeError)
return return
+15 -2
View File
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"time" "time"
@@ -25,7 +26,7 @@ func getWeChatIdByCode(code string) (string, error) {
if code == "" { if code == "" {
return "", errors.New("无效的参数") 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 { if err != nil {
return "", err return "", err
} }
@@ -121,6 +122,10 @@ func WeChatAuth(c *gin.Context) {
setupLogin(&user, c) setupLogin(&user, c)
} }
type wechatBindRequest struct {
Code string `json:"code"`
}
func WeChatBind(c *gin.Context) { func WeChatBind(c *gin.Context) {
if !common.WeChatAuthEnabled { if !common.WeChatAuthEnabled {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
@@ -129,7 +134,15 @@ func WeChatBind(c *gin.Context) {
}) })
return 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) wechatId, err := getWeChatIdByCode(code)
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ 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) apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
// OAuth routes - specific routes must come before :provider wildcard // OAuth routes - specific routes must come before :provider wildcard
apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode) 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 // Non-standard OAuth (WeChat, Telegram) - keep original routes
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth) 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/login", middleware.CriticalRateLimit(), controller.TelegramLogin)
apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), controller.TelegramBind) apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), controller.TelegramBind)
// Standard OAuth providers (GitHub, Discord, OIDC, LinuxDO) - unified route // Standard OAuth providers (GitHub, Discord, OIDC, LinuxDO) - unified route
@@ -306,9 +306,9 @@ const PersonalSetting = () => {
const bindWeChat = async () => { const bindWeChat = async () => {
if (inputs.wechat_verification_code === '') return; if (inputs.wechat_verification_code === '') return;
const res = await API.get( const res = await API.post('/api/oauth/wechat/bind', {
`/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`, code: inputs.wechat_verification_code,
); });
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
showSuccess(t('微信账户绑定成功!')); showSuccess(t('微信账户绑定成功!'));
@@ -378,9 +378,10 @@ const PersonalSetting = () => {
return; return;
} }
setLoading(true); setLoading(true);
const res = await API.get( const res = await API.post('/api/oauth/email/bind', {
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`, email: inputs.email,
); code: inputs.email_verification_code,
});
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
showSuccess(t('邮箱账户绑定成功!')); showSuccess(t('邮箱账户绑定成功!'));