mirror of
https://github.com/YFGaia/dify-plus.git
synced 2026-06-04 10:14:00 +08:00
fix: 综合修复(请求头打印、邮箱/用户名匹配、密钥显示、签名校验、删除报错等)
Made-with: Cursor
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(按序列号)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 用户信息中获取邮箱或用户唯一标识")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 />2)Token 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 || '删除失败' })
|
||||
|
||||
Reference in New Issue
Block a user