mirror of
https://gh-proxy.org/https://github.com/klinsmaya/new-api.git
synced 2026-11-08 17:36:59 +08:00
Require scoped, single-use verification for account bindings and password operations. Bind OAuth authorization and email confirmations to the initiating session; preserve the last usable login method and audit operation outcomes. Apply Unicode-aware password length limits, Argon2id writes with bcrypt compatibility, and long-password encryption. Return has_password with the existing profile SELECT without extra queries. Reuse the existing security dialogs and add all seven locale translations. Validation: - Go: go test ./common ./model ./service ./middleware ./controller ./router -count=1 - DB: SQLite 3.50.4, MySQL 8.4.11, PostgreSQL 16.15; separate main/log databases - MySQL/PostgreSQL: TEST_SECURITY_DIALECT=<dialect> with TEST_<DIALECT>_DSN, go test ./controller -run '^(TestSecurityAccount|TestSecurityEnrollment|TestGenerateOAuthCode|TestOAuthBind|TestTelegramOAuth)' -count=1 -v - Web: relevant Vitest suites, bun run typecheck, targeted oxlint/format, bun run i18n:sync, and bun run build Roll out dual-format readers to every instance with ACCOUNT_PASSWORD_HASH_ALGORITHM=bcrypt before enabling Argon2id writes and the new UI. Rollbacks must retain Argon2id and v2 envelope readers. Relevant controls: ASVS 5.0.0 6.2.1-6.2.3, 6.2.5-6.2.9, 6.3.7, 7.4.3, 7.5.1; this change does not assert application-wide ASVS certification.
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, 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]interface{}{"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
|
|
}
|