fix: 综合修复(请求头打印、邮箱/用户名匹配、密钥显示、签名校验、删除报错等)

Made-with: Cursor
This commit is contained in:
npc0-hue
2026-03-12 09:21:21 +08:00
parent 4b5e2eaf35
commit d1b32f4310
9 changed files with 180 additions and 125 deletions
+23 -4
View File
@@ -23,6 +23,13 @@ type ForwardProxyApi struct{}
// @Param path path string true "上游路径"
// @Router /gaia/forward/proxy/{path} [get,post,put,patch,delete]
func (f *ForwardProxyApi) ForwardProxy(c *gin.Context) {
// 打印请求 Header,便于排查转发问题
global.GVA_LOG.Info("ForwardProxy 请求头",
zap.Any("headers", c.Request.Header),
zap.String("method", c.Request.Method),
zap.String("path", c.Request.URL.Path),
)
// 1. 读取转发配置
integrate := systemIntegratedService.GetIntegratedConfig(gaiaModel.SystemIntegrationDingTalk)
configMap, err := systemIntegratedService.ParseDingTalkConfig(integrate.Config)
@@ -33,12 +40,24 @@ func (f *ForwardProxyApi) ForwardProxy(c *gin.Context) {
// 2. 获取并校验 forwarding token(存在有效 Token 即视为开启转发能力)
dingId := c.GetHeader("X-Ding-Id")
apiKey := c.GetHeader("X-Api-Key")
bearer := c.GetHeader("Authorization")
token := c.GetHeader("X-Forward-Token")
if len(bearer) > 7 && len(dingId) == 0 {
// 从 Token 中验签并提取 ding_id
dingId, err = systemIntegratedService.ParseForwardToken(bearer[7:], configMap.ForwardConfig.Tokens)
if err != nil {
if (len(bearer) > gaiaModel.BearerLength || len(apiKey) > gaiaModel.BearerLength) && len(dingId) == 0 {
if len(bearer) > gaiaModel.BearerLength {
if bearer[:gaiaModel.BearerLength] == "Bearer " {
bearer = bearer[gaiaModel.BearerLength:]
}
} else if len(apiKey) > gaiaModel.BearerLength {
if apiKey[:gaiaModel.BearerLength] == "Bearer " {
bearer = apiKey[gaiaModel.BearerLength:]
} else {
bearer = apiKey
}
}
if dingId, err = systemIntegratedService.ParseForwardToken(bearer, configMap.ForwardConfig.Tokens); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": gin.H{"message": "Token 验证失败: " + err.Error()}})
return
}
+51 -29
View File
@@ -2,10 +2,13 @@ package gaia
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
@@ -14,6 +17,7 @@ import (
"github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request"
gaiaResp "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/response"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
serviceGaia "github.com/flipped-aurora/gin-vue-admin/server/service/gaia"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -154,10 +158,11 @@ func (systemApi *SystemApi) GetForwardTokens(c *gin.Context) {
}
tokens := make([]gaiaResp.ForwardTokenInfo, 0, len(configMap.ForwardConfig.Tokens))
for _, token := range configMap.ForwardConfig.Tokens {
for i, token := range configMap.ForwardConfig.Tokens {
tokens = append(tokens, gaiaResp.ForwardTokenInfo{
ID: utils.AddAsteriskToString(token.ID),
ID: utils.AddAsteriskToString(token.TokenSecret),
CreatedAt: token.CreatedAt,
Seq: i + 1,
})
}
@@ -205,15 +210,24 @@ func (systemApi *SystemApi) CreateForwardToken(c *gin.Context) {
// 生成唯一 ID 和哈希
tokenID := "tok_" + uuid.New().String()
tokenHash := fmt.Sprintf("%x", sha256.Sum256([]byte(req.Token)))
// 生成 HMAC 签名密钥(仅创建时回传一次)
secretBytes := make([]byte, 32)
if _, err := rand.Read(secretBytes); err != nil {
response.FailWithMessage("生成 TokenSecret 失败:"+err.Error(), c)
return
}
tokenSecret := base64.RawURLEncoding.EncodeToString(secretBytes)
newToken := request.ForwardToken{
ID: tokenID,
TokenHash: tokenHash,
CreatedAt: time.Now(),
ID: tokenID,
TokenHash: tokenHash,
CreatedAt: time.Now(),
TokenSecret: tokenSecret,
}
// 添加到配置
configMap.ForwardConfig.Tokens = append(configMap.ForwardConfig.Tokens, newToken)
seq := len(configMap.ForwardConfig.Tokens) // 1..N
configJSON, _ := json.Marshal(configMap)
integrate.Config = string(configJSON)
@@ -225,9 +239,10 @@ func (systemApi *SystemApi) CreateForwardToken(c *gin.Context) {
// 返回明文 token(仅此次展示)
response.OkWithData(gin.H{
"id": tokenID,
"token": req.Token,
"created_at": newToken.CreatedAt,
"seq": seq,
"token": req.Token,
"token_secret": tokenSecret,
"created_at": newToken.CreatedAt,
}, c)
}
@@ -237,14 +252,19 @@ func (systemApi *SystemApi) CreateForwardToken(c *gin.Context) {
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param id path string true "Token ID"
// @Param seq path int true "Token 序列号(从列表获取,1..N"
// @Param password body string true "当前用户密码"
// @Success 200 {object} response.Response{msg=string} "删除成功"
// @Router /gaia/system/forward-tokens/:id [delete]
// @Router /gaia/system/forward-tokens/:seq [delete]
func (systemApi *SystemApi) DeleteForwardToken(c *gin.Context) {
tokenID := c.Param("id")
if tokenID == "" {
response.FailWithMessage("Token ID 不能为空", c)
seqStr := c.Param("seq")
if seqStr == "" {
response.FailWithMessage("Token 序列号不能为空", c)
return
}
seq, err := strconv.Atoi(seqStr)
if err != nil || seq <= 0 {
response.FailWithMessage("Token 序列号非法", c)
return
}
@@ -256,14 +276,22 @@ func (systemApi *SystemApi) DeleteForwardToken(c *gin.Context) {
return
}
// 验证当前用户密码
// 验证当前用户密码(使用 Dify account 密码体系)
userID := utils.GetUserUuid(c).String()
var user system.SysUser
if err := global.GVA_DB.Select("password").First(&user, userID).Error; err != nil {
if err := global.GVA_DB.Select("email").Where(
"uuid = ?", userID).First(&user).Error; err != nil {
response.FailWithMessage("查询用户失败:"+err.Error(), c)
return
}
if !utils.BcryptCheck(req.Password, user.Password) {
account, err := user.GetAccount()
if err != nil {
response.FailWithMessage("查询账号失败:"+err.Error(), c)
return
}
var pwd serviceGaia.PasswdEncode
if ok, pwdErr := pwd.ComparePassword(
req.Password, account.Password, account.PasswordSalt); pwdErr != nil || !ok {
response.FailWithMessage("密码错误", c)
return
}
@@ -272,34 +300,28 @@ func (systemApi *SystemApi) DeleteForwardToken(c *gin.Context) {
integrate := systemIntegratedService.GetIntegratedConfig(gaia.SystemIntegrationDingTalk)
var configMap request.DingTalkConfigRequest
if integrate.Config != "" {
if err := json.Unmarshal([]byte(integrate.Config), &configMap); err != nil {
if err = json.Unmarshal([]byte(integrate.Config), &configMap); err != nil {
response.FailWithMessage("解析配置失败:"+err.Error(), c)
return
}
}
// 查找并删除 token
found := false
newTokens := make([]request.ForwardToken, 0, len(configMap.ForwardConfig.Tokens))
for _, token := range configMap.ForwardConfig.Tokens {
if token.ID == tokenID {
found = true
continue
}
newTokens = append(newTokens, token)
}
if !found {
if seq > len(configMap.ForwardConfig.Tokens) {
response.FailWithMessage("Token 不存在", c)
return
}
idx := seq - 1
newTokens := make([]request.ForwardToken, 0, len(configMap.ForwardConfig.Tokens)-1)
newTokens = append(newTokens, configMap.ForwardConfig.Tokens[:idx]...)
newTokens = append(newTokens, configMap.ForwardConfig.Tokens[idx+1:]...)
// 更新配置
configMap.ForwardConfig.Tokens = newTokens
configJSON, _ := json.Marshal(configMap)
integrate.Config = string(configJSON)
if err := systemIntegratedService.SetIntegratedConfig(integrate, "", false); err != nil {
if err = systemIntegratedService.SetIntegratedConfig(integrate, "", false); err != nil {
response.FailWithMessage("保存失败:"+err.Error(), c)
return
}
+3 -2
View File
@@ -2,9 +2,10 @@ package response
import "time"
// ForwardTokenInfo 转发 Token 列表项(脱敏后的 Token ID
// ForwardTokenInfo 转发 Token 列表项(不暴露内部 ID
type ForwardTokenInfo struct {
ID string `json:"id"`
ID string `json:"id"` // token
Seq int `json:"seq"` // 1..N 序列号(用于删除)
CreatedAt time.Time `json:"created_at"`
}
@@ -5,6 +5,7 @@ const SystemIntegrationDingTalk = uint(1) // 钉钉集成
const SystemIntegrationWeiXin = uint(2) // 微信集成
const SystemIntegrationFeiShu = uint(3) // 飞书集成
const SystemIntegrationOAuth2 = uint(4) // OAuth2集成
const BearerLength = 7 // OAuth2集成
// SystemIntegration 系统集成表
type SystemIntegration struct {
+1 -1
View File
@@ -21,7 +21,7 @@ func (s *SystemRouter) InitSystemRouter(Router *gin.RouterGroup) {
// 转发 Token 管理
systemRouter.GET("forward-tokens", systemApi.GetForwardTokens) // 获取转发 Token 列表
systemRouter.POST("forward-tokens", systemApi.CreateForwardToken) // 新增转发 Token
systemRouter.DELETE("forward-tokens/:id", systemApi.DeleteForwardToken) // 删除转发 Token
systemRouter.DELETE("forward-tokens/:seq", systemApi.DeleteForwardToken) // 删除转发 Token(按序列号)
}
}
+22 -30
View File
@@ -16,7 +16,6 @@ import (
"io"
"net/http"
"net/url"
"os"
"strings"
)
@@ -241,6 +240,7 @@ func (e *SystemIntegratedService) DingTalkCodeLogin(req request.GaiaDingTalkLogi
}
var dingUser map[string]interface{}
fmt.Println("sssssssss", string(userBody))
if err = json.Unmarshal(userBody, &dingUser); err != nil {
return nil, fmt.Errorf("解析钉钉用户信息失败")
}
@@ -256,7 +256,8 @@ func (e *SystemIntegratedService) DingTalkCodeLogin(req request.GaiaDingTalkLogi
}
}
// 解析邮箱配置
// 解析用户名配置
var emailList []string
var configMap request.DingTalkConfigRequest
var emailConfig request.EmailApiConfig
if integrate.Config != "" {
@@ -271,13 +272,11 @@ func (e *SystemIntegratedService) DingTalkCodeLogin(req request.GaiaDingTalkLogi
}
}
// 优先通过邮箱 API 获取邮箱(新格式)
// 优先通过用户名 API 获取用户名(新格式)
if emailConfig.Enabled && dingId != "" {
email, apiErr := e.callEmailApi(dingId, emailConfig)
if apiErr == nil && email != "" {
global.GVA_LOG.Info("DingTalkCodeLogin: 通过第三方邮箱 API 获取邮箱",
zap.String("ding_id", dingId), zap.String("email", email))
sysUser, findErr := e.findUserByEmail(email)
emailList, err = e.callEmailApi(dingId, emailConfig)
if err == nil && len(emailList) > 0 {
sysUser, findErr := e.findUserByEmail(emailList)
if findErr != nil {
return nil, findErr
}
@@ -288,7 +287,7 @@ func (e *SystemIntegratedService) DingTalkCodeLogin(req request.GaiaDingTalkLogi
return &response.GaiaLoginResult{User: *sysUser, Token: token, RedirectURI: req.RedirectURI, State: req.State}, nil
}
global.GVA_LOG.Warn("DingTalkCodeLogin: 第三方邮箱 API 获取失败,尝试钉钉直接返回邮箱",
zap.String("ding_id", dingId), zap.Error(apiErr))
zap.String("ding_id", dingId), zap.Error(err))
}
// 回退:直接从钉钉用户信息获取邮箱
@@ -301,7 +300,7 @@ func (e *SystemIntegratedService) DingTalkCodeLogin(req request.GaiaDingTalkLogi
return nil, fmt.Errorf("钉钉未返回邮箱")
}
sysUser, err := e.findUserByEmail(email)
sysUser, err := e.findUserByEmail([]string{email})
if err != nil {
return nil, err
}
@@ -318,7 +317,8 @@ func getStringFromMap(m map[string]interface{}, keys ...string) string {
continue
}
if v, ok := m[k]; ok && v != nil {
if s, ok := v.(string); ok {
var s string
if s, ok = v.(string); ok {
return s
}
}
@@ -384,21 +384,14 @@ func getStringByPathOrKeys(m map[string]interface{}, path string, fallbackKeys .
return getStringFromMap(m, fallbackKeys...)
}
// findUserByEmail 按邮箱查找已存在的用户(需在 gaia.accounts 中有对应记录方可签发 JWT)
func (e *SystemIntegratedService) findUserByEmail(email string) (*system.SysUser, error) {
var u system.SysUser
var mailList []string
mailList = append(mailList, email)
parts := strings.Split(email, "@")
defaultMail := os.Getenv(gaia.EmailDomainEnv)
if len(defaultMail) > 0 && len(parts) == 2 {
mailList = append(mailList, parts[0]+"@"+defaultMail)
}
// findUserByEmail 按username查找已存在的用户(需在 gaia.accounts 中有对应记录方可签发 JWT)
func (e *SystemIntegratedService) findUserByEmail(mailList []string) (*system.SysUser, error) {
// 查询关联邮箱
var u system.SysUser
if err := global.GVA_DB.Where("email IN (?)", mailList).Preload(
"Authorities").Preload("Authority").First(&u).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("邮箱%s尚未开通账号,请联系管理员", email)
return nil, fmt.Errorf("%s尚未开通账号,请联系管理员", mailList[0])
}
return nil, err
}
@@ -410,20 +403,19 @@ func (e *SystemIntegratedService) findUserByEmail(email string) (*system.SysUser
}
// findUserByEmailOrPhone 按邮箱或用户唯一标识(如手机号)查找用户,优先邮箱
func (e *SystemIntegratedService) findUserByEmailOrPhone(email, userID string) (*system.SysUser, error) {
if email != "" {
u, err := e.findUserByEmail(email)
if err == nil {
func (e *SystemIntegratedService) findUserByEmailOrPhone(mail, userID string) (u *system.SysUser, err error) {
if mail != "" {
if u, err = e.findUserByEmail([]string{mail}); err == nil {
return u, nil
}
// 仅当“未开通”时再尝试按 userID(phone) 查,其他错误直接返回
if err != nil && !strings.Contains(err.Error(), "尚未开通") {
if !strings.Contains(err.Error(), "尚未开通") {
return nil, err
}
}
if userID != "" {
var u system.SysUser
if err := global.GVA_DB.Where("phone = ?", userID).Preload("Authorities").Preload("Authority").First(&u).Error; err != nil {
if err = global.GVA_DB.Where("phone = ?", userID).Preload(
"Authorities").Preload("Authority").First(&u).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("该用户唯一标识尚未开通后台账号,请联系管理员")
}
@@ -432,7 +424,7 @@ func (e *SystemIntegratedService) findUserByEmailOrPhone(email, userID string) (
if u.Enable != 1 {
return nil, fmt.Errorf("账号已被禁用")
}
return &u, nil
return u, nil
}
return nil, fmt.Errorf("无法从 OAuth2 用户信息中获取邮箱或用户唯一标识")
}
+21 -22
View File
@@ -5,7 +5,6 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
@@ -727,23 +726,32 @@ func buildURL(baseURL string, config request.EmailApiConfig, dingId string) stri
}
// callEmailApi 调用第三方邮箱 API,使用 ding_id(用户名) 获取邮箱
func (e *SystemIntegratedService) callEmailApi(dingId string, config request.EmailApiConfig) (string, error) {
func (e *SystemIntegratedService) callEmailApi(
dingId string, config request.EmailApiConfig) (mailList []string, err error) {
// init
respBody, _, err := e.doEmailApiRequest(dingId, config)
if err != nil {
return "", err
return mailList, err
}
var respJSON map[string]interface{}
if err = json.Unmarshal(respBody, &respJSON); err != nil {
return "", fmt.Errorf("解析响应 JSON 失败:%s", err.Error())
return mailList, fmt.Errorf("解析响应 JSON 失败:%s", err.Error())
}
email := extractJSONPathAdvanced(respJSON, config.ResponseEmailField)
if email == "" {
return "", fmt.Errorf("响应中未找到邮箱(路径:%s)", config.ResponseEmailField)
return mailList, fmt.Errorf("响应中未找到邮箱(路径:%s)", config.ResponseEmailField)
}
//
mailList = append(mailList, email)
parts := strings.Split(email, "@")
defaultMail := os.Getenv(gaia.EmailDomainEnv)
if len(defaultMail) > 0 && len(parts) > 1 && len(parts[0]) > 0 {
mailList = append(mailList, parts[0]+"@"+defaultMail)
}
return email, nil
return mailList, nil
}
// doEmailApiRequest 构建并执行邮箱 API 请求,返回响应体字节和状态码
@@ -920,10 +928,8 @@ func (e *SystemIntegratedService) ParseForwardToken(
if t.TokenSecret == "" {
continue
}
secret, err := hex.DecodeString(t.TokenSecret)
if err != nil {
continue
}
// 直接使用原始字节作为 HMAC 密钥,兼容任意字符串格式的密钥
secret := []byte(t.TokenSecret)
// 验证 HMAC 签名
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(payloadB64))
@@ -952,13 +958,13 @@ func (e *SystemIntegratedService) ParseForwardToken(
// ResolveAccountByDingId 通过钉钉 ID 解析 gaia account_id
// 解析顺序:Redis 缓存 → AccountDingTalkExtend 本地表 → 第三方 EmailApi(邮箱 API
func (e *SystemIntegratedService) ResolveAccountByDingId(dingId string, apiConfig request.EmailApiConfig) (string, error) {
ctx := context.Background()
redisKey := "gaia:forward:ding:" + dingId
func (e *SystemIntegratedService) ResolveAccountByDingId(
dingId string, apiConfig request.EmailApiConfig) (string, error) {
// 1. 查 Redis 缓存
ctx := context.Background()
redisKey := "gaia:forward:ding:" + dingId
if cached, err := global.GVA_REDIS.Get(ctx, redisKey).Result(); err == nil && cached != "" {
global.GVA_LOG.Info("ResolveAccountByDingId: Redis 命中", zap.String("ding_id", dingId), zap.String("account_id", cached))
return cached, nil
}
@@ -966,7 +972,6 @@ func (e *SystemIntegratedService) ResolveAccountByDingId(dingId string, apiConfi
var extend gaia.AccountDingTalkExtend
if err := global.GVA_DB.Where("ding_talk = ?", dingId).First(&extend).Error; err == nil {
accountID := extend.ID.String()
global.GVA_LOG.Info("ResolveAccountByDingId: 本地表命中", zap.String("ding_id", dingId), zap.String("account_id", accountID))
global.GVA_REDIS.Set(ctx, redisKey, accountID, 24*time.Hour)
return accountID, nil
}
@@ -984,7 +989,7 @@ func (e *SystemIntegratedService) ResolveAccountByDingId(dingId string, apiConfi
// 4. 按邮箱查 accounts 表(匹配 email 字段)
var account gaia.Account
if err = global.GVA_DB.Where("email = ?", email).First(&account).Error; err != nil {
return "", fmt.Errorf("邮箱 %s 不存在(来自第三方邮箱 API)", email)
return "", fmt.Errorf("用户 %s 不存在(来自第三方邮箱 API)", email)
}
accountID := account.ID.String()
@@ -997,11 +1002,5 @@ func (e *SystemIntegratedService) ResolveAccountByDingId(dingId string, apiConfi
// 6. 写 Redis 缓存
global.GVA_REDIS.Set(ctx, redisKey, accountID, 24*time.Hour)
global.GVA_LOG.Info("ResolveAccountByDingId: 第三方邮箱 API 解析成功",
zap.String("ding_id", dingId),
zap.String("email", email),
zap.String("account_id", accountID))
return accountID, nil
}
+3 -3
View File
@@ -80,10 +80,10 @@ export const createForwardToken = (data) => {
// @Tags systrm
// @Summary 删除转发 Token
// @Security ApiKeyAuth
// @Router /gaia/system/forward-tokens/:id [delete]
export const deleteForwardToken = (id, password) => {
// @Router /gaia/system/forward-tokens/:seq [delete]
export const deleteForwardToken = (seq, password) => {
return service({
url: `/gaia/system/forward-tokens/${id}`,
url: `/gaia/system/forward-tokens/${seq}`,
method: 'delete',
data: { password },
})
@@ -96,17 +96,17 @@
<div class="card-section">
<div class="section-title">
第三方邮箱配置
第三方用户名提取配置
</div>
<div class="bg-gray-50 dark:bg-slate-800 p-5 border dark:border-slate-700 rounded-lg">
<!-- 基础配置 -->
<div class="flex items-center mb-4">
<span class="info-label">邮箱详情的URL:</span>
<span class="info-label">第三方的URL:</span>
<el-input
v-if="openEdit"
v-model="emailApiConfig.url"
class="info-value flex-1"
placeholder="请输入钉钉通过用户名获取邮箱地址的链接地址"
placeholder="请输入钉钉id获取用户名的链接地址"
/>
<span v-else class="info-value">{{ emailApiConfig.url || '未配置' }}</span>
</div>
@@ -403,9 +403,9 @@
</div>
</div>
<!-- 邮箱信息提取路径 -->
<!-- 用户名路径 -->
<div class="flex items-center mb-4 mt-4">
<span class="info-label">邮箱信息提取:</span>
<span class="info-label">用户名路径:</span>
<el-input
v-if="openEdit"
v-model="emailApiConfig.response_email_field"
@@ -453,7 +453,7 @@
</div>
<el-table :data="forwardTokenList" border size="small" class="w-full">
<el-table-column label="Token ID" prop="id" min-width="240">
<el-table-column label="token" prop="seq" align="center">
<template #default="{ row }">
<span class="font-mono text-xs">{{ row.id }}</span>
</template>
@@ -465,7 +465,7 @@
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ row }">
<el-button type="danger" link size="small" @click="openDeleteTokenDialog(row.id)">
<el-button type="danger" link size="small" @click="openDeleteTokenDialog(row.seq)">
删除
</el-button>
</template>
@@ -499,29 +499,47 @@
<!-- 新增 Token 弹窗前端随机生成 保存到后端 自动复制到剪贴板并提示 -->
<el-dialog v-model="showCreateTokenDialog" title="新增转发 Token" width="480px" :close-on-click-modal="false">
<div v-if="!newTokenValue">
<div v-if="!newPlainToken">
<p class="text-gray-600 mb-4">
点击生成并保存将随机生成 Token保存后会自动复制到系统剪贴板请粘贴到安全位置保管Token 仅展示一次
点击生成并保存将随机生成 Token并返回两种凭证
<br />2Token Secret用于生成 Authorization: Bearer ... 的签名密钥
<br />两者仅展示一次请务必复制到安全位置保管
</p>
</div>
<div v-else>
<el-alert type="success" title="Token 已生成并已复制到剪贴板,请妥善保管。此处仅展示一次。" :closable="false" class="mb-4" />
<el-input v-model="newTokenValue" readonly>
<template #append>
<el-button @click="copyToken(newTokenValue)">
复制
</el-button>
</template>
</el-input>
<el-alert
type="success"
title="Token 已生成,并已将 Token Secret 复制到剪贴板,请妥善保管(以下两项仅展示一次)。"
:closable="false"
class="mb-4"
/>
<div class="mb-3">
<div class="text-xs text-gray-500 mb-1">明文 Token可用于 X-Forward-Token</div>
<el-input v-model="newPlainToken" readonly>
<template #append>
<el-button @click="copyToken(newPlainToken)">
复制 Token
</el-button>
</template>
</el-input>
</div>
</div>
<template #footer>
<el-button v-if="!newTokenValue" @click="showCreateTokenDialog = false">
<el-button v-if="!newPlainToken" @click="showCreateTokenDialog = false">
取消
</el-button>
<el-button v-if="!newTokenValue" type="primary" :loading="creatingToken" @click="handleCreateToken">
<el-button v-if="!newPlainToken" type="primary" :loading="creatingToken" @click="handleCreateToken">
生成并保存
</el-button>
<el-button v-if="newTokenValue" type="primary" @click="showCreateTokenDialog = false; newTokenValue = ''; initForm()">
<el-button
v-else
type="primary"
@click="
showCreateTokenDialog = false;
newPlainToken = '';
initForm();
"
>
完成
</el-button>
</template>
@@ -534,7 +552,7 @@
</p>
<el-input v-model="deleteTokenPassword" type="password" placeholder="请输入您的登录密码" show-password />
<template #footer>
<el-button @click="showDeleteTokenDialog = false; deleteTokenPassword = ''; deletingTokenId = ''">
<el-button @click="showDeleteTokenDialog = false; deleteTokenPassword = ''; deletingTokenSeq = null">
取消
</el-button>
<el-button type="danger" :loading="deletingToken" @click="handleDeleteToken">
@@ -624,7 +642,7 @@
class="ml-auto"
@click="selectJsonField(path, item)"
>
选为邮箱字段
选为用户名字段
</el-button>
</div>
</div>
@@ -658,10 +676,9 @@ import { ref, computed, watch, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { QuestionFilled, Loading } from '@element-plus/icons-vue'
import { useClipboard } from '@vueuse/core'
const { copy, isSupported } = useClipboard()
import { getSystemDingTalk, setSystemDingTalk, getForwardTokens, createForwardToken, deleteForwardToken, testEmailApiConfig, getDingTalkTestAuthUrl } from "@/api/gaia/system";
const { copy: copyToClipboard, isSupported: isClipboardSupported } = useClipboard()
defineOptions({
name: 'IntegratedDingTalk',
})
@@ -710,13 +727,15 @@ const forwardTokenList = ref([])
// 新增 Token 弹窗(前端随机生成 → 保存 → 复制到剪贴板)
const showCreateTokenDialog = ref(false)
const newTokenValue = ref('')
// 明文 token:可用于 X-Forward-Token 模式
const newPlainToken = ref('')
// token_secret:用于生成 Authorization: Bearer ... 的 HMAC 密钥
const creatingToken = ref(false)
// 删除 Token 弹窗
const showDeleteTokenDialog = ref(false)
const deleteTokenPassword = ref('')
const deletingTokenId = ref('')
const deletingTokenSeq = ref(null)
const deletingToken = ref(false)
// 格式化日期
@@ -747,12 +766,13 @@ const generateToken = () => {
const copyToken = async (token) => {
if (!token) return
try {
if (isClipboardSupported.value) {
await copyToClipboard(token)
if (copy) {
await copy(token)
} else {
await navigator.clipboard.writeText(token)
}
ElMessage({ type: 'success', message: 'Token 已复制到剪贴板' })
// eslint-disable-next-line no-unused-vars
} catch (e) {
ElMessage({ type: 'warning', message: '复制失败,请手动复制' })
}
@@ -765,8 +785,9 @@ const handleCreateToken = async () => {
try {
const res = await createForwardToken({ token })
if (res.code === 0) {
newTokenValue.value = res.data?.token ?? token
await copyToken(newTokenValue.value)
newPlainToken.value = res.data?.token_secret || ''
// 默认复制 token_secret,方便用于 Bearer Token 生成
await copyToken(newPlainToken.value)
} else {
ElMessage({ type: 'error', message: res.msg || '保存失败' })
}
@@ -776,8 +797,8 @@ const handleCreateToken = async () => {
}
// 打开删除 Token 弹窗
const openDeleteTokenDialog = (id) => {
deletingTokenId.value = id
const openDeleteTokenDialog = (seq) => {
deletingTokenSeq.value = seq
deleteTokenPassword.value = ''
showDeleteTokenDialog.value = true
}
@@ -789,13 +810,13 @@ const handleDeleteToken = async () => {
return
}
deletingToken.value = true
const res = await deleteForwardToken(deletingTokenId.value, deleteTokenPassword.value)
const res = await deleteForwardToken(deletingTokenSeq.value, deleteTokenPassword.value)
deletingToken.value = false
if (res.code === 0) {
ElMessage({ type: 'success', message: '删除成功' })
showDeleteTokenDialog.value = false
deleteTokenPassword.value = ''
deletingTokenId.value = ''
deletingTokenSeq.value = null
await initForm()
} else {
ElMessage({ type: 'error', message: res.msg || '删除失败' })