Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
201 lines
4.8 KiB
Go
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
|
|
}
|