Compare commits

...

10 Commits

43 changed files with 1578 additions and 292 deletions

View File

@@ -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
View 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
View 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, &params)
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)
}

View File

@@ -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)
}

View File

@@ -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
View File

@@ -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
View File

@@ -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
View 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
}

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package mcp
package mcpself
import (
"fmt"

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package mcp
package mcpself
import (
"strings"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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
View 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
}

View File

@@ -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"`

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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")
}

View File

@@ -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 @@
});
}
};
})();
})();

View File

@@ -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
View 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")}&nbsp;&nbsp;&nbsp;&nbsp;
<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
View 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")}&nbsp;&nbsp;&nbsp;&nbsp;
<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;

View File

@@ -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} />)} />

View File

@@ -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"))} :

View File

@@ -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"},

View File

@@ -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"))} :

View File

@@ -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");
}

View File

@@ -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
View 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;

View File

@@ -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"}} >

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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",

View File

@@ -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",
};
}

View 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());
}

View File

@@ -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)}`, {

View File

@@ -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}`));
}