mirror of
https://github.com/YFGaia/dify-plus.git
synced 2026-06-04 10:14:00 +08:00
feat:新增oauth2.0登录
This commit is contained in:
@@ -8,6 +8,7 @@ type ApiGroup struct {
|
||||
TenantsApi
|
||||
SystemApi
|
||||
TestApi
|
||||
SystemOAuth2Api
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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(`<html><body><script>const channel = new BroadcastChannel('oAuth2');channel.postMessage({code: '%s', timestamp: Date.now() });channel.close();window.close();</script></body></html>`, code)
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.String(http.StatusOK, htmlContent)
|
||||
}
|
||||
|
||||
// Extend Stop: oAuth2 callback verification
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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代码
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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配置
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()+"表数据初始化失败!")
|
||||
|
||||
@@ -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()+") 数据初始化失败!")
|
||||
|
||||
@@ -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
|
||||
|
||||
// 二开部分
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
<template>
|
||||
<div id="oauth2" class="system">
|
||||
<el-form
|
||||
ref="form"
|
||||
:model="config"
|
||||
label-width="240px"
|
||||
>
|
||||
<div class="page-header mb-6">
|
||||
<h2 class="text-xl font-bold">
|
||||
OAuth2 应用集成配置
|
||||
</h2>
|
||||
<p class="text-gray-500 mt-2">
|
||||
配置 OAuth2 单点登录相关参数
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<el-tabs class="oauth2-tabs">
|
||||
<div class="card">
|
||||
<div class="card-header flex items-center justify-between">
|
||||
<span class="text-lg font-medium">启用状态</span>
|
||||
<div class="flex items-center">
|
||||
<el-switch
|
||||
v-model="config.status"
|
||||
active-text="已启用"
|
||||
:disabled="!isConfigValid"
|
||||
@change="handleStatusChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<div class="card-section">
|
||||
<div class="section-title">
|
||||
OAuth2 回调域名配置
|
||||
</div>
|
||||
<div class="text-gray-600 mb-3">
|
||||
<p>回调域名:此信息将在创建 OAuth2 授权应用时使用</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<el-input v-model="host" disabled readonly class="flex-1" />
|
||||
<el-button type="primary" class="ml-2" icon="copy-document" @click="copyHost">
|
||||
复制
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<div class="card-section">
|
||||
<div class="section-title">
|
||||
应用信息配置
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<el-button v-if="!openEdit" type="primary" class="config-btn w-full" icon="setting" @click="openConfig">
|
||||
配置链接应用信息
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-5 border rounded-lg">
|
||||
<div class="flex items-center mb-4">
|
||||
<span class="info-label">OAuth2 服务器地址:</span>
|
||||
<el-input v-if="openEdit" v-model="config.server_url" class="info-value flex-1" placeholder="例如: https://oauth2.example.com" />
|
||||
<span v-else class="info-value">{{ config.server_url || '未配置' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center mb-4">
|
||||
<span class="info-label">授权页面地址:</span>
|
||||
<el-input v-if="openEdit" v-model="config.authorize_url" class="info-value flex-1" placeholder="例如: /oauth/authorize" />
|
||||
<span v-else class="info-value">{{ config.authorize_url }}</span>
|
||||
</div>
|
||||
<div class="flex items-center mb-4">
|
||||
<span class="info-label">获取 Token URL:</span>
|
||||
<el-input v-if="openEdit" v-model="config.token_url" class="info-value flex-1" placeholder="例如: /oauth2/token" />
|
||||
<span v-else class="info-value">{{ config.token_url || '未配置' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center mb-4">
|
||||
<span class="info-label">获取用户信息 URL:</span>
|
||||
<el-input v-if="openEdit" v-model="config.userinfo_url" class="info-value flex-1" placeholder="例如: /oauth2/userinfo" />
|
||||
<span v-else class="info-value">{{ config.userinfo_url || '未配置' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center mb-4">
|
||||
<span class="info-label">退出登录回调 URL:</span>
|
||||
<el-input v-if="openEdit" v-model="config.logout_url" class="info-value flex-1" placeholder="例如: /oauth2/logout" />
|
||||
<span v-else class="info-value">{{ config.logout_url || '未配置' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center mb-4">
|
||||
<span class="info-label">Client ID:</span>
|
||||
<el-input v-if="openEdit" v-model="config.app_id" class="info-value flex-1" />
|
||||
<span v-else class="info-value">{{ config.app_id || '未配置' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center mb-4">
|
||||
<span class="info-label">Client Secret:</span>
|
||||
<el-input v-if="openEdit" v-model="config.app_secret" class="info-value flex-1" type="text" />
|
||||
<span v-else class="info-value">{{ config.app_secret ? config.app_secret : '未配置' }}</span>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div class="section-title mb-4">
|
||||
用户信息映射配置
|
||||
</div>
|
||||
<div class="flex items-center mb-4">
|
||||
<span class="info-label">用户名字段:</span>
|
||||
<el-input v-if="openEdit" v-model="config.user_name_field" class="info-value flex-1" placeholder="例如: data.name" />
|
||||
<span v-else class="info-value">{{ config.user_name_field || '未配置' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center mb-4">
|
||||
<span class="info-label">邮箱字段:</span>
|
||||
<el-input v-if="openEdit" v-model="config.user_email_field" class="info-value flex-1" placeholder="例如: data.email" />
|
||||
<span v-else class="info-value">{{ config.user_email_field || '未配置' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center mb-4">
|
||||
<span class="info-label">用户唯一标识字段:</span>
|
||||
<el-input v-if="openEdit" v-model="config.user_id_field" class="info-value flex-1" placeholder="例如: data.sub 或 data.id" />
|
||||
<span v-else class="info-value">{{ config.user_id_field || '未配置' }}</span>
|
||||
</div>
|
||||
<div class="float-right">
|
||||
<el-button type="primary" plain icon="connection" @click="testConnection">
|
||||
测试连接
|
||||
</el-button>
|
||||
<el-button v-if="openEdit" type="primary" icon="goods-filled" @click="update">
|
||||
保存
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="clear-both" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<div class="card-section">
|
||||
<div class="section-title text-amber-500">
|
||||
<el-icon><warning-filled /></el-icon>
|
||||
<span>温馨提示</span>
|
||||
</div>
|
||||
<div class="tips-content">
|
||||
<p class="tip-item">
|
||||
1. 请确保您的 OAuth2 服务器已正确配置,并支持授权码模式
|
||||
</p>
|
||||
<p class="tip-item">
|
||||
2. Client ID 和 Client Secret 是应用在 OAuth2 服务器中的唯一标识
|
||||
</p>
|
||||
<p class="tip-item">
|
||||
3. 用户信息映射字段应与 OAuth2 服务器返回的用户信息字段一致
|
||||
</p>
|
||||
<p class="tip-item">
|
||||
4. 请确保在 OAuth2 服务器上正确配置回调域名,否则会导致认证失败
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { getSystemOAuth2, setSystemOAuth2 } from "@/api/gaia/system";
|
||||
import { WarningFilled } from '@element-plus/icons-vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'IntegratedOAuth2',
|
||||
})
|
||||
|
||||
const bc = ref()
|
||||
const host = ref("")
|
||||
const openEdit = ref(false)
|
||||
const config = ref({
|
||||
id: 0,
|
||||
classify: 0,
|
||||
status: false,
|
||||
server_url: "",
|
||||
token_url: "",
|
||||
userinfo_url: "",
|
||||
logout_url: "",
|
||||
app_id: "",
|
||||
app_app: "",
|
||||
authorize_url: "",
|
||||
app_secret: "",
|
||||
user_name_field: "",
|
||||
user_email_field: "",
|
||||
user_id_field: "",
|
||||
test: false,
|
||||
})
|
||||
|
||||
// 验证配置是否有效
|
||||
const isConfigValid = computed(() => {
|
||||
return !!(
|
||||
config.value.server_url &&
|
||||
config.value.token_url &&
|
||||
config.value.userinfo_url &&
|
||||
config.value.app_id &&
|
||||
config.value.app_secret &&
|
||||
config.value.user_id_field
|
||||
);
|
||||
})
|
||||
|
||||
// 处理状态变更
|
||||
const handleStatusChange = (val) => {
|
||||
if (val && !isConfigValid.value) {
|
||||
config.value.status = false;
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: '请先填写应用信息配置'
|
||||
});
|
||||
return;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
// 打开编辑
|
||||
const openConfig = () => {
|
||||
openEdit.value = true
|
||||
}
|
||||
|
||||
// 复制回调URL
|
||||
const copyHost = () => {
|
||||
navigator.clipboard.writeText(host.value);
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '复制成功'
|
||||
});
|
||||
}
|
||||
|
||||
// 测试连接
|
||||
const testConnection = async () => {
|
||||
let host = config.value.server_url
|
||||
let authorizeUrl = `${host}${config.value.authorize_url}`
|
||||
let redirectUri = encodeURIComponent(`${location.protocol}//${location.host}/api/base/auth2/callback`)
|
||||
window.open(`${authorizeUrl}?client_id=${config.value.app_id}&response_type=code&scope=all&redirect_uri=${redirectUri}`)
|
||||
}
|
||||
|
||||
const initForm = async() => {
|
||||
const res = await getSystemOAuth2()
|
||||
if (res.code === 0) {
|
||||
host.value = res.data.host
|
||||
config.value = res.data.config
|
||||
}
|
||||
}
|
||||
|
||||
const update = async() => {
|
||||
config.value.test = false;
|
||||
|
||||
if (config.value.status && !isConfigValid.value) {
|
||||
config.value.status = false;
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: '请先填写应用信息配置'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await setSystemOAuth2(config.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '设置成功',
|
||||
})
|
||||
await initForm()
|
||||
openEdit.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// start
|
||||
onMounted(() => {
|
||||
initForm()
|
||||
bc.value = new BroadcastChannel('oAuth2');
|
||||
bc.value.onmessage = async function (event) {
|
||||
// 尝试激活标签
|
||||
window.focus();
|
||||
config.value.test = true;
|
||||
config.value.code = event.data.code;
|
||||
if (config.value.status && !isConfigValid.value) {
|
||||
config.value.status = false;
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: '请先填写应用信息配置'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await setSystemOAuth2(config.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '链接成功',
|
||||
})
|
||||
}
|
||||
};
|
||||
})
|
||||
// clone
|
||||
onBeforeUnmount(() => {
|
||||
console.log('clone')
|
||||
bc.value.close()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.system {
|
||||
@apply bg-white p-9 rounded dark:bg-slate-900;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply bg-white dark:bg-slate-900 rounded-lg overflow-hidden;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.card-section {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@apply text-lg font-medium mb-4 flex items-center;
|
||||
|
||||
.el-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-label {
|
||||
width: 180px;
|
||||
@apply text-gray-600;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
@apply font-medium;
|
||||
}
|
||||
|
||||
.tips-content {
|
||||
@apply text-gray-600;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.config-btn {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user