From 64c7f3de25795f7c82c99cef060f1ba3981b3045 Mon Sep 17 00:00:00 2001 From: npc0-hue Date: Thu, 17 Apr 2025 14:34:26 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E6=96=B0=E5=A2=9Eoauth2.0?= =?UTF-8?q?=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/server/api/v1/gaia/enter.go | 1 + admin/server/api/v1/gaia/system.go | 6 +- admin/server/api/v1/gaia/system_oauth2.go | 86 +++++ admin/server/api/v1/system/sys_user_extend.go | 28 ++ admin/server/initialize/gorm.go | 3 +- admin/server/model/gaia/request/system.go | 25 ++ admin/server/model/gaia/system_integration.go | 2 + admin/server/router/gaia/enter.go | 1 + admin/server/router/gaia/system.go | 12 +- admin/server/router/system/sys_base.go | 3 +- admin/server/service/gaia/system.go | 126 ++++++- admin/server/source/system/api.go | 5 + admin/server/source/system/casbin.go | 5 + admin/server/source/system/menu.go | 5 + admin/web/src/api/gaia/system.js | 27 ++ admin/web/src/pathInfo.json | 2 +- .../view/systemIntegrated/oauth2/index.vue | 349 ++++++++++++++++++ api/controllers/console/auth/oauth.py | 11 +- api/libs/oauth.py | 128 +++++-- api/models/system_extend.py | 3 + api/services/feature_service.py | 13 +- .../header/account-dropdown/index.tsx | 5 +- web/app/signin/components/oauth2.tsx | 36 ++ web/app/signin/normalForm.tsx | 9 +- web/models/common.ts | 6 + web/types/feature.ts | 1 + 26 files changed, 817 insertions(+), 81 deletions(-) create mode 100644 admin/server/api/v1/gaia/system_oauth2.go create mode 100644 admin/server/model/gaia/request/system.go create mode 100644 admin/web/src/view/systemIntegrated/oauth2/index.vue create mode 100644 web/app/signin/components/oauth2.tsx diff --git a/admin/server/api/v1/gaia/enter.go b/admin/server/api/v1/gaia/enter.go index 262a11a20..47fbe72b1 100644 --- a/admin/server/api/v1/gaia/enter.go +++ b/admin/server/api/v1/gaia/enter.go @@ -8,6 +8,7 @@ type ApiGroup struct { TenantsApi SystemApi TestApi + SystemOAuth2Api } var ( diff --git a/admin/server/api/v1/gaia/system.go b/admin/server/api/v1/gaia/system.go index b5bf6735d..2779f2e72 100644 --- a/admin/server/api/v1/gaia/system.go +++ b/admin/server/api/v1/gaia/system.go @@ -1,7 +1,6 @@ package gaia import ( - "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" "github.com/gin-gonic/gin" @@ -20,7 +19,7 @@ type SystemApi struct{} // @Router /gaia/system/dingtalk [get] func (systemApi *SystemApi) GetDingTalk(c *gin.Context) { var config = make(map[string]interface{}) - config["host"] = global.GVA_CONFIG.Gaia.Url + config["host"] = c.Request.Header.Get("Referer") config["config"] = systemIntegratedService.GetIntegratedConfig(gaia.SystemIntegrationDingTalk) response.OkWithData(config, c) } @@ -43,7 +42,8 @@ func (systemApi *SystemApi) SetDingTalk(c *gin.Context) { } // update req.Classify = gaia.SystemIntegrationDingTalk - if err = systemIntegratedService.SetIntegratedConfig(req, req.Test); err != nil { + if err = systemIntegratedService.SetIntegratedConfig( + req, "", req.Test); err != nil { response.FailWithMessage(err.Error(), c) return } diff --git a/admin/server/api/v1/gaia/system_oauth2.go b/admin/server/api/v1/gaia/system_oauth2.go new file mode 100644 index 000000000..06cbbca68 --- /dev/null +++ b/admin/server/api/v1/gaia/system_oauth2.go @@ -0,0 +1,86 @@ +package gaia + +import ( + "encoding/json" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type SystemOAuth2Api struct{} + +// GetOAuth2Config +// @Tags System +// @Summary 获取OAuth2集成配置 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /gaia/system/oauth2 [get] +func (s *SystemOAuth2Api) GetOAuth2Config(c *gin.Context) { + var configMap request.SystemOAuth2Request + var config = make(map[string]interface{}) + // 直接使用service层获取request.SystemOAuth2Request结构 + integrated := systemIntegratedService.GetIntegratedConfig(gaia.SystemIntegrationOAuth2) + _ = json.Unmarshal([]byte(integrated.Config), &configMap) + // setting + configMap.AppID = integrated.AppID + configMap.Status = integrated.Status + configMap.Classify = integrated.Classify + configMap.AppSecret = integrated.AppSecret + config["host"] = c.Request.Header.Get("Referer") + config["config"] = configMap + response.OkWithData(config, c) +} + +// SetOAuth2Config +// @Tags System +// @Summary 修改OAuth2集成配置 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.SystemOAuth2Request true "修改数据" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}" +// @Router /gaia/system/oauth2 [post] +func (s *SystemOAuth2Api) SetOAuth2Config(c *gin.Context) { + var req request.SystemOAuth2Request + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + // 序列化为JSON + configBytes, err := json.Marshal(&map[string]string{ + "server_url": req.ServerURL, + "authorize_url": req.AuthorizeURL, + "token_url": req.TokenURL, + "userinfo_url": req.UserinfoURL, + "logout_url": req.LogoutURL, + "user_name_field": req.UserNameField, + "user_email_field": req.UserEmailField, + "user_id_field": req.UserIDField, + }) + if err != nil { + global.GVA_LOG.Error("序列化OAuth2配置失败!", zap.Error(err)) + response.FailWithMessage("配置序列化失败", c) + return + } + + // 更新配置 + if err = systemIntegratedService.SetIntegratedConfig(gaia.SystemIntegration{ + Classify: gaia.SystemIntegrationOAuth2, + Config: string(configBytes), + AppSecret: req.AppSecret, + Status: req.Status, + AppID: req.AppID, + }, req.Code, req.Test); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + response.OkWithData("设置成功", c) +} diff --git a/admin/server/api/v1/system/sys_user_extend.go b/admin/server/api/v1/system/sys_user_extend.go index 853fb3ec7..25ae7d8fc 100644 --- a/admin/server/api/v1/system/sys_user_extend.go +++ b/admin/server/api/v1/system/sys_user_extend.go @@ -4,6 +4,8 @@ import ( "encoding/json" "errors" "fmt" + "net/http" + "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" @@ -156,3 +158,29 @@ func (b *BaseApi) OaLogin(c *gin.Context) { return } + +// Extend Start: oAuth2 callback verification + +// OAuth2Callback +// @Tags Base +// @Summary oAuth2回调校验 +// @Produce application/json +// @Param code query string true "授权码" +// @Success 200 {string} string "返回HTML内容,包含授权码" +// @Router /base/auth2/callback [get] +func (b *BaseApi) OAuth2Callback(c *gin.Context) { + // 获取授权码 + code := c.Request.URL.Query().Get("code") + if code == "" { + global.GVA_LOG.Error("OAuth2回调未获取到授权码") + c.String(http.StatusBadRequest, "授权码不能为空") + return + } + + // 返回HTML内容,通过BroadcastChannel将授权码传递给前端 + htmlContent := fmt.Sprintf(``, code) + c.Header("Content-Type", "text/html; charset=utf-8") + c.String(http.StatusOK, htmlContent) +} + +// Extend Stop: oAuth2 callback verification diff --git a/admin/server/initialize/gorm.go b/admin/server/initialize/gorm.go index 43bce835f..bb138f203 100644 --- a/admin/server/initialize/gorm.go +++ b/admin/server/initialize/gorm.go @@ -1,11 +1,12 @@ package initialize import ( + "os" + "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/example" "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" "github.com/flipped-aurora/gin-vue-admin/server/model/system" - "os" "go.uber.org/zap" "gorm.io/gorm" diff --git a/admin/server/model/gaia/request/system.go b/admin/server/model/gaia/request/system.go new file mode 100644 index 000000000..5a5b63345 --- /dev/null +++ b/admin/server/model/gaia/request/system.go @@ -0,0 +1,25 @@ +package request + +// SystemOAuth2Error OAuth2 错误返回 +type SystemOAuth2Error struct { + Code int `json:"code" gorm:"comment:分类"` // 错误代码 + Info string `json:"info" gorm:"comment:错误详情"` // 错误详情 +} + +// SystemOAuth2Request OAuth2 集成配置 +type SystemOAuth2Request struct { + Classify uint `json:"classify" gorm:"comment:分类"` // 分类 + Status bool `json:"status" gorm:"comment:状态"` // 状态 + ServerURL string `json:"server_url" gorm:"comment:服务器地址"` // OAuth2 服务器地址 + AuthorizeURL string `json:"authorize_url" gorm:"comment:申请认证的URL"` // 申请认证的URL + TokenURL string `json:"token_url" gorm:"comment:获取Token的URL"` // 获取Token的URL + UserinfoURL string `json:"userinfo_url" gorm:"comment:获取用户信息URL"` // 获取用户信息的URL + LogoutURL string `json:"logout_url" gorm:"comment:退出登录回调URL"` // 退出登录回调URL + AppID string `json:"app_id" gorm:"comment:Client ID"` // Client ID + AppSecret string `json:"app_secret" gorm:"comment:Client Secret"` // Client Secret + UserNameField string `json:"user_name_field" gorm:"comment:用户名字段"` // 用户名字段 + UserEmailField string `json:"user_email_field" gorm:"comment:邮箱字段"` // 邮箱字段 + UserIDField string `json:"user_id_field" gorm:"comment:用户唯一标识字段"` // 用户唯一标识字段 + Test bool `json:"test" gorm:"default:0;comment:是否测试链接联通性"` // 是否测试链接联通性 + Code string `json:"code" gorm:"default:0;comment:code代码"` // code代码 +} diff --git a/admin/server/model/gaia/system_integration.go b/admin/server/model/gaia/system_integration.go index df41ef4b3..d83f24013 100644 --- a/admin/server/model/gaia/system_integration.go +++ b/admin/server/model/gaia/system_integration.go @@ -3,6 +3,7 @@ package gaia const SystemIntegrationDingTalk = uint(1) // 钉钉集成 const SystemIntegrationWeiXin = uint(2) // 微信集成 const SystemIntegrationFeiShu = uint(3) // 飞书集成 +const SystemIntegrationOAuth2 = uint(4) // OAuth2集成 // SystemIntegration 系统集成表 type SystemIntegration struct { @@ -15,6 +16,7 @@ type SystemIntegration struct { AppKey string `json:"app_key" gorm:"default:;comment:加密key"` AppSecret string `json:"app_secret" gorm:"default:;comment:加密密钥"` Test bool `json:"test" gorm:"default:0;comment:是否测试链接联通性"` + Config string `json:"config" gorm:"type:text;default:;comment:其他配置"` } // TableName system_integration_extend表 SystemIntegration自定义表名 system_integration_extend diff --git a/admin/server/router/gaia/enter.go b/admin/server/router/gaia/enter.go index db632ba3b..39fa09088 100644 --- a/admin/server/router/gaia/enter.go +++ b/admin/server/router/gaia/enter.go @@ -14,6 +14,7 @@ var ( dashboardApi = api.ApiGroupApp.GaiaApiGroup.DashboardApi tenantsApi = api.ApiGroupApp.GaiaApiGroup.TenantsApi ) +var systemOAuth2Api = api.ApiGroupApp.GaiaApiGroup.SystemOAuth2Api var systemApi = api.ApiGroupApp.GaiaApiGroup.SystemApi var quotaApi = api.ApiGroupApp.GaiaApiGroup.QuotaApi var testApi = api.ApiGroupApp.GaiaApiGroup.TestApi diff --git a/admin/server/router/gaia/system.go b/admin/server/router/gaia/system.go index 7e640b24c..5021c20db 100644 --- a/admin/server/router/gaia/system.go +++ b/admin/server/router/gaia/system.go @@ -6,11 +6,13 @@ import ( type SystemRouter struct{} -// InitSystemRouter 初始化 Dify 系统 关联系统表 路由信息 -func (d *SystemRouter) InitSystemRouter(Router *gin.RouterGroup) { - dashboardRouterWithoutRecord := Router.Group("gaia/system") +// InitSystemRouter 初始化系统路由 +func (s *SystemRouter) InitSystemRouter(Router *gin.RouterGroup) { + systemRouter := Router.Group("gaia/system") { - dashboardRouterWithoutRecord.GET("dingtalk", systemApi.GetDingTalk) // 获取钉钉系统配置 - dashboardRouterWithoutRecord.POST("dingtalk", systemApi.SetDingTalk) // 设置钉钉系统配置 + systemRouter.GET("dingtalk", systemApi.GetDingTalk) // 获取钉钉系统配置 + systemRouter.POST("dingtalk", systemApi.SetDingTalk) // 设置钉钉系统配置 + systemRouter.GET("oauth2", systemOAuth2Api.GetOAuth2Config) // 获取OAuth2配置 + systemRouter.POST("oauth2", systemOAuth2Api.SetOAuth2Config) // 设置OAuth2配置 } } diff --git a/admin/server/router/system/sys_base.go b/admin/server/router/system/sys_base.go index 2d539c109..e4995ae4d 100644 --- a/admin/server/router/system/sys_base.go +++ b/admin/server/router/system/sys_base.go @@ -11,7 +11,8 @@ func (s *BaseRouter) InitBaseRouter(Router *gin.RouterGroup) (R gin.IRoutes) { { baseRouter.POST("login", baseApi.Login) baseRouter.POST("captcha", baseApi.Captcha) - baseRouter.POST("oaLogin", baseApi.OaLogin) // 新增OA登录 + baseRouter.POST("oaLogin", baseApi.OaLogin) // 新增OA登录 + baseRouter.GET("auth2/callback", baseApi.OAuth2Callback) // 新增oAuth2回调校验 } return baseRouter } diff --git a/admin/server/service/gaia/system.go b/admin/server/service/gaia/system.go index 23360ac40..95e6ebcad 100644 --- a/admin/server/service/gaia/system.go +++ b/admin/server/service/gaia/system.go @@ -1,17 +1,22 @@ package gaia import ( + "encoding/json" "errors" - "net/http" - "net/url" - "os" - + "fmt" "github.com/faabiosr/cachego/file" "github.com/fastwego/dingding" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/gaia" + "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/google/uuid" + "go.uber.org/zap" + "io" + "net/http" + "net/url" + "os" + "strings" ) type SystemIntegratedService struct{} @@ -48,7 +53,10 @@ func (e *SystemIntegratedService) GetIntegratedConfig(classID uint) (integrate g // @Security ApiKeyAuth // @accept application/json // @Produce application/json -func (e *SystemIntegratedService) SetIntegratedConfig(integrate gaia.SystemIntegration, test bool) (err error) { +// @param: integrate gaia.SystemIntegration, code string, test bool +// @return: err error +func (e *SystemIntegratedService) SetIntegratedConfig( + integrate gaia.SystemIntegration, code string, test bool) (err error) { // classID是否在 var log gaia.SystemIntegration if err = global.GVA_DB.Where("classify = ?", integrate.Classify).First(&log).Error; err != nil { @@ -74,7 +82,6 @@ func (e *SystemIntegratedService) SetIntegratedConfig(integrate gaia.SystemInteg } } // CorpID - var ding *dingding.Client if utils.AddAsteriskToString(log.CorpID) != integrate.CorpID { log.CorpID = integrate.CorpID } @@ -82,12 +89,9 @@ func (e *SystemIntegratedService) SetIntegratedConfig(integrate gaia.SystemInteg log.AppID = integrate.AppID // 关闭不需要请求 if integrate.Status || test { - if ding, err = e.DingTalkConfigAvailable(integrate); err != nil { - return errors.New("钉钉链接失败" + err.Error()) - } - // token - if _, err = ding.AccessTokenManager.GetAccessToken(); err != nil { - return errors.New("钉钉token获取失败:" + err.Error()) + // 测试连接 + if err = e.TestConnection(integrate, code); err != nil { + return errors.New("连接失败:" + err.Error()) } } // Test completed @@ -95,7 +99,9 @@ func (e *SystemIntegratedService) SetIntegratedConfig(integrate gaia.SystemInteg return err } // save - if err = global.GVA_DB.Model(&gaia.SystemIntegration{}).Where("id=?", log.Id).Updates(&map[string]interface{}{ + if err = global.GVA_DB.Model(&gaia.SystemIntegration{}).Where( + "id=?", log.Id).Updates(&map[string]interface{}{ + "config": integrate.Config, "status": integrate.Status, "agent_id": integrate.AgentID, "app_key": integrate.AppKey, @@ -134,3 +140,97 @@ func (e *SystemIntegratedService) DingTalkConfigAvailable(req gaia.SystemIntegra }, }), err } + +// TestConnection 测试连接 +// @Tags System Integrated +// @Summary 测试系统集成连接 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @param: integrate gaia.SystemIntegration, code string +// @return: error +func (e *SystemIntegratedService) TestConnection(integrate gaia.SystemIntegration, code string) error { + switch integrate.Classify { + case gaia.SystemIntegrationDingTalk: + // 测试钉钉连接 + if _, err := e.DingTalkConfigAvailable(integrate); err != nil { + return errors.New("钉钉链接失败: " + err.Error()) + } + return nil + case gaia.SystemIntegrationOAuth2: + // 测试OAuth2连接 + return e.TestOAuth2Connection(integrate, code) + default: + return errors.New("不支持的集成类型") + } +} + +// TestOAuth2Connection 测试OAuth2连接 +// @Tags System Integrated +// @Summary 测试OAuth2连接 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @param: integrate gaia.SystemIntegration, code string +// @return: error +func (e *SystemIntegratedService) TestOAuth2Connection(integrate gaia.SystemIntegration, code string) (err error) { + // 解析Config字段 + var configMap request.SystemOAuth2Request + if err = json.Unmarshal([]byte(integrate.Config), &configMap); err != nil { + global.GVA_LOG.Error("解析OAuth2配置失败!", zap.Error(err)) + return err + } + // 没有code的(保存操作) + if len(code) == 0 { + return nil + } + // 检查必要字段 + if configMap.ServerURL == "" || configMap.TokenURL == "" || integrate.AppID == "" || integrate.AppSecret == "" { + return errors.New("请填写完整的 OAuth2 配置信息") + } + + // 合成请求byte + formData := url.Values{} + formData.Set("grant_type", "authorization_code") + formData.Set("client_secret", integrate.AppSecret) + formData.Set("client_id", integrate.AppID) + formData.Set("redirect_uri", "") + formData.Set("code", code) + + // 发送请求 + var req *http.Request + client := &http.Client{} + req, err = http.NewRequest("POST", fmt.Sprintf( + "%s%s", configMap.ServerURL, configMap.TokenURL), strings.NewReader(formData.Encode())) + if err != nil { + global.GVA_LOG.Error("创建测试请求失败", zap.Error(err)) + return errors.New(fmt.Sprintf("创建测试请求失败: %s", err.Error())) + } + + // 设置Content-Type + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + // 发送请求 + resp, err := client.Do(req) + if err != nil { + global.GVA_LOG.Error("测试 OAuth2 连接失败", zap.Error(err)) + return errors.New(fmt.Sprintf("连接 OAuth2 服务器失败: %s", err.Error())) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + global.GVA_LOG.Error("测试 OAuth2 连接失败", zap.Int("status", resp.StatusCode)) + return errors.New(fmt.Sprintf("OAuth2 服务器返回错误状态码: %d", resp.StatusCode)) + } + var bodyByte []byte + if bodyByte, err = io.ReadAll(resp.Body); err != nil { + return fmt.Errorf("OAuth2 request io.ReadAll: %s", resp.Status) + } + + var tokenMap request.SystemOAuth2Error + if err = json.Unmarshal(bodyByte, &tokenMap); err == nil && tokenMap.Code != 0 { + return fmt.Errorf("OAuth2 Eroor: %s", tokenMap.Info) + } + + return nil +} diff --git a/admin/server/source/system/api.go b/admin/server/source/system/api.go index 54fb74f1a..923979a47 100644 --- a/admin/server/source/system/api.go +++ b/admin/server/source/system/api.go @@ -202,6 +202,11 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { {ApiGroup: "应用集成配置", Method: "GET", Path: "/gaia/system/dingtalk", Description: "获取钉钉系统配置"}, {ApiGroup: "应用集成配置", Method: "POST", Path: "/gaia/system/dingtalk", Description: "设置钉钉系统配置"}, // Extend Stop: system integration + + // Extend Start: oauth2 + {ApiGroup: "应用集成配置", Method: "GET", Path: "/gaia/system/oauth2", Description: "设置OAuth2配置"}, + {ApiGroup: "应用集成配置", Method: "POST", Path: "/gaia/system/oauth2", Description: "获取OAuth2集成配置"}, + // Extend Stop: oauth2 } if err := db.Create(&entities).Error; err != nil { return ctx, errors.Wrap(err, sysModel.SysApi{}.TableName()+"表数据初始化失败!") diff --git a/admin/server/source/system/casbin.go b/admin/server/source/system/casbin.go index 5ff4269fe..caab8f718 100644 --- a/admin/server/source/system/casbin.go +++ b/admin/server/source/system/casbin.go @@ -288,6 +288,11 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error {Ptype: "p", V0: "888", V1: "/gaia/system/dingtalk", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/gaia/system/dingtalk", V2: "POST"}, // Extend Stop: system integration + + // Extend Start: oauth2 + {Ptype: "p", V0: "888", V1: "/gaia/system/oauth2", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/gaia/system/oauth2", V2: "POST"}, + // Extend Stop: oauth2 } if err := db.Create(&entities).Error; err != nil { return ctx, errors.Wrap(err, "Casbin 表 ("+i.InitializerName()+") 数据初始化失败!") diff --git a/admin/server/source/system/menu.go b/admin/server/source/system/menu.go index 5bd29bfea..8c4e9ed31 100644 --- a/admin/server/source/system/menu.go +++ b/admin/server/source/system/menu.go @@ -92,6 +92,11 @@ func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, er // Extend Start: system integration {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "SystemIntegrated", Name: "SystemIntegrated", Component: "view/systemIntegrated/index.vue", Sort: 1, Meta: Meta{Title: "系统集成", Icon: "box"}}, {MenuLevel: 0, Hidden: false, ParentId: 38, Path: "IntegratedDingTalk", Name: "IntegratedDingTalk", Component: "view/systemIntegrated/dingTalk/index.vue", Sort: 1, Meta: Meta{Title: "钉钉", Icon: "turn-off"}}, + {MenuLevel: 0, Hidden: false, ParentId: 38, Path: "IntegratedOAuth2", Name: "IntegratedOAuth2", Component: "view/systemIntegrated/oauth2/index.vue", Sort: 2, Meta: Meta{Title: "OAuth2", Icon: "connection"}}, + // Extend Stop: system integration + + // Extend Start: system integration + {MenuLevel: 0, Hidden: false, ParentId: 46, Path: "integratedOAuth2", Name: "integratedOAuth2", Component: "view/systemIntegrated/oauth2/index.vue", Sort: 2, Meta: Meta{Title: "OAuth2", Icon: "share"}}, // Extend Stop: system integration // 二开部分 diff --git a/admin/web/src/api/gaia/system.js b/admin/web/src/api/gaia/system.js index 1f8ecb68e..2ee9208ac 100644 --- a/admin/web/src/api/gaia/system.js +++ b/admin/web/src/api/gaia/system.js @@ -26,3 +26,30 @@ export const setSystemDingTalk = (data) => { data, }) } + +// @Tags systrm +// @Summary 获取OAuth2集成配置 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}" +// @Router /gaia/system/oauth2 [get] +export const getSystemOAuth2 = () => { + return service({ + url: '/gaia/system/oauth2', + method: 'get' + }) +} + +// @Tags systrm +// @Summary 修改OAuth2集成配置 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}" +// @Router /gaia/system/oauth2 [post] +export const setSystemOAuth2 = (data) => { + return service({ + url: '/gaia/system/oauth2', + method: 'post', + data, + }) +} diff --git a/admin/web/src/pathInfo.json b/admin/web/src/pathInfo.json index 5ddffae33..f4a441ecf 100644 --- a/admin/web/src/pathInfo.json +++ b/admin/web/src/pathInfo.json @@ -25,7 +25,6 @@ "/src/view/gaia/dashboard/components/charts-people-numbers.vue": "ChartsPeopleNumbers", "/src/view/gaia/dashboard/components/charts.vue": "Charts", "/src/view/gaia/dashboard/index.vue": "GaiaDashboard", - "/src/view/gaia/providers/providers.vue": "ProviderManage", "/src/view/gaia/tenants/tenants.vue": "TenantList", "/src/view/init/index.vue": "Init", "/src/view/layout/aside/asideComponent/asyncSubmenu.vue": "AsyncSubmenu", @@ -64,6 +63,7 @@ "/src/view/system/state.vue": "State", "/src/view/systemIntegrated/dingTalk/index.vue": "IntegratedDingTalk", "/src/view/systemIntegrated/index.vue": "SystemIntegrated", + "/src/view/systemIntegrated/oauth2/index.vue": "IntegratedOAuth2", "/src/view/systemTools/autoCode/component/fieldDialog.vue": "FieldDialog", "/src/view/systemTools/autoCode/component/previewCodeDialog.vue": "PreviewCodeDialog", "/src/view/systemTools/autoCode/index.vue": "AutoCode", diff --git a/admin/web/src/view/systemIntegrated/oauth2/index.vue b/admin/web/src/view/systemIntegrated/oauth2/index.vue new file mode 100644 index 000000000..dc9da76b2 --- /dev/null +++ b/admin/web/src/view/systemIntegrated/oauth2/index.vue @@ -0,0 +1,349 @@ + + + + + diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index 52f3480eb..fc91db3d4 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -45,16 +45,9 @@ def get_oauth_providers(): redirect_uri=dify_config.CONSOLE_API_URL + "/console/api/oauth/authorize/google", ) - if not dify_config.OAUTH2_CLIENT_ID or not dify_config.OAUTH2_CLIENT_SECRET: - oa_oauth = None - else: - oa_oauth = OaOAuth( - client_id=dify_config.OAUTH2_CLIENT_ID, - client_secret=dify_config.OAUTH2_CLIENT_SECRET, - redirect_uri=dify_config.CONSOLE_API_URL + "/console/api/oauth/authorize/oauth2", - ) + oauth2 = OaOAuth(client_id='', client_secret='', redirect_uri='') # Extend: oauth2 - OAUTH_PROVIDERS = {"github": github_oauth, "google": google_oauth, "oauth2": oa_oauth} + OAUTH_PROVIDERS = {"github": github_oauth, "google": google_oauth, "oauth2": oauth2} return OAUTH_PROVIDERS diff --git a/api/libs/oauth.py b/api/libs/oauth.py index 521f8d622..87ff5b348 100644 --- a/api/libs/oauth.py +++ b/api/libs/oauth.py @@ -1,10 +1,13 @@ +import json import logging # 二开部分,针对oa登录报错问题,记录返回的code import urllib.parse from dataclasses import dataclass from typing import Optional import requests -from flask import current_app +from configs import dify_config # Extend OAuto third-party login +from extensions.ext_database import db # Extend OAuto third-party login +from models.system_extend import SystemIntegrationExtend, SystemIntegrationClassify # Extend OAuto third-party login @dataclass @@ -135,35 +138,85 @@ class GoogleOAuth(OAuth): return OAuthUserInfo(id=str(raw_info["sub"]), name="", email=raw_info["email"]) +# Extend Start: OAuth2 class OaOAuth(OAuth): - _AUTH_URL = "" - _Host = "" - _TOKEN_URL = "" - _USER_INFO_URL = "" + + def get_auto2_conf(self): + # oauth start + integration = db.session.query(SystemIntegrationExtend).filter( + SystemIntegrationExtend.classify == SystemIntegrationClassify.SYSTEM_INTEGRATION_OAUTH_TWO).first() + if integration is None or (integration and not integration.status): + return { + "integration": integration, + "config": {}, + "passwd": "" + } + return { + "integration": integration, + "passwd": integration.decodeSecret(), + "config": json.loads(integration.config) + } + + def extract_data(self, dictionary, path): + """ + 从字典中提取指定路径的数据 + 支持通配符'*'获取列表中所有元素的特定字段 + + Args: + dictionary (dict): 源字典 + path (str): 以点分隔的路径,如 "data.info.name" 或 "data.items.*.name" + + Returns: + 提取的数据 + """ + parts = path.split('.') + current = dictionary + + for i, part in enumerate(parts): + if part == '*' and isinstance(current, list): + # 处理列表中的每个元素 + remainder = '.'.join(parts[i + 1:]) + if remainder: + return [self.extract_data(item, remainder) for item in current] + else: + return current + elif isinstance(current, dict) and part in current: + current = current[part] + else: + return None + + return current def get_authorization_url(self, invite_token: Optional[str] = None): - params = { - "client_id": self.client_id, - "redirect_uri": self.redirect_uri, - } - with current_app.app_context(): - self._Host = current_app.config.get("OAUTH2_CLIENT_URL") - self._TOKEN_URL = current_app.config.get("OAUTH2_TOKEN_URL") - self._USER_INFO_URL = current_app.config.get("OAUTH2_USER_INFO_URL") - return f"{self._Host}{self._AUTH_URL}?{urllib.parse.urlencode(params)}" + auto2_conf = self.get_auto2_conf() + integration = auto2_conf.get('integration') + if integration is None: + return + # 构建查询参数 + query_string = urllib.parse.urlencode({ + 'redirect_uri': dify_config.CONSOLE_API_URL + "/console/api/oauth/authorize/oauth2", + 'client_id': integration.app_id, + }) + + # 构建完整URL + config = auto2_conf.get('config') + return f"{config.get('server_url')}{config.get('authorize_url')}?{query_string}" def get_access_token(self, code: str): + auto2_conf = self.get_auto2_conf() + integration = auto2_conf.get('integration') + if integration is None: + return "" data = { - "client_id": self.client_id, - "client_secret": self.client_secret, "code": code, + "client_id": integration.app_id, "grant_type": "authorization_code", - "redirect_uri": self.redirect_uri, + "client_secret": auto2_conf.get('passwd'), + "redirect_uri": dify_config.CONSOLE_API_URL + "/console/api/oauth/authorize/oauth2", } - with current_app.app_context(): - self._Host = current_app.config.get("OAUTH2_CLIENT_URL") headers = {"Accept": "application/json"} - response = requests.post(self._Host + self._TOKEN_URL, data=data, headers=headers) + config = auto2_conf.get('config') + response = requests.post(f"{config.get('server_url')}{config.get('token_url')}", data=data, headers=headers) response.encoding = "utf-8" if response.status_code != 200: return "" @@ -176,42 +229,35 @@ class OaOAuth(OAuth): return access_token def get_raw_user_info(self, token: str): - with current_app.app_context(): - self._Host = current_app.config.get("OAUTH2_CLIENT_URL") + auto2_conf = self.get_auto2_conf() + if auto2_conf.get('integration') is None: + return "" + config = auto2_conf.get('config') headers = {"Authorization": f"Bearer {token}"} - response = requests.get(self._Host + self._USER_INFO_URL, headers=headers) + response = requests.get(f"{config.get('server_url')}{config.get('userinfo_url')}", headers=headers) response.raise_for_status() return response.json() def _transform_user_info(self, raw_info: dict) -> OAuthUserInfo: # 检查 raw_info 是否为空或为 None - if not raw_info or not isinstance(raw_info, dict): + auto2_conf = self.get_auto2_conf() + if not raw_info or not isinstance(raw_info, dict) or auto2_conf.get('integration') is None: return OAuthUserInfo( id="", name="", email="", ) - - # 如果data为空说明报错了 - data = raw_info.get("data") - if data == {} or data is None or not isinstance(data, dict): - code = raw_info.get("code", "") - msg = raw_info.get("info", "") - logging.info(f"raw_info {raw_info}") - return OAuthUserInfo( - id="", - name="", - email="", - ) - - username = data.get("username") - name = data.get("name") - email = data.get("email") + # 提取参数 + config = auto2_conf.get('config') + name = self.extract_data(raw_info, config.get('user_name_field')) + email = self.extract_data(raw_info, config.get('user_email_field')) + username = self.extract_data(raw_info, config.get('user_id_field')) if not username: - raise ValueError("OA系统返回用户数据格式不正确。请返回进行重新登录。") + raise ValueError("OAuth2返回用户数据格式不正确。请返回进行重新登录。") return OAuthUserInfo( id=str(username) if username is not None else None, name=str(name) if name is not None else None, email=str(email) if email is not None else None, ) +# Extend Stop: OAuth diff --git a/api/models/system_extend.py b/api/models/system_extend.py index bfbe8ae5a..1551f32ef 100644 --- a/api/models/system_extend.py +++ b/api/models/system_extend.py @@ -8,6 +8,7 @@ class SystemIntegrationClassify: SYSTEM_INTEGRATION_DINGTALK = 1 # 钉钉 SYSTEM_INTEGRATION_WEIXIN = 2 # 微信 SYSTEM_INTEGRATION_FEI_SU = 3 # 飞书 + SYSTEM_INTEGRATION_OAUTH_TWO = 4 # OAuth2 class SystemIntegrationExtend(db.Model): @@ -21,8 +22,10 @@ class SystemIntegrationExtend(db.Model): status = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) corp_id = db.Column(db.String(120), nullable=True) agent_id = db.Column(db.String(120), nullable=True) + app_id = db.Column(db.String(120), nullable=True) app_key = db.Column(db.String(120), nullable=True) app_secret = db.Column(db.Text, nullable=True) + config = db.Column(db.Text, nullable=True) def decodeSecret(self): if len(self.app_secret) == 0: diff --git a/api/services/feature_service.py b/api/services/feature_service.py index a73baddb9..247d91f3b 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -1,3 +1,4 @@ +import json from enum import StrEnum from pydantic import BaseModel, ConfigDict @@ -83,6 +84,7 @@ class SystemFeatureModel(BaseModel): is_email_setup: bool = False license: LicenseModel = LicenseModel() is_custom_auth2: str = "" # extend: Customizing AUTH2 + is_custom_auth2_logout: str = "" # extend: Customizing AUTH2 ding_talk_client_id: str = "" # extend: DingTalk third-party login ding_talk_corp_id: str = "" # extend: DingTalk sidebar login ding_talk: bool = "" # extend: DingTalk sidebar login @@ -134,15 +136,20 @@ class FeatureService: system_features.is_allow_register = dify_config.ALLOW_REGISTER system_features.is_allow_create_workspace = dify_config.ALLOW_CREATE_WORKSPACE system_features.is_email_setup = dify_config.MAIL_TYPE is not None and dify_config.MAIL_TYPE != "" - # extend start: Customizing AUTH2 - system_features.is_custom_auth2 = dify_config.OAUTH2_CLIENT_URL - # extend stop: Customizing AUTH2 # extend start: DingTalk third-party login for i in db.session.query(SystemIntegrationExtend).filter(SystemIntegrationExtend.status == True).all(): if i.classify == SystemIntegrationClassify.SYSTEM_INTEGRATION_DINGTALK: system_features.ding_talk_client_id = i.app_key system_features.ding_talk_corp_id = i.corp_id system_features.ding_talk = i.status + # Extend: OAuth2 Start + elif i.classify == SystemIntegrationClassify.SYSTEM_INTEGRATION_OAUTH_TWO: + config = json.loads(i.config) + system_features.is_custom_auth2 = i.status + if "logout_url" in config.keys(): + system_features.is_custom_auth2_logout = "{}{}".format( + config['server_url'], config['logout_url']) + # Extend: OAuth2 Stop # extend stop: DingTalk third-party login @classmethod diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx index d5192e534..9dd58d06a 100644 --- a/web/app/components/header/account-dropdown/index.tsx +++ b/web/app/components/header/account-dropdown/index.tsx @@ -65,8 +65,9 @@ export default function AppSelector() { // 二开部分 - End 解决切换账号对话记录不存在问题 // Start: Automatic login/logout Extend - if (window.location !== undefined && `${process.env.NEXT_PUBLIC_AUTH0_LOGOUT_URL}` !== '' && process.env.NEXT_PUBLIC_AUTH0_LOGOUT_URL !== undefined) - window.location.href = `${process.env.NEXT_PUBLIC_AUTH0_LOGOUT_URL}&redirect_url=${window.location.href}` + console.log(systemFeatures, 2344) + if (window.location !== undefined && `${systemFeatures.is_custom_auth2_logout}` !== '' && systemFeatures.is_custom_auth2_logout !== undefined) + window.location.href = `${systemFeatures.is_custom_auth2_logout}&redirect_url=${window.location.href}` // Stop: Automatic login/logout Extend router.push('/signin') } diff --git a/web/app/signin/components/oauth2.tsx b/web/app/signin/components/oauth2.tsx new file mode 100644 index 000000000..983cb7ecf --- /dev/null +++ b/web/app/signin/components/oauth2.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { useRouter } from 'next/navigation' +import { useTranslation } from 'react-i18next' +import style from '../page.module.css' +import Button from '@/app/components/base/button' +import classNames from '@/utils/classnames' +import { apiPrefix } from '@/config' + +export default function OAuth2() { + const { t } = useTranslation() + const router = useRouter() + + /* Extend: start 钉钉快捷登录按钮 */ + const OAuth2Login = () => { + router.replace(`${apiPrefix}/oauth/login/oauth2`) + } + + return <> +
+ + + +
+ +} diff --git a/web/app/signin/normalForm.tsx b/web/app/signin/normalForm.tsx index 343436667..93ff2f262 100644 --- a/web/app/signin/normalForm.tsx +++ b/web/app/signin/normalForm.tsx @@ -15,6 +15,7 @@ import Toast from '@/app/components/base/toast' import { IS_CE_EDITION } from '@/config' // extend : support ding_talk login import DingTalkAuth from '@/app/signin/components/dingtalk-auth' +import OAuth2 from '@/app/signin/components/oauth2' // extend: add oauth2 // 声明一个变量来存储钉钉SDK let dd: any = null @@ -231,8 +232,12 @@ const NormalForm = () => { {systemFeatures.sso_enforced_for_signin &&
} - {/* extend : support ding_talk login */} - {systemFeatures.ding_talk && ()} { /* Extend: add DingTalk Auth */ } + {/* Extend start: ding_talk login */} + {systemFeatures.ding_talk && ()} + { /* Extend stop: DingTalk login */ } + {/* Extend start: oauth2 login */} + {systemFeatures.is_custom_auth2 && ()} + { /* Extend stop: oauth2 login */ } {showORLine &&
diff --git a/web/models/common.ts b/web/models/common.ts index 972e7696a..25363dd14 100644 --- a/web/models/common.ts +++ b/web/models/common.ts @@ -37,6 +37,12 @@ export type UserProfileResponse = { // ----------------------- 二开部分Stop 添加用户权限 - -------------------------------- } +// ----------------------- 二开部分Start oauth2 - -------------------------------- +export type OAuth2 = { + logout_url: string +} +// ----------------------- 二开部分Start oauth2 - -------------------------------- + export type UserProfileOriginResponse = { json: () => Promise bodyUsed: boolean diff --git a/web/types/feature.ts b/web/types/feature.ts index f55befcc9..10fc0c428 100644 --- a/web/types/feature.ts +++ b/web/types/feature.ts @@ -33,6 +33,7 @@ export type SystemFeatures = { is_email_setup: boolean license: License is_custom_auth2: string // extend: Customizing AUTH2 + is_custom_auth2_logout: string // extend: AUTH2 logout url ding_talk_client_id: string // Extend: DingTalk third-party login ding_talk_corp_id: string // Extend: DingTalk sidebar login ding_talk: boolean // Extend: switch DingTalk sidebar login