forked from casdoor/casdoor
Compare commits
10 Commits
copilot/fi
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39394d3e58 | ||
|
|
a3e9ce5109 | ||
|
|
f5af87683d | ||
|
|
df47f5785c | ||
|
|
4879926977 | ||
|
|
7148c9db85 | ||
|
|
29dccbe32f | ||
|
|
65755d3b28 | ||
|
|
239e8bd694 | ||
|
|
d23e8b205b |
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
"github.com/beego/beego/v2/core/logs"
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/casdoor/casdoor/mcpself"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -291,3 +292,14 @@ func (c *ApiController) Finish() {
|
||||
}
|
||||
c.Controller.Finish()
|
||||
}
|
||||
|
||||
func (c *ApiController) McpResponseError(id interface{}, code int, message string, data interface{}) {
|
||||
resp := mcpself.BuildMcpResponse(id, nil, &mcpself.McpError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
})
|
||||
c.Ctx.Output.Header("Content-Type", "application/json")
|
||||
c.Data["json"] = resp
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
222
controllers/key.go
Normal file
222
controllers/key.go
Normal file
@@ -0,0 +1,222 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// GetKeys
|
||||
// @Title GetKeys
|
||||
// @Tag Key API
|
||||
// @Description get keys
|
||||
// @Param owner query string true "The owner of keys"
|
||||
// @Success 200 {array} object.Key The Response object
|
||||
// @router /get-keys [get]
|
||||
func (c *ApiController) GetKeys() {
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
keys, err := object.GetKeys(owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
maskedKeys, err := object.GetMaskedKeys(keys, true, nil)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(maskedKeys)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
count, err := object.GetKeyCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
keys, err := object.GetPaginationKeys(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
maskedKeys, err := object.GetMaskedKeys(keys, true, nil)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(maskedKeys, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
// GetGlobalKeys
|
||||
// @Title GetGlobalKeys
|
||||
// @Tag Key API
|
||||
// @Description get global keys
|
||||
// @Success 200 {array} object.Key The Response object
|
||||
// @router /get-global-keys [get]
|
||||
func (c *ApiController) GetGlobalKeys() {
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
keys, err := object.GetGlobalKeys()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
maskedKeys, err := object.GetMaskedKeys(keys, true, nil)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(maskedKeys)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
count, err := object.GetGlobalKeyCount(field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
keys, err := object.GetPaginationGlobalKeys(paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
maskedKeys, err := object.GetMaskedKeys(keys, true, nil)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(maskedKeys, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
// GetKey
|
||||
// @Title GetKey
|
||||
// @Tag Key API
|
||||
// @Description get key
|
||||
// @Param id query string true "The id ( owner/name ) of the key"
|
||||
// @Success 200 {object} object.Key The Response object
|
||||
// @router /get-key [get]
|
||||
func (c *ApiController) GetKey() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
key, err := object.GetKey(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(key)
|
||||
}
|
||||
|
||||
// UpdateKey
|
||||
// @Title UpdateKey
|
||||
// @Tag Key API
|
||||
// @Description update key
|
||||
// @Param id query string true "The id ( owner/name ) of the key"
|
||||
// @Param body body object.Key true "The details of the key"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-key [post]
|
||||
func (c *ApiController) UpdateKey() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
oldKey, err := object.GetKey(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if oldKey == nil {
|
||||
c.Data["json"] = wrapActionResponse(false)
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
var key object.Key
|
||||
err = json.Unmarshal(c.Ctx.Input.RequestBody, &key)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !c.IsGlobalAdmin() && oldKey.Owner != key.Owner {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateKey(id, &key))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddKey
|
||||
// @Title AddKey
|
||||
// @Tag Key API
|
||||
// @Description add key
|
||||
// @Param body body object.Key true "The details of the key"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-key [post]
|
||||
func (c *ApiController) AddKey() {
|
||||
var key object.Key
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &key)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddKey(&key))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteKey
|
||||
// @Title DeleteKey
|
||||
// @Tag Key API
|
||||
// @Description delete key
|
||||
// @Param body body object.Key true "The details of the key"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-key [post]
|
||||
func (c *ApiController) DeleteKey() {
|
||||
var key object.Key
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &key)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteKey(&key))
|
||||
c.ServeJSON()
|
||||
}
|
||||
112
controllers/mcp_server.go
Normal file
112
controllers/mcp_server.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"github.com/casdoor/casdoor/mcpself"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// ProxyServer
|
||||
// @Title ProxyServer
|
||||
// @Tag Server API
|
||||
// @Description proxy request to the upstream MCP server by Server URL
|
||||
// @Param owner path string true "The owner name of the server"
|
||||
// @Param name path string true "The name of the server"
|
||||
// @Success 200 {object} mcp.McpResponse The Response object
|
||||
// @router /server/:owner/:name [post]
|
||||
func (c *ApiController) ProxyServer() {
|
||||
owner := c.Ctx.Input.Param(":owner")
|
||||
name := c.Ctx.Input.Param(":name")
|
||||
|
||||
var mcpReq *mcpself.McpRequest
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &mcpReq)
|
||||
if err != nil {
|
||||
c.McpResponseError(1, -32700, "Parse error", err.Error())
|
||||
return
|
||||
}
|
||||
if util.IsStringsEmpty(owner, name) {
|
||||
c.McpResponseError(1, -32600, "invalid server identifier", nil)
|
||||
return
|
||||
}
|
||||
|
||||
server, err := object.GetServer(util.GetId(owner, name))
|
||||
if err != nil {
|
||||
c.McpResponseError(mcpReq.ID, -32600, "server not found", err.Error())
|
||||
return
|
||||
}
|
||||
if server == nil {
|
||||
c.McpResponseError(mcpReq.ID, -32600, "server not found", nil)
|
||||
return
|
||||
}
|
||||
if server.Url == "" {
|
||||
c.McpResponseError(mcpReq.ID, -32600, "server URL is empty", nil)
|
||||
return
|
||||
}
|
||||
|
||||
targetUrl, err := url.Parse(server.Url)
|
||||
if err != nil || !targetUrl.IsAbs() || targetUrl.Host == "" {
|
||||
c.McpResponseError(mcpReq.ID, -32600, "server URL is invalid", nil)
|
||||
return
|
||||
}
|
||||
if targetUrl.Scheme != "http" && targetUrl.Scheme != "https" {
|
||||
c.McpResponseError(mcpReq.ID, -32600, "server URL scheme is invalid", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if mcpReq.Method == "tools/call" {
|
||||
var params mcpself.McpCallToolParams
|
||||
err = json.Unmarshal(mcpReq.Params, ¶ms)
|
||||
if err != nil {
|
||||
c.McpResponseError(mcpReq.ID, -32600, "Invalid request", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, tool := range server.Tools {
|
||||
if tool.Name == params.Name && !tool.IsAllowed {
|
||||
c.McpResponseError(mcpReq.ID, -32600, "tool is forbidden", nil)
|
||||
return
|
||||
} else if tool.Name == params.Name {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(targetUrl)
|
||||
proxy.ErrorHandler = func(writer http.ResponseWriter, request *http.Request, proxyErr error) {
|
||||
c.Ctx.Output.SetStatus(http.StatusBadGateway)
|
||||
c.McpResponseError(mcpReq.ID, -32603, "failed to proxy server request: %s", proxyErr.Error())
|
||||
}
|
||||
proxy.Director = func(request *http.Request) {
|
||||
request.URL.Scheme = targetUrl.Scheme
|
||||
request.URL.Host = targetUrl.Host
|
||||
request.Host = targetUrl.Host
|
||||
request.URL.Path = targetUrl.Path
|
||||
request.URL.RawPath = ""
|
||||
request.URL.RawQuery = targetUrl.RawQuery
|
||||
|
||||
if server.Token != "" {
|
||||
request.Header.Set("Authorization", "Bearer "+server.Token)
|
||||
}
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)
|
||||
}
|
||||
@@ -16,10 +16,6 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@@ -151,61 +147,3 @@ func (c *ApiController) DeleteServer() {
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteServer(&server))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// ProxyServer
|
||||
// @Title ProxyServer
|
||||
// @Tag Server API
|
||||
// @Description proxy request to the upstream MCP server by Server URL
|
||||
// @Param owner path string true "The owner name of the server"
|
||||
// @Param name path string true "The name of the server"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /server/:owner/:name [get,post]
|
||||
func (c *ApiController) ProxyServer() {
|
||||
owner := c.Ctx.Input.Param(":owner")
|
||||
name := c.Ctx.Input.Param(":name")
|
||||
if util.IsStringsEmpty(owner, name) {
|
||||
c.ResponseError("invalid server identifier")
|
||||
return
|
||||
}
|
||||
|
||||
server, err := object.GetServer(util.GetId(owner, name))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if server == nil {
|
||||
c.ResponseError("server not found")
|
||||
return
|
||||
}
|
||||
if server.Url == "" {
|
||||
c.ResponseError("server URL is empty")
|
||||
return
|
||||
}
|
||||
|
||||
targetUrl, err := url.Parse(server.Url)
|
||||
if err != nil || !targetUrl.IsAbs() || targetUrl.Host == "" {
|
||||
c.ResponseError("server URL is invalid")
|
||||
return
|
||||
}
|
||||
if targetUrl.Scheme != "http" && targetUrl.Scheme != "https" {
|
||||
c.ResponseError("server URL scheme is invalid")
|
||||
return
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(targetUrl)
|
||||
proxy.ErrorHandler = func(writer http.ResponseWriter, request *http.Request, proxyErr error) {
|
||||
c.Ctx.Output.SetStatus(http.StatusBadGateway)
|
||||
c.ResponseError(fmt.Sprintf("failed to proxy server request: %s", proxyErr.Error()))
|
||||
}
|
||||
proxy.Director = func(request *http.Request) {
|
||||
request.URL.Scheme = targetUrl.Scheme
|
||||
request.URL.Host = targetUrl.Host
|
||||
request.Host = targetUrl.Host
|
||||
request.URL.Path = targetUrl.Path
|
||||
request.URL.RawPath = ""
|
||||
|
||||
request.URL.RawQuery = targetUrl.RawQuery
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)
|
||||
}
|
||||
|
||||
@@ -730,29 +730,6 @@ func (c *ApiController) GetUserCount() {
|
||||
c.ResponseOk(count)
|
||||
}
|
||||
|
||||
// AddUserKeys
|
||||
// @Title AddUserKeys
|
||||
// @router /add-user-keys [post]
|
||||
// @Tag User API
|
||||
// @Success 200 {object} object.Userinfo The Response object
|
||||
func (c *ApiController) AddUserKeys() {
|
||||
var user object.User
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
isAdmin := c.IsAdmin()
|
||||
affected, err := object.AddUserKeys(&user, isAdmin)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(affected)
|
||||
}
|
||||
|
||||
func (c *ApiController) RemoveUserFromGroup() {
|
||||
owner := c.Ctx.Request.Form.Get("owner")
|
||||
name := c.Ctx.Request.Form.Get("name")
|
||||
|
||||
13
go.mod
13
go.mod
@@ -46,7 +46,7 @@ require (
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
||||
github.com/go-webauthn/webauthn v0.10.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hsluoyz/modsecurity-go v0.0.7
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4
|
||||
@@ -59,6 +59,7 @@ require (
|
||||
github.com/markbates/goth v1.82.0
|
||||
github.com/microsoft/go-mssqldb v1.9.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.0
|
||||
github.com/nyaruka/phonenumbers v1.2.2
|
||||
github.com/polarsource/polar-go v0.12.0
|
||||
github.com/pquerna/otp v1.4.0
|
||||
@@ -81,7 +82,7 @@ require (
|
||||
github.com/xorm-io/xorm v1.1.6
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/net v0.49.0
|
||||
golang.org/x/oauth2 v0.32.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/text v0.33.0
|
||||
golang.org/x/time v0.8.0
|
||||
google.golang.org/api v0.215.0
|
||||
@@ -179,6 +180,7 @@ require (
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/go-tpm v0.9.0 // indirect
|
||||
github.com/google/jsonschema-go v0.4.2 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
@@ -237,6 +239,8 @@ require (
|
||||
github.com/rs/zerolog v1.33.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||
github.com/scim2/filter-parser/v2 v2.2.0 // indirect
|
||||
github.com/segmentio/asm v1.1.3 // indirect
|
||||
github.com/segmentio/encoding v0.5.3 // indirect
|
||||
github.com/sendgrid/rest v2.6.9+incompatible // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
|
||||
@@ -268,6 +272,7 @@ require (
|
||||
github.com/volcengine/volc-sdk-golang v1.0.117 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.mau.fi/util v0.8.3 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
@@ -285,10 +290,10 @@ require (
|
||||
go.uber.org/zap v1.19.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect
|
||||
golang.org/x/image v0.18.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
|
||||
26
go.sum
26
go.sum
@@ -1114,8 +1114,8 @@ github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
@@ -1192,6 +1192,8 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17
|
||||
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
|
||||
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
|
||||
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@@ -1496,6 +1498,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
|
||||
github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.0 h1:u0kr8lbJc1oBcawK7Df+/ajNMpIDFE41OEPxdeTLOn8=
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.0/go.mod h1:Nxc2n+n/GdCebUaqCOhTetptS17SXXNu9IfNTaLDi1E=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -1656,6 +1660,10 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
|
||||
github.com/scim2/filter-parser/v2 v2.2.0 h1:QGadEcsmypxg8gYChRSM2j1edLyE/2j72j+hdmI4BJM=
|
||||
github.com/scim2/filter-parser/v2 v2.2.0/go.mod h1:jWnkDToqX/Y0ugz0P5VvpVEUKcWcyHHj+X+je9ce5JA=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
|
||||
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
|
||||
github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
|
||||
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
|
||||
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
|
||||
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
|
||||
github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBUpXyTcTXy/Rl9N9aZ/Qw=
|
||||
@@ -1794,6 +1802,8 @@ github.com/xorm-io/core v0.7.4 h1:qIznlqqmYNEb03ewzRXCrNkbbxpkgc/44nVF8yoFV7Y=
|
||||
github.com/xorm-io/core v0.7.4/go.mod h1:GueyhafDnkB0KK0fXX/dEhr/P1EAGW0GLmoNDUEE1Mo=
|
||||
github.com/xorm-io/xorm v1.1.6 h1:s4fDpUXJx8Zr/PBovXNaadn+v1P3h/U3iV4OxAkWS8s=
|
||||
github.com/xorm-io/xorm v1.1.6/go.mod h1:7nsSUdmgLIcqHSSaKOzbVQiZtzIzbpGf1GGSYp6DD70=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -1975,8 +1985,8 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.0.0-20171115151908-9dfe39835686/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -2097,8 +2107,8 @@ golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw
|
||||
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
|
||||
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
|
||||
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20171101214715-fd80eb99c8f6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -2383,8 +2393,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
51
mcp/util.go
Normal file
51
mcp/util.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func GetServerTools(owner, name, url, token string) ([]*mcpsdk.Tool, error) {
|
||||
var session *mcpsdk.ClientSession
|
||||
var err error
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10)
|
||||
defer cancel()
|
||||
client := mcpsdk.NewClient(&mcpsdk.Implementation{Name: util.GetId(owner, name), Version: "1.0.0"}, nil)
|
||||
if token != "" {
|
||||
httpClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}))
|
||||
session, err = client.Connect(ctx, &mcpsdk.StreamableClientTransport{Endpoint: url, HTTPClient: httpClient}, nil)
|
||||
} else {
|
||||
session, err = client.Connect(ctx, &mcpsdk.StreamableClientTransport{Endpoint: url}, nil)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
toolResult, err := session.ListTools(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toolResult.Tools, nil
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcp
|
||||
package mcpself
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcp
|
||||
package mcpself
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcp
|
||||
package mcpself
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcp
|
||||
package mcpself
|
||||
|
||||
import (
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@@ -90,7 +90,6 @@ func getBuiltInAccountItems() []*AccountItem {
|
||||
{Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
|
||||
{Name: "Register type", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
|
||||
{Name: "Register source", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
|
||||
{Name: "API key", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Roles", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
|
||||
{Name: "Permissions", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
|
||||
{Name: "Groups", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
|
||||
|
||||
208
object/key.go
Normal file
208
object/key.go
Normal file
@@ -0,0 +1,208 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
|
||||
type Key struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
|
||||
// Type indicates the scope this key belongs to: "Organization", "Application", "User", or "General"
|
||||
Type string `xorm:"varchar(100)" json:"type"`
|
||||
Organization string `xorm:"varchar(100)" json:"organization"`
|
||||
Application string `xorm:"varchar(100)" json:"application"`
|
||||
User string `xorm:"varchar(100)" json:"user"`
|
||||
|
||||
AccessKey string `xorm:"varchar(100) index" json:"accessKey"`
|
||||
AccessSecret string `xorm:"varchar(100)" json:"accessSecret"`
|
||||
|
||||
ExpireTime string `xorm:"varchar(100)" json:"expireTime"`
|
||||
State string `xorm:"varchar(100)" json:"state"`
|
||||
}
|
||||
|
||||
func GetKeyCount(owner, field, value string) (int64, error) {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
return session.Count(&Key{})
|
||||
}
|
||||
|
||||
func GetGlobalKeyCount(field, value string) (int64, error) {
|
||||
session := GetSession("", -1, -1, field, value, "", "")
|
||||
return session.Count(&Key{})
|
||||
}
|
||||
|
||||
func GetKeys(owner string) ([]*Key, error) {
|
||||
keys := []*Key{}
|
||||
err := ormer.Engine.Desc("created_time").Find(&keys, &Key{Owner: owner})
|
||||
if err != nil {
|
||||
return keys, err
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func GetPaginationKeys(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Key, error) {
|
||||
keys := []*Key{}
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Find(&keys)
|
||||
if err != nil {
|
||||
return keys, err
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func GetGlobalKeys() ([]*Key, error) {
|
||||
keys := []*Key{}
|
||||
err := ormer.Engine.Desc("created_time").Find(&keys)
|
||||
if err != nil {
|
||||
return keys, err
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func GetPaginationGlobalKeys(offset, limit int, field, value, sortField, sortOrder string) ([]*Key, error) {
|
||||
keys := []*Key{}
|
||||
session := GetSession("", offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Find(&keys)
|
||||
if err != nil {
|
||||
return keys, err
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func getKey(owner, name string) (*Key, error) {
|
||||
if owner == "" || name == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
key := Key{Owner: owner, Name: name}
|
||||
existed, err := ormer.Engine.Get(&key)
|
||||
if err != nil {
|
||||
return &key, err
|
||||
}
|
||||
|
||||
if existed {
|
||||
return &key, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func GetKey(id string) (*Key, error) {
|
||||
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getKey(owner, name)
|
||||
}
|
||||
|
||||
func GetMaskedKey(key *Key, isMaskEnabled bool) *Key {
|
||||
if !isMaskEnabled {
|
||||
return key
|
||||
}
|
||||
|
||||
if key == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if key.AccessSecret != "" {
|
||||
key.AccessSecret = "***"
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func GetMaskedKeys(keys []*Key, isMaskEnabled bool, err error) ([]*Key, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
GetMaskedKey(key, isMaskEnabled)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func UpdateKey(id string, key *Key) (bool, error) {
|
||||
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if k, err := getKey(owner, name); err != nil {
|
||||
return false, err
|
||||
} else if k == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
key.UpdatedTime = util.GetCurrentTime()
|
||||
|
||||
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func AddKey(key *Key) (bool, error) {
|
||||
if key.AccessKey == "" {
|
||||
key.AccessKey = util.GenerateId()
|
||||
}
|
||||
if key.AccessSecret == "" {
|
||||
key.AccessSecret = util.GenerateId()
|
||||
}
|
||||
|
||||
affected, err := ormer.Engine.Insert(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func DeleteKey(key *Key) (bool, error) {
|
||||
affected, err := ormer.Engine.ID(core.PK{key.Owner, key.Name}).Delete(&Key{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func (key *Key) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", key.Owner, key.Name)
|
||||
}
|
||||
|
||||
func GetKeyByAccessKey(accessKey string) (*Key, error) {
|
||||
if accessKey == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
key := Key{AccessKey: accessKey}
|
||||
existed, err := ormer.Engine.Get(&key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existed {
|
||||
return &key, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
@@ -67,6 +67,7 @@ type Organization struct {
|
||||
PasswordExpireDays int `json:"passwordExpireDays"`
|
||||
CountryCodes []string `xorm:"mediumtext" json:"countryCodes"`
|
||||
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
|
||||
UsePermanentAvatar bool `xorm:"bool" json:"usePermanentAvatar"`
|
||||
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
|
||||
UserTypes []string `xorm:"mediumtext" json:"userTypes"`
|
||||
Tags []string `xorm:"mediumtext" json:"tags"`
|
||||
|
||||
@@ -343,6 +343,11 @@ func (a *Ormer) createTable() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Key))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Role))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -16,11 +16,19 @@ package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/casdoor/casdoor/mcp"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
|
||||
type Tool struct {
|
||||
mcpsdk.Tool
|
||||
IsAllowed bool `json:"isAllowed"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
@@ -28,8 +36,10 @@ type Server struct {
|
||||
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
|
||||
Url string `xorm:"varchar(500)" json:"url"`
|
||||
Application string `xorm:"varchar(100)" json:"application"`
|
||||
Url string `xorm:"varchar(500)" json:"url"`
|
||||
Token string `xorm:"varchar(500)" json:"-"`
|
||||
Application string `xorm:"varchar(100)" json:"application"`
|
||||
Tools []*Tool `xorm:"mediumtext" json:"tools"`
|
||||
}
|
||||
|
||||
func GetServers(owner string) ([]*Server, error) {
|
||||
@@ -70,6 +80,7 @@ func UpdateServer(id string, server *Server) (bool, error) {
|
||||
|
||||
server.UpdatedTime = util.GetCurrentTime()
|
||||
|
||||
syncServerTools(server)
|
||||
_, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(server)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -78,6 +89,37 @@ func UpdateServer(id string, server *Server) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func syncServerTools(server *Server) {
|
||||
if server.Tools == nil {
|
||||
server.Tools = []*Tool{}
|
||||
}
|
||||
|
||||
tools, err := mcp.GetServerTools(server.Owner, server.Name, server.Url, server.Token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var newTools []*Tool
|
||||
for _, tool := range tools {
|
||||
oldToolIndex := slices.IndexFunc(server.Tools, func(oldTool *Tool) bool {
|
||||
return oldTool.Name == tool.Name
|
||||
})
|
||||
|
||||
isAllowed := true
|
||||
if oldToolIndex != -1 {
|
||||
isAllowed = server.Tools[oldToolIndex].IsAllowed
|
||||
}
|
||||
|
||||
newTool := Tool{
|
||||
Tool: *tool,
|
||||
IsAllowed: isAllowed,
|
||||
}
|
||||
newTools = append(newTools, &newTool)
|
||||
}
|
||||
|
||||
server.Tools = newTools
|
||||
}
|
||||
|
||||
func AddServer(server *Server) (bool, error) {
|
||||
affected, err := ormer.Engine.Insert(server)
|
||||
if err != nil {
|
||||
|
||||
@@ -112,8 +112,6 @@ type UserWithoutThirdIdp struct {
|
||||
PreHash string `xorm:"varchar(100)" json:"preHash"`
|
||||
RegisterType string `xorm:"varchar(100)" json:"registerType"`
|
||||
RegisterSource string `xorm:"varchar(100)" json:"registerSource"`
|
||||
AccessKey string `xorm:"varchar(100)" json:"accessKey"`
|
||||
AccessSecret string `xorm:"varchar(100)" json:"accessSecret"`
|
||||
|
||||
GitHub string `xorm:"github varchar(100)" json:"github"`
|
||||
Google string `xorm:"varchar(100)" json:"google"`
|
||||
@@ -267,8 +265,6 @@ func getUserWithoutThirdIdp(user *User) *UserWithoutThirdIdp {
|
||||
PreHash: user.PreHash,
|
||||
RegisterType: user.RegisterType,
|
||||
RegisterSource: user.RegisterSource,
|
||||
AccessKey: user.AccessKey,
|
||||
AccessSecret: user.AccessSecret,
|
||||
|
||||
GitHub: user.GitHub,
|
||||
Google: user.Google,
|
||||
|
||||
@@ -109,8 +109,6 @@ type User struct {
|
||||
PreHash string `xorm:"varchar(100)" json:"preHash"`
|
||||
RegisterType string `xorm:"varchar(100)" json:"registerType"`
|
||||
RegisterSource string `xorm:"varchar(100)" json:"registerSource"`
|
||||
AccessKey string `xorm:"varchar(100)" json:"accessKey"`
|
||||
AccessSecret string `xorm:"varchar(100)" json:"accessSecret"`
|
||||
AccessToken string `xorm:"mediumtext" json:"accessToken"`
|
||||
OriginalToken string `xorm:"mediumtext" json:"originalToken"`
|
||||
OriginalRefreshToken string `xorm:"mediumtext" json:"originalRefreshToken"`
|
||||
@@ -639,23 +637,6 @@ func GetUserByInvitationCode(owner string, invitationCode string) (*User, error)
|
||||
}
|
||||
}
|
||||
|
||||
func GetUserByAccessKey(accessKey string) (*User, error) {
|
||||
if accessKey == "" {
|
||||
return nil, nil
|
||||
}
|
||||
user := User{AccessKey: accessKey}
|
||||
existed, err := ormer.Engine.Get(&user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existed {
|
||||
return &user, nil
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetUser(id string) (*User, error) {
|
||||
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
@@ -683,9 +664,6 @@ func GetMaskedUser(user *User, isAdminOrSelf bool, errs ...error) (*User, error)
|
||||
}
|
||||
|
||||
if !isAdminOrSelf {
|
||||
if user.AccessSecret != "" {
|
||||
user.AccessSecret = "***"
|
||||
}
|
||||
if user.OriginalToken != "" {
|
||||
user.OriginalToken = "***"
|
||||
}
|
||||
@@ -865,7 +843,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
||||
"owner", "display_name", "avatar", "first_name", "last_name",
|
||||
"location", "address", "addresses", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application", "register_type", "register_source",
|
||||
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "mfa_items", "last_change_password_time", "managedAccounts", "face_ids", "mfaAccounts",
|
||||
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled", "email_verified",
|
||||
"signin_wrong_times", "last_signin_wrong_time", "groups", "mfa_phone_enabled", "mfa_email_enabled", "email_verified",
|
||||
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
|
||||
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "kwai", "line", "amazon",
|
||||
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
|
||||
@@ -1396,17 +1374,6 @@ func (user *User) GetPreferredMfaProps(masked bool) *MfaProps {
|
||||
return user.GetMfaProps(user.PreferredMfaType, masked)
|
||||
}
|
||||
|
||||
func AddUserKeys(user *User, isAdmin bool) (bool, error) {
|
||||
if user == nil {
|
||||
return false, fmt.Errorf("the user is not found")
|
||||
}
|
||||
|
||||
user.AccessKey = util.GenerateId()
|
||||
user.AccessSecret = util.GenerateId()
|
||||
|
||||
return UpdateUser(user.GetId(), user, []string{}, isAdmin)
|
||||
}
|
||||
|
||||
func (user *User) IsApplicationAdmin(application *Application) bool {
|
||||
if user == nil {
|
||||
return false
|
||||
|
||||
@@ -249,9 +249,17 @@ func SetUserOAuthProperties(organization *Organization, user *User, providerType
|
||||
|
||||
if userInfo.AvatarUrl != "" {
|
||||
propertyName := fmt.Sprintf("oauth_%s_avatarUrl", providerType)
|
||||
setUserProperty(user, propertyName, userInfo.AvatarUrl)
|
||||
if user.Avatar == "" || user.Avatar == organization.DefaultAvatar {
|
||||
user.Avatar = userInfo.AvatarUrl
|
||||
|
||||
if organization.UsePermanentAvatar {
|
||||
err := syncOAuthAvatarToPermanentStorage(organization, user, propertyName, userInfo.AvatarUrl)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
setUserProperty(user, propertyName, userInfo.AvatarUrl)
|
||||
if user.Avatar == "" || user.Avatar == organization.DefaultAvatar {
|
||||
user.Avatar = userInfo.AvatarUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,6 +292,45 @@ func SetUserOAuthProperties(organization *Organization, user *User, providerType
|
||||
return UpdateUserForAllFields(user.GetId(), user)
|
||||
}
|
||||
|
||||
// syncOAuthAvatarToPermanentStorage ensures the user's avatar is stored in permanent storage.
|
||||
// It checks whether a permanent avatar already exists for the given sourceAvatarURL.
|
||||
// If not, it uploads the avatar and retrieves a permanent URL.
|
||||
// Finally, it updates the user's avatar fields with the resolved permanent URL.
|
||||
func syncOAuthAvatarToPermanentStorage(organization *Organization, user *User, propertyName, sourceAvatarUrl string) error {
|
||||
oldAvatarUrl := getUserProperty(user, propertyName)
|
||||
|
||||
avatarUrl := sourceAvatarUrl
|
||||
permanentAvatarUrl, err := getPermanentAvatarUrl(user.Owner, user.Name, sourceAvatarUrl, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if permanentAvatarUrl != "" {
|
||||
avatarUrl = permanentAvatarUrl
|
||||
|
||||
if oldAvatarUrl != permanentAvatarUrl {
|
||||
avatarUrl, err = getPermanentAvatarUrl(user.Owner, user.Name, sourceAvatarUrl, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if avatarUrl == "" {
|
||||
avatarUrl = permanentAvatarUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setUserProperty(user, propertyName, avatarUrl)
|
||||
|
||||
if user.Avatar == "" ||
|
||||
user.Avatar == organization.DefaultAvatar ||
|
||||
user.Avatar == sourceAvatarUrl ||
|
||||
(oldAvatarUrl != "" && user.Avatar == oldAvatarUrl) {
|
||||
user.Avatar = avatarUrl
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyUserMapping(user *User, extraClaims map[string]string, userMapping map[string]string) {
|
||||
// Map of user fields that can be set from IDP claims
|
||||
for userField, claimName := range userMapping {
|
||||
|
||||
@@ -32,10 +32,8 @@ import (
|
||||
)
|
||||
|
||||
type Object struct {
|
||||
Owner string `json:"owner"`
|
||||
Name string `json:"name"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
AccessSecret string `json:"accessSecret"`
|
||||
Owner string `json:"owner"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ObjectWithOrg struct {
|
||||
@@ -49,10 +47,6 @@ func getUsername(ctx *context.Context) (username string) {
|
||||
username, _ = getUsernameByClientIdSecret(ctx)
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
username, _ = getUsernameByKeys(ctx)
|
||||
}
|
||||
|
||||
session := ctx.Input.Session("SessionData")
|
||||
if session == nil {
|
||||
return
|
||||
@@ -185,30 +179,6 @@ func getObject(ctx *context.Context) (string, string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func getKeys(ctx *context.Context) (string, string) {
|
||||
method := ctx.Request.Method
|
||||
|
||||
if method == http.MethodGet {
|
||||
accessKey := ctx.Input.Query("accessKey")
|
||||
accessSecret := ctx.Input.Query("accessSecret")
|
||||
return accessKey, accessSecret
|
||||
} else {
|
||||
body := ctx.Input.RequestBody
|
||||
|
||||
if len(body) == 0 {
|
||||
return ctx.Request.Form.Get("accessKey"), ctx.Request.Form.Get("accessSecret")
|
||||
}
|
||||
|
||||
var obj Object
|
||||
err := json.Unmarshal(body, &obj)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return obj.AccessKey, obj.AccessSecret
|
||||
}
|
||||
}
|
||||
|
||||
func willLog(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
|
||||
if subOwner == "anonymous" && subName == "anonymous" && method == "GET" && (urlPath == "/api/get-account" || urlPath == "/api/get-app-login") && objOwner == "" && objName == "" {
|
||||
return false
|
||||
@@ -334,7 +304,7 @@ func ApiFilter(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if !isAllowed {
|
||||
if urlPath == "/api/mcp" {
|
||||
if urlPath == "/api/mcp" || strings.HasPrefix(urlPath, "/api/server/") {
|
||||
denyMcpRequest(ctx)
|
||||
} else {
|
||||
denyRequest(ctx)
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
"github.com/casdoor/casdoor/mcp"
|
||||
"github.com/casdoor/casdoor/mcpself"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -31,7 +31,7 @@ func AutoSigninFilter(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
if urlPath == "/api/mcp" {
|
||||
var req mcp.McpRequest
|
||||
var req mcpself.McpRequest
|
||||
if err := json.Unmarshal(ctx.Input.RequestBody, &req); err == nil {
|
||||
if req.Method == "initialize" || req.Method == "notifications/initialized" || req.Method == "ping" || req.Method == "tools/list" {
|
||||
return
|
||||
@@ -86,17 +86,6 @@ func AutoSigninFilter(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
accessKey := ctx.Input.Query("accessKey")
|
||||
accessSecret := ctx.Input.Query("accessSecret")
|
||||
if accessKey != "" && accessSecret != "" {
|
||||
userId, err := getUsernameByKeys(ctx)
|
||||
if err != nil {
|
||||
responseError(ctx, err.Error())
|
||||
}
|
||||
|
||||
setSessionUser(ctx, userId)
|
||||
}
|
||||
|
||||
// "/page?clientId=123&clientSecret=456"
|
||||
userId, err := getUsernameByClientIdSecret(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/mcp"
|
||||
"github.com/casdoor/casdoor/mcpself"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -75,7 +75,7 @@ func denyRequest(ctx *context.Context) {
|
||||
}
|
||||
|
||||
func denyMcpRequest(ctx *context.Context) {
|
||||
req := mcp.McpRequest{}
|
||||
req := mcpself.McpRequest{}
|
||||
err := json.Unmarshal(ctx.Input.RequestBody, &req)
|
||||
if err != nil {
|
||||
ctx.Output.SetStatus(http.StatusBadRequest)
|
||||
@@ -88,7 +88,7 @@ func denyMcpRequest(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
resp := mcp.BuildMcpResponse(req.ID, nil, &mcp.McpError{
|
||||
resp := mcpself.BuildMcpResponse(req.ID, nil, &mcpself.McpError{
|
||||
Code: -32001,
|
||||
Message: "Unauthorized",
|
||||
Data: T(ctx, "auth:Unauthorized operation"),
|
||||
@@ -135,24 +135,6 @@ func getUsernameByClientIdSecret(ctx *context.Context) (string, error) {
|
||||
return fmt.Sprintf("app/%s", application.Name), nil
|
||||
}
|
||||
|
||||
func getUsernameByKeys(ctx *context.Context) (string, error) {
|
||||
accessKey, accessSecret := getKeys(ctx)
|
||||
user, err := object.GetUserByAccessKey(accessKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return "", fmt.Errorf("user not found for access key: %s", accessKey)
|
||||
}
|
||||
|
||||
if accessSecret != user.AccessSecret {
|
||||
return "", fmt.Errorf("incorrect access secret for user: %s", user.Name)
|
||||
}
|
||||
|
||||
return user.GetId(), nil
|
||||
}
|
||||
|
||||
func getSessionUser(ctx *context.Context) string {
|
||||
user := ctx.Input.CruSession.Get(stdcontext.Background(), "username")
|
||||
if user == nil {
|
||||
|
||||
@@ -26,7 +26,7 @@ package routers
|
||||
import (
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/casdoor/casdoor/controllers"
|
||||
"github.com/casdoor/casdoor/mcp"
|
||||
"github.com/casdoor/casdoor/mcpself"
|
||||
)
|
||||
|
||||
func InitAPI() {
|
||||
@@ -87,7 +87,6 @@ func InitAPI() {
|
||||
web.Router("/api/get-user-count", &controllers.ApiController{}, "GET:GetUserCount")
|
||||
web.Router("/api/get-user", &controllers.ApiController{}, "GET:GetUser")
|
||||
web.Router("/api/update-user", &controllers.ApiController{}, "POST:UpdateUser")
|
||||
web.Router("/api/add-user-keys", &controllers.ApiController{}, "POST:AddUserKeys")
|
||||
web.Router("/api/add-user", &controllers.ApiController{}, "POST:AddUser")
|
||||
web.Router("/api/delete-user", &controllers.ApiController{}, "POST:DeleteUser")
|
||||
web.Router("/api/upload-users", &controllers.ApiController{}, "POST:UploadUsers")
|
||||
@@ -139,7 +138,7 @@ func InitAPI() {
|
||||
web.Router("/api/update-server", &controllers.ApiController{}, "POST:UpdateServer")
|
||||
web.Router("/api/add-server", &controllers.ApiController{}, "POST:AddServer")
|
||||
web.Router("/api/delete-server", &controllers.ApiController{}, "POST:DeleteServer")
|
||||
web.Router("/api/server/:owner/:name", &controllers.ApiController{}, "GET,POST:ProxyServer")
|
||||
web.Router("/api/server/:owner/:name", &controllers.ApiController{}, "POST:ProxyServer")
|
||||
|
||||
web.Router("/api/get-rules", &controllers.ApiController{}, "GET:GetRules")
|
||||
web.Router("/api/get-rule", &controllers.ApiController{}, "GET:GetRule")
|
||||
@@ -155,6 +154,13 @@ func InitAPI() {
|
||||
web.Router("/api/delete-cert", &controllers.ApiController{}, "POST:DeleteCert")
|
||||
web.Router("/api/update-cert-domain-expire", &controllers.ApiController{}, "POST:UpdateCertDomainExpire")
|
||||
|
||||
web.Router("/api/get-keys", &controllers.ApiController{}, "GET:GetKeys")
|
||||
web.Router("/api/get-global-keys", &controllers.ApiController{}, "GET:GetGlobalKeys")
|
||||
web.Router("/api/get-key", &controllers.ApiController{}, "GET:GetKey")
|
||||
web.Router("/api/update-key", &controllers.ApiController{}, "POST:UpdateKey")
|
||||
web.Router("/api/add-key", &controllers.ApiController{}, "POST:AddKey")
|
||||
web.Router("/api/delete-key", &controllers.ApiController{}, "POST:DeleteKey")
|
||||
|
||||
web.Router("/api/get-roles", &controllers.ApiController{}, "GET:GetRoles")
|
||||
web.Router("/api/get-role", &controllers.ApiController{}, "GET:GetRole")
|
||||
web.Router("/api/update-role", &controllers.ApiController{}, "POST:UpdateRole")
|
||||
@@ -366,7 +372,7 @@ func InitAPI() {
|
||||
|
||||
web.Router("/scim/*", &controllers.RootController{}, "*:HandleScim")
|
||||
|
||||
web.Router("/api/mcp", &mcp.McpController{}, "POST:HandleMcp")
|
||||
web.Router("/api/mcp", &mcpself.McpController{}, "POST:HandleMcp")
|
||||
|
||||
web.Router("/api/faceid-signin-begin", &controllers.ApiController{}, "GET:FaceIDSigninBegin")
|
||||
}
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
codeChallenge: getRefinedValue(innerParams.get("code_challenge")),
|
||||
responseMode: getRefinedValue(innerParams.get("response_mode")),
|
||||
relayState: getRefinedValue(lowercaseQueries["relaystate"]),
|
||||
resource: getRefinedValue(innerParams.get("resource")),
|
||||
type: "code"
|
||||
};
|
||||
}
|
||||
@@ -168,6 +169,10 @@
|
||||
return "";
|
||||
}
|
||||
|
||||
var resourceQuery = oAuthParams.resource
|
||||
? "&resource=" + encodeURIComponent(oAuthParams.resource)
|
||||
: "";
|
||||
|
||||
return "?clientId=" + oAuthParams.clientId +
|
||||
"&responseType=" + oAuthParams.responseType +
|
||||
"&redirectUri=" + encodeURIComponent(oAuthParams.redirectUri) +
|
||||
@@ -176,7 +181,8 @@
|
||||
"&state=" + oAuthParams.state +
|
||||
"&nonce=" + oAuthParams.nonce +
|
||||
"&code_challenge_method=" + oAuthParams.challengeMethod +
|
||||
"&code_challenge=" + oAuthParams.codeChallenge;
|
||||
"&code_challenge=" + oAuthParams.codeChallenge +
|
||||
resourceQuery;
|
||||
}
|
||||
|
||||
function createFormAndSubmit(action, params) {
|
||||
@@ -424,4 +430,4 @@
|
||||
});
|
||||
}
|
||||
};
|
||||
})();
|
||||
})();
|
||||
|
||||
@@ -174,7 +174,7 @@ class App extends Component {
|
||||
const validMenuItems = [
|
||||
"/", "/shortcuts", "/apps", // Home group
|
||||
"/organizations", "/groups", "/users", "/invitations", // User Management
|
||||
"/applications", "/providers", "/resources", "/certs", // Identity
|
||||
"/applications", "/providers", "/resources", "/certs", "/keys", // Identity
|
||||
"/roles", "/permissions", "/models", "/adapters", "/enforcers", // Authorization
|
||||
"/sessions", "/records", "/tokens", "/verifications", // Logging & Auditing
|
||||
"/products", "/orders", "/payments", "/plans", "/pricings", "/subscriptions", "/transactions", // Business
|
||||
@@ -215,6 +215,8 @@ class App extends Component {
|
||||
} else if (uri.includes("/certs")) {
|
||||
return "/certs";
|
||||
}
|
||||
} else if (uri.includes("/keys")) {
|
||||
return "/keys";
|
||||
} else if (uri.includes("/roles") || uri.includes("/permissions") || uri.includes("/models") || uri.includes("/adapters") || uri.includes("/enforcers")) {
|
||||
if (uri.includes("/roles")) {
|
||||
return "/roles";
|
||||
@@ -293,7 +295,7 @@ class App extends Component {
|
||||
this.setState({selectedMenuKey: "/home"});
|
||||
} else if (uri.includes("/organizations") || uri.includes("/trees") || uri.includes("/groups") || uri.includes("/users") || uri.includes("/invitations")) {
|
||||
this.setState({selectedMenuKey: "/orgs"});
|
||||
} else if (uri.includes("/applications") || uri.includes("/providers") || uri.includes("/resources") || uri.includes("/certs")) {
|
||||
} else if (uri.includes("/applications") || uri.includes("/providers") || uri.includes("/resources") || uri.includes("/certs") || uri.includes("/keys")) {
|
||||
this.setState({selectedMenuKey: "/identity"});
|
||||
} else if (uri.includes("/roles") || uri.includes("/permissions") || uri.includes("/models") || uri.includes("/adapters") || uri.includes("/enforcers")) {
|
||||
this.setState({selectedMenuKey: "/auth"});
|
||||
|
||||
313
web/src/KeyEditPage.js
Normal file
313
web/src/KeyEditPage.js
Normal file
@@ -0,0 +1,313 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, DatePicker, Input, Row, Select} from "antd";
|
||||
import * as KeyBackend from "./backend/KeyBackend";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import moment from "moment";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
class KeyEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
organizationName: props.match.params.organizationName,
|
||||
keyName: props.match.params.keyName,
|
||||
key: null,
|
||||
organizations: [],
|
||||
applications: [],
|
||||
users: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getKey();
|
||||
this.getOrganizations();
|
||||
}
|
||||
|
||||
getKey() {
|
||||
KeyBackend.getKey(this.state.organizationName, this.state.keyName)
|
||||
.then((res) => {
|
||||
if (res.data === null) {
|
||||
this.props.history.push("/404");
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", res.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
key: res.data,
|
||||
});
|
||||
|
||||
this.getApplicationsByOrganization(res.data.organization || this.state.organizationName);
|
||||
this.getUsersByOrganization(res.data.organization || this.state.organizationName);
|
||||
});
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
organizations: res.data || [],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getApplicationsByOrganization(organizationName) {
|
||||
ApplicationBackend.getApplicationsByOrganization("admin", organizationName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
applications: res.data || [],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getUsersByOrganization(organizationName) {
|
||||
UserBackend.getUsers(organizationName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
users: res.data || [],
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
parseKeyField(key, value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
updateKeyField(key, value) {
|
||||
value = this.parseKeyField(key, value);
|
||||
|
||||
const keyObj = this.state.key;
|
||||
keyObj[key] = value;
|
||||
this.setState({
|
||||
key: keyObj,
|
||||
});
|
||||
}
|
||||
|
||||
renderKey() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{this.state.mode === "add" ? i18next.t("key:New Key") : i18next.t("key:Edit Key")}
|
||||
<Button onClick={() => this.submitKeyEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitKeyEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteKey()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.key.owner} onChange={(value => {
|
||||
this.updateKeyField("owner", value);
|
||||
this.updateKeyField("organization", value);
|
||||
this.getApplicationsByOrganization(value);
|
||||
this.getUsersByOrganization(value);
|
||||
})}>
|
||||
{
|
||||
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.key.name} onChange={e => {
|
||||
this.updateKeyField("name", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.key.displayName} onChange={e => {
|
||||
this.updateKeyField("displayName", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.key.type} onChange={(value => {
|
||||
this.updateKeyField("type", value);
|
||||
})}>
|
||||
<Option value="Organization">{i18next.t("general:Organization")}</Option>
|
||||
<Option value="Application">{i18next.t("general:Application")}</Option>
|
||||
<Option value="User">{i18next.t("general:User")}</Option>
|
||||
<Option value="General">{i18next.t("general:General")}</Option>
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.key.type === "Application" ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Application"), i18next.t("general:Application - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.key.application} onChange={(value => {
|
||||
this.updateKeyField("application", value);
|
||||
})}>
|
||||
{
|
||||
this.state.applications.map((application, index) => <Option key={index} value={application.name}>{application.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.key.type === "User" ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.key.user} onChange={(value => {
|
||||
this.updateKeyField("user", value);
|
||||
})}>
|
||||
{
|
||||
this.state.users.map((user, index) => <Option key={index} value={user.name}>{user.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("key:Access key"), i18next.t("key:Access key - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.key.accessKey} readOnly={true} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("key:Access secret"), i18next.t("key:Access secret - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input.Password value={this.state.key.accessSecret} readOnly={true} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Expire time"), i18next.t("general:Expire time - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<DatePicker
|
||||
showTime
|
||||
value={this.state.key.expireTime ? moment(this.state.key.expireTime) : null}
|
||||
onChange={(value, dateString) => {
|
||||
this.updateKeyField("expireTime", dateString);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.key.state} onChange={(value => {
|
||||
this.updateKeyField("state", value);
|
||||
})}>
|
||||
<Option value="Active">{i18next.t("subscription:Active")}</Option>
|
||||
<Option value="Inactive">{i18next.t("key:Inactive")}</Option>
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
submitKeyEdit(exitAfterSave) {
|
||||
const key = Setting.deepCopy(this.state.key);
|
||||
KeyBackend.updateKey(this.state.organizationName, this.state.keyName, key)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully saved"));
|
||||
this.setState({
|
||||
organizationName: this.state.key.owner,
|
||||
keyName: this.state.key.name,
|
||||
});
|
||||
|
||||
if (exitAfterSave) {
|
||||
this.props.history.push("/keys");
|
||||
} else {
|
||||
this.props.history.push(`/keys/${this.state.key.owner}/${this.state.key.name}`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
|
||||
this.updateKeyField("owner", this.state.organizationName);
|
||||
this.updateKeyField("name", this.state.keyName);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteKey() {
|
||||
KeyBackend.deleteKey(this.state.key)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push("/keys");
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
this.state.key !== null ? this.renderKey() : null
|
||||
}
|
||||
<div style={{marginTop: "20px", marginLeft: "40px"}}>
|
||||
<Button size="large" onClick={() => this.submitKeyEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitKeyEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default KeyEditPage;
|
||||
254
web/src/KeyListPage.js
Normal file
254
web/src/KeyListPage.js
Normal file
@@ -0,0 +1,254 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Table} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as KeyBackend from "./backend/KeyBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
|
||||
class KeyListPage extends BaseListPage {
|
||||
newKey() {
|
||||
const randomName = Setting.getRandomName();
|
||||
const owner = Setting.getRequestOrganization(this.props.account);
|
||||
return {
|
||||
owner: owner,
|
||||
name: `key_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
updatedTime: moment().format(),
|
||||
displayName: `New Key - ${randomName}`,
|
||||
type: "Organization",
|
||||
organization: owner,
|
||||
application: "",
|
||||
user: "",
|
||||
accessKey: "",
|
||||
accessSecret: "",
|
||||
expireTime: "",
|
||||
state: "Active",
|
||||
};
|
||||
}
|
||||
|
||||
addKey() {
|
||||
const newKey = this.newKey();
|
||||
KeyBackend.addKey(newKey)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push({pathname: `/keys/${newKey.owner}/${newKey.name}`, mode: "add"});
|
||||
Setting.showMessage("success", i18next.t("general:Successfully added"));
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteKey(i) {
|
||||
KeyBackend.deleteKey(this.state.data[i])
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
|
||||
this.fetch({
|
||||
pagination: {
|
||||
...this.state.pagination,
|
||||
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(keys) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "140px",
|
||||
fixed: "left",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("name"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/keys/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Organization"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
width: "150px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("owner"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/organizations/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Created time"),
|
||||
dataIndex: "createdTime",
|
||||
key: "createdTime",
|
||||
width: "160px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Display name"),
|
||||
dataIndex: "displayName",
|
||||
key: "displayName",
|
||||
width: "170px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("displayName"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: "140px",
|
||||
sorter: true,
|
||||
filterMultiple: false,
|
||||
filters: [
|
||||
{text: i18next.t("general:Organization"), value: "Organization"},
|
||||
{text: i18next.t("general:Application"), value: "Application"},
|
||||
{text: i18next.t("general:User"), value: "User"},
|
||||
{text: i18next.t("general:General"), value: "General"},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: i18next.t("key:Access key"),
|
||||
dataIndex: "accessKey",
|
||||
key: "accessKey",
|
||||
width: "300px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("accessKey"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Expire time"),
|
||||
dataIndex: "expireTime",
|
||||
key: "expireTime",
|
||||
width: "160px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:State"),
|
||||
dataIndex: "state",
|
||||
key: "state",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("state"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
key: "op",
|
||||
width: "180px",
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/keys/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<PopconfirmModal
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteKey(index)}
|
||||
>
|
||||
</PopconfirmModal>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const paginationProps = {
|
||||
total: this.state.pagination.total,
|
||||
showQuickJumper: true,
|
||||
showSizeChanger: true,
|
||||
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={keys} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Keys")}
|
||||
<Button type="primary" size="small" onClick={this.addKey.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fetch = (params = {}) => {
|
||||
let field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
if (params.type !== undefined && params.type !== null) {
|
||||
field = "type";
|
||||
value = params.type;
|
||||
}
|
||||
this.setState({loading: true});
|
||||
(Setting.isDefaultOrganizationSelected(this.props.account) ? KeyBackend.getGlobalKeys(params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
: KeyBackend.getKeys(Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder))
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
data: res.data,
|
||||
pagination: {
|
||||
...params.pagination,
|
||||
total: res.data2,
|
||||
},
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (Setting.isResponseDenied(res)) {
|
||||
this.setState({
|
||||
isAuthorized: false,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default KeyListPage;
|
||||
@@ -47,6 +47,8 @@ import RecordListPage from "./RecordListPage";
|
||||
import ResourceListPage from "./ResourceListPage";
|
||||
import CertListPage from "./CertListPage";
|
||||
import CertEditPage from "./CertEditPage";
|
||||
import KeyListPage from "./KeyListPage";
|
||||
import KeyEditPage from "./KeyEditPage";
|
||||
import RoleListPage from "./RoleListPage";
|
||||
import RoleEditPage from "./RoleEditPage";
|
||||
import PermissionListPage from "./PermissionListPage";
|
||||
@@ -329,6 +331,9 @@ function ManagementPage(props) {
|
||||
Setting.getItem(<Link to="/providers">{i18next.t("application:Providers")}</Link>, "/providers"),
|
||||
Setting.getItem(<Link to="/resources">{i18next.t("general:Resources")}</Link>, "/resources"),
|
||||
Setting.getItem(<Link to="/certs">{i18next.t("general:Certs")}</Link>, "/certs"),
|
||||
Setting.getItem(<Link to="/keys">{i18next.t("general:Keys")}</Link>, "/keys"),
|
||||
Setting.getItem(<Link to="/sites">{i18next.t("general:Sites")}</Link>, "/sites"),
|
||||
Setting.getItem(<Link to="/rules">{i18next.t("general:Rules")}</Link>, "/rules"),
|
||||
]));
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/roles">{i18next.t("general:Authorization")}</Link>, "/auth", <SafetyCertificateTwoTone twoToneColor={twoToneColor} />, [
|
||||
@@ -450,7 +455,11 @@ function ManagementPage(props) {
|
||||
} else if (props.account === undefined) {
|
||||
return null;
|
||||
} else if (props.account.needUpdatePassword) {
|
||||
return <Redirect to={"/forget/" + props.application.name} />;
|
||||
if (window.location.pathname === "/account") {
|
||||
return component;
|
||||
} else {
|
||||
return <Redirect to="/account" />;
|
||||
}
|
||||
} else {
|
||||
return component;
|
||||
}
|
||||
@@ -485,6 +494,8 @@ function ManagementPage(props) {
|
||||
<Route exact path="/resources" render={(props) => renderLoginIfNotLoggedIn(<ResourceListPage account={account} {...props} />)} />
|
||||
<Route exact path="/certs" render={(props) => renderLoginIfNotLoggedIn(<CertListPage account={account} {...props} />)} />
|
||||
<Route exact path="/certs/:organizationName/:certName" render={(props) => renderLoginIfNotLoggedIn(<CertEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/keys" render={(props) => renderLoginIfNotLoggedIn(<KeyListPage account={account} {...props} />)} />
|
||||
<Route exact path="/keys/:organizationName/:keyName" render={(props) => renderLoginIfNotLoggedIn(<KeyEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/servers" render={(props) => renderLoginIfNotLoggedIn(<ServerListPage account={account} {...props} />)} />
|
||||
<Route exact path="/servers/:organizationName/:serverName" render={(props) => renderLoginIfNotLoggedIn(<ServerEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/sites" render={(props) => renderLoginIfNotLoggedIn(<SiteListPage account={account} {...props} />)} />
|
||||
|
||||
@@ -633,6 +633,16 @@ class OrganizationEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Use permanent avatar"), i18next.t("organization:Use permanent avatar - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.organization.usePermanentAvatar} onChange={checked => {
|
||||
this.updateOrganizationField("usePermanentAvatar", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Admin navbar items"), i18next.t("organization:Admin navbar items - Tooltip"))} :
|
||||
|
||||
@@ -94,7 +94,6 @@ class OrganizationListPage extends BaseListPage {
|
||||
{name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Register type", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Register source", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "API key", label: i18next.t("general:API key"), modifyRule: "Self"},
|
||||
{name: "Groups", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Roles", visible: true, viewRule: "Public", modifyRule: "Immutable"},
|
||||
{name: "Permissions", visible: true, viewRule: "Public", modifyRule: "Immutable"},
|
||||
|
||||
@@ -20,6 +20,7 @@ import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
import ToolTable from "./ToolTable";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
@@ -107,7 +108,7 @@ class ServerEditPage extends React.Component {
|
||||
mode: "edit",
|
||||
owner: server.owner,
|
||||
serverName: server.name,
|
||||
});
|
||||
}, () => {this.getServer();});
|
||||
this.props.history.push(`/servers/${server.owner}/${server.name}`);
|
||||
}
|
||||
} else {
|
||||
@@ -186,6 +187,16 @@ class ServerEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("token:Access token"), i18next.t("token:Access token - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input.Password placeholder={"***"} value={this.state.server.token} onChange={e => {
|
||||
this.updateServerField("token", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Application"), i18next.t("general:Application - Tooltip"))} :
|
||||
@@ -198,6 +209,17 @@ class ServerEditPage extends React.Component {
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Tool"), i18next.t("general:Tool - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<ToolTable
|
||||
tools={this.state.server?.tools || []}
|
||||
onUpdateTable={(value) => {this.updateServerField("tools", value);}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Base URL"), i18next.t("provider:Base URL - Tooltip"))} :
|
||||
|
||||
@@ -462,7 +462,7 @@ export const UserFields = ["owner", "name", "password", "display_name", "id", "t
|
||||
"avatar_type", "permanent_avatar", "email_verified", "region", "location", "address",
|
||||
"affiliation", "title", "id_card_type", "id_card", "real_name", "is_verified", "bio", "tag", "language",
|
||||
"education", "score", "karma", "ranking", "balance", "balance_credit", "balance_currency", "currency", "is_default_avatar", "is_online",
|
||||
"is_forbidden", "is_deleted", "signup_application", "register_type", "register_source", "hash", "pre_hash", "access_key", "access_secret", "access_token",
|
||||
"is_forbidden", "is_deleted", "signup_application", "register_type", "register_source", "hash", "pre_hash", "access_token",
|
||||
"created_ip", "last_signin_time", "last_signin_ip", "github", "google", "qq", "wechat", "facebook", "dingtalk",
|
||||
"weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs", "baidu", "alipay", "casdoor", "infoflow", "apple",
|
||||
"azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "kwai", "line", "amazon", "auth0",
|
||||
@@ -2361,7 +2361,7 @@ export function getApiPaths() {
|
||||
res.push("place-order", "cancel-order", "pay-order");
|
||||
}
|
||||
if (obj === "user") {
|
||||
res.push("add-user-keys", "remove-user-from-group", "upload-users");
|
||||
res.push("remove-user-from-group", "upload-users");
|
||||
res.push("check-user-password", "set-password", "reset-email-or-phone");
|
||||
res.push("verify-identification");
|
||||
}
|
||||
|
||||
@@ -20,6 +20,15 @@ import * as TourConfig from "./TourConfig";
|
||||
import i18next from "i18next";
|
||||
import PrometheusInfoTable from "./table/PrometheusInfoTable";
|
||||
|
||||
const getProgressColor = (percent) => {
|
||||
if (percent >= 90) {
|
||||
return "#ff4d4f";
|
||||
} else if (percent >= 70) {
|
||||
return "#faad14";
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
class SystemInfo extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
@@ -144,16 +153,18 @@ class SystemInfo extends React.Component {
|
||||
render() {
|
||||
const cpuUi = this.state.systemInfo.cpuUsage?.length <= 0 ? i18next.t("general:Failed to get") :
|
||||
this.state.systemInfo.cpuUsage.map((usage, i) => {
|
||||
const percent = Number(usage.toFixed(1));
|
||||
return (
|
||||
<Progress key={i} percent={Number(usage.toFixed(1))} />
|
||||
<Progress key={i} percent={percent} strokeColor={getProgressColor(percent)} format={p => `${p}%`} />
|
||||
);
|
||||
});
|
||||
|
||||
const memPercent = Number((Number(this.state.systemInfo.memoryUsed) / Number(this.state.systemInfo.memoryTotal) * 100).toFixed(2));
|
||||
const memUi = this.state.systemInfo.memoryUsed && this.state.systemInfo.memoryTotal && this.state.systemInfo.memoryTotal <= 0 ? i18next.t("general:Failed to get") :
|
||||
<div>
|
||||
{Setting.getFriendlyFileSize(this.state.systemInfo.memoryUsed)} / {Setting.getFriendlyFileSize(this.state.systemInfo.memoryTotal)}
|
||||
<br /> <br />
|
||||
<Progress type="circle" percent={Number((Number(this.state.systemInfo.memoryUsed) / Number(this.state.systemInfo.memoryTotal) * 100).toFixed(2))} />
|
||||
<Progress type="circle" percent={memPercent} strokeColor={getProgressColor(memPercent)} format={p => `${p}%`} />
|
||||
</div>;
|
||||
const latencyUi = this.state.prometheusInfo?.apiLatency === null || this.state.prometheusInfo?.apiLatency?.length <= 0 ? <Spin size="large" /> :
|
||||
<PrometheusInfoTable prometheusInfo={this.state.prometheusInfo} table={"latency"} />;
|
||||
|
||||
78
web/src/ToolTable.js
Normal file
78
web/src/ToolTable.js
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Switch, Table} from "antd";
|
||||
import i18next from "i18next";
|
||||
|
||||
class ToolTable extends React.Component {
|
||||
updateTable(table) {
|
||||
this.props.onUpdateTable(table);
|
||||
}
|
||||
|
||||
updateToolEnable(table, index, value) {
|
||||
const newTable = [...(table || [])];
|
||||
newTable[index] = {
|
||||
...newTable[index],
|
||||
isAllowed: value,
|
||||
};
|
||||
this.updateTable(newTable);
|
||||
}
|
||||
|
||||
renderTable(table) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "260px",
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Description"),
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Is allowed"),
|
||||
dataIndex: "isAllowed",
|
||||
key: "isAllowed",
|
||||
width: "120px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch checked={record.isAllowed} onChange={(checked) => {
|
||||
this.updateToolEnable(table, index, checked);
|
||||
}} />
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
rowKey={(record, index) => record.name || `tool-${index}`}
|
||||
columns={columns}
|
||||
dataSource={table || []}
|
||||
size="middle"
|
||||
bordered
|
||||
pagination={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.renderTable(this.props.tools || []);
|
||||
}
|
||||
}
|
||||
|
||||
export default ToolTable;
|
||||
@@ -137,17 +137,6 @@ class UserEditPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
addUserKeys() {
|
||||
UserBackend.addUserKeys(this.state.user)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.getUser();
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
@@ -971,39 +960,6 @@ class UserEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "API key") {
|
||||
return (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:API key"), i18next.t("general:API key - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Access key"), i18next.t("general:Access key - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.user.accessKey} disabled={true} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Access secret"), i18next.t("general:Access secret - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.user.accessSecret} disabled={true} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px", marginBottom: "20px"}} >
|
||||
<Col span={22} >
|
||||
<Button type="primary" onClick={() => this.addUserKeys()}>
|
||||
{i18next.t("general:Generate")}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "Roles") {
|
||||
return (
|
||||
<Row style={{marginTop: "20px", alignItems: "center"}} >
|
||||
|
||||
@@ -56,8 +56,12 @@ export function oAuthParamsToQuery(oAuthParams) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const resourceQuery = oAuthParams.resource
|
||||
? `&resource=${encodeURIComponent(oAuthParams.resource)}`
|
||||
: "";
|
||||
|
||||
// code
|
||||
return `?clientId=${oAuthParams.clientId}&responseType=${oAuthParams.responseType}&redirectUri=${encodeURIComponent(oAuthParams.redirectUri)}&type=${oAuthParams.type}&scope=${oAuthParams.scope}&state=${oAuthParams.state}&nonce=${oAuthParams.nonce}&code_challenge_method=${oAuthParams.challengeMethod}&code_challenge=${oAuthParams.codeChallenge}`;
|
||||
return `?clientId=${oAuthParams.clientId}&responseType=${oAuthParams.responseType}&redirectUri=${encodeURIComponent(oAuthParams.redirectUri)}&type=${oAuthParams.type}&scope=${oAuthParams.scope}&state=${oAuthParams.state}&nonce=${oAuthParams.nonce}&code_challenge_method=${oAuthParams.challengeMethod}&code_challenge=${oAuthParams.codeChallenge}${resourceQuery}`;
|
||||
}
|
||||
|
||||
export function getApplicationLogin(params) {
|
||||
|
||||
@@ -95,7 +95,7 @@ class AuthCallback extends React.Component {
|
||||
if (responseType === "login") {
|
||||
if (res.data3) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
Setting.goToLinkSoft(this, "/account");
|
||||
return;
|
||||
}
|
||||
Setting.showMessage("success", "Logged in successfully");
|
||||
@@ -104,7 +104,7 @@ class AuthCallback extends React.Component {
|
||||
} else if (responseType === "code") {
|
||||
if (res.data3) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
Setting.goToLinkSoft(this, "/account");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ class AuthCallback extends React.Component {
|
||||
} else if (responseTypes.includes("token") || responseTypes.includes("id_token")) {
|
||||
if (res.data3) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
Setting.goToLinkSoft(this, "/account");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ class AuthCallback extends React.Component {
|
||||
} else {
|
||||
if (res.data3) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
Setting.goToLinkSoft(this, "/account");
|
||||
return;
|
||||
}
|
||||
const SAMLResponse = res.data;
|
||||
|
||||
@@ -365,7 +365,7 @@ class LoginPage extends React.Component {
|
||||
|
||||
if (resp.data3) {
|
||||
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
|
||||
Setting.goToLinkSoft(ths, `/forget/${application.name}`);
|
||||
Setting.goToLinkSoft(ths, "/account");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -537,7 +537,8 @@ class LoginPage extends React.Component {
|
||||
if (responseType === "login") {
|
||||
if (res.data3) {
|
||||
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
|
||||
Setting.goToLinkSoft(this, `/forget/${this.state.applicationName}`);
|
||||
Setting.goToLinkSoft(this, "/account");
|
||||
return;
|
||||
}
|
||||
Setting.showMessage("success", i18next.t("application:Logged in successfully"));
|
||||
this.props.onLoginSuccess();
|
||||
@@ -551,7 +552,8 @@ class LoginPage extends React.Component {
|
||||
} else if (responseTypes.includes("token") || responseTypes.includes("id_token")) {
|
||||
if (res.data3) {
|
||||
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
|
||||
Setting.goToLinkSoft(this, `/forget/${this.state.applicationName}`);
|
||||
Setting.goToLinkSoft(this, "/account");
|
||||
return;
|
||||
}
|
||||
const amendatoryResponseType = responseType === "token" ? "access_token" : responseType;
|
||||
const accessToken = res.data;
|
||||
@@ -573,7 +575,8 @@ class LoginPage extends React.Component {
|
||||
}
|
||||
if (res.data3) {
|
||||
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
|
||||
Setting.goToLinkSoft(this, `/forget/${this.state.applicationName}`);
|
||||
Setting.goToLinkSoft(this, "/account");
|
||||
return;
|
||||
}
|
||||
if (res.data2.method === "POST") {
|
||||
this.setState({
|
||||
@@ -1269,9 +1272,12 @@ class LoginPage extends React.Component {
|
||||
const rawId = assertion.rawId;
|
||||
const sig = assertion.response.signature;
|
||||
const userHandle = assertion.response.userHandle;
|
||||
const resourceQuery = oAuthParams?.resource
|
||||
? `&resource=${encodeURIComponent(oAuthParams.resource)}`
|
||||
: "";
|
||||
let finishUrl = `${Setting.ServerUrl}/api/webauthn/signin/finish?responseType=${values["type"]}`;
|
||||
if (values["type"] === "code") {
|
||||
finishUrl = `${Setting.ServerUrl}/api/webauthn/signin/finish?responseType=${values["type"]}&clientId=${oAuthParams.clientId}&scope=${oAuthParams.scope}&redirectUri=${oAuthParams.redirectUri}&nonce=${oAuthParams.nonce}&state=${oAuthParams.state}&codeChallenge=${oAuthParams.codeChallenge}&challengeMethod=${oAuthParams.challengeMethod}`;
|
||||
finishUrl = `${Setting.ServerUrl}/api/webauthn/signin/finish?responseType=${values["type"]}&clientId=${oAuthParams.clientId}&scope=${oAuthParams.scope}&redirectUri=${oAuthParams.redirectUri}&nonce=${oAuthParams.nonce}&state=${oAuthParams.state}&codeChallenge=${oAuthParams.codeChallenge}&challengeMethod=${oAuthParams.challengeMethod}${resourceQuery}`;
|
||||
}
|
||||
return fetch(finishUrl, {
|
||||
method: "POST",
|
||||
|
||||
@@ -141,6 +141,7 @@ export function getOAuthGetParameters(params) {
|
||||
const samlRequest = getRefinedValue(lowercaseQueries["samlRequest".toLowerCase()]);
|
||||
const relayState = getRefinedValue(lowercaseQueries["RelayState".toLowerCase()]);
|
||||
const noRedirect = getRefinedValue(lowercaseQueries["noRedirect".toLowerCase()]);
|
||||
const resource = getRefinedValue(queries.get("resource"));
|
||||
|
||||
if (clientId === "" && samlRequest === "") {
|
||||
// login
|
||||
@@ -160,6 +161,7 @@ export function getOAuthGetParameters(params) {
|
||||
samlRequest: samlRequest,
|
||||
relayState: relayState,
|
||||
noRedirect: noRedirect,
|
||||
resource: resource,
|
||||
type: "code",
|
||||
};
|
||||
}
|
||||
|
||||
81
web/src/backend/KeyBackend.js
Normal file
81
web/src/backend/KeyBackend.js
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as Setting from "../Setting";
|
||||
|
||||
export function getKeys(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-keys?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getGlobalKeys(page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-global-keys?p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getKey(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-key?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function updateKey(owner, name, key) {
|
||||
const newKey = Setting.deepCopy(key);
|
||||
return fetch(`${Setting.ServerUrl}/api/update-key?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newKey),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addKey(key) {
|
||||
const newKey = Setting.deepCopy(key);
|
||||
return fetch(`${Setting.ServerUrl}/api/add-key`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newKey),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function deleteKey(key) {
|
||||
const newKey = Setting.deepCopy(key);
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-key`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newKey),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
@@ -45,17 +45,6 @@ export function getUser(owner, name) {
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addUserKeys(user) {
|
||||
return fetch(`${Setting.ServerUrl}/api/add-user-keys`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(user),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function updateUser(owner, name, user) {
|
||||
const newUser = Setting.deepCopy(user);
|
||||
return fetch(`${Setting.ServerUrl}/api/update-user?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
|
||||
@@ -129,6 +129,9 @@ export const PasswordModal = (props) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("user:Password set successfully"));
|
||||
setVisible(false);
|
||||
if (account.owner === user.owner && account.name === userName) {
|
||||
account.needUpdatePassword = false;
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", i18next.t(`user:${res.msg}`));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user