feat:新增oauth2.0登录

This commit is contained in:
npc0-hue
2025-04-17 14:34:26 +08:00
parent 385b3d5aaa
commit 64c7f3de25
26 changed files with 817 additions and 81 deletions
+1
View File
@@ -8,6 +8,7 @@ type ApiGroup struct {
TenantsApi
SystemApi
TestApi
SystemOAuth2Api
}
var (
+3 -3
View File
@@ -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
}
+86
View File
@@ -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
+2 -1
View File
@@ -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"
+25
View File
@@ -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
+1
View File
@@ -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
+7 -5
View File
@@ -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配置
}
}
+2 -1
View File
@@ -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
}
+113 -13
View File
@@ -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
}
+5
View File
@@ -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()+"表数据初始化失败!")
+5
View File
@@ -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()+") 数据初始化失败!")
+5
View File
@@ -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
// 二开部分
+27
View File
@@ -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,
})
}
+1 -1
View File
@@ -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>