Files
new-api/controller/wechat.go
T
CaIon 2906e4f779 fix(oauth): require account evidence before migrating legacy GitHub bindings
Legacy GitHub bindings stored the login name, while current bindings store
the numeric account ID. A legacy match no longer signs the user in on its
own. When the account has two-factor or a passkey, the binding is rewritten
only after that verification completes, inside the transaction that issues
the session. Otherwise it is rewritten when one of the GitHub account's
verified emails matches the account email. Without either, the login is
declined with guidance to sign in another way and relink GitHub in account
settings. All-digit login names never take part in the comparison, the bind
flow no longer compares login names, and every migration or declined
attempt is recorded as an account binding audit event.
2026-09-18 23:00:37 +08:00

201 lines
4.8 KiB
Go

package controller
import (
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/middleware"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type wechatLoginResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data string `json:"data"`
}
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, url.QueryEscape(code)), nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", common.WeChatServerToken)
client := http.Client{
Timeout: 5 * time.Second,
}
httpResponse, err := client.Do(req)
if err != nil {
return "", err
}
defer httpResponse.Body.Close()
var res wechatLoginResponse
err = common.DecodeJson(httpResponse.Body, &res)
if err != nil {
return "", err
}
if !res.Success {
return "", errors.New(res.Message)
}
if res.Data == "" {
return "", errors.New("验证码错误或已过期")
}
return res.Data, nil
}
func WeChatAuth(c *gin.Context) {
if !common.WeChatAuthEnabled {
c.JSON(http.StatusOK, gin.H{
"message": "管理员未开启通过微信登录以及注册",
"success": false,
})
return
}
code := c.Query("code")
wechatId, err := getWeChatIdByCode(code)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"message": err.Error(),
"success": false,
})
return
}
user := model.User{
WeChatId: wechatId,
}
if model.IsWeChatIdAlreadyTaken(wechatId) {
err := user.FillUserByWeChatId()
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
if user.Id == 0 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "用户已注销",
})
return
}
} else {
if common.RegisterEnabled {
user.Username = "wechat_" + strconv.Itoa(model.GetMaxUserId()+1)
user.DisplayName = "WeChat User"
user.Role = common.RoleCommonUser
user.Status = common.UserStatusEnabled
if err := user.Insert(0); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
} else {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "管理员关闭了新用户注册",
})
return
}
}
if user.Status != common.UserStatusEnabled {
c.JSON(http.StatusOK, gin.H{
"message": "用户已被封禁",
"success": false,
})
return
}
setupLogin(&user, nil, c)
}
type wechatBindRequest struct {
Code string `json:"code"`
}
func WeChatBind(c *gin.Context) {
identity, ok := middleware.GetSessionAuthIdentity(c)
if !ok {
writeSecurityOperationError(c, service.ErrAuthTokenInvalid)
return
}
succeeded, notificationFailed := false, false
defer func() {
recordUserSecurityAudit(c, identity.UserID, "user.binding_bind", map[string]any{"provider": "wechat", "success": succeeded, "notification_failed": notificationFailed})
}()
if !common.WeChatAuthEnabled {
c.JSON(http.StatusOK, gin.H{
"message": "管理员未开启通过微信登录以及注册",
"success": false,
})
return
}
var req wechatBindRequest
if err := common.DecodeJson(c.Request.Body, &req); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的请求",
})
return
}
code := strings.TrimSpace(req.Code)
context, err := common.Marshal(service.AccountBindingContext{Provider: "wechat", Code: code})
if err != nil {
writeSecurityOperationError(c, err)
return
}
if middleware.RequireSecurityProof(c, service.VerificationOperation{Scope: service.VerificationScopeAccountBind, Context: context}) == nil {
return
}
wechatId, err := getWeChatIdByCode(code)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"message": err.Error(),
"success": false,
})
return
}
if model.IsWeChatIdAlreadyTaken(wechatId) {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "该微信账号已被绑定",
})
return
}
// 只更新绑定列,避免完整用户快照覆盖并发的封禁、降权或分组变更。
if err := model.DB.Transaction(func(tx *gorm.DB) error {
return model.UpdateUserBindColumnForSessionWithTx(tx, identity, "wechat_id", wechatId)
}); err != nil {
writeSecurityOperationError(c, err)
return
}
succeeded = true
user, err := model.GetUserById(identity.UserID, false)
if err != nil {
writeSecurityOperationError(c, err)
return
}
notificationFailed = service.NotifyAccountSecurityChange(user.Email, "WeChat account linked") != nil
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": gin.H{"notification_warning": notificationFailed},
})
return
}