Files
dify-plus/admin/server/api/v1/system/sys_user_extend.go
T
npc0-hue 10ec0eb953 feat: 合并近期功能与修复
- GLM/MiniMax 模型支持及 provider_name 修复
- OAuth2 登录跳转与重定向 hash 保留
- Azure 模型支持与转发特殊处理
- 后台登录与钉钉邮箱默认域名
- 转发获取密钥、Jinja 路径、RSA 私钥加载
- 模型管理可用模型输入与新增
- 自动更新权限、健康监测、admin 配置等

Co-authored-by: Cursor <github@npc0.com>
2026-02-24 16:24:23 +08:00

281 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package system
import (
"encoding/json"
"errors"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
gaiaReq "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request"
gaiaResponse "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/response"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
"github.com/flipped-aurora/gin-vue-admin/server/service"
sysSvc "github.com/flipped-aurora/gin-vue-admin/server/service/system"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"github.com/gin-gonic/gin"
"github.com/go-resty/resty/v2"
"go.uber.org/zap"
"gorm.io/gorm"
"net/http"
"strings"
)
var gaiaSystemIntegratedService = service.ServiceGroupApp.GaiaServiceGroup.SystemIntegratedService
// Extend Start: sync user
// SyncUser
// @Tags Base
// @Summary 用户同步
// @Produce application/json
// @Param data body systemReq.Login true "用户名, 密码, 验证码"
// @Success 200 {object} response.Response{data=systemRes.LoginResponse,msg=string} "返回包括用户信息,token,过期时间"
// @Router /user/sync [post]
func (b *BaseApi) SyncUser(c *gin.Context) {
userExtendService.SyncUser()
response.OkWithMessage("同步中", c)
}
// Extend Stop: sync user
// OaLogin
// @Tags Base
// @Summary 用户登录
// @Produce application/json
// @Param data body systemReq.Login true "用户名, 密码, 验证码"
// @Success 200 {object} response.Response{data=systemRes.LoginResponse,msg=string} "返回包括用户信息,token,过期时间"
// @Router /base/oaLogin [post]
func (b *BaseApi) OaLogin(c *gin.Context) {
var l systemReq.OaLoginReq
err := c.ShouldBindJSON(&l)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
err = utils.Verify(l, utils.OaLoginVerify)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
/**
请求获取accessToken
*/
clientGetToken := resty.New().R()
var accessTokenResponse *resty.Response
getTokenUrl := fmt.Sprintf("%s%s", global.GVA_CONFIG.OaLogin.Url, global.GVA_CONFIG.OaLogin.GetTokenByCodeApiPath)
var postParams = map[string]string{
"client_id": global.GVA_CONFIG.OaLogin.Oauth2ClientId,
"client_secret": global.GVA_CONFIG.OaLogin.Oauth2ClientSecret,
"code": l.AuthorizeCode,
"grant_type": "authorization_code",
"redirect_uri": "",
}
accessTokenResponse, err = clientGetToken.
SetFormData(postParams).
Post(getTokenUrl)
if err != nil {
global.GVA_LOG.Error("请求OA用户信息失败,响应数据为:", zap.Error(errors.New(accessTokenResponse.String())))
response.FailWithMessage("请求OA用户信息失败:"+err.Error(), c)
return
}
tokenRes := accessTokenResponse.String()
var oaAccessToken systemRes.OaAccessTokenRes
err = json.Unmarshal([]byte(tokenRes), &oaAccessToken)
if err != nil {
global.GVA_LOG.Error("解析OA AccessToken接口返回数据失败,响应数据为:", zap.Error(errors.New(accessTokenResponse.String())))
response.FailWithMessage("解析OA AccessToken接口返回数据失败:"+err.Error(), c)
return
}
/**
请求OA,返回用户信息
*/
getUserInfoUrl := fmt.Sprintf("%s%s", global.GVA_CONFIG.OaLogin.Url, global.GVA_CONFIG.OaLogin.GetUserApiPath)
clientGetUser := resty.New().R()
var userInfoResponse *resty.Response
userInfoResponse, err = clientGetUser.SetHeader("Authorization", oaAccessToken.AccessToken).Post(getUserInfoUrl)
if err != nil {
global.GVA_LOG.Error("请求OA用户信息失败,响应数据为:", zap.Error(errors.New(userInfoResponse.String())))
response.FailWithMessage("请求OA用户信息失败:"+err.Error(), c)
return
}
userInfoRes := userInfoResponse.String()
var oaUserInfo systemRes.OaUserInfoRes
err = json.Unmarshal([]byte(userInfoRes), &oaUserInfo)
if err != nil {
global.GVA_LOG.Error("解析OA用户信息接口返回数据失败,响应数据为:", zap.Error(errors.New(userInfoRes)))
global.GVA_LOG.Error("解析OA用户信息接口返回数据失败,请求Token为:", zap.String("", oaAccessToken.AccessToken))
global.GVA_LOG.Error("解析OA用户信息接口返回数据失败,请求Token为:", zap.String("", accessTokenResponse.String()))
response.FailWithMessage("解析OA用户信息接口返回数据失败:"+err.Error(), c)
return
}
// 查询数据库数据
sysUser := &system.SysUser{}
err = global.GVA_DB.Where("email", oaUserInfo.Data.Email).First(&sysUser).Error
if err != nil && err != gorm.ErrRecordNotFound {
response.FailWithMessage("查询数据库信息失败:"+err.Error(), c)
return
}
// 判断是否需要注册
if sysUser.ID == 0 {
// TODO 从ldap中获取用户详细数据
// 注册用户
sysUser = &system.SysUser{
Username: oaUserInfo.Data.Username,
NickName: oaUserInfo.Data.Username,
HeaderImg: "https://hn1.oss-cn-shenzhen.aliyuncs.com/w.jpg",
AuthorityId: system.NormalAuthorityId,
Authorities: []system.SysAuthority{{AuthorityId: system.NormalAuthorityId}},
Enable: 1,
//Phone: r.Phone, // TODO 手机需要从ldap中获取用户详细数据
Email: oaUserInfo.Data.Email,
Password: utils.RandomString(16),
}
var userReturn system.SysUser
userReturn, err = userService.Register(*sysUser, "")
if err != nil {
global.GVA_LOG.Error("注册失败!", zap.Error(err))
response.FailWithDetailed(systemRes.SysUserResponse{User: userReturn}, "注册失败", c)
return
} else {
global.GVA_LOG.Info("注册成功!", zap.Any("username", userReturn.Username))
}
sysUser = &userReturn
}
var user *system.SysUser
user, err = userExtendService.OaLogin(sysUser) // 注意这个方法不检查密码
if err != nil {
global.GVA_LOG.Error("登陆失败! 用户名不存在!", zap.Error(err))
response.FailWithMessage("用户名不存在", c)
return
}
if sysUser.Enable != 1 {
global.GVA_LOG.Error("登陆失败! 用户被禁止登录!")
response.FailWithMessage("用户被禁止登录", c)
return
}
b.TokenNext(c, *user)
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
// GetGaiaLoginOptions 获取 Gaia 登录方式(钉钉/OAuth2 是否启用及授权地址),供登录页展示,无需鉴权
// @Tags Base
// @Summary 获取登录方式选项
// @Produce application/json
// @Param origin query string false "前端 origin,用于拼回调地址"
// @Router /base/gaiaLoginOptions [get]
func (b *BaseApi) GetGaiaLoginOptions(c *gin.Context) {
origin := c.Query("origin")
if origin == "" {
origin = c.GetHeader("Origin")
}
if origin == "" {
origin = strings.TrimSuffix(global.GVA_CONFIG.Gaia.Url, "/")
}
opts := gaiaSystemIntegratedService.GetLoginOptions(origin)
response.OkWithData(opts, c)
}
// GaiaOAuth2Login 使用系统集成 OAuth2 的 code 或 access_tokenExtend: 兼容 casdoor)登录,返回 JWT;若带 redirect_uri/state 则一并返回供前端回调第三方
// @Tags Base
// @Summary Gaia OAuth2 登录
// @Produce application/json
// @Param data body gaiaReq.GaiaOAuth2LoginReq true "code 或 access_token 二选一、redirect_uri、state"
// @Router /base/gaiaOAuth2Login [post]
func (b *BaseApi) GaiaOAuth2Login(c *gin.Context) {
// init
var err error
var req gaiaReq.GaiaOAuth2LoginReq
var data = make(map[string]interface{})
var result *gaiaResponse.GaiaLoginResult
if err = c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if result, err = gaiaSystemIntegratedService.OAuth2CodeLogin(req); err != nil {
global.GVA_LOG.Error("Gaia OAuth2 登录失败", zap.Error(err))
response.FailWithMessage(err.Error(), c)
return
}
data["expiresAt"] = 0
data["user"] = result.User
data["token"] = result.Token
sysSvc.MenuServiceApp.UserAuthorityDefaultRouter(&result.User)
if result.RedirectURI != "" {
data["redirect_uri"] = result.RedirectURI
}
if result.State != "" {
data["state"] = result.State
}
response.OkWithDetailed(data, "登录成功", c)
}
// GaiaDingTalkLogin 钉钉 code 登录,返回 JWT
// @Tags Base
// @Summary 钉钉登录
// @Produce application/json
// @Param data body gaiaReq.GaiaDingTalkLoginReq true "auth_code、redirect_uri、state"
// @Router /base/dingtalkLogin [post]
func (b *BaseApi) GaiaDingTalkLogin(c *gin.Context) {
var req gaiaReq.GaiaDingTalkLoginReq
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
result, err := gaiaSystemIntegratedService.DingTalkCodeLogin(req)
if err != nil {
global.GVA_LOG.Error("钉钉登录失败", zap.Error(err))
response.FailWithMessage(err.Error(), c)
return
}
sysSvc.MenuServiceApp.UserAuthorityDefaultRouter(&result.User)
data := map[string]interface{}{
"user": result.User,
"token": result.Token,
"expiresAt": 0,
}
if result.RedirectURI != "" {
data["redirect_uri"] = result.RedirectURI
}
if result.State != "" {
data["state"] = result.State
}
response.OkWithDetailed(data, "登录成功", c)
}
// Extend Stop: gaia login