forked from casdoor/casdoor
Compare commits
2 Commits
copilot/fi
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bee8bf0758 | ||
|
|
601f059acb |
52
.github/workflows/build.yml
vendored
52
.github/workflows/build.yml
vendored
@@ -1,8 +1,5 @@
|
||||
name: Build
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.25.8"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -10,6 +7,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
go-tests:
|
||||
name: Running Go tests
|
||||
runs-on: ubuntu-latest
|
||||
@@ -26,7 +24,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
go-version: '1.23'
|
||||
cache-dependency-path: ./go.mod
|
||||
- name: Tests
|
||||
run: |
|
||||
@@ -36,13 +34,13 @@ jobs:
|
||||
frontend:
|
||||
name: Front-end
|
||||
runs-on: ubuntu-latest
|
||||
needs: [go-tests]
|
||||
needs: [ go-tests ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "yarn"
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: ./web/yarn.lock
|
||||
- run: yarn install && CI=false yarn run build
|
||||
working-directory: ./web
|
||||
@@ -56,12 +54,12 @@ jobs:
|
||||
backend:
|
||||
name: Back-end
|
||||
runs-on: ubuntu-latest
|
||||
needs: [go-tests]
|
||||
needs: [ go-tests ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
go-version: '1.23'
|
||||
cache-dependency-path: ./go.mod
|
||||
- run: go version
|
||||
- name: Build
|
||||
@@ -72,28 +70,27 @@ jobs:
|
||||
linter:
|
||||
name: Go-Linter
|
||||
runs-on: ubuntu-latest
|
||||
needs: [go-tests]
|
||||
needs: [ go-tests ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
go-version: '1.23'
|
||||
cache: false
|
||||
|
||||
- name: Sync vendor tree
|
||||
run: go mod vendor
|
||||
# gen a dummy config file
|
||||
- run: touch dummy.yml
|
||||
|
||||
# CI and local `make lint` both use the repo's gofumpt-only golangci-lint config.
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9.2.0
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v2.11.4
|
||||
args: --config .golangci.yml ./...
|
||||
version: latest
|
||||
args: --disable-all -c dummy.yml -E=gofumpt --max-same-issues=0 --timeout 5m --modules-download-mode=mod
|
||||
|
||||
e2e:
|
||||
name: e2e-test
|
||||
runs-on: ubuntu-latest
|
||||
needs: [go-tests]
|
||||
needs: [ go-tests ]
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
@@ -107,7 +104,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
go-version: '1.23'
|
||||
cache-dependency-path: ./go.mod
|
||||
- name: start backend
|
||||
run: nohup go run ./main.go > /tmp/backend.log 2>&1 &
|
||||
@@ -132,7 +129,7 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "yarn"
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: ./web/yarn.lock
|
||||
- run: yarn install
|
||||
working-directory: ./web
|
||||
@@ -140,7 +137,7 @@ jobs:
|
||||
with:
|
||||
browser: chrome
|
||||
start: yarn start
|
||||
wait-on: "http://localhost:7001"
|
||||
wait-on: 'http://localhost:7001'
|
||||
wait-on-timeout: 210
|
||||
working-directory: ./web
|
||||
|
||||
@@ -162,7 +159,7 @@ jobs:
|
||||
contents: write
|
||||
issues: write
|
||||
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push'
|
||||
needs: [frontend, backend, linter, e2e]
|
||||
needs: [ frontend, backend, linter, e2e ]
|
||||
outputs:
|
||||
new-release-published: ${{ steps.semantic.outputs.new_release_published }}
|
||||
new-release-version: ${{ steps.semantic.outputs.new_release_version }}
|
||||
@@ -183,18 +180,13 @@ jobs:
|
||||
contents: write
|
||||
issues: write
|
||||
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && needs.tag-release.outputs.new-release-published == 'true'
|
||||
needs: [tag-release]
|
||||
needs: [ tag-release ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache-dependency-path: ./go.mod
|
||||
|
||||
- name: Free disk space
|
||||
uses: jlumbroso/free-disk-space@v1.3.1
|
||||
with:
|
||||
@@ -221,7 +213,7 @@ jobs:
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: "~> v2"
|
||||
version: '~> v2'
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -233,7 +225,7 @@ jobs:
|
||||
contents: write
|
||||
issues: write
|
||||
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && needs.tag-release.outputs.new-release-published == 'true'
|
||||
needs: [tag-release]
|
||||
needs: [ tag-release ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -303,7 +295,7 @@ jobs:
|
||||
if: steps.should_push.outputs.push=='true'
|
||||
with:
|
||||
repository: casdoor/casdoor-helm
|
||||
ref: "master"
|
||||
ref: 'master'
|
||||
token: ${{ secrets.GH_BOT_TOKEN }}
|
||||
|
||||
- name: Update Helm Chart
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,7 +20,6 @@ bin/
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/settings.json
|
||||
.claude
|
||||
|
||||
tmp/
|
||||
tmpFiles/
|
||||
|
||||
@@ -1,26 +1,42 @@
|
||||
version: "2"
|
||||
run:
|
||||
relative-path-mode: gomod
|
||||
modules-download-mode: vendor
|
||||
linters:
|
||||
default: none
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- gofumpt
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
- deadcode
|
||||
- dupl
|
||||
- errcheck
|
||||
- goconst
|
||||
- gocyclo
|
||||
- gofmt
|
||||
- goimports
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- lll
|
||||
- misspell
|
||||
- nakedret
|
||||
- prealloc
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- revive
|
||||
- exportloopref
|
||||
run:
|
||||
deadline: 5m
|
||||
skip-dirs:
|
||||
- api
|
||||
# skip-files:
|
||||
# - ".*_test\\.go$"
|
||||
modules-download-mode: mod
|
||||
# all available settings of specific linters
|
||||
linters-settings:
|
||||
lll:
|
||||
# max line length, lines longer will be reported. Default is 120.
|
||||
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
|
||||
line-length: 150
|
||||
# tab width in spaces. Default to 1.
|
||||
tab-width: 1
|
||||
@@ -9,7 +9,7 @@ RUN yarn install --frozen-lockfile --network-timeout 1000000
|
||||
COPY ./web .
|
||||
RUN NODE_OPTIONS="--max-old-space-size=4096" yarn run build
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.25.8 AS BACK
|
||||
FROM --platform=$BUILDPLATFORM golang:1.24.13 AS BACK
|
||||
WORKDIR /go/src/casdoor
|
||||
|
||||
# Copy only go.mod and go.sum first for dependency caching
|
||||
|
||||
8
Makefile
8
Makefile
@@ -90,12 +90,12 @@ deps: ## Run dependencies for local development
|
||||
docker compose up -d db
|
||||
|
||||
lint-install: ## Install golangci-lint
|
||||
@# Keep the local golangci-lint version aligned with CI. Both local and CI lint run the gofumpt-only ruleset from .golangci.yml.
|
||||
GOTOOLCHAIN=go1.25.8 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4
|
||||
@# The following installs a specific version of golangci-lint, which is appropriate for a CI server to avoid different results from build to build
|
||||
go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.40.1
|
||||
|
||||
lint: vendor ## Run golangci-lint
|
||||
lint: ## Run golangci-lint
|
||||
@echo "---lint---"
|
||||
golangci-lint run ./...
|
||||
golangci-lint run --modules-download-mode=vendor ./...
|
||||
|
||||
##@ Deployment
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ p, *, *, GET, /api/get-resources, *, *
|
||||
p, *, *, GET, /api/get-records, *, *
|
||||
p, *, *, GET, /api/get-product, *, *
|
||||
p, *, *, GET, /api/get-products, *, *
|
||||
p, *, *, POST, /api/buy-product, *, *
|
||||
p, *, *, GET, /api/get-order, *, *
|
||||
p, *, *, GET, /api/get-orders, *, *
|
||||
p, *, *, GET, /api/get-user-orders, *, *
|
||||
@@ -86,9 +85,6 @@ p, *, *, POST, /api/send-verification-code, *, *
|
||||
p, *, *, GET, /api/get-captcha, *, *
|
||||
p, *, *, POST, /api/verify-captcha, *, *
|
||||
p, *, *, POST, /api/verify-code, *, *
|
||||
p, *, *, POST, /api/v1/traces, *, *
|
||||
p, *, *, POST, /api/v1/metrics, *, *
|
||||
p, *, *, POST, /api/v1/logs, *, *
|
||||
p, *, *, POST, /api/reset-email-or-phone, *, *
|
||||
p, *, *, POST, /api/upload-resource, *, *
|
||||
p, *, *, GET, /.well-known/openid-configuration, *, *
|
||||
|
||||
@@ -25,8 +25,6 @@ showGithubCorner = false
|
||||
forceLanguage = ""
|
||||
defaultLanguage = "en"
|
||||
aiAssistantUrl = "https://ai.casbin.com"
|
||||
defaultApplication = "app-built-in"
|
||||
maxItemsForFlatMenu = 7
|
||||
enableErrorMask = false
|
||||
enableGzip = true
|
||||
inactiveTimeoutMinutes =
|
||||
|
||||
@@ -15,14 +15,12 @@
|
||||
package conf
|
||||
|
||||
type WebConfig struct {
|
||||
ShowGithubCorner bool `json:"showGithubCorner"`
|
||||
ForceLanguage string `json:"forceLanguage"`
|
||||
DefaultLanguage string `json:"defaultLanguage"`
|
||||
IsDemoMode bool `json:"isDemoMode"`
|
||||
StaticBaseUrl string `json:"staticBaseUrl"`
|
||||
AiAssistantUrl string `json:"aiAssistantUrl"`
|
||||
DefaultApplication string `json:"defaultApplication"`
|
||||
MaxItemsForFlatMenu int64 `json:"maxItemsForFlatMenu"`
|
||||
ShowGithubCorner bool `json:"showGithubCorner"`
|
||||
ForceLanguage string `json:"forceLanguage"`
|
||||
DefaultLanguage string `json:"defaultLanguage"`
|
||||
IsDemoMode bool `json:"isDemoMode"`
|
||||
StaticBaseUrl string `json:"staticBaseUrl"`
|
||||
AiAssistantUrl string `json:"aiAssistantUrl"`
|
||||
}
|
||||
|
||||
func GetWebConfig() *WebConfig {
|
||||
@@ -34,16 +32,6 @@ func GetWebConfig() *WebConfig {
|
||||
config.IsDemoMode = IsDemoMode()
|
||||
config.StaticBaseUrl = GetConfigString("staticBaseUrl")
|
||||
config.AiAssistantUrl = GetConfigString("aiAssistantUrl")
|
||||
config.DefaultApplication = GetConfigString("defaultApplication")
|
||||
if config.DefaultApplication == "" {
|
||||
config.DefaultApplication = "app-built-in"
|
||||
}
|
||||
|
||||
maxItemsForFlatMenu, err := GetConfigInt64("maxItemsForFlatMenu")
|
||||
if err != nil {
|
||||
maxItemsForFlatMenu = 7
|
||||
}
|
||||
config.MaxItemsForFlatMenu = maxItemsForFlatMenu
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
// 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/server/web/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// GetAgents
|
||||
// @Title GetAgents
|
||||
// @Tag Agent API
|
||||
// @Description get agents
|
||||
// @Param owner query string true "The owner of agents"
|
||||
// @Success 200 {array} object.Agent The Response object
|
||||
// @router /get-agents [get]
|
||||
func (c *ApiController) GetAgents() {
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
if owner == "admin" {
|
||||
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 == "" {
|
||||
agents, err := object.GetAgents(owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.ResponseOk(agents)
|
||||
return
|
||||
}
|
||||
|
||||
limitInt := util.ParseInt(limit)
|
||||
count, err := object.GetAgentCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limitInt, count)
|
||||
agents, err := object.GetPaginationAgents(owner, paginator.Offset(), limitInt, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(agents, paginator.Nums())
|
||||
}
|
||||
|
||||
// GetAgent
|
||||
// @Title GetAgent
|
||||
// @Tag Agent API
|
||||
// @Description get agent
|
||||
// @Param id query string true "The id ( owner/name ) of the agent"
|
||||
// @Success 200 {object} object.Agent The Response object
|
||||
// @router /get-agent [get]
|
||||
func (c *ApiController) GetAgent() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
agent, err := object.GetAgent(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(agent)
|
||||
}
|
||||
|
||||
// UpdateAgent
|
||||
// @Title UpdateAgent
|
||||
// @Tag Agent API
|
||||
// @Description update agent
|
||||
// @Param id query string true "The id ( owner/name ) of the agent"
|
||||
// @Param body body object.Agent true "The details of the agent"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-agent [post]
|
||||
func (c *ApiController) UpdateAgent() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var agent object.Agent
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &agent)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateAgent(id, &agent))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddAgent
|
||||
// @Title AddAgent
|
||||
// @Tag Agent API
|
||||
// @Description add agent
|
||||
// @Param body body object.Agent true "The details of the agent"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-agent [post]
|
||||
func (c *ApiController) AddAgent() {
|
||||
var agent object.Agent
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &agent)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddAgent(&agent))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteAgent
|
||||
// @Title DeleteAgent
|
||||
// @Tag Agent API
|
||||
// @Description delete agent
|
||||
// @Param body body object.Agent true "The details of the agent"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-agent [post]
|
||||
func (c *ApiController) DeleteAgent() {
|
||||
var agent object.Agent
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &agent)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteAgent(&agent))
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
@@ -937,7 +938,14 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
|
||||
if tmpUser != nil {
|
||||
uidStr := strings.Split(util.GenerateUUID(), "-")
|
||||
var uid uuid.UUID
|
||||
uid, err = uuid.NewRandom()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
uidStr := strings.Split(uid.String(), "-")
|
||||
userInfo.Username = fmt.Sprintf("%s_%s", userInfo.Username, uidStr[1])
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
)
|
||||
|
||||
type CLIVersionInfo struct {
|
||||
@@ -166,11 +164,6 @@ func processArgsToTempFiles(args []string) ([]string, []string, error) {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /run-casbin-command [get]
|
||||
func (c *ApiController) RunCasbinCommand() {
|
||||
if !conf.IsDemoMode() && !c.IsAdmin() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateIdentifier(c); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -447,8 +446,8 @@ func downloadCLI() error {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /refresh-engines [post]
|
||||
func (c *ApiController) RefreshEngines() {
|
||||
if !conf.IsDemoMode() && !c.IsAdmin() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
if !web.AppConfig.DefaultBool("isDemoMode", false) {
|
||||
c.ResponseError("refresh engines is only available in demo mode")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
// 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/server/web/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// GetEntries
|
||||
// @Title GetEntries
|
||||
// @Tag Entry API
|
||||
// @Description get entries
|
||||
// @Param owner query string true "The owner of entries"
|
||||
// @Success 200 {array} object.Entry The Response object
|
||||
// @router /get-entries [get]
|
||||
func (c *ApiController) GetEntries() {
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
if owner == "admin" {
|
||||
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 == "" {
|
||||
entries, err := object.GetEntries(owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.ResponseOk(entries)
|
||||
return
|
||||
}
|
||||
|
||||
limitInt := util.ParseInt(limit)
|
||||
count, err := object.GetEntryCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limitInt, count)
|
||||
entries, err := object.GetPaginationEntries(owner, paginator.Offset(), limitInt, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(entries, paginator.Nums())
|
||||
}
|
||||
|
||||
// GetEntry
|
||||
// @Title GetEntry
|
||||
// @Tag Entry API
|
||||
// @Description get entry
|
||||
// @Param id query string true "The id ( owner/name ) of the entry"
|
||||
// @Success 200 {object} object.Entry The Response object
|
||||
// @router /get-entry [get]
|
||||
func (c *ApiController) GetEntry() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
entry, err := object.GetEntry(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(entry)
|
||||
}
|
||||
|
||||
// UpdateEntry
|
||||
// @Title UpdateEntry
|
||||
// @Tag Entry API
|
||||
// @Description update entry
|
||||
// @Param id query string true "The id ( owner/name ) of the entry"
|
||||
// @Param body body object.Entry true "The details of the entry"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-entry [post]
|
||||
func (c *ApiController) UpdateEntry() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var entry object.Entry
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &entry)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateEntry(id, &entry))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddEntry
|
||||
// @Title AddEntry
|
||||
// @Tag Entry API
|
||||
// @Description add entry
|
||||
// @Param body body object.Entry true "The details of the entry"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-entry [post]
|
||||
func (c *ApiController) AddEntry() {
|
||||
var entry object.Entry
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &entry)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddEntry(&entry))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteEntry
|
||||
// @Title DeleteEntry
|
||||
// @Tag Entry API
|
||||
// @Description delete entry
|
||||
// @Param body body object.Entry true "The details of the entry"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-entry [post]
|
||||
func (c *ApiController) DeleteEntry() {
|
||||
var entry object.Entry
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &entry)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteEntry(&entry))
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
// 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 (
|
||||
collogspb "go.opentelemetry.io/proto/otlp/collector/logs/v1"
|
||||
colmetricspb "go.opentelemetry.io/proto/otlp/collector/metrics/v1"
|
||||
coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// @Title AddOtlpTrace
|
||||
// @Tag OTLP API
|
||||
// @Description receive otlp trace protobuf
|
||||
// @Success 200 {object} string
|
||||
// @router /api/v1/traces [post]
|
||||
func (c *ApiController) AddOtlpTrace() {
|
||||
body := readProtobufBody(c.Ctx)
|
||||
if body == nil {
|
||||
return
|
||||
}
|
||||
provider, status, err := resolveOpenClawProvider(c.Ctx)
|
||||
if err != nil {
|
||||
responseOtlpError(c.Ctx, status, body, "%s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var req coltracepb.ExportTraceServiceRequest
|
||||
if err := proto.Unmarshal(body, &req); err != nil {
|
||||
responseOtlpError(c.Ctx, 400, body, "bad protobuf: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
message, err := protojson.MarshalOptions{Multiline: true, Indent: " "}.Marshal(&req)
|
||||
if err != nil {
|
||||
responseOtlpError(c.Ctx, 500, body, "marshal trace failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||
userAgent := c.Ctx.Request.Header.Get("User-Agent")
|
||||
if err := provider.AddTrace(message, clientIp, userAgent); err != nil {
|
||||
responseOtlpError(c.Ctx, 500, body, "save trace failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, _ := proto.Marshal(&coltracepb.ExportTraceServiceResponse{})
|
||||
c.Ctx.Output.Header("Content-Type", "application/x-protobuf")
|
||||
c.Ctx.Output.SetStatus(200)
|
||||
c.Ctx.Output.Body(resp)
|
||||
}
|
||||
|
||||
// @Title AddOtlpMetrics
|
||||
// @Tag OTLP API
|
||||
// @Description receive otlp metrics protobuf
|
||||
// @Success 200 {object} string
|
||||
// @router /api/v1/metrics [post]
|
||||
func (c *ApiController) AddOtlpMetrics() {
|
||||
body := readProtobufBody(c.Ctx)
|
||||
if body == nil {
|
||||
return
|
||||
}
|
||||
provider, status, err := resolveOpenClawProvider(c.Ctx)
|
||||
if err != nil {
|
||||
responseOtlpError(c.Ctx, status, body, "%s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var req colmetricspb.ExportMetricsServiceRequest
|
||||
if err := proto.Unmarshal(body, &req); err != nil {
|
||||
responseOtlpError(c.Ctx, 400, body, "bad protobuf: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
message, err := protojson.MarshalOptions{Multiline: true, Indent: " "}.Marshal(&req)
|
||||
if err != nil {
|
||||
responseOtlpError(c.Ctx, 500, body, "marshal metrics failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||
userAgent := c.Ctx.Request.Header.Get("User-Agent")
|
||||
if err := provider.AddMetrics(message, clientIp, userAgent); err != nil {
|
||||
responseOtlpError(c.Ctx, 500, body, "save metrics failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, _ := proto.Marshal(&colmetricspb.ExportMetricsServiceResponse{})
|
||||
c.Ctx.Output.Header("Content-Type", "application/x-protobuf")
|
||||
c.Ctx.Output.SetStatus(200)
|
||||
c.Ctx.Output.Body(resp)
|
||||
}
|
||||
|
||||
// @Title AddOtlpLogs
|
||||
// @Tag OTLP API
|
||||
// @Description receive otlp logs protobuf
|
||||
// @Success 200 {object} string
|
||||
// @router /api/v1/logs [post]
|
||||
func (c *ApiController) AddOtlpLogs() {
|
||||
body := readProtobufBody(c.Ctx)
|
||||
if body == nil {
|
||||
return
|
||||
}
|
||||
provider, status, err := resolveOpenClawProvider(c.Ctx)
|
||||
if err != nil {
|
||||
responseOtlpError(c.Ctx, status, body, "%s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var req collogspb.ExportLogsServiceRequest
|
||||
if err := proto.Unmarshal(body, &req); err != nil {
|
||||
responseOtlpError(c.Ctx, 400, body, "bad protobuf: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
message, err := protojson.MarshalOptions{Multiline: true, Indent: " "}.Marshal(&req)
|
||||
if err != nil {
|
||||
responseOtlpError(c.Ctx, 500, body, "marshal logs failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||
userAgent := c.Ctx.Request.Header.Get("User-Agent")
|
||||
if err := provider.AddLogs(message, clientIp, userAgent); err != nil {
|
||||
responseOtlpError(c.Ctx, 500, body, "save logs failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, _ := proto.Marshal(&collogspb.ExportLogsServiceResponse{})
|
||||
c.Ctx.Output.Header("Content-Type", "application/x-protobuf")
|
||||
c.Ctx.Output.SetStatus(200)
|
||||
c.Ctx.Output.Body(resp)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
"github.com/casdoor/casdoor/log"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func responseOtlpError(ctx *context.Context, status int, body []byte, format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
req := ctx.Request
|
||||
bodyInfo := "(no body)"
|
||||
if len(body) > 0 {
|
||||
bodyInfo = fmt.Sprintf("%d bytes: %q", len(body), truncate(body, 256))
|
||||
}
|
||||
fmt.Printf("responseOtlpError: [%d] %s | %s %s | remoteAddr=%s | Content-Type=%s | User-Agent=%s | body=%s\n",
|
||||
status, msg,
|
||||
req.Method, req.URL.Path,
|
||||
req.RemoteAddr,
|
||||
req.Header.Get("Content-Type"),
|
||||
req.Header.Get("User-Agent"),
|
||||
bodyInfo,
|
||||
)
|
||||
ctx.Output.SetStatus(status)
|
||||
ctx.Output.Body([]byte(msg))
|
||||
}
|
||||
|
||||
func truncate(b []byte, max int) []byte {
|
||||
if len(b) <= max {
|
||||
return b
|
||||
}
|
||||
return b[:max]
|
||||
}
|
||||
|
||||
func resolveOpenClawProvider(ctx *context.Context) (*log.OpenClawProvider, int, error) {
|
||||
clientIP := util.GetClientIpFromRequest(ctx.Request)
|
||||
provider, err := object.GetOpenClawProviderByIP(clientIP)
|
||||
if err != nil {
|
||||
return nil, 500, fmt.Errorf("provider lookup failed: %w", err)
|
||||
}
|
||||
if provider == nil {
|
||||
return nil, 403, fmt.Errorf("forbidden: no OpenClaw provider configured for IP %s", clientIP)
|
||||
}
|
||||
return provider, 0, nil
|
||||
}
|
||||
|
||||
func readProtobufBody(ctx *context.Context) []byte {
|
||||
if !strings.HasPrefix(ctx.Input.Header("Content-Type"), "application/x-protobuf") {
|
||||
preview, _ := io.ReadAll(io.LimitReader(ctx.Request.Body, 256))
|
||||
responseOtlpError(ctx, 415, preview, "unsupported content type")
|
||||
return nil
|
||||
}
|
||||
body, err := io.ReadAll(ctx.Request.Body)
|
||||
if err != nil {
|
||||
responseOtlpError(ctx, 400, nil, "read body failed")
|
||||
return nil
|
||||
}
|
||||
return body
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MfaSetupInitiate
|
||||
@@ -76,7 +77,7 @@ func (c *ApiController) MfaSetupInitiate() {
|
||||
return
|
||||
}
|
||||
|
||||
recoveryCode := util.GenerateUUID()
|
||||
recoveryCode := uuid.NewString()
|
||||
mfaProps.RecoveryCodes = []string{recoveryCode}
|
||||
mfaProps.MfaRememberInHours = organization.MfaRememberInHours
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@@ -151,78 +149,3 @@ func (c *ApiController) DeleteProduct() {
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteProduct(&product))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// BuyProduct
|
||||
// @Title BuyProduct (Deprecated)
|
||||
// @Tag Product API
|
||||
// @Description buy product using the deprecated compatibility endpoint, prefer place-order plus pay-order for new integrations
|
||||
// @Param id query string true "The id ( owner/name ) of the product"
|
||||
// @Param providerName query string true "The name of the provider"
|
||||
// @Param pricingName query string false "The name of the pricing (for subscription)"
|
||||
// @Param planName query string false "The name of the plan (for subscription)"
|
||||
// @Param userName query string false "The username to buy product for (admin only)"
|
||||
// @Param paymentEnv query string false "The payment environment"
|
||||
// @Param customPrice query number false "Custom price for recharge products"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /buy-product [post]
|
||||
func (c *ApiController) BuyProduct() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
host := c.Ctx.Request.Host
|
||||
providerName := c.Ctx.Input.Query("providerName")
|
||||
paymentEnv := c.Ctx.Input.Query("paymentEnv")
|
||||
customPriceStr := c.Ctx.Input.Query("customPrice")
|
||||
if customPriceStr == "" {
|
||||
customPriceStr = "0"
|
||||
}
|
||||
|
||||
customPrice, err := strconv.ParseFloat(customPriceStr, 64)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
pricingName := c.Ctx.Input.Query("pricingName")
|
||||
planName := c.Ctx.Input.Query("planName")
|
||||
paidUserName := c.Ctx.Input.Query("userName")
|
||||
|
||||
owner, _, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var userId string
|
||||
if paidUserName != "" {
|
||||
userId = util.GetId(owner, paidUserName)
|
||||
if userId != c.GetSessionUsername() && !c.IsAdmin() && userId != c.GetPaidUsername() {
|
||||
c.ResponseError(c.T("general:Only admin user can specify user"))
|
||||
return
|
||||
}
|
||||
|
||||
c.SetSession("paidUsername", "")
|
||||
} else {
|
||||
userId = c.GetSessionUsername()
|
||||
}
|
||||
if userId == "" {
|
||||
c.ResponseError(c.T("general:Please login first"))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := object.GetUser(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
|
||||
return
|
||||
}
|
||||
|
||||
payment, attachInfo, err := object.BuyProduct(id, user, providerName, pricingName, planName, host, paymentEnv, customPrice, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(payment, attachInfo)
|
||||
}
|
||||
|
||||
@@ -110,30 +110,6 @@ func (c *ApiController) UpdateServer() {
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// SyncMcpTool
|
||||
// @Title SyncMcpTool
|
||||
// @Tag Server API
|
||||
// @Description sync MCP tools for a server and return sync errors directly
|
||||
// @Param id query string true "The id ( owner/name ) of the server"
|
||||
// @Param isCleared query bool false "Whether to clear all tools instead of syncing"
|
||||
// @Param body body object.Server true "The details of the server"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /sync-mcp-tool [post]
|
||||
func (c *ApiController) SyncMcpTool() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
isCleared := c.Ctx.Input.Query("isCleared") == "1"
|
||||
|
||||
var server object.Server
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &server)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.SyncMcpTool(id, &server, isCleared))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddServer
|
||||
// @Title AddServer
|
||||
// @Tag Server API
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const onlineServerListUrl = "https://mcp.casdoor.org/registry.json"
|
||||
|
||||
// GetOnlineServers
|
||||
// @Title GetOnlineServers
|
||||
// @Tag Server API
|
||||
// @Description get online MCP server list
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /get-online-servers [get]
|
||||
func (c *ApiController) GetOnlineServers() {
|
||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := httpClient.Get(onlineServerListUrl)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
c.ResponseError(fmt.Sprintf("failed to get online server list, status code: %d", resp.StatusCode))
|
||||
return
|
||||
}
|
||||
|
||||
var onlineServers interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&onlineServers)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(onlineServers)
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSyncTimeoutMs = 1200
|
||||
defaultSyncMaxConcurrency = 32
|
||||
maxSyncHosts = 1024
|
||||
)
|
||||
|
||||
var (
|
||||
defaultSyncPorts = []int{3000, 8080, 80}
|
||||
defaultSyncPaths = []string{"/", "/mcp", "/sse", "/mcp/sse"}
|
||||
)
|
||||
|
||||
type SyncInnerServersRequest struct {
|
||||
CIDR []string `json:"cidr"`
|
||||
Scheme string `json:"scheme"`
|
||||
Ports []string `json:"ports"`
|
||||
Paths []string `json:"paths"`
|
||||
TimeoutMs int `json:"timeoutMs"`
|
||||
MaxConcurrency int `json:"maxConcurrency"`
|
||||
}
|
||||
|
||||
type SyncInnerServersResult struct {
|
||||
CIDR []string `json:"cidr"`
|
||||
ScannedHosts int `json:"scannedHosts"`
|
||||
OnlineHosts []string `json:"onlineHosts"`
|
||||
Servers []*mcp.InnerMcpServer `json:"servers"`
|
||||
}
|
||||
|
||||
// SyncIntranetServers
|
||||
// @Title SyncIntranetServers
|
||||
// @Tag Server API
|
||||
// @Description scan intranet IP/CIDR targets and detect MCP servers by probing common ports and paths
|
||||
// @Param body body controllers.SyncInnerServersRequest true "Intranet MCP server scan request"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /sync-intranet-servers [post]
|
||||
func (c *ApiController) SyncIntranetServers() {
|
||||
_, ok := c.RequireAdmin()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var req SyncInnerServersRequest
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &req); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for i := range req.CIDR {
|
||||
req.CIDR[i] = strings.TrimSpace(req.CIDR[i])
|
||||
}
|
||||
if len(req.CIDR) == 0 {
|
||||
c.ResponseError("scan target (CIDR/IP) is required")
|
||||
return
|
||||
}
|
||||
|
||||
hosts, err := mcp.ParseScanTargets(req.CIDR, maxSyncHosts)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
timeout := mcp.SanitizeTimeout(req.TimeoutMs, defaultSyncTimeoutMs, 10000)
|
||||
concurrency := mcp.SanitizeConcurrency(req.MaxConcurrency, defaultSyncMaxConcurrency, 256)
|
||||
ports := mcp.SanitizePorts(req.Ports, defaultSyncPorts)
|
||||
paths := mcp.SanitizePaths(req.Paths, defaultSyncPaths)
|
||||
scheme := mcp.SanitizeScheme(req.Scheme)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
onlineHostSet := map[string]struct{}{}
|
||||
serverMap := map[string]*mcp.InnerMcpServer{}
|
||||
mutex := sync.Mutex{}
|
||||
waitGroup := sync.WaitGroup{}
|
||||
sem := make(chan struct{}, concurrency)
|
||||
|
||||
for _, host := range hosts {
|
||||
host := host.String()
|
||||
waitGroup.Add(1)
|
||||
go func() {
|
||||
defer waitGroup.Done()
|
||||
|
||||
select {
|
||||
case sem <- struct{}{}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
defer func() { <-sem }()
|
||||
|
||||
isOnline, servers := mcp.ProbeHost(ctx, client, scheme, host, ports, paths, timeout)
|
||||
if !isOnline {
|
||||
return
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
onlineHostSet[host] = struct{}{}
|
||||
for _, server := range servers {
|
||||
serverMap[server.Url] = server
|
||||
}
|
||||
mutex.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
waitGroup.Wait()
|
||||
|
||||
onlineHosts := make([]string, 0, len(onlineHostSet))
|
||||
for host := range onlineHostSet {
|
||||
onlineHosts = append(onlineHosts, host)
|
||||
}
|
||||
slices.Sort(onlineHosts)
|
||||
|
||||
servers := make([]*mcp.InnerMcpServer, 0, len(serverMap))
|
||||
for _, server := range serverMap {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
slices.SortFunc(servers, func(a, b *mcp.InnerMcpServer) int {
|
||||
if a.Url < b.Url {
|
||||
return -1
|
||||
}
|
||||
if a.Url > b.Url {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
c.ResponseOk(&SyncInnerServersResult{
|
||||
CIDR: req.CIDR,
|
||||
ScannedHosts: len(hosts),
|
||||
OnlineHosts: onlineHosts,
|
||||
Servers: servers,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *ApiController) SyncInnerServers() {
|
||||
c.SyncIntranetServers()
|
||||
}
|
||||
@@ -440,8 +440,6 @@ func (c *ApiController) ResetEmailOrPhone() {
|
||||
return
|
||||
}
|
||||
|
||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||
|
||||
destType := c.Ctx.Request.Form.Get("type")
|
||||
dest := c.Ctx.Request.Form.Get("dest")
|
||||
code := c.Ctx.Request.Form.Get("code")
|
||||
@@ -496,9 +494,13 @@ func (c *ApiController) ResetEmailOrPhone() {
|
||||
}
|
||||
}
|
||||
|
||||
err = object.CheckVerifyCodeWithLimitAndIp(user, clientIp, checkDest, code, c.GetAcceptLanguage())
|
||||
result, err := object.CheckVerificationCode(checkDest, code, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
c.ResponseError(c.T(err.Error()))
|
||||
return
|
||||
}
|
||||
if result.Code != object.VerificationSuccess {
|
||||
c.ResponseError(result.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -596,8 +598,7 @@ func (c *ApiController) VerifyCode() {
|
||||
}
|
||||
|
||||
if !passed {
|
||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||
err = object.CheckVerifyCodeWithLimitAndIp(user, clientIp, checkDest, authForm.Code, c.GetAcceptLanguage())
|
||||
err = object.CheckVerifyCodeWithLimit(user, checkDest, authForm.Code, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
const defaultWebhookEventListLimit = 100
|
||||
|
||||
func (c *ApiController) getScopedWebhookEventQuery() (string, string, bool) {
|
||||
organization, ok := c.RequireAdmin()
|
||||
if !ok {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
owner := ""
|
||||
if c.IsGlobalAdmin() {
|
||||
owner = c.Ctx.Input.Query("owner")
|
||||
|
||||
requestedOrganization := c.Ctx.Input.Query("organization")
|
||||
if requestedOrganization != "" {
|
||||
organization = requestedOrganization
|
||||
}
|
||||
}
|
||||
|
||||
return owner, organization, true
|
||||
}
|
||||
|
||||
func (c *ApiController) checkWebhookEventAccess(event *object.WebhookEvent, organization string) bool {
|
||||
if event == nil || c.IsGlobalAdmin() {
|
||||
return true
|
||||
}
|
||||
|
||||
if event.Organization != organization {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GetWebhookEvents
|
||||
// @Title GetWebhookEvents
|
||||
// @Tag Webhook Event API
|
||||
// @Description get webhook events with filtering
|
||||
// @Param owner query string false "The owner of webhook events"
|
||||
// @Param organization query string false "The organization"
|
||||
// @Param webhookName query string false "The webhook name"
|
||||
// @Param status query string false "Event status (pending, success, failed, retrying)"
|
||||
// @Success 200 {array} object.WebhookEvent The Response object
|
||||
// @router /get-webhook-events [get]
|
||||
func (c *ApiController) GetWebhookEvents() {
|
||||
owner, organization, ok := c.getScopedWebhookEventQuery()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
webhookName := c.Ctx.Input.Query("webhookName")
|
||||
status := c.Ctx.Input.Query("status")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit != "" && page != "" {
|
||||
limit := util.ParseInt(limit)
|
||||
count, err := object.GetWebhookEventCount(owner, organization, webhookName, object.WebhookEventStatus(status))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
events, err := object.GetWebhookEvents(owner, organization, webhookName, object.WebhookEventStatus(status), paginator.Offset(), limit, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(events, paginator.Nums())
|
||||
} else {
|
||||
events, err := object.GetWebhookEvents(owner, organization, webhookName, object.WebhookEventStatus(status), 0, defaultWebhookEventListLimit, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(events)
|
||||
}
|
||||
}
|
||||
|
||||
// GetWebhookEvent
|
||||
// @Title GetWebhookEvent
|
||||
// @Tag Webhook Event API
|
||||
// @Description get webhook event
|
||||
// @Param id query string true "The id ( owner/name ) of the webhook event"
|
||||
// @Success 200 {object} object.WebhookEvent The Response object
|
||||
// @router /get-webhook-event-detail [get]
|
||||
func (c *ApiController) GetWebhookEvent() {
|
||||
organization, ok := c.RequireAdmin()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
event, err := object.GetWebhookEvent(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !c.checkWebhookEventAccess(event, organization) {
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(event)
|
||||
}
|
||||
|
||||
// ReplayWebhookEvent
|
||||
// @Title ReplayWebhookEvent
|
||||
// @Tag Webhook Event API
|
||||
// @Description replay a webhook event
|
||||
// @Param id query string true "The id ( owner/name ) of the webhook event"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /replay-webhook-event [post]
|
||||
func (c *ApiController) ReplayWebhookEvent() {
|
||||
organization, ok := c.RequireAdmin()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
event, err := object.GetWebhookEvent(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !c.checkWebhookEventAccess(event, organization) {
|
||||
return
|
||||
}
|
||||
|
||||
err = object.ReplayWebhookEvent(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk("Webhook event replay triggered")
|
||||
}
|
||||
|
||||
// DeleteWebhookEvent
|
||||
// @Title DeleteWebhookEvent
|
||||
// @Tag Webhook Event API
|
||||
// @Description delete webhook event
|
||||
// @Param body body object.WebhookEvent true "The details of the webhook event"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-webhook-event [post]
|
||||
func (c *ApiController) DeleteWebhookEvent() {
|
||||
organization, ok := c.RequireAdmin()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var event object.WebhookEvent
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &event)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
storedEvent, err := object.GetWebhookEvent(event.GetId())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !c.checkWebhookEventAccess(storedEvent, organization) {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteWebhookEvent(&event))
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -141,7 +141,7 @@ func (a *AzureACSEmailProvider) Send(fromAddress string, fromName string, toAddr
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("repeatability-request-id", util.GenerateUUID())
|
||||
req.Header.Set("repeatability-request-id", uuid.New().String())
|
||||
req.Header.Set("repeatability-first-sent", time.Now().UTC().Format(http.TimeFormat))
|
||||
|
||||
client := &http.Client{}
|
||||
|
||||
6
go.mod
6
go.mod
@@ -1,8 +1,8 @@
|
||||
module github.com/casdoor/casdoor
|
||||
|
||||
go 1.25.0
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.25.8
|
||||
toolchain go1.24.13
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.3
|
||||
@@ -80,7 +80,6 @@ require (
|
||||
github.com/xorm-io/builder v0.3.13
|
||||
github.com/xorm-io/core v0.7.4
|
||||
github.com/xorm-io/xorm v1.1.6
|
||||
go.opentelemetry.io/proto/otlp v1.7.1
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/net v0.49.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
@@ -187,7 +186,6 @@ require (
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/gregdel/pushover v1.3.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1280,8 +1280,6 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
|
||||
github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
|
||||
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -1857,8 +1855,6 @@ go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTq
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
// 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 log
|
||||
|
||||
import "fmt"
|
||||
|
||||
// OtlpAdder persists a single OTLP record into the backing store.
|
||||
// Parameters: entryType ("trace"/"metrics"/"log"), message (JSON payload),
|
||||
// clientIp and userAgent from the originating HTTP request.
|
||||
// The unique entry name is generated by the implementation.
|
||||
type OtlpAdder func(entryType, message, clientIp, userAgent string) error
|
||||
|
||||
// OpenClawProvider receives OpenTelemetry data pushed by an OpenClaw agent over
|
||||
// HTTP and persists each record as an Entry row via the OtlpAdder supplied at
|
||||
// construction time. It is passive (push-based via HTTP): Start/Stop are no-ops
|
||||
// and Write is not applicable.
|
||||
type OpenClawProvider struct {
|
||||
providerName string
|
||||
addOtlpEntry OtlpAdder
|
||||
}
|
||||
|
||||
// NewOpenClawProvider creates an OpenClawProvider backed by addOtlpEntry.
|
||||
func NewOpenClawProvider(providerName string, addOtlpEntry OtlpAdder) *OpenClawProvider {
|
||||
return &OpenClawProvider{providerName: providerName, addOtlpEntry: addOtlpEntry}
|
||||
}
|
||||
|
||||
// Write is not applicable for an HTTP-push provider and always returns an error.
|
||||
func (p *OpenClawProvider) Write(_, _ string) error {
|
||||
return fmt.Errorf("OpenClawProvider receives data over HTTP and does not accept Write calls")
|
||||
}
|
||||
|
||||
// Start is a no-op; OpenClawProvider is passive and has no background goroutine.
|
||||
func (p *OpenClawProvider) Start(_ EntryAdder, _ func(error)) error { return nil }
|
||||
|
||||
// Stop is a no-op.
|
||||
func (p *OpenClawProvider) Stop() error { return nil }
|
||||
|
||||
// AddTrace persists an OTLP trace payload (already serialised to JSON).
|
||||
func (p *OpenClawProvider) AddTrace(message []byte, clientIp, userAgent string) error {
|
||||
return p.addOtlpEntry("trace", string(message), clientIp, userAgent)
|
||||
}
|
||||
|
||||
// AddMetrics persists an OTLP metrics payload (already serialised to JSON).
|
||||
func (p *OpenClawProvider) AddMetrics(message []byte, clientIp, userAgent string) error {
|
||||
return p.addOtlpEntry("metrics", string(message), clientIp, userAgent)
|
||||
}
|
||||
|
||||
// AddLogs persists an OTLP logs payload (already serialised to JSON).
|
||||
func (p *OpenClawProvider) AddLogs(message []byte, clientIp, userAgent string) error {
|
||||
return p.addOtlpEntry("log", string(message), clientIp, userAgent)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// 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 log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PermissionLogProvider records Casbin authorization decisions as Entry rows.
|
||||
// It is push-based: callers supply log lines via Write, which are immediately
|
||||
// persisted through the injected EntryAdder. Start and Stop are no-ops.
|
||||
type PermissionLogProvider struct {
|
||||
providerName string
|
||||
addEntry EntryAdder
|
||||
}
|
||||
|
||||
// NewPermissionLogProvider creates a PermissionLogProvider backed by addEntry.
|
||||
func NewPermissionLogProvider(providerName string, addEntry EntryAdder) *PermissionLogProvider {
|
||||
return &PermissionLogProvider{providerName: providerName, addEntry: addEntry}
|
||||
}
|
||||
|
||||
// Write stores one permission-log entry.
|
||||
// severity follows syslog conventions (e.g. info, warning, err).
|
||||
func (p *PermissionLogProvider) Write(severity string, message string) error {
|
||||
createdTime := time.Now().UTC().Format(time.RFC3339)
|
||||
return p.addEntry("built-in", createdTime, p.providerName, fmt.Sprintf("[%s] %s", severity, message))
|
||||
}
|
||||
|
||||
// Start is a no-op for PermissionLogProvider; it received its EntryAdder at
|
||||
// construction time and does not require background collection.
|
||||
func (p *PermissionLogProvider) Start(_ EntryAdder, _ func(error)) error { return nil }
|
||||
|
||||
// Stop is a no-op for PermissionLogProvider.
|
||||
func (p *PermissionLogProvider) Stop() error { return nil }
|
||||
@@ -1,75 +0,0 @@
|
||||
// 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 log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/thanhpk/randstr"
|
||||
)
|
||||
|
||||
// GenerateEntryName returns a cryptographically random 32-character hex string
|
||||
// suitable for use as an Entry.Name primary key.
|
||||
func GenerateEntryName() string {
|
||||
return randstr.Hex(16)
|
||||
}
|
||||
|
||||
// EntryAdder persists a collected log entry into the backing store.
|
||||
// Parameters map to the Entry table columns: owner, createdTime (RFC3339),
|
||||
// provider (the log provider name), and message. The unique entry name is
|
||||
// generated by the implementation, so callers do not need to supply one.
|
||||
// Defined here so it is shared by all LogProvider implementations without
|
||||
// creating import cycles with the object package.
|
||||
type EntryAdder func(owner, createdTime, provider, message string) error
|
||||
|
||||
// LogProvider is the common interface for all log providers.
|
||||
//
|
||||
// Push-based providers (e.g. PermissionLogProvider) receive individual log
|
||||
// lines through Write and persist them immediately. Start and Stop are no-ops
|
||||
// for these providers.
|
||||
//
|
||||
// Pull-based providers (e.g. SystemLogProvider) actively collect logs from an
|
||||
// external source. Start begins a background collection goroutine that calls
|
||||
// addEntry for every new record; Stop halts collection. Write returns an error
|
||||
// for these providers as they are not designed to accept external input.
|
||||
type LogProvider interface {
|
||||
// Write records a single log line. Used by push-based providers.
|
||||
Write(severity string, message string) error
|
||||
// Start begins background log collection with the given EntryAdder.
|
||||
// For push-based providers this is a no-op (they received addEntry at
|
||||
// construction time). onError is called from the background goroutine
|
||||
// when collection stops with a fatal error; it may be nil.
|
||||
Start(addEntry EntryAdder, onError func(error)) error
|
||||
// Stop halts background collection and releases any OS resources.
|
||||
Stop() error
|
||||
}
|
||||
|
||||
// GetLogProvider returns a concrete log provider for the given type and connection settings.
|
||||
// The title parameter is used as the OS log tag for System Log.
|
||||
// Types that are not yet implemented return a non-nil error.
|
||||
func GetLogProvider(typ string, _ string, _ int, title string) (LogProvider, error) {
|
||||
switch typ {
|
||||
case "System Log":
|
||||
tag := title
|
||||
if tag == "" {
|
||||
tag = "casdoor"
|
||||
}
|
||||
return NewSystemLogProvider(tag)
|
||||
case "SELinux Log":
|
||||
return NewSELinuxLogProvider()
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported log provider type: %s", typ)
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// 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 log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SELinuxLogProvider collects SELinux audit events (AVC denials and related
|
||||
// records) from the local system and stores each record as an Entry row via
|
||||
// the EntryAdder supplied to Start.
|
||||
//
|
||||
// It is pull-based: Write is not applicable and returns an error.
|
||||
// Start launches the background collector; Stop cancels it.
|
||||
// On platforms where SELinux is not supported, Start returns an error.
|
||||
type SELinuxLogProvider struct {
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// NewSELinuxLogProvider creates a SELinuxLogProvider.
|
||||
// Call Start to begin collection.
|
||||
func NewSELinuxLogProvider() (*SELinuxLogProvider, error) {
|
||||
return &SELinuxLogProvider{}, nil
|
||||
}
|
||||
|
||||
// Write is not applicable for a pull-based collector and always returns an error.
|
||||
func (s *SELinuxLogProvider) Write(severity string, message string) error {
|
||||
return fmt.Errorf("SELinuxLogProvider is a log collector and does not accept Write calls")
|
||||
}
|
||||
|
||||
// Start launches a background goroutine that reads new SELinux audit records
|
||||
// and persists each one by calling addEntry. Returns immediately; collection
|
||||
// runs until Stop is called. If the goroutine encounters a fatal error,
|
||||
// onError is called with that error (onError may be nil).
|
||||
func (s *SELinuxLogProvider) Start(addEntry EntryAdder, onError func(error)) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s.cancel = cancel
|
||||
go func() {
|
||||
if err := collectSELinuxLogs(ctx, addEntry); err != nil && onError != nil {
|
||||
onError(err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop cancels background collection. It is safe to call multiple times.
|
||||
func (s *SELinuxLogProvider) Stop() error {
|
||||
if s.cancel != nil {
|
||||
s.cancel()
|
||||
s.cancel = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
// 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.
|
||||
|
||||
//go:build linux
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const auditLogPath = "/var/log/audit/audit.log"
|
||||
|
||||
// selinuxAuditTypes is the set of audit record types that are SELinux-related.
|
||||
var selinuxAuditTypes = map[string]bool{
|
||||
"AVC": true,
|
||||
"USER_AVC": true,
|
||||
"SELINUX_ERR": true,
|
||||
"MAC_POLICY_LOAD": true,
|
||||
"MAC_STATUS": true,
|
||||
}
|
||||
|
||||
// auditTimestampRe matches the msg=audit(seconds.millis:serial) field.
|
||||
var auditTimestampRe = regexp.MustCompile(`msg=audit\((\d+)\.\d+:\d+\)`)
|
||||
|
||||
// CheckSELinuxAvailable returns nil if SELinux is active and the audit log is
|
||||
// readable on this system. Returns a descriptive error otherwise.
|
||||
func CheckSELinuxAvailable() error {
|
||||
if _, err := os.Stat("/sys/fs/selinux/enforce"); os.IsNotExist(err) {
|
||||
return fmt.Errorf("SELinux is not available or not mounted on this system")
|
||||
}
|
||||
if _, err := os.Stat(auditLogPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("SELinux audit log not found at %s (is auditd running?)", auditLogPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectSELinuxLogs tails /var/log/audit/audit.log and persists each
|
||||
// SELinux-related audit record via addEntry until ctx is cancelled.
|
||||
func collectSELinuxLogs(ctx context.Context, addEntry EntryAdder) error {
|
||||
if err := CheckSELinuxAvailable(); err != nil {
|
||||
return fmt.Errorf("SELinuxLogProvider: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "tail", "-f", "-n", "0", auditLogPath)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("SELinuxLogProvider: failed to open audit log pipe: %w", err)
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("SELinuxLogProvider: failed to start tail: %w", err)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
line := scanner.Text()
|
||||
if !isSELinuxAuditLine(line) {
|
||||
continue
|
||||
}
|
||||
|
||||
severity := selinuxSeverity(line)
|
||||
createdTime := parseAuditTimestamp(line)
|
||||
if err := addEntry("built-in", createdTime, "",
|
||||
fmt.Sprintf("[%s] %s", severity, line)); err != nil {
|
||||
return fmt.Errorf("SELinuxLogProvider: failed to persist audit entry: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("SELinuxLogProvider: audit log read error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isSELinuxAuditLine reports whether the audit log line is an SELinux record.
|
||||
func isSELinuxAuditLine(line string) bool {
|
||||
// Audit lines start with "type=<TYPE> "
|
||||
const prefix = "type="
|
||||
if !strings.HasPrefix(line, prefix) {
|
||||
return false
|
||||
}
|
||||
end := strings.IndexByte(line[len(prefix):], ' ')
|
||||
var typ string
|
||||
if end < 0 {
|
||||
typ = line[len(prefix):]
|
||||
} else {
|
||||
typ = line[len(prefix) : len(prefix)+end]
|
||||
}
|
||||
return selinuxAuditTypes[typ]
|
||||
}
|
||||
|
||||
// selinuxSeverity maps SELinux audit record types to a syslog severity name.
|
||||
func selinuxSeverity(line string) string {
|
||||
if strings.HasPrefix(line, "type=AVC") || strings.HasPrefix(line, "type=USER_AVC") || strings.HasPrefix(line, "type=SELINUX_ERR") {
|
||||
return "warning"
|
||||
}
|
||||
return "info"
|
||||
}
|
||||
|
||||
// parseAuditTimestamp extracts the Unix timestamp from an audit log line and
|
||||
// returns it as an RFC3339 string. Falls back to the current time on failure.
|
||||
func parseAuditTimestamp(line string) string {
|
||||
m := auditTimestampRe.FindStringSubmatch(line)
|
||||
if m == nil {
|
||||
return time.Now().UTC().Format(time.RFC3339)
|
||||
}
|
||||
sec, err := strconv.ParseInt(m[1], 10, 64)
|
||||
if err != nil {
|
||||
return time.Now().UTC().Format(time.RFC3339)
|
||||
}
|
||||
return time.Unix(sec, 0).UTC().Format(time.RFC3339)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// 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.
|
||||
|
||||
//go:build !linux
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// CheckSELinuxAvailable always returns an error on non-Linux platforms.
|
||||
func CheckSELinuxAvailable() error {
|
||||
return fmt.Errorf("SELinux is not supported on %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
// collectSELinuxLogs is a no-op on non-Linux platforms.
|
||||
func collectSELinuxLogs(_ context.Context, _ EntryAdder) error {
|
||||
return CheckSELinuxAvailable()
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// 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 log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// platformCollector is an OS-specific log reader.
|
||||
// Implementations are in system_log_unix.go and system_log_windows.go.
|
||||
type platformCollector interface {
|
||||
// collect blocks and streams new OS log records to addEntry until ctx is
|
||||
// cancelled or a fatal error occurs. It must return promptly when
|
||||
// ctx.Done() is closed. A non-nil error means collection stopped
|
||||
// unexpectedly and should be reported to the operator.
|
||||
collect(ctx context.Context, addEntry EntryAdder) error
|
||||
}
|
||||
|
||||
// SystemLogProvider collects log records from the operating-system's native
|
||||
// logging facility (journald/syslog on Linux/Unix, Event Log on Windows) and
|
||||
// stores each record as an Entry row via the EntryAdder supplied to Start.
|
||||
//
|
||||
// It is pull-based: Write is not applicable and returns an error.
|
||||
// Start launches the background collector; Stop cancels it.
|
||||
type SystemLogProvider struct {
|
||||
tag string
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// NewSystemLogProvider creates a SystemLogProvider that will identify itself
|
||||
// with the given tag when collecting OS log records.
|
||||
// Call Start to begin collection.
|
||||
func NewSystemLogProvider(tag string) (*SystemLogProvider, error) {
|
||||
return &SystemLogProvider{tag: tag}, nil
|
||||
}
|
||||
|
||||
// Write is not applicable for a pull-based collector and always returns an
|
||||
// error. Callers in the permission-log path should skip System Log providers.
|
||||
func (s *SystemLogProvider) Write(severity string, message string) error {
|
||||
return fmt.Errorf("SystemLogProvider is a log collector and does not accept Write calls")
|
||||
}
|
||||
|
||||
// Start launches a background goroutine that reads new OS log records and
|
||||
// persists each one by calling addEntry. It returns immediately; collection
|
||||
// runs until Stop is called. If the goroutine encounters a fatal error,
|
||||
// onError is called with that error (onError may be nil).
|
||||
func (s *SystemLogProvider) Start(addEntry EntryAdder, onError func(error)) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s.cancel = cancel
|
||||
collector := newPlatformCollector(s.tag)
|
||||
go func() {
|
||||
if err := collector.collect(ctx, addEntry); err != nil && onError != nil {
|
||||
onError(err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop cancels background collection. It is safe to call multiple times.
|
||||
func (s *SystemLogProvider) Stop() error {
|
||||
if s.cancel != nil {
|
||||
s.cancel()
|
||||
s.cancel = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
// 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.
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type unixCollector struct {
|
||||
tag string
|
||||
}
|
||||
|
||||
func newPlatformCollector(tag string) platformCollector {
|
||||
return &unixCollector{tag: tag}
|
||||
}
|
||||
|
||||
// collect streams new journald records to addEntry until ctx is cancelled or
|
||||
// a fatal error occurs. It runs `journalctl -n 0 -f --output=json` so only
|
||||
// records that arrive after Start is called are collected (no backfill).
|
||||
// Returns nil when ctx is cancelled normally; returns a non-nil error if the
|
||||
// process could not be started or the output pipe broke unexpectedly.
|
||||
func (u *unixCollector) collect(ctx context.Context, addEntry EntryAdder) error {
|
||||
cmd := exec.CommandContext(ctx, "journalctl", "-n", "0", "-f", "--output=json")
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("SystemLogProvider: failed to open journalctl stdout pipe: %w", err)
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("SystemLogProvider: failed to start journalctl: %w", err)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
// journald JSON lines can be large; use a 1 MB buffer.
|
||||
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||
|
||||
for scanner.Scan() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
var fields map[string]interface{}
|
||||
if err := json.Unmarshal(scanner.Bytes(), &fields); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
severity := journalSeverity(fields)
|
||||
message := journalMessage(fields)
|
||||
createdTime := journalTimestamp(fields)
|
||||
if err := addEntry("built-in", createdTime, u.tag,
|
||||
fmt.Sprintf("[%s] %s", severity, message)); err != nil {
|
||||
return fmt.Errorf("SystemLogProvider: failed to persist journal entry: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
// A cancelled context causes the pipe to close; treat that as normal exit.
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("SystemLogProvider: journalctl output error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// journalSeverity maps the journald PRIORITY field to a syslog severity name.
|
||||
// PRIORITY values: 0=emerg 1=alert 2=crit 3=err 4=warning 5=notice 6=info 7=debug
|
||||
func journalSeverity(fields map[string]interface{}) string {
|
||||
mapping := map[string]string{
|
||||
"0": "emerg", "1": "alert", "2": "crit", "3": "err",
|
||||
"4": "warning", "5": "notice", "6": "info", "7": "debug",
|
||||
}
|
||||
if p, ok := fields["PRIORITY"].(string); ok {
|
||||
if s, ok2 := mapping[p]; ok2 {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return "info"
|
||||
}
|
||||
|
||||
// journalMessage extracts the human-readable message from journald JSON.
|
||||
func journalMessage(fields map[string]interface{}) string {
|
||||
if msg, ok := fields["MESSAGE"].(string); ok {
|
||||
return msg
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// journalTimestamp converts the journald __REALTIME_TIMESTAMP (microseconds
|
||||
// since Unix epoch) to an RFC3339 string.
|
||||
func journalTimestamp(fields map[string]interface{}) string {
|
||||
if ts, ok := fields["__REALTIME_TIMESTAMP"].(string); ok {
|
||||
usec, err := strconv.ParseInt(ts, 10, 64)
|
||||
if err == nil {
|
||||
t := time.Unix(usec/1_000_000, (usec%1_000_000)*1_000).UTC()
|
||||
return t.Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
return time.Now().UTC().Format(time.RFC3339)
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
// 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.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Windows Event Log channels to collect from.
|
||||
var eventLogChannels = []string{"System", "Application"}
|
||||
|
||||
type windowsCollector struct {
|
||||
tag string
|
||||
}
|
||||
|
||||
func newPlatformCollector(tag string) platformCollector {
|
||||
return &windowsCollector{tag: tag}
|
||||
}
|
||||
|
||||
// collect polls Windows Event Log channels every 5 seconds via wevtutil.exe
|
||||
// and persists new records to addEntry. Only events that arrive after Start
|
||||
// is called are collected; historical events are not backfilled.
|
||||
// Returns nil when ctx is cancelled normally.
|
||||
func (w *windowsCollector) collect(ctx context.Context, addEntry EntryAdder) error {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
lastCheck := time.Now().UTC()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case tick := <-ticker.C:
|
||||
for _, channel := range eventLogChannels {
|
||||
if err := w.queryChannel(ctx, channel, lastCheck, addEntry); err != nil {
|
||||
return fmt.Errorf("SystemLogProvider: error querying channel %s: %w", channel, err)
|
||||
}
|
||||
}
|
||||
lastCheck = tick.UTC()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// queryChannel runs wevtutil.exe to fetch events from channel that were
|
||||
// created after since, then stores each event via addEntry.
|
||||
// Returns a non-nil error if the wevtutil command fails or XML parsing fails.
|
||||
func (w *windowsCollector) queryChannel(ctx context.Context, channel string, since time.Time, addEntry EntryAdder) error {
|
||||
sinceStr := since.Format("2006-01-02T15:04:05.000Z")
|
||||
query := fmt.Sprintf("*[System[TimeCreated[@SystemTime>='%s']]]", sinceStr)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "wevtutil.exe", "qe", channel,
|
||||
"/f:RenderedXml", "/rd:false",
|
||||
fmt.Sprintf("/q:%s", query),
|
||||
)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
// A cancelled context is a normal shutdown, not an error.
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("wevtutil.exe failed for channel %s: %w", channel, err)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return w.parseAndPersistEvents(out, channel, addEntry)
|
||||
}
|
||||
|
||||
// parseAndPersistEvents decodes wevtutil XML output and persists each Event
|
||||
// record via addEntry. wevtutil outputs one <Event> element per record;
|
||||
// the output is wrapped in a synthetic <Events> root so the decoder can
|
||||
// handle multiple records in one pass. Token()+DecodeElement() is used to
|
||||
// skip the wrapper element without triggering an XMLName mismatch error.
|
||||
func (w *windowsCollector) parseAndPersistEvents(out []byte, channel string, addEntry EntryAdder) error {
|
||||
wrapped := "<Events>" + string(out) + "</Events>"
|
||||
decoder := xml.NewDecoder(strings.NewReader(wrapped))
|
||||
|
||||
for {
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return fmt.Errorf("SystemLogProvider: failed to parse event XML (channel=%s): %w", channel, err)
|
||||
}
|
||||
se, ok := token.(xml.StartElement)
|
||||
if !ok || se.Name.Local != "Event" {
|
||||
continue
|
||||
}
|
||||
|
||||
var event winEvent
|
||||
if err := decoder.DecodeElement(&event, &se); err != nil {
|
||||
return fmt.Errorf("SystemLogProvider: failed to decode event XML (channel=%s): %w", channel, err)
|
||||
}
|
||||
|
||||
severity := winEventSeverity(event.System.Level)
|
||||
message := strings.TrimSpace(event.RenderingInfo.Message)
|
||||
if message == "" {
|
||||
message = fmt.Sprintf("EventID=%d Source=%s", event.System.EventID, event.System.Provider.Name)
|
||||
}
|
||||
createdTime := winEventTimestamp(event.System.TimeCreated.SystemTime)
|
||||
if err := addEntry("built-in", createdTime, w.tag,
|
||||
fmt.Sprintf("[%s] [%s] %s", severity, channel, message)); err != nil {
|
||||
return fmt.Errorf("SystemLogProvider: failed to persist event (channel=%s EventID=%d): %w",
|
||||
channel, event.System.EventID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// winEvent represents the subset of the Windows Event XML schema that we need.
|
||||
type winEvent struct {
|
||||
XMLName xml.Name `xml:"Event"`
|
||||
System struct {
|
||||
Provider struct {
|
||||
Name string `xml:"Name,attr"`
|
||||
} `xml:"Provider"`
|
||||
EventID int `xml:"EventID"`
|
||||
Level int `xml:"Level"`
|
||||
TimeCreated struct {
|
||||
SystemTime string `xml:"SystemTime,attr"`
|
||||
} `xml:"TimeCreated"`
|
||||
} `xml:"System"`
|
||||
RenderingInfo struct {
|
||||
Message string `xml:"Message"`
|
||||
} `xml:"RenderingInfo"`
|
||||
}
|
||||
|
||||
// winEventSeverity maps Windows Event Log Level values to syslog severity names.
|
||||
// Level: 1=Critical 2=Error 3=Warning 4=Information 5=Verbose
|
||||
func winEventSeverity(level int) string {
|
||||
switch level {
|
||||
case 1:
|
||||
return "crit"
|
||||
case 2:
|
||||
return "err"
|
||||
case 3:
|
||||
return "warning"
|
||||
case 5:
|
||||
return "debug"
|
||||
default: // 4=Information and anything else
|
||||
return "info"
|
||||
}
|
||||
}
|
||||
|
||||
// winEventTimestamp parses a Windows Event SystemTime attribute string to RFC3339.
|
||||
func winEventTimestamp(s string) string {
|
||||
// SystemTime is in the form "2024-01-15T10:30:00.000000000Z"
|
||||
t, err := time.Parse(time.RFC3339Nano, s)
|
||||
if err != nil {
|
||||
// Try without nanoseconds
|
||||
t, err = time.Parse("2006-01-02T15:04:05.000000000Z", s)
|
||||
if err != nil {
|
||||
return time.Now().UTC().Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
return t.UTC().Format(time.RFC3339)
|
||||
}
|
||||
4
main.go
4
main.go
@@ -66,7 +66,6 @@ func main() {
|
||||
}
|
||||
|
||||
object.InitDefaultStorageProvider()
|
||||
object.InitLogProviders()
|
||||
object.InitLdapAutoSynchronizer()
|
||||
proxy.InitHttpClient()
|
||||
authz.InitApi()
|
||||
@@ -133,9 +132,6 @@ func main() {
|
||||
go radius.StartRadiusServer()
|
||||
go object.ClearThroughputPerSecond()
|
||||
|
||||
// Start webhook delivery worker
|
||||
object.StartWebhookDeliveryWorker()
|
||||
|
||||
if len(object.SiteMap) != 0 {
|
||||
service.Start()
|
||||
}
|
||||
|
||||
306
mcp/util.go
306
mcp/util.go
@@ -16,12 +16,6 @@ package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@@ -29,13 +23,6 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type InnerMcpServer struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Path string `json:"path"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
func GetServerTools(owner, name, url, token string) ([]*mcpsdk.Tool, error) {
|
||||
var session *mcpsdk.ClientSession
|
||||
var err error
|
||||
@@ -62,296 +49,3 @@ func GetServerTools(owner, name, url, token string) ([]*mcpsdk.Tool, error) {
|
||||
|
||||
return toolResult.Tools, nil
|
||||
}
|
||||
|
||||
func SanitizeScheme(scheme string) string {
|
||||
scheme = strings.ToLower(strings.TrimSpace(scheme))
|
||||
if scheme == "https" {
|
||||
return "https"
|
||||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
func SanitizeTimeout(timeoutMs int, defaultTimeoutMs int, maxTimeoutMs int) time.Duration {
|
||||
if timeoutMs <= 0 {
|
||||
timeoutMs = defaultTimeoutMs
|
||||
}
|
||||
if timeoutMs > maxTimeoutMs {
|
||||
timeoutMs = maxTimeoutMs
|
||||
}
|
||||
return time.Duration(timeoutMs) * time.Millisecond
|
||||
}
|
||||
|
||||
func SanitizeConcurrency(maxConcurrency int, defaultConcurrency int, maxAllowed int) int {
|
||||
if maxConcurrency <= 0 {
|
||||
maxConcurrency = defaultConcurrency
|
||||
}
|
||||
if maxConcurrency > maxAllowed {
|
||||
maxConcurrency = maxAllowed
|
||||
}
|
||||
return maxConcurrency
|
||||
}
|
||||
|
||||
func SanitizePorts(portInputs []string, defaultPorts []int) []int {
|
||||
if len(portInputs) == 0 {
|
||||
return append([]int{}, defaultPorts...)
|
||||
}
|
||||
|
||||
portSet := map[int]struct{}{}
|
||||
result := make([]int, 0, len(portInputs))
|
||||
for _, portInput := range portInputs {
|
||||
portInput = strings.TrimSpace(portInput)
|
||||
if portInput == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(portInput, "-") {
|
||||
parts := strings.SplitN(portInput, "-", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
start, err := strconv.Atoi(strings.TrimSpace(parts[0]))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
end, err := strconv.Atoi(strings.TrimSpace(parts[1]))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if start > end {
|
||||
continue
|
||||
}
|
||||
|
||||
if start < 1 {
|
||||
start = 1
|
||||
}
|
||||
if end > 65535 {
|
||||
end = 65535
|
||||
}
|
||||
if start > end {
|
||||
continue
|
||||
}
|
||||
|
||||
for port := start; port <= end; port++ {
|
||||
if _, ok := portSet[port]; ok {
|
||||
continue
|
||||
}
|
||||
portSet[port] = struct{}{}
|
||||
result = append(result, port)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portInput)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if port <= 0 || port > 65535 {
|
||||
continue
|
||||
}
|
||||
if _, ok := portSet[port]; ok {
|
||||
continue
|
||||
}
|
||||
portSet[port] = struct{}{}
|
||||
result = append(result, port)
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return append([]int{}, defaultPorts...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func SanitizePaths(paths []string, defaultPaths []string) []string {
|
||||
if len(paths) == 0 {
|
||||
return append([]string{}, defaultPaths...)
|
||||
}
|
||||
|
||||
pathSet := map[string]struct{}{}
|
||||
result := make([]string, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
if _, ok := pathSet[path]; ok {
|
||||
continue
|
||||
}
|
||||
pathSet[path] = struct{}{}
|
||||
result = append(result, path)
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return append([]string{}, defaultPaths...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ParseScanTargets(targets []string, maxHosts int) ([]net.IP, error) {
|
||||
hostSet := map[uint32]struct{}{}
|
||||
hosts := make([]net.IP, 0)
|
||||
|
||||
addHost := func(ipv4 net.IP) error {
|
||||
value := binary.BigEndian.Uint32(ipv4)
|
||||
if _, ok := hostSet[value]; ok {
|
||||
return nil
|
||||
}
|
||||
if len(hosts) >= maxHosts {
|
||||
return fmt.Errorf("scan targets exceed max %d hosts", maxHosts)
|
||||
}
|
||||
hostSet[value] = struct{}{}
|
||||
host := make(net.IP, net.IPv4len)
|
||||
copy(host, ipv4)
|
||||
hosts = append(hosts, host)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
target = strings.TrimSpace(target)
|
||||
if target == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if ip := net.ParseIP(target); ip != nil {
|
||||
ipv4 := ip.To4()
|
||||
if ipv4 == nil {
|
||||
return nil, fmt.Errorf("only IPv4 is supported: %s", target)
|
||||
}
|
||||
if !util.IsIntranetIp(ipv4.String()) {
|
||||
return nil, fmt.Errorf("target must be intranet: %s", target)
|
||||
}
|
||||
if err := addHost(ipv4); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
cidrHosts, err := ParseCIDRHosts(target, maxHosts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, host := range cidrHosts {
|
||||
if !util.IsIntranetIp(host.String()) {
|
||||
return nil, fmt.Errorf("target must be intranet: %s", target)
|
||||
}
|
||||
if err = addHost(host.To4()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(hosts) == 0 {
|
||||
return nil, fmt.Errorf("cidr is required")
|
||||
}
|
||||
|
||||
return hosts, nil
|
||||
}
|
||||
|
||||
func ParseCIDRHosts(cidr string, maxHosts int) ([]net.IP, error) {
|
||||
baseIp, ipNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ipv4 := baseIp.To4()
|
||||
if ipv4 == nil {
|
||||
return nil, fmt.Errorf("only IPv4 CIDR is supported")
|
||||
}
|
||||
if !util.IsIntranetIp(ipv4.String()) {
|
||||
return nil, fmt.Errorf("cidr must be intranet: %s", cidr)
|
||||
}
|
||||
|
||||
ones, bits := ipNet.Mask.Size()
|
||||
hostBits := bits - ones
|
||||
if hostBits < 0 {
|
||||
return nil, fmt.Errorf("invalid cidr mask: %s", cidr)
|
||||
}
|
||||
|
||||
if hostBits >= 63 {
|
||||
return nil, fmt.Errorf("cidr range is too large")
|
||||
}
|
||||
total := uint64(1) << hostBits
|
||||
if total > uint64(maxHosts)+2 {
|
||||
return nil, fmt.Errorf("cidr range is too large, max %d hosts", maxHosts)
|
||||
}
|
||||
|
||||
totalInt := int(total)
|
||||
start := binary.BigEndian.Uint32(ipv4.Mask(ipNet.Mask))
|
||||
end := start + uint32(total) - 1
|
||||
hosts := make([]net.IP, 0, totalInt)
|
||||
for value := start; value <= end; value++ {
|
||||
if total > 2 && (value == start || value == end) {
|
||||
continue
|
||||
}
|
||||
|
||||
candidate := make(net.IP, net.IPv4len)
|
||||
binary.BigEndian.PutUint32(candidate, value)
|
||||
if ipNet.Contains(candidate) {
|
||||
hosts = append(hosts, candidate)
|
||||
}
|
||||
}
|
||||
|
||||
if len(hosts) == 0 {
|
||||
return nil, fmt.Errorf("cidr has no usable hosts: %s", cidr)
|
||||
}
|
||||
|
||||
return hosts, nil
|
||||
}
|
||||
|
||||
func ProbeHost(ctx context.Context, client *http.Client, scheme, host string, ports []int, paths []string, timeout time.Duration) (bool, []*InnerMcpServer) {
|
||||
if !util.IsIntranetIp(host) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{Timeout: timeout}
|
||||
isOnline := false
|
||||
var servers []*InnerMcpServer
|
||||
|
||||
for _, port := range ports {
|
||||
address := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
conn, err := dialer.DialContext(ctx, "tcp", address)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_ = conn.Close()
|
||||
isOnline = true
|
||||
|
||||
for _, path := range paths {
|
||||
server, ok := probeMcpInitialize(ctx, client, scheme, host, port, path)
|
||||
if ok {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isOnline, servers
|
||||
}
|
||||
|
||||
func probeMcpInitialize(ctx context.Context, client *http.Client, scheme, host string, port int, path string) (*InnerMcpServer, bool) {
|
||||
fullUrl := fmt.Sprintf("%s://%s%s", scheme, net.JoinHostPort(host, strconv.Itoa(port)), path)
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, fullUrl, nil)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return &InnerMcpServer{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Path: path,
|
||||
Url: fullUrl,
|
||||
}, true
|
||||
}
|
||||
|
||||
118
object/agent.go
118
object/agent.go
@@ -1,118 +0,0 @@
|
||||
// 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 Agent 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"`
|
||||
|
||||
Url string `xorm:"varchar(500)" json:"url"`
|
||||
Token string `xorm:"varchar(500)" json:"token"`
|
||||
Application string `xorm:"varchar(100)" json:"application"`
|
||||
}
|
||||
|
||||
func GetAgents(owner string) ([]*Agent, error) {
|
||||
agents := []*Agent{}
|
||||
err := ormer.Engine.Desc("created_time").Find(&agents, &Agent{Owner: owner})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return agents, nil
|
||||
}
|
||||
|
||||
func getAgent(owner string, name string) (*Agent, error) {
|
||||
agent := Agent{Owner: owner, Name: name}
|
||||
existed, err := ormer.Engine.Get(&agent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existed {
|
||||
return &agent, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func GetAgent(id string) (*Agent, error) {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
return getAgent(owner, name)
|
||||
}
|
||||
|
||||
func UpdateAgent(id string, agent *Agent) (bool, error) {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
if a, err := getAgent(owner, name); err != nil {
|
||||
return false, err
|
||||
} else if a == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
agent.UpdatedTime = util.GetCurrentTime()
|
||||
|
||||
_, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(agent)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func AddAgent(agent *Agent) (bool, error) {
|
||||
affected, err := ormer.Engine.Insert(agent)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func DeleteAgent(agent *Agent) (bool, error) {
|
||||
affected, err := ormer.Engine.ID(core.PK{agent.Owner, agent.Name}).Delete(&Agent{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func (agent *Agent) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", agent.Owner, agent.Name)
|
||||
}
|
||||
|
||||
func GetAgentCount(owner, field, value string) (int64, error) {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
return session.Count(&Agent{})
|
||||
}
|
||||
|
||||
func GetPaginationAgents(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Agent, error) {
|
||||
agents := []*Agent{}
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Find(&agents)
|
||||
if err != nil {
|
||||
return agents, err
|
||||
}
|
||||
|
||||
return agents, nil
|
||||
}
|
||||
@@ -17,6 +17,8 @@ package object
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@@ -94,7 +96,6 @@ type Application struct {
|
||||
HeaderHtml string `xorm:"mediumtext" json:"headerHtml"`
|
||||
EnablePassword bool `json:"enablePassword"`
|
||||
EnableSignUp bool `json:"enableSignUp"`
|
||||
EnableGuestSignin bool `json:"enableGuestSignin"`
|
||||
DisableSignin bool `json:"disableSignin"`
|
||||
EnableSigninSession bool `json:"enableSigninSession"`
|
||||
EnableAutoSignin bool `json:"enableAutoSignin"`
|
||||
@@ -220,6 +221,192 @@ func GetPaginationOrganizationApplications(owner, organization string, offset, l
|
||||
return applications, nil
|
||||
}
|
||||
|
||||
func getProviderMap(owner string) (m map[string]*Provider, err error) {
|
||||
providers, err := GetProviders(owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m = map[string]*Provider{}
|
||||
for _, provider := range providers {
|
||||
m[provider.Name] = GetMaskedProvider(provider, true)
|
||||
}
|
||||
|
||||
return m, err
|
||||
}
|
||||
|
||||
func extendApplicationWithProviders(application *Application) (err error) {
|
||||
m, err := getProviderMap(application.Organization)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, providerItem := range application.Providers {
|
||||
if provider, ok := m[providerItem.Name]; ok {
|
||||
providerItem.Provider = provider
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func extendApplicationWithOrg(application *Application) (err error) {
|
||||
organization, err := getOrganization(application.Owner, application.Organization)
|
||||
application.OrganizationObj = organization
|
||||
return
|
||||
}
|
||||
|
||||
func extendApplicationWithSigninItems(application *Application) (err error) {
|
||||
if len(application.SigninItems) == 0 {
|
||||
signinItem := &SigninItem{
|
||||
Name: "Back button",
|
||||
Visible: true,
|
||||
CustomCss: ".back-button {\n top: 65px;\n left: 15px;\n position: absolute;\n}\n.back-inner-button{}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Languages",
|
||||
Visible: true,
|
||||
CustomCss: ".login-languages {\n top: 55px;\n right: 5px;\n position: absolute;\n}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Logo",
|
||||
Visible: true,
|
||||
CustomCss: ".login-logo-box {}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Signin methods",
|
||||
Visible: true,
|
||||
CustomCss: ".signin-methods {}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Username",
|
||||
Visible: true,
|
||||
CustomCss: ".login-username {}\n.login-username-input{}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Password",
|
||||
Visible: true,
|
||||
CustomCss: ".login-password {}\n.login-password-input{}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Verification code",
|
||||
Visible: true,
|
||||
CustomCss: ".verification-code {}\n.verification-code-input{}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Agreement",
|
||||
Visible: true,
|
||||
CustomCss: ".login-agreement {}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Forgot password?",
|
||||
Visible: true,
|
||||
CustomCss: ".login-forget-password {\n display: inline-flex;\n justify-content: space-between;\n width: 320px;\n margin-bottom: 25px;\n}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Login button",
|
||||
Visible: true,
|
||||
CustomCss: ".login-button-box {\n margin-bottom: 5px;\n}\n.login-button {\n width: 100%;\n}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Signup link",
|
||||
Visible: true,
|
||||
CustomCss: ".login-signup-link {\n margin-bottom: 24px;\n display: flex;\n justify-content: end;\n}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Providers",
|
||||
Visible: true,
|
||||
CustomCss: ".provider-img {\n width: 30px;\n margin: 5px;\n}\n.provider-big-img {\n margin-bottom: 10px;\n}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
}
|
||||
for idx, item := range application.SigninItems {
|
||||
if item.Label != "" && item.CustomCss == "" {
|
||||
application.SigninItems[idx].CustomCss = item.Label
|
||||
application.SigninItems[idx].Label = ""
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func extendApplicationWithSigninMethods(application *Application) (err error) {
|
||||
if len(application.SigninMethods) == 0 {
|
||||
if application.EnablePassword {
|
||||
signinMethod := &SigninMethod{Name: "Password", DisplayName: "Password", Rule: "All"}
|
||||
application.SigninMethods = append(application.SigninMethods, signinMethod)
|
||||
}
|
||||
if application.EnableCodeSignin {
|
||||
signinMethod := &SigninMethod{Name: "Verification code", DisplayName: "Verification code", Rule: "All"}
|
||||
application.SigninMethods = append(application.SigninMethods, signinMethod)
|
||||
}
|
||||
if application.EnableWebAuthn {
|
||||
signinMethod := &SigninMethod{Name: "WebAuthn", DisplayName: "WebAuthn", Rule: "None"}
|
||||
application.SigninMethods = append(application.SigninMethods, signinMethod)
|
||||
}
|
||||
|
||||
signinMethod := &SigninMethod{Name: "Face ID", DisplayName: "Face ID", Rule: "None"}
|
||||
application.SigninMethods = append(application.SigninMethods, signinMethod)
|
||||
}
|
||||
|
||||
if len(application.SigninMethods) == 0 {
|
||||
signinMethod := &SigninMethod{Name: "Password", DisplayName: "Password", Rule: "All"}
|
||||
application.SigninMethods = append(application.SigninMethods, signinMethod)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func extendApplicationWithSignupItems(application *Application) (err error) {
|
||||
if len(application.SignupItems) == 0 {
|
||||
application.SignupItems = []*SignupItem{
|
||||
{Name: "ID", Visible: false, Required: true, Prompted: false, Rule: "Random"},
|
||||
{Name: "Username", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
{Name: "Display name", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
{Name: "Password", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
{Name: "Confirm password", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
{Name: "Email", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
{Name: "Phone", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
{Name: "Agreement", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getApplication(owner string, name string) (*Application, error) {
|
||||
if owner == "" || name == "" {
|
||||
return nil, nil
|
||||
@@ -372,6 +559,155 @@ func GetApplication(id string) (*Application, error) {
|
||||
return getApplication(owner, name)
|
||||
}
|
||||
|
||||
func GetMaskedApplication(application *Application, userId string) *Application {
|
||||
if application == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if application.TokenFields == nil {
|
||||
application.TokenFields = []string{}
|
||||
}
|
||||
|
||||
if application.FailedSigninLimit == 0 {
|
||||
application.FailedSigninLimit = DefaultFailedSigninLimit
|
||||
}
|
||||
if application.FailedSigninFrozenTime == 0 {
|
||||
application.FailedSigninFrozenTime = DefaultFailedSigninFrozenTime
|
||||
}
|
||||
|
||||
isOrgUser := false
|
||||
if userId != "" {
|
||||
if isUserIdGlobalAdmin(userId) {
|
||||
return application
|
||||
}
|
||||
|
||||
user, err := GetUser(userId)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if user != nil {
|
||||
if user.IsApplicationAdmin(application) {
|
||||
return application
|
||||
}
|
||||
|
||||
if user.Owner == application.Organization {
|
||||
isOrgUser = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
application.ClientSecret = "***"
|
||||
application.Cert = "***"
|
||||
application.EnablePassword = false
|
||||
application.EnableSigninSession = false
|
||||
application.EnableCodeSignin = false
|
||||
application.EnableSamlCompress = false
|
||||
application.EnableSamlC14n10 = false
|
||||
application.EnableSamlPostBinding = false
|
||||
application.DisableSamlAttributes = false
|
||||
application.EnableWebAuthn = false
|
||||
application.EnableLinkWithEmail = false
|
||||
application.SamlReplyUrl = "***"
|
||||
|
||||
providerItems := []*ProviderItem{}
|
||||
for _, providerItem := range application.Providers {
|
||||
if providerItem.Provider != nil && (providerItem.Provider.Category == "OAuth" || providerItem.Provider.Category == "Web3" || providerItem.Provider.Category == "Captcha" || providerItem.Provider.Category == "SAML" || providerItem.Provider.Category == "Face ID") {
|
||||
providerItems = append(providerItems, providerItem)
|
||||
}
|
||||
}
|
||||
application.Providers = providerItems
|
||||
|
||||
application.GrantTypes = []string{}
|
||||
application.RedirectUris = []string{}
|
||||
application.TokenFormat = "***"
|
||||
application.TokenFields = []string{}
|
||||
application.ExpireInHours = -1
|
||||
application.RefreshExpireInHours = -1
|
||||
application.FailedSigninLimit = -1
|
||||
application.FailedSigninFrozenTime = -1
|
||||
|
||||
if application.OrganizationObj != nil {
|
||||
application.OrganizationObj.MasterPassword = "***"
|
||||
application.OrganizationObj.DefaultPassword = "***"
|
||||
application.OrganizationObj.MasterVerificationCode = "***"
|
||||
application.OrganizationObj.PasswordType = "***"
|
||||
application.OrganizationObj.PasswordSalt = "***"
|
||||
application.OrganizationObj.InitScore = -1
|
||||
application.OrganizationObj.EnableSoftDeletion = false
|
||||
|
||||
if !isOrgUser {
|
||||
application.OrganizationObj.MfaItems = nil
|
||||
if !application.OrganizationObj.IsProfilePublic {
|
||||
application.OrganizationObj.AccountItems = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return application
|
||||
}
|
||||
|
||||
func GetMaskedApplications(applications []*Application, userId string) []*Application {
|
||||
if isUserIdGlobalAdmin(userId) {
|
||||
return applications
|
||||
}
|
||||
|
||||
for _, application := range applications {
|
||||
application = GetMaskedApplication(application, userId)
|
||||
}
|
||||
return applications
|
||||
}
|
||||
|
||||
func GetAllowedApplications(applications []*Application, userId string, lang string) ([]*Application, error) {
|
||||
if userId == "" {
|
||||
return nil, errors.New(i18n.Translate(lang, "auth:Unauthorized operation"))
|
||||
}
|
||||
|
||||
if isUserIdGlobalAdmin(userId) {
|
||||
return applications, nil
|
||||
}
|
||||
|
||||
user, err := GetUser(userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, errors.New(i18n.Translate(lang, "auth:Unauthorized operation"))
|
||||
}
|
||||
|
||||
if user.IsAdmin {
|
||||
return applications, nil
|
||||
}
|
||||
|
||||
res := []*Application{}
|
||||
for _, application := range applications {
|
||||
var allowed bool
|
||||
allowed, err = CheckLoginPermission(userId, application)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if allowed {
|
||||
res = append(res, application)
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func checkMultipleCaptchaProviders(application *Application, lang string) error {
|
||||
var captchaProviders []string
|
||||
for _, providerItem := range application.Providers {
|
||||
if providerItem.Provider != nil && providerItem.Provider.Category == "Captcha" {
|
||||
captchaProviders = append(captchaProviders, providerItem.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(captchaProviders) > 1 {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:Multiple captcha providers are not allowed in the same application: %s"), strings.Join(captchaProviders, ", "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateApplication(id string, application *Application, isGlobalAdmin bool, lang string) (bool, error) {
|
||||
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
@@ -508,3 +844,205 @@ func DeleteApplication(application *Application) (bool, error) {
|
||||
|
||||
return deleteApplication(application)
|
||||
}
|
||||
|
||||
func (application *Application) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", application.Owner, application.Name)
|
||||
}
|
||||
|
||||
func (application *Application) IsRedirectUriValid(redirectUri string) bool {
|
||||
isValid, err := util.IsValidOrigin(redirectUri)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if isValid {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, targetUri := range application.RedirectUris {
|
||||
if targetUri == "" {
|
||||
continue
|
||||
}
|
||||
targetUriRegex := regexp.MustCompile(targetUri)
|
||||
if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (application *Application) IsPasswordEnabled() bool {
|
||||
if len(application.SigninMethods) == 0 {
|
||||
return application.EnablePassword
|
||||
} else {
|
||||
for _, signinMethod := range application.SigninMethods {
|
||||
if signinMethod.Name == "Password" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (application *Application) IsPasswordWithLdapEnabled() bool {
|
||||
if len(application.SigninMethods) == 0 {
|
||||
return application.EnablePassword
|
||||
} else {
|
||||
for _, signinMethod := range application.SigninMethods {
|
||||
if signinMethod.Name == "Password" && signinMethod.Rule == "All" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (application *Application) IsCodeSigninViaEmailEnabled() bool {
|
||||
if len(application.SigninMethods) == 0 {
|
||||
return application.EnableCodeSignin
|
||||
} else {
|
||||
for _, signinMethod := range application.SigninMethods {
|
||||
if signinMethod.Name == "Verification code" && signinMethod.Rule != "Phone only" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (application *Application) IsCodeSigninViaSmsEnabled() bool {
|
||||
if len(application.SigninMethods) == 0 {
|
||||
return application.EnableCodeSignin
|
||||
} else {
|
||||
for _, signinMethod := range application.SigninMethods {
|
||||
if signinMethod.Name == "Verification code" && signinMethod.Rule != "Email only" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (application *Application) IsLdapEnabled() bool {
|
||||
if len(application.SigninMethods) > 0 {
|
||||
for _, signinMethod := range application.SigninMethods {
|
||||
if signinMethod.Name == "LDAP" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (application *Application) IsFaceIdEnabled() bool {
|
||||
if len(application.SigninMethods) > 0 {
|
||||
for _, signinMethod := range application.SigninMethods {
|
||||
if signinMethod.Name == "Face ID" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsOriginAllowed(origin string) (bool, error) {
|
||||
applications, err := GetApplications("")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, application := range applications {
|
||||
if application.IsRedirectUriValid(origin) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func getApplicationMap(organization string) (map[string]*Application, error) {
|
||||
applicationMap := make(map[string]*Application)
|
||||
applications, err := GetOrganizationApplications("admin", organization)
|
||||
if err != nil {
|
||||
return applicationMap, err
|
||||
}
|
||||
|
||||
for _, application := range applications {
|
||||
applicationMap[application.Name] = application
|
||||
}
|
||||
|
||||
return applicationMap, nil
|
||||
}
|
||||
|
||||
func ExtendManagedAccountsWithUser(user *User) (*User, error) {
|
||||
if user.ManagedAccounts == nil || len(user.ManagedAccounts) == 0 {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
applicationMap, err := getApplicationMap(user.Owner)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
var managedAccounts []ManagedAccount
|
||||
for _, managedAccount := range user.ManagedAccounts {
|
||||
application := applicationMap[managedAccount.Application]
|
||||
if application != nil {
|
||||
managedAccount.SigninUrl = application.SigninUrl
|
||||
managedAccounts = append(managedAccounts, managedAccount)
|
||||
}
|
||||
}
|
||||
user.ManagedAccounts = managedAccounts
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func applicationChangeTrigger(oldName string, newName string) error {
|
||||
session := ormer.Engine.NewSession()
|
||||
defer session.Close()
|
||||
|
||||
err := session.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
organization := new(Organization)
|
||||
organization.DefaultApplication = newName
|
||||
_, err = session.Where("default_application=?", oldName).Update(organization)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := new(User)
|
||||
user.SignupApplication = newName
|
||||
_, err = session.Where("signup_application=?", oldName).Update(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resource := new(Resource)
|
||||
resource.Application = newName
|
||||
_, err = session.Where("application=?", oldName).Update(resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var permissions []*Permission
|
||||
err = ormer.Engine.Find(&permissions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 0; i < len(permissions); i++ {
|
||||
permissionResoureces := permissions[i].Resources
|
||||
for j := 0; j < len(permissionResoureces); j++ {
|
||||
if permissionResoureces[j] == oldName {
|
||||
permissionResoureces[j] = newName
|
||||
}
|
||||
}
|
||||
permissions[i].Resources = permissionResoureces
|
||||
_, err = session.Where("owner=?", permissions[i].Owner).Where("name=?", permissions[i].Name).Update(permissions[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return session.Commit()
|
||||
}
|
||||
|
||||
@@ -1,615 +0,0 @@
|
||||
// Copyright 2021 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func getProviderMap(owner string) (m map[string]*Provider, err error) {
|
||||
providers, err := GetProviders(owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m = map[string]*Provider{}
|
||||
for _, provider := range providers {
|
||||
m[provider.Name] = GetMaskedProvider(provider, true)
|
||||
}
|
||||
|
||||
return m, err
|
||||
}
|
||||
|
||||
func extendApplicationWithProviders(application *Application) (err error) {
|
||||
m, err := getProviderMap(application.Organization)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, providerItem := range application.Providers {
|
||||
if provider, ok := m[providerItem.Name]; ok {
|
||||
providerItem.Provider = provider
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func extendApplicationWithOrg(application *Application) (err error) {
|
||||
organization, err := getOrganization(application.Owner, application.Organization)
|
||||
application.OrganizationObj = organization
|
||||
return
|
||||
}
|
||||
|
||||
func extendApplicationWithSigninItems(application *Application) (err error) {
|
||||
if len(application.SigninItems) == 0 {
|
||||
signinItem := &SigninItem{
|
||||
Name: "Back button",
|
||||
Visible: true,
|
||||
CustomCss: ".back-button {\n top: 65px;\n left: 15px;\n position: absolute;\n}\n.back-inner-button{}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Languages",
|
||||
Visible: true,
|
||||
CustomCss: ".login-languages {\n top: 55px;\n right: 5px;\n position: absolute;\n}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Logo",
|
||||
Visible: true,
|
||||
CustomCss: ".login-logo-box {}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Signin methods",
|
||||
Visible: true,
|
||||
CustomCss: ".signin-methods {}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Username",
|
||||
Visible: true,
|
||||
CustomCss: ".login-username {}\n.login-username-input{}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Password",
|
||||
Visible: true,
|
||||
CustomCss: ".login-password {}\n.login-password-input{}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Verification code",
|
||||
Visible: true,
|
||||
CustomCss: ".verification-code {}\n.verification-code-input{}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Agreement",
|
||||
Visible: true,
|
||||
CustomCss: ".login-agreement {}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Forgot password?",
|
||||
Visible: true,
|
||||
CustomCss: ".login-forget-password {\n display: inline-flex;\n justify-content: space-between;\n width: 320px;\n margin-bottom: 25px;\n}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Login button",
|
||||
Visible: true,
|
||||
CustomCss: ".login-button-box {\n margin-bottom: 5px;\n}\n.login-button {\n width: 100%;\n}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Signup link",
|
||||
Visible: true,
|
||||
CustomCss: ".login-signup-link {\n margin-bottom: 24px;\n display: flex;\n justify-content: end;\n}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Providers",
|
||||
Visible: true,
|
||||
CustomCss: ".provider-img {\n width: 30px;\n margin: 5px;\n}\n.provider-big-img {\n margin-bottom: 10px;\n}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
}
|
||||
for idx, item := range application.SigninItems {
|
||||
if item.Label != "" && item.CustomCss == "" {
|
||||
application.SigninItems[idx].CustomCss = item.Label
|
||||
application.SigninItems[idx].Label = ""
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func extendApplicationWithSigninMethods(application *Application) (err error) {
|
||||
if len(application.SigninMethods) == 0 {
|
||||
if application.EnablePassword {
|
||||
signinMethod := &SigninMethod{Name: "Password", DisplayName: "Password", Rule: "All"}
|
||||
application.SigninMethods = append(application.SigninMethods, signinMethod)
|
||||
}
|
||||
if application.EnableCodeSignin {
|
||||
signinMethod := &SigninMethod{Name: "Verification code", DisplayName: "Verification code", Rule: "All"}
|
||||
application.SigninMethods = append(application.SigninMethods, signinMethod)
|
||||
}
|
||||
if application.EnableWebAuthn {
|
||||
signinMethod := &SigninMethod{Name: "WebAuthn", DisplayName: "WebAuthn", Rule: "None"}
|
||||
application.SigninMethods = append(application.SigninMethods, signinMethod)
|
||||
}
|
||||
|
||||
signinMethod := &SigninMethod{Name: "Face ID", DisplayName: "Face ID", Rule: "None"}
|
||||
application.SigninMethods = append(application.SigninMethods, signinMethod)
|
||||
}
|
||||
|
||||
if len(application.SigninMethods) == 0 {
|
||||
signinMethod := &SigninMethod{Name: "Password", DisplayName: "Password", Rule: "All"}
|
||||
application.SigninMethods = append(application.SigninMethods, signinMethod)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func extendApplicationWithSignupItems(application *Application) (err error) {
|
||||
if len(application.SignupItems) == 0 {
|
||||
application.SignupItems = []*SignupItem{
|
||||
{Name: "ID", Visible: false, Required: true, Prompted: false, Rule: "Random"},
|
||||
{Name: "Username", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
{Name: "Display name", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
{Name: "Password", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
{Name: "Confirm password", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
{Name: "Email", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
{Name: "Phone", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
{Name: "Agreement", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetMaskedApplication(application *Application, userId string) *Application {
|
||||
if application == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if application.TokenFields == nil {
|
||||
application.TokenFields = []string{}
|
||||
}
|
||||
|
||||
if application.FailedSigninLimit == 0 {
|
||||
application.FailedSigninLimit = DefaultFailedSigninLimit
|
||||
}
|
||||
if application.FailedSigninFrozenTime == 0 {
|
||||
application.FailedSigninFrozenTime = DefaultFailedSigninFrozenTime
|
||||
}
|
||||
|
||||
isOrgUser := false
|
||||
if userId != "" {
|
||||
if isUserIdGlobalAdmin(userId) {
|
||||
return application
|
||||
}
|
||||
|
||||
user, err := GetUser(userId)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if user != nil {
|
||||
if user.IsApplicationAdmin(application) {
|
||||
return application
|
||||
}
|
||||
|
||||
if user.Owner == application.Organization {
|
||||
isOrgUser = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
application.ClientSecret = "***"
|
||||
application.Cert = "***"
|
||||
application.EnablePassword = false
|
||||
application.EnableSigninSession = false
|
||||
application.EnableCodeSignin = false
|
||||
application.EnableSamlCompress = false
|
||||
application.EnableSamlC14n10 = false
|
||||
application.EnableSamlPostBinding = false
|
||||
application.DisableSamlAttributes = false
|
||||
application.EnableWebAuthn = false
|
||||
application.EnableLinkWithEmail = false
|
||||
application.SamlReplyUrl = "***"
|
||||
|
||||
providerItems := []*ProviderItem{}
|
||||
for _, providerItem := range application.Providers {
|
||||
if providerItem.Provider != nil && (providerItem.Provider.Category == "OAuth" || providerItem.Provider.Category == "Web3" || providerItem.Provider.Category == "Captcha" || providerItem.Provider.Category == "SAML" || providerItem.Provider.Category == "Face ID") {
|
||||
providerItems = append(providerItems, providerItem)
|
||||
}
|
||||
}
|
||||
application.Providers = providerItems
|
||||
|
||||
application.GrantTypes = []string{}
|
||||
application.RedirectUris = []string{}
|
||||
application.TokenFormat = "***"
|
||||
application.TokenFields = []string{}
|
||||
application.ExpireInHours = -1
|
||||
application.RefreshExpireInHours = -1
|
||||
application.FailedSigninLimit = -1
|
||||
application.FailedSigninFrozenTime = -1
|
||||
|
||||
if application.OrganizationObj != nil {
|
||||
application.OrganizationObj.MasterPassword = "***"
|
||||
application.OrganizationObj.DefaultPassword = "***"
|
||||
application.OrganizationObj.MasterVerificationCode = "***"
|
||||
application.OrganizationObj.PasswordType = "***"
|
||||
application.OrganizationObj.PasswordSalt = "***"
|
||||
application.OrganizationObj.InitScore = -1
|
||||
application.OrganizationObj.EnableSoftDeletion = false
|
||||
|
||||
if !isOrgUser {
|
||||
application.OrganizationObj.MfaItems = nil
|
||||
if !application.OrganizationObj.IsProfilePublic {
|
||||
application.OrganizationObj.AccountItems = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return application
|
||||
}
|
||||
|
||||
func GetMaskedApplications(applications []*Application, userId string) []*Application {
|
||||
if isUserIdGlobalAdmin(userId) {
|
||||
return applications
|
||||
}
|
||||
|
||||
for _, application := range applications {
|
||||
application = GetMaskedApplication(application, userId)
|
||||
}
|
||||
return applications
|
||||
}
|
||||
|
||||
func GetAllowedApplications(applications []*Application, userId string, lang string) ([]*Application, error) {
|
||||
if userId == "" {
|
||||
return nil, errors.New(i18n.Translate(lang, "auth:Unauthorized operation"))
|
||||
}
|
||||
|
||||
if isUserIdGlobalAdmin(userId) {
|
||||
return applications, nil
|
||||
}
|
||||
|
||||
user, err := GetUser(userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, errors.New(i18n.Translate(lang, "auth:Unauthorized operation"))
|
||||
}
|
||||
|
||||
if user.IsAdmin {
|
||||
return applications, nil
|
||||
}
|
||||
|
||||
res := []*Application{}
|
||||
for _, application := range applications {
|
||||
var allowed bool
|
||||
allowed, err = CheckLoginPermission(userId, application)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if allowed {
|
||||
res = append(res, application)
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func checkMultipleCaptchaProviders(application *Application, lang string) error {
|
||||
var captchaProviders []string
|
||||
for _, providerItem := range application.Providers {
|
||||
if providerItem.Provider != nil && providerItem.Provider.Category == "Captcha" {
|
||||
captchaProviders = append(captchaProviders, providerItem.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(captchaProviders) > 1 {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:Multiple captcha providers are not allowed in the same application: %s"), strings.Join(captchaProviders, ", "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (application *Application) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", application.Owner, application.Name)
|
||||
}
|
||||
|
||||
func (application *Application) IsRedirectUriValid(redirectUri string) bool {
|
||||
isValid, err := util.IsValidOrigin(redirectUri)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if isValid {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, targetUri := range application.RedirectUris {
|
||||
if redirectUriMatchesPattern(redirectUri, targetUri) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func redirectUriMatchesPattern(redirectUri, targetUri string) bool {
|
||||
if targetUri == "" {
|
||||
return false
|
||||
}
|
||||
if redirectUri == targetUri {
|
||||
return true
|
||||
}
|
||||
|
||||
redirectUriObj, err := url.Parse(redirectUri)
|
||||
if err != nil || redirectUriObj.Host == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
targetUriObj, err := url.Parse(targetUri)
|
||||
if err == nil && targetUriObj.Host != "" {
|
||||
return redirectUriMatchesTarget(redirectUriObj, targetUriObj)
|
||||
}
|
||||
|
||||
withScheme, parseErr := url.Parse("https://" + targetUri)
|
||||
if parseErr == nil && withScheme.Host != "" {
|
||||
redirectHost := redirectUriObj.Hostname()
|
||||
targetHost := withScheme.Hostname()
|
||||
var hostMatches bool
|
||||
if strings.HasPrefix(targetHost, ".") {
|
||||
hostMatches = strings.HasSuffix(redirectHost, targetHost)
|
||||
} else {
|
||||
hostMatches = redirectHost == targetHost || strings.HasSuffix(redirectHost, "."+targetHost)
|
||||
}
|
||||
schemeOk := redirectUriObj.Scheme == "http" || redirectUriObj.Scheme == "https"
|
||||
pathMatches := withScheme.Path == "" || withScheme.Path == "/" || redirectUriObj.Path == withScheme.Path
|
||||
return schemeOk && hostMatches && pathMatches
|
||||
}
|
||||
|
||||
anchoredPattern := "^(?:" + targetUri + ")$"
|
||||
targetUriRegex, err := regexp.Compile(anchoredPattern)
|
||||
return err == nil && targetUriRegex.MatchString(redirectUri)
|
||||
}
|
||||
|
||||
func redirectUriMatchesTarget(redirectUri, targetUri *url.URL) bool {
|
||||
if redirectUri.Scheme != targetUri.Scheme {
|
||||
return false
|
||||
}
|
||||
if redirectUri.Port() != targetUri.Port() {
|
||||
return false
|
||||
}
|
||||
redirectHost := redirectUri.Hostname()
|
||||
targetHost := targetUri.Hostname()
|
||||
if redirectHost != targetHost && !strings.HasSuffix(redirectHost, "."+targetHost) {
|
||||
return false
|
||||
}
|
||||
if redirectUri.Path != targetUri.Path {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (application *Application) IsPasswordEnabled() bool {
|
||||
if len(application.SigninMethods) == 0 {
|
||||
return application.EnablePassword
|
||||
} else {
|
||||
for _, signinMethod := range application.SigninMethods {
|
||||
if signinMethod.Name == "Password" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (application *Application) IsPasswordWithLdapEnabled() bool {
|
||||
if len(application.SigninMethods) == 0 {
|
||||
return application.EnablePassword
|
||||
} else {
|
||||
for _, signinMethod := range application.SigninMethods {
|
||||
if signinMethod.Name == "Password" && signinMethod.Rule == "All" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (application *Application) IsCodeSigninViaEmailEnabled() bool {
|
||||
if len(application.SigninMethods) == 0 {
|
||||
return application.EnableCodeSignin
|
||||
} else {
|
||||
for _, signinMethod := range application.SigninMethods {
|
||||
if signinMethod.Name == "Verification code" && signinMethod.Rule != "Phone only" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (application *Application) IsCodeSigninViaSmsEnabled() bool {
|
||||
if len(application.SigninMethods) == 0 {
|
||||
return application.EnableCodeSignin
|
||||
} else {
|
||||
for _, signinMethod := range application.SigninMethods {
|
||||
if signinMethod.Name == "Verification code" && signinMethod.Rule != "Email only" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (application *Application) IsLdapEnabled() bool {
|
||||
if len(application.SigninMethods) > 0 {
|
||||
for _, signinMethod := range application.SigninMethods {
|
||||
if signinMethod.Name == "LDAP" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (application *Application) IsFaceIdEnabled() bool {
|
||||
if len(application.SigninMethods) > 0 {
|
||||
for _, signinMethod := range application.SigninMethods {
|
||||
if signinMethod.Name == "Face ID" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsOriginAllowed(origin string) (bool, error) {
|
||||
applications, err := GetApplications("")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, application := range applications {
|
||||
if application.IsRedirectUriValid(origin) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func getApplicationMap(organization string) (map[string]*Application, error) {
|
||||
applicationMap := make(map[string]*Application)
|
||||
applications, err := GetOrganizationApplications("admin", organization)
|
||||
if err != nil {
|
||||
return applicationMap, err
|
||||
}
|
||||
|
||||
for _, application := range applications {
|
||||
applicationMap[application.Name] = application
|
||||
}
|
||||
|
||||
return applicationMap, nil
|
||||
}
|
||||
|
||||
func ExtendManagedAccountsWithUser(user *User) (*User, error) {
|
||||
if user.ManagedAccounts == nil || len(user.ManagedAccounts) == 0 {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
applicationMap, err := getApplicationMap(user.Owner)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
var managedAccounts []ManagedAccount
|
||||
for _, managedAccount := range user.ManagedAccounts {
|
||||
application := applicationMap[managedAccount.Application]
|
||||
if application != nil {
|
||||
managedAccount.SigninUrl = application.SigninUrl
|
||||
managedAccounts = append(managedAccounts, managedAccount)
|
||||
}
|
||||
}
|
||||
user.ManagedAccounts = managedAccounts
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func applicationChangeTrigger(oldName string, newName string) error {
|
||||
session := ormer.Engine.NewSession()
|
||||
defer session.Close()
|
||||
|
||||
err := session.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
organization := new(Organization)
|
||||
organization.DefaultApplication = newName
|
||||
_, err = session.Where("default_application=?", oldName).Update(organization)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := new(User)
|
||||
user.SignupApplication = newName
|
||||
_, err = session.Where("signup_application=?", oldName).Update(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resource := new(Resource)
|
||||
resource.Application = newName
|
||||
_, err = session.Where("application=?", oldName).Update(resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var permissions []*Permission
|
||||
err = ormer.Engine.Find(&permissions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 0; i < len(permissions); i++ {
|
||||
permissionResoureces := permissions[i].Resources
|
||||
for j := 0; j < len(permissionResoureces); j++ {
|
||||
if permissionResoureces[j] == oldName {
|
||||
permissionResoureces[j] = newName
|
||||
}
|
||||
}
|
||||
permissions[i].Resources = permissionResoureces
|
||||
_, err = session.Where("owner=?", permissions[i].Owner).Where("name=?", permissions[i].Name).Update(permissions[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return session.Commit()
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// 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 "testing"
|
||||
|
||||
func TestRedirectUriMatchesPattern(t *testing.T) {
|
||||
tests := []struct {
|
||||
redirectUri string
|
||||
targetUri string
|
||||
want bool
|
||||
}{
|
||||
// Exact match
|
||||
{"https://login.example.com/callback", "https://login.example.com/callback", true},
|
||||
|
||||
// Full URL pattern: exact host
|
||||
{"https://login.example.com/callback", "https://login.example.com/callback", true},
|
||||
{"https://login.example.com/other", "https://login.example.com/callback", false},
|
||||
|
||||
// Full URL pattern: subdomain of configured host
|
||||
{"https://def.abc.com/callback", "abc.com", true},
|
||||
{"https://def.abc.com/callback", ".abc.com", true},
|
||||
{"https://def.abc.com/callback", ".abc.com/", true},
|
||||
{"https://deep.app.example.com/callback", "https://example.com/callback", true},
|
||||
|
||||
// Full URL pattern: unrelated host must not match
|
||||
{"https://evil.com/callback", "https://example.com/callback", false},
|
||||
// Suffix collision: evilexample.com must not match example.com
|
||||
{"https://evilexample.com/callback", "https://example.com/callback", false},
|
||||
|
||||
// Full URL pattern: scheme mismatch
|
||||
{"http://app.example.com/callback", "https://example.com/callback", false},
|
||||
|
||||
// Full URL pattern: path mismatch
|
||||
{"https://app.example.com/other", "https://example.com/callback", false},
|
||||
|
||||
// Scheme-less pattern: exact host
|
||||
{"https://login.example.com/callback", "login.example.com/callback", true},
|
||||
{"http://login.example.com/callback", "login.example.com/callback", true},
|
||||
|
||||
// Scheme-less pattern: subdomain of configured host
|
||||
{"https://app.login.example.com/callback", "login.example.com/callback", true},
|
||||
|
||||
// Scheme-less pattern: unrelated host must not match
|
||||
{"https://evil.com/callback", "login.example.com/callback", false},
|
||||
|
||||
// Scheme-less pattern: query-string injection must not match
|
||||
{"https://evil.com/?r=https://login.example.com/callback", "login.example.com/callback", false},
|
||||
{"https://evil.com/page?redirect=https://login.example.com/callback", "login.example.com/callback", false},
|
||||
|
||||
// Scheme-less pattern: path mismatch
|
||||
{"https://login.example.com/other", "login.example.com/callback", false},
|
||||
|
||||
// Scheme-less pattern: non-http scheme must not match
|
||||
{"ftp://login.example.com/callback", "login.example.com/callback", false},
|
||||
|
||||
// Empty target
|
||||
{"https://login.example.com/callback", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := redirectUriMatchesPattern(tt.redirectUri, tt.targetUri)
|
||||
if got != tt.want {
|
||||
t.Errorf("redirectUriMatchesPattern(%q, %q) = %v, want %v", tt.redirectUri, tt.targetUri, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
122
object/entry.go
122
object/entry.go
@@ -1,122 +0,0 @@
|
||||
// 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 Entry 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"`
|
||||
|
||||
Provider string `xorm:"varchar(100)" json:"provider"`
|
||||
Application string `xorm:"varchar(100)" json:"application"`
|
||||
|
||||
Type string `xorm:"varchar(100)" json:"type"`
|
||||
ClientIp string `xorm:"varchar(100)" json:"clientIp"`
|
||||
UserAgent string `xorm:"varchar(500)" json:"userAgent"`
|
||||
Message string `xorm:"mediumtext" json:"message"`
|
||||
}
|
||||
|
||||
func GetEntries(owner string) ([]*Entry, error) {
|
||||
entries := []*Entry{}
|
||||
err := ormer.Engine.Desc("created_time").Find(&entries, &Entry{Owner: owner})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func getEntry(owner string, name string) (*Entry, error) {
|
||||
entry := Entry{Owner: owner, Name: name}
|
||||
existed, err := ormer.Engine.Get(&entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existed {
|
||||
return &entry, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func GetEntry(id string) (*Entry, error) {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
return getEntry(owner, name)
|
||||
}
|
||||
|
||||
func UpdateEntry(id string, entry *Entry) (bool, error) {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
if e, err := getEntry(owner, name); err != nil {
|
||||
return false, err
|
||||
} else if e == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
entry.UpdatedTime = util.GetCurrentTime()
|
||||
|
||||
_, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(entry)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func AddEntry(entry *Entry) (bool, error) {
|
||||
affected, err := ormer.Engine.Insert(entry)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func DeleteEntry(entry *Entry) (bool, error) {
|
||||
affected, err := ormer.Engine.ID(core.PK{entry.Owner, entry.Name}).Delete(&Entry{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func (entry *Entry) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", entry.Owner, entry.Name)
|
||||
}
|
||||
|
||||
func GetEntryCount(owner, field, value string) (int64, error) {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
return session.Count(&Entry{})
|
||||
}
|
||||
|
||||
func GetPaginationEntries(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Entry, error) {
|
||||
entries := []*Entry{}
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Find(&entries)
|
||||
if err != nil {
|
||||
return entries, err
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
@@ -349,7 +349,7 @@ func GetPaginationGroupUsers(groupId string, offset, limit int, field, value, so
|
||||
session = session.And(fmt.Sprintf("%s.%s like ?", prefixedUserTable, util.CamelToSnakeCase(field)), "%"+value+"%")
|
||||
}
|
||||
|
||||
if sortField == "" || sortOrder == "" || !util.FilterField(sortField) {
|
||||
if sortField == "" || sortOrder == "" {
|
||||
sortField = "created_time"
|
||||
}
|
||||
|
||||
|
||||
@@ -200,25 +200,16 @@ func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]LdapUser, error) {
|
||||
SearchAttributes = append(SearchAttributes, attribute)
|
||||
}
|
||||
|
||||
// Some LDAP servers/configs use "{}" as a placeholder (e.g. "(uid={})").
|
||||
// Casdoor doesn't interpolate it. For listing users, interpret it as a wildcard.
|
||||
filter := strings.TrimSpace(ldapServer.Filter)
|
||||
if filter == "" {
|
||||
filter = "(objectClass=*)"
|
||||
} else if strings.Contains(filter, "{}") {
|
||||
filter = strings.ReplaceAll(filter, "{}", "*")
|
||||
}
|
||||
|
||||
searchReq := goldap.NewSearchRequest(ldapServer.BaseDn, goldap.ScopeWholeSubtree, goldap.NeverDerefAliases,
|
||||
0, 0, false,
|
||||
filter, SearchAttributes, nil)
|
||||
ldapServer.Filter, SearchAttributes, nil)
|
||||
searchResult, err := l.Conn.SearchWithPaging(searchReq, 100)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(searchResult.Entries) == 0 {
|
||||
return []LdapUser{}, nil
|
||||
return nil, errors.New("no result")
|
||||
}
|
||||
|
||||
var ldapUsers []LdapUser
|
||||
@@ -868,22 +859,13 @@ func (ldapUser *LdapUser) GetLdapUuid() string {
|
||||
}
|
||||
|
||||
func (ldap *Ldap) buildAuthFilterString(user *User) string {
|
||||
// Tolerate configs that use "{}" as a placeholder, e.g. "(uid={})".
|
||||
// Casdoor doesn't interpolate it; treat it as wildcard so the base filter remains valid.
|
||||
baseFilter := strings.TrimSpace(ldap.Filter)
|
||||
if baseFilter == "" {
|
||||
baseFilter = "(objectClass=*)"
|
||||
} else if strings.Contains(baseFilter, "{}") {
|
||||
baseFilter = strings.ReplaceAll(baseFilter, "{}", "*")
|
||||
}
|
||||
|
||||
if len(ldap.FilterFields) == 0 {
|
||||
return fmt.Sprintf("(&%s(uid=%s))", baseFilter, goldap.EscapeFilter(user.Name))
|
||||
return fmt.Sprintf("(&%s(uid=%s))", ldap.Filter, user.Name)
|
||||
}
|
||||
|
||||
filter := fmt.Sprintf("(&%s(|", baseFilter)
|
||||
filter := fmt.Sprintf("(&%s(|", ldap.Filter)
|
||||
for _, field := range ldap.FilterFields {
|
||||
filter = fmt.Sprintf("%s(%s=%s)", filter, field, goldap.EscapeFilter(user.getFieldFromLdapAttribute(field)))
|
||||
filter = fmt.Sprintf("%s(%s=%s)", filter, field, user.getFieldFromLdapAttribute(field))
|
||||
}
|
||||
filter = fmt.Sprintf("%s))", filter)
|
||||
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
// 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"
|
||||
"sync"
|
||||
|
||||
"github.com/casdoor/casdoor/log"
|
||||
)
|
||||
|
||||
var (
|
||||
runningCollectors = map[string]log.LogProvider{} // providerGetId() -> LogProvider
|
||||
runningCollectorsMu sync.Mutex
|
||||
)
|
||||
|
||||
// InitLogProviders scans all globally-configured Log providers and starts
|
||||
// background collection for pull-based providers (e.g. System Log, SELinux Log)
|
||||
// and registers passive providers (e.g. OpenClaw).
|
||||
// It is called once from main() after the database is ready.
|
||||
func InitLogProviders() {
|
||||
providers, err := GetGlobalProviders()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, p := range providers {
|
||||
if p.Category != "Log" {
|
||||
continue
|
||||
}
|
||||
switch p.Type {
|
||||
case "System Log", "SELinux Log":
|
||||
startLogCollector(p)
|
||||
case "Agent":
|
||||
if p.SubType == "OpenClaw" {
|
||||
startOpenClawProvider(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startLogCollector starts a pull-based log collector (System Log / SELinux Log)
|
||||
// for the given provider. If a collector for the same provider is already
|
||||
// running it is stopped first.
|
||||
func startLogCollector(provider *Provider) {
|
||||
runningCollectorsMu.Lock()
|
||||
defer runningCollectorsMu.Unlock()
|
||||
|
||||
id := provider.GetId()
|
||||
|
||||
if existing, ok := runningCollectors[id]; ok {
|
||||
_ = existing.Stop()
|
||||
delete(runningCollectors, id)
|
||||
}
|
||||
|
||||
lp, err := log.GetLogProvider(provider.Type, provider.Host, provider.Port, provider.Title)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
providerName := provider.Name
|
||||
addEntry := func(owner, createdTime, _ string, message string) error {
|
||||
name := log.GenerateEntryName()
|
||||
entry := &Entry{
|
||||
Owner: owner,
|
||||
Name: name,
|
||||
CreatedTime: createdTime,
|
||||
UpdatedTime: createdTime,
|
||||
DisplayName: name,
|
||||
Provider: providerName,
|
||||
Message: message,
|
||||
}
|
||||
_, err := AddEntry(entry)
|
||||
return err
|
||||
}
|
||||
|
||||
onError := func(err error) {
|
||||
fmt.Printf("InitLogProviders: collector for provider %s stopped with error: %v\n", providerName, err)
|
||||
}
|
||||
if err := lp.Start(addEntry, onError); err != nil {
|
||||
fmt.Printf("InitLogProviders: failed to start collector for provider %s: %v\n", providerName, err)
|
||||
return
|
||||
}
|
||||
|
||||
runningCollectors[id] = lp
|
||||
}
|
||||
|
||||
// startOpenClawProvider registers an OpenClaw provider in runningCollectors so
|
||||
// that incoming OTLP requests can be routed to it by IP.
|
||||
func startOpenClawProvider(provider *Provider) {
|
||||
runningCollectorsMu.Lock()
|
||||
defer runningCollectorsMu.Unlock()
|
||||
|
||||
id := provider.GetId()
|
||||
|
||||
if existing, ok := runningCollectors[id]; ok {
|
||||
_ = existing.Stop()
|
||||
delete(runningCollectors, id)
|
||||
}
|
||||
|
||||
lp, err := GetLogProviderFromProvider(provider)
|
||||
if err != nil {
|
||||
fmt.Printf("InitLogProviders: failed to create OpenClaw provider %s: %v\n", provider.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
runningCollectors[id] = lp
|
||||
}
|
||||
|
||||
// GetOpenClawProviderByIP returns the running OpenClawProvider whose Host field
|
||||
// matches clientIP, or whose Host is empty (meaning any IP is allowed).
|
||||
// Returns nil if no matching provider is registered.
|
||||
func GetOpenClawProviderByIP(clientIP string) (*log.OpenClawProvider, error) {
|
||||
providers := []*Provider{}
|
||||
err := ormer.Engine.Where("category = ? AND type = ? AND sub_type = ?", "Log", "Agent", "OpenClaw").Find(&providers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
runningCollectorsMu.Lock()
|
||||
defer runningCollectorsMu.Unlock()
|
||||
|
||||
for _, p := range providers {
|
||||
if p.Host == "" || p.Host == clientIP {
|
||||
if lp, ok := runningCollectors[p.GetId()]; ok {
|
||||
if ocp, ok := lp.(*log.OpenClawProvider); ok {
|
||||
return ocp, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/notification"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type PushMfa struct {
|
||||
@@ -111,7 +111,7 @@ func (mfa *PushMfa) sendPushNotification(title string, message string) error {
|
||||
// Generate a unique challenge ID for this notification
|
||||
// Note: In a full implementation, this would be stored in a cache/database
|
||||
// to validate callbacks from the mobile app
|
||||
mfa.challengeId = util.GenerateUUID()
|
||||
mfa.challengeId = uuid.NewString()
|
||||
mfa.challengeExp = time.Now().Add(5 * time.Minute) // Challenge expires in 5 minutes
|
||||
|
||||
// Get the notification provider
|
||||
|
||||
@@ -433,11 +433,6 @@ func (a *Ormer) createTable() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(WebhookEvent))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(VerificationRecord))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -468,21 +463,6 @@ func (a *Ormer) createTable() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Agent))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Server))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Entry))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Site))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -492,4 +472,9 @@ func (a *Ormer) createTable() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Server))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func GetSession(owner string, offset, limit int, field, value, sortField, sortOr
|
||||
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
|
||||
}
|
||||
}
|
||||
if sortField == "" || sortOrder == "" || !util.FilterField(sortField) {
|
||||
if sortField == "" || sortOrder == "" {
|
||||
sortField = "created_time"
|
||||
}
|
||||
if sortOrder == "ascend" {
|
||||
@@ -66,7 +66,7 @@ func GetSessionForUser(owner string, offset, limit int, field, value, sortField,
|
||||
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
|
||||
}
|
||||
}
|
||||
if sortField == "" || sortOrder == "" || !util.FilterField(sortField) {
|
||||
if sortField == "" || sortOrder == "" {
|
||||
sortField = "created_time"
|
||||
}
|
||||
|
||||
|
||||
@@ -257,26 +257,6 @@ func (product *Product) getProvider(providerName string) (*Provider, error) {
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func BuyProduct(id string, user *User, providerName, pricingName, planName, host, paymentEnv string, customPrice float64, lang string) (payment *Payment, attachInfo map[string]interface{}, err error) {
|
||||
owner, productName, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
order, err := PlaceOrder(owner, []ProductInfo{{
|
||||
Name: productName,
|
||||
Price: customPrice,
|
||||
Quantity: 1,
|
||||
PricingName: pricingName,
|
||||
PlanName: planName,
|
||||
}}, user)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return PayOrder(providerName, host, paymentEnv, order, lang)
|
||||
}
|
||||
|
||||
func ExtendProductWithProviders(product *Product) error {
|
||||
if product == nil {
|
||||
return nil
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/idp"
|
||||
"github.com/casdoor/casdoor/idv"
|
||||
"github.com/casdoor/casdoor/log"
|
||||
"github.com/casdoor/casdoor/pp"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
@@ -611,51 +610,3 @@ func GetIdvProviderFromProvider(provider *Provider) idv.IdvProvider {
|
||||
}
|
||||
return idv.GetIdvProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Endpoint)
|
||||
}
|
||||
|
||||
func GetLogProviderFromProvider(provider *Provider) (log.LogProvider, error) {
|
||||
if provider.Category != "Log" {
|
||||
return nil, fmt.Errorf("provider %s category is not Log", provider.Name)
|
||||
}
|
||||
|
||||
if provider.Type == "Casdoor Permission Log" {
|
||||
return log.NewPermissionLogProvider(provider.Name, func(owner, createdTime, providerName, message string) error {
|
||||
name := log.GenerateEntryName()
|
||||
entry := &Entry{
|
||||
Owner: owner,
|
||||
Name: name,
|
||||
CreatedTime: createdTime,
|
||||
UpdatedTime: createdTime,
|
||||
DisplayName: name,
|
||||
Provider: providerName,
|
||||
Application: CasdoorApplication,
|
||||
Message: message,
|
||||
}
|
||||
_, err := AddEntry(entry)
|
||||
return err
|
||||
}), nil
|
||||
}
|
||||
|
||||
if provider.Type == "Agent" && provider.SubType == "OpenClaw" {
|
||||
providerName := provider.Name
|
||||
return log.NewOpenClawProvider(providerName, func(entryType, message, clientIp, userAgent string) error {
|
||||
name := log.GenerateEntryName()
|
||||
currentTime := util.GetCurrentTime()
|
||||
entry := &Entry{
|
||||
Owner: CasdoorOrganization,
|
||||
Name: name,
|
||||
CreatedTime: currentTime,
|
||||
UpdatedTime: currentTime,
|
||||
DisplayName: name,
|
||||
Provider: providerName,
|
||||
Type: entryType,
|
||||
ClientIp: clientIp,
|
||||
UserAgent: userAgent,
|
||||
Message: message,
|
||||
}
|
||||
_, err := AddEntry(entry)
|
||||
return err
|
||||
}), nil
|
||||
}
|
||||
|
||||
return log.GetLogProvider(provider.Type, provider.Host, provider.Port, provider.Title)
|
||||
}
|
||||
|
||||
@@ -332,26 +332,27 @@ func SendWebhooks(record *Record) error {
|
||||
if webhook.IsUserExtended {
|
||||
user, err = getUser(record.Organization, record.User)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("webhook %s: failed to get user: %w", webhook.GetId(), err))
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
user, err = GetMaskedUser(user, false, err)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("webhook %s: failed to mask user: %w", webhook.GetId(), err))
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Create webhook event for tracking and retry
|
||||
_, err = CreateWebhookEventFromRecord(webhook, &record2, user)
|
||||
statusCode, respBody, err := sendWebhook(webhook, &record2, user)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("webhook %s: failed to create event: %w", webhook.GetId(), err))
|
||||
continue
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
err = addWebhookRecord(webhook, &record2, statusCode, respBody, err)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
// The webhook will be delivered by the background worker
|
||||
// This provides automatic retry and replay capability
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
|
||||
@@ -31,8 +31,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
saml "github.com/russellhaering/gosaml2"
|
||||
dsig "github.com/russellhaering/goxmldsig"
|
||||
)
|
||||
@@ -50,7 +50,7 @@ func NewSamlResponse(application *Application, user *User, host string, certific
|
||||
samlResponse.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion")
|
||||
samlResponse.CreateAttr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
samlResponse.CreateAttr("xmlns:xs", "http://www.w3.org/2001/XMLSchema")
|
||||
arId := util.GenerateUUID()
|
||||
arId := uuid.New()
|
||||
|
||||
samlResponse.CreateAttr("ID", fmt.Sprintf("_%s", arId))
|
||||
samlResponse.CreateAttr("Version", "2.0")
|
||||
@@ -65,7 +65,7 @@ func NewSamlResponse(application *Application, user *User, host string, certific
|
||||
assertion.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion")
|
||||
assertion.CreateAttr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
assertion.CreateAttr("xmlns:xs", "http://www.w3.org/2001/XMLSchema")
|
||||
assertion.CreateAttr("ID", fmt.Sprintf("_%s", util.GenerateUUID()))
|
||||
assertion.CreateAttr("ID", fmt.Sprintf("_%s", uuid.New()))
|
||||
assertion.CreateAttr("Version", "2.0")
|
||||
assertion.CreateAttr("IssueInstant", now)
|
||||
assertion.CreateElement("saml:Issuer").SetText(host)
|
||||
@@ -100,7 +100,7 @@ func NewSamlResponse(application *Application, user *User, host string, certific
|
||||
}
|
||||
authnStatement := assertion.CreateElement("saml:AuthnStatement")
|
||||
authnStatement.CreateAttr("AuthnInstant", now)
|
||||
authnStatement.CreateAttr("SessionIndex", fmt.Sprintf("_%s", util.GenerateUUID()))
|
||||
authnStatement.CreateAttr("SessionIndex", fmt.Sprintf("_%s", uuid.New()))
|
||||
authnStatement.CreateAttr("SessionNotOnOrAfter", expireTime)
|
||||
authnStatement.CreateElement("saml:AuthnContext").CreateElement("saml:AuthnContextClassRef").SetText("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport")
|
||||
|
||||
@@ -460,7 +460,7 @@ func NewSamlResponse11(application *Application, user *User, requestID string, h
|
||||
samlResponse.CreateAttr("MajorVersion", "1")
|
||||
samlResponse.CreateAttr("MinorVersion", "1")
|
||||
|
||||
responseID := util.GenerateUUID()
|
||||
responseID := uuid.New()
|
||||
samlResponse.CreateAttr("ResponseID", fmt.Sprintf("_%s", responseID))
|
||||
samlResponse.CreateAttr("InResponseTo", requestID)
|
||||
|
||||
@@ -476,7 +476,7 @@ func NewSamlResponse11(application *Application, user *User, requestID string, h
|
||||
assertion.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:1.0:assertion")
|
||||
assertion.CreateAttr("MajorVersion", "1")
|
||||
assertion.CreateAttr("MinorVersion", "1")
|
||||
assertion.CreateAttr("AssertionID", util.GenerateUUID())
|
||||
assertion.CreateAttr("AssertionID", uuid.New().String())
|
||||
assertion.CreateAttr("Issuer", host)
|
||||
assertion.CreateAttr("IssueInstant", now)
|
||||
|
||||
|
||||
@@ -21,10 +21,13 @@ import (
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/idp"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
saml2 "github.com/russellhaering/gosaml2"
|
||||
dsig "github.com/russellhaering/goxmldsig"
|
||||
)
|
||||
@@ -110,7 +113,7 @@ func GenerateSamlRequest(id, relayState, host, lang string) (auth string, method
|
||||
func buildSp(provider *Provider, samlResponse string, host string) (*saml2.SAMLServiceProvider, error) {
|
||||
_, origin := getOriginFromHost(host)
|
||||
|
||||
certStore, err := buildSpCertificateStore(provider)
|
||||
certStore, err := buildSpCertificateStore(provider, samlResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -149,10 +152,15 @@ func buildSpKeyStore() (dsig.X509KeyStore, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildSpCertificateStore(provider *Provider) (certStore dsig.MemoryX509CertificateStore, err error) {
|
||||
certEncodedData := provider.IdP
|
||||
if certEncodedData == "" {
|
||||
return dsig.MemoryX509CertificateStore{}, fmt.Errorf("the IdP certificate of provider: %s is empty", provider.Name)
|
||||
func buildSpCertificateStore(provider *Provider, samlResponse string) (certStore dsig.MemoryX509CertificateStore, err error) {
|
||||
certEncodedData := ""
|
||||
if samlResponse != "" {
|
||||
certEncodedData, err = getCertificateFromSamlResponse(samlResponse, provider.Type)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else if provider.IdP != "" {
|
||||
certEncodedData = provider.IdP
|
||||
}
|
||||
|
||||
var certData []byte
|
||||
@@ -178,3 +186,30 @@ func buildSpCertificateStore(provider *Provider) (certStore dsig.MemoryX509Certi
|
||||
}
|
||||
return certStore, nil
|
||||
}
|
||||
|
||||
func getCertificateFromSamlResponse(samlResponse string, providerType string) (string, error) {
|
||||
de, err := base64.StdEncoding.DecodeString(samlResponse)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var (
|
||||
expression string
|
||||
deStr = strings.Replace(string(de), "\n", "", -1)
|
||||
tagMap = map[string]string{
|
||||
"Aliyun IDaaS": "ds",
|
||||
"Keycloak": "dsig",
|
||||
}
|
||||
)
|
||||
tag := tagMap[providerType]
|
||||
if tag == "" {
|
||||
// <ds:X509Certificate>...</ds:X509Certificate>
|
||||
// <dsig:X509Certificate>...</dsig:X509Certificate>
|
||||
// <X509Certificate>...</X509Certificate>
|
||||
// ...
|
||||
expression = "<[^>]*:?X509Certificate>([\\s\\S]*?)<[^>]*:?X509Certificate>"
|
||||
} else {
|
||||
expression = fmt.Sprintf("<%s:X509Certificate>([\\s\\S]*?)</%s:X509Certificate>", tag, tag)
|
||||
}
|
||||
res := regexp.MustCompile(expression).FindStringSubmatch(deStr)
|
||||
return res[1], nil
|
||||
}
|
||||
|
||||
@@ -72,23 +72,16 @@ func GetServer(id string) (*Server, error) {
|
||||
|
||||
func UpdateServer(id string, server *Server) (bool, error) {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
oldServer, err := getServer(owner, name)
|
||||
if err != nil {
|
||||
if s, err := getServer(owner, name); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if oldServer == nil {
|
||||
} else if s == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if server.Token == "" {
|
||||
server.Token = oldServer.Token
|
||||
}
|
||||
|
||||
server.UpdatedTime = util.GetCurrentTime()
|
||||
|
||||
_ = syncServerTools(server)
|
||||
|
||||
_, err = ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(server)
|
||||
syncServerTools(server)
|
||||
_, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(server)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -96,66 +89,25 @@ func UpdateServer(id string, server *Server) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func SyncMcpTool(id string, server *Server, isCleared bool) (bool, error) {
|
||||
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
|
||||
|
||||
if isCleared {
|
||||
server.Tools = nil
|
||||
server.UpdatedTime = util.GetCurrentTime()
|
||||
_, err := ormer.Engine.ID(core.PK{owner, name}).Cols("tools", "updated_time").Update(server)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
oldServer, err := getServer(owner, name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if oldServer == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if server.Token == "" {
|
||||
server.Token = oldServer.Token
|
||||
}
|
||||
|
||||
server.UpdatedTime = util.GetCurrentTime()
|
||||
|
||||
err = syncServerTools(server)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
_, err = ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(server)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func syncServerTools(server *Server) error {
|
||||
oldTools := server.Tools
|
||||
if oldTools == nil {
|
||||
oldTools = []*Tool{}
|
||||
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 err
|
||||
return
|
||||
}
|
||||
|
||||
var newTools []*Tool
|
||||
for _, tool := range tools {
|
||||
oldToolIndex := slices.IndexFunc(oldTools, func(oldTool *Tool) bool {
|
||||
oldToolIndex := slices.IndexFunc(server.Tools, func(oldTool *Tool) bool {
|
||||
return oldTool.Name == tool.Name
|
||||
})
|
||||
|
||||
isAllowed := true
|
||||
if oldToolIndex != -1 {
|
||||
isAllowed = oldTools[oldToolIndex].IsAllowed
|
||||
isAllowed = server.Tools[oldToolIndex].IsAllowed
|
||||
}
|
||||
|
||||
newTool := Tool{
|
||||
@@ -166,7 +118,6 @@ func syncServerTools(server *Server) error {
|
||||
}
|
||||
|
||||
server.Tools = newTools
|
||||
return nil
|
||||
}
|
||||
|
||||
func AddServer(server *Server) (bool, error) {
|
||||
|
||||
@@ -16,7 +16,9 @@ package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
@@ -232,7 +234,8 @@ func (site *Site) GetChallengeMap() map[string]string {
|
||||
|
||||
func (site *Site) GetHost() string {
|
||||
if len(site.Hosts) != 0 {
|
||||
return site.Hosts[util.RandomIntn(len(site.Hosts))]
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
return site.Hosts[rand.Intn(len(site.Hosts))]
|
||||
}
|
||||
|
||||
if site.Host != "" {
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"encoding/pem"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -273,7 +273,7 @@ func GenerateCasToken(userId string, service string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
st := fmt.Sprintf("ST-%d", util.RandomIntn(math.MaxInt))
|
||||
st := fmt.Sprintf("ST-%d", rand.Int())
|
||||
stToServiceResponse.Store(st, &CasAuthenticationSuccessWrapper{
|
||||
AuthenticationSuccess: &authenticationSuccess,
|
||||
Service: service,
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/idp"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/google/uuid"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
|
||||
@@ -310,11 +311,11 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
|
||||
case "client_credentials": // Client Credentials Grant
|
||||
token, tokenError, err = GetClientCredentialsToken(application, clientSecret, scope, host)
|
||||
case "token", "id_token": // Implicit Grant
|
||||
token, tokenError, err = GetImplicitToken(application, username, password, scope, nonce, host)
|
||||
token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host)
|
||||
case "urn:ietf:params:oauth:grant-type:jwt-bearer":
|
||||
token, tokenError, err = GetJwtBearerToken(application, assertion, scope, nonce, host)
|
||||
case "urn:ietf:params:oauth:grant-type:device_code":
|
||||
token, tokenError, err = GetImplicitToken(application, username, password, scope, nonce, host)
|
||||
token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host)
|
||||
case "urn:ietf:params:oauth:grant-type:token-exchange": // Token Exchange Grant (RFC 8693)
|
||||
token, tokenError, err = GetTokenExchangeToken(application, clientSecret, subjectToken, subjectTokenType, audience, scope, host)
|
||||
case "refresh_token":
|
||||
@@ -741,7 +742,12 @@ func createGuestUserToken(application *Application, clientSecret string, verifie
|
||||
|
||||
// generateGuestUsername generates a unique username for guest users
|
||||
func generateGuestUsername() string {
|
||||
return fmt.Sprintf("guest_%s", util.GenerateUUID())
|
||||
uid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
// Fallback to a timestamp-based unique ID if UUID generation fails
|
||||
return fmt.Sprintf("guest_%d", time.Now().UnixNano())
|
||||
}
|
||||
return fmt.Sprintf("guest_%s", uid.String())
|
||||
}
|
||||
|
||||
// GetAuthorizationCodeToken
|
||||
@@ -756,24 +762,6 @@ func GetAuthorizationCodeToken(application *Application, clientSecret string, co
|
||||
|
||||
// Handle guest user creation
|
||||
if code == "guest-user" {
|
||||
if application.Organization == "built-in" {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "guest signin is not allowed for built-in organization",
|
||||
}, nil
|
||||
}
|
||||
if !application.EnableGuestSignin {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "guest signin is not enabled for this application",
|
||||
}, nil
|
||||
}
|
||||
if !application.EnableSignUp {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "sign up is not enabled for this application",
|
||||
}, nil
|
||||
}
|
||||
return createGuestUserToken(application, clientSecret, verifier)
|
||||
}
|
||||
|
||||
@@ -988,9 +976,9 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
|
||||
return token, nil, nil
|
||||
}
|
||||
|
||||
// mintImplicitToken mints a token for an already-authenticated user.
|
||||
// Callers must verify user identity before calling this function.
|
||||
func mintImplicitToken(application *Application, username string, scope string, nonce string, host string) (*Token, *TokenError, error) {
|
||||
// GetImplicitToken
|
||||
// Implicit flow
|
||||
func GetImplicitToken(application *Application, username string, scope string, nonce string, host string) (*Token, *TokenError, error) {
|
||||
expandedScope, ok := IsScopeValidAndExpand(scope, application)
|
||||
if !ok {
|
||||
return nil, &TokenError{
|
||||
@@ -1024,41 +1012,6 @@ func mintImplicitToken(application *Application, username string, scope string,
|
||||
return token, nil, nil
|
||||
}
|
||||
|
||||
// GetImplicitToken
|
||||
// Implicit flow - requires password verification before minting a token
|
||||
func GetImplicitToken(application *Application, username string, password string, scope string, nonce string, host string) (*Token, *TokenError, error) {
|
||||
user, err := GetUserByFields(application.Organization, username)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "the user does not exist",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if user.Ldap != "" {
|
||||
err = CheckLdapUserPassword(user, password, "en")
|
||||
} else {
|
||||
if user.Password == "" {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "OAuth users cannot use implicit grant type, please use authorization code flow",
|
||||
}, nil
|
||||
}
|
||||
err = CheckPassword(user, password, "en")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("invalid username or password: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return mintImplicitToken(application, username, scope, nonce, host)
|
||||
}
|
||||
|
||||
// GetJwtBearerToken
|
||||
// RFC 7523
|
||||
func GetJwtBearerToken(application *Application, assertion string, scope string, nonce string, host string) (*Token, *TokenError, error) {
|
||||
@@ -1077,8 +1030,7 @@ func GetJwtBearerToken(application *Application, assertion string, scope string,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// JWT assertion has already been validated above; skip password re-verification
|
||||
return mintImplicitToken(application, claims.Subject, scope, nonce, host)
|
||||
return GetImplicitToken(application, claims.Subject, scope, nonce, host)
|
||||
}
|
||||
|
||||
func ValidateJwtAssertion(clientAssertion string, application *Application, host string) (bool, *Claims, error) {
|
||||
@@ -1290,67 +1242,6 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin
|
||||
return token, nil, nil
|
||||
}
|
||||
|
||||
// parseAndValidateSubjectToken validates a subject_token for RFC 8693 token exchange.
|
||||
// It uses the ISSUING application's certificate (not the requesting client's) and
|
||||
// enforces audience binding to prevent cross-client token reuse.
|
||||
func parseAndValidateSubjectToken(subjectToken string, requestingClientId string) (owner, name, scope string, tokenErr *TokenError, err error) {
|
||||
unverifiedToken, err := ParseJwtTokenWithoutValidation(subjectToken)
|
||||
if err != nil {
|
||||
return "", "", "", &TokenError{Error: InvalidGrant, ErrorDescription: fmt.Sprintf("invalid subject_token: %s", err.Error())}, nil
|
||||
}
|
||||
|
||||
unverifiedClaims, ok := unverifiedToken.Claims.(*Claims)
|
||||
if !ok || unverifiedClaims.Azp == "" {
|
||||
return "", "", "", &TokenError{Error: InvalidGrant, ErrorDescription: "subject_token is missing the azp claim"}, nil
|
||||
}
|
||||
|
||||
issuingApp, err := GetApplicationByClientId(unverifiedClaims.Azp)
|
||||
if err != nil {
|
||||
return "", "", "", nil, err
|
||||
}
|
||||
if issuingApp == nil {
|
||||
return "", "", "", &TokenError{Error: InvalidGrant, ErrorDescription: fmt.Sprintf("subject_token issuing application not found: %s", unverifiedClaims.Azp)}, nil
|
||||
}
|
||||
|
||||
cert, err := getCertByApplication(issuingApp)
|
||||
if err != nil {
|
||||
return "", "", "", nil, err
|
||||
}
|
||||
if cert == nil {
|
||||
return "", "", "", &TokenError{Error: EndpointError, ErrorDescription: fmt.Sprintf("cert for issuing application %s cannot be found", unverifiedClaims.Azp)}, nil
|
||||
}
|
||||
|
||||
if issuingApp.TokenFormat == "JWT-Standard" {
|
||||
standardClaims, err := ParseStandardJwtToken(subjectToken, cert)
|
||||
if err != nil {
|
||||
return "", "", "", &TokenError{Error: InvalidGrant, ErrorDescription: fmt.Sprintf("invalid subject_token: %s", err.Error())}, nil
|
||||
}
|
||||
return standardClaims.Owner, standardClaims.Name, standardClaims.Scope, nil, nil
|
||||
}
|
||||
|
||||
claims, err := ParseJwtToken(subjectToken, cert)
|
||||
if err != nil {
|
||||
return "", "", "", &TokenError{Error: InvalidGrant, ErrorDescription: fmt.Sprintf("invalid subject_token: %s", err.Error())}, nil
|
||||
}
|
||||
|
||||
// Audience binding: requesting client must be the issuer itself or appear in token's aud.
|
||||
// Prevents an attacker from exchanging App A's token to obtain an App B token (RFC 8693 §2.1).
|
||||
if issuingApp.ClientId != requestingClientId {
|
||||
audienceMatched := false
|
||||
for _, aud := range claims.Audience {
|
||||
if aud == requestingClientId {
|
||||
audienceMatched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !audienceMatched {
|
||||
return "", "", "", &TokenError{Error: InvalidGrant, ErrorDescription: fmt.Sprintf("subject_token audience does not include the requesting client '%s'", requestingClientId)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return claims.Owner, claims.Name, claims.Scope, nil, nil
|
||||
}
|
||||
|
||||
// GetTokenExchangeToken
|
||||
// Token Exchange Grant (RFC 8693)
|
||||
// Exchanges a subject token for a new token with different audience or scope
|
||||
@@ -1399,12 +1290,42 @@ func GetTokenExchangeToken(application *Application, clientSecret string, subjec
|
||||
}, nil
|
||||
}
|
||||
|
||||
subjectOwner, subjectName, subjectScope, tokenError, err := parseAndValidateSubjectToken(subjectToken, application.ClientId)
|
||||
// Get certificate for token validation
|
||||
cert, err := getCertByApplication(application)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if tokenError != nil {
|
||||
return nil, tokenError, nil
|
||||
if cert == nil {
|
||||
return nil, &TokenError{
|
||||
Error: EndpointError,
|
||||
ErrorDescription: fmt.Sprintf("cert: %s cannot be found", application.Cert),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Parse and validate the subject token
|
||||
var subjectOwner, subjectName, subjectScope string
|
||||
if application.TokenFormat == "JWT-Standard" {
|
||||
standardClaims, err := ParseStandardJwtToken(subjectToken, cert)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("invalid subject_token: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
subjectOwner = standardClaims.Owner
|
||||
subjectName = standardClaims.Name
|
||||
subjectScope = standardClaims.Scope
|
||||
} else {
|
||||
claims, err := ParseJwtToken(subjectToken, cert)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("invalid subject_token: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
subjectOwner = claims.Owner
|
||||
subjectName = claims.Name
|
||||
subjectScope = claims.Scope
|
||||
}
|
||||
|
||||
// Get the user from the subject token
|
||||
|
||||
@@ -15,11 +15,10 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -477,13 +476,10 @@ func GetVerifyType(username string) (verificationCodeType string) {
|
||||
var stdNums = []byte("0123456789")
|
||||
|
||||
func getRandomCode(length int) string {
|
||||
result := make([]byte, length)
|
||||
var result []byte
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
for i := 0; i < length; i++ {
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(stdNums))))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
result[i] = stdNums[n.Int64()]
|
||||
result = append(result, stdNums[r.Intn(len(stdNums))])
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
)
|
||||
|
||||
// Hard-coded thresholds for OTP / verification-code brute force protection (per IP + dest).
|
||||
// These can be made configurable later if needed.
|
||||
const (
|
||||
defaultVerifyCodeIpLimit = 5
|
||||
defaultVerifyCodeIpFrozenMinute = 10
|
||||
)
|
||||
|
||||
var (
|
||||
verifyCodeIpErrorMap = map[string]*verifyCodeErrorInfo{}
|
||||
verifyCodeIpErrorMapLock sync.Mutex
|
||||
)
|
||||
|
||||
func getVerifyCodeIpErrorKey(remoteAddr, dest string) string {
|
||||
return fmt.Sprintf("%s:%s", remoteAddr, dest)
|
||||
}
|
||||
|
||||
func checkVerifyCodeIpErrorTimes(remoteAddr, dest, lang string) error {
|
||||
if remoteAddr == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
key := getVerifyCodeIpErrorKey(remoteAddr, dest)
|
||||
|
||||
verifyCodeIpErrorMapLock.Lock()
|
||||
defer verifyCodeIpErrorMapLock.Unlock()
|
||||
|
||||
errorInfo, ok := verifyCodeIpErrorMap[key]
|
||||
if !ok || errorInfo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if errorInfo.wrongTimes < defaultVerifyCodeIpLimit {
|
||||
return nil
|
||||
}
|
||||
|
||||
minutesLeft := int64(defaultVerifyCodeIpFrozenMinute) - int64(time.Now().UTC().Sub(errorInfo.lastWrongTime).Minutes())
|
||||
if minutesLeft > 0 {
|
||||
return fmt.Errorf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), minutesLeft)
|
||||
}
|
||||
|
||||
delete(verifyCodeIpErrorMap, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func recordVerifyCodeIpErrorInfo(remoteAddr, dest, lang string) error {
|
||||
// If remoteAddr is missing, still return a normal "wrong code" error.
|
||||
if remoteAddr == "" {
|
||||
return errors.New(i18n.Translate(lang, "verification:Wrong verification code!"))
|
||||
}
|
||||
|
||||
key := getVerifyCodeIpErrorKey(remoteAddr, dest)
|
||||
|
||||
verifyCodeIpErrorMapLock.Lock()
|
||||
defer verifyCodeIpErrorMapLock.Unlock()
|
||||
|
||||
errorInfo, ok := verifyCodeIpErrorMap[key]
|
||||
if !ok || errorInfo == nil {
|
||||
errorInfo = &verifyCodeErrorInfo{}
|
||||
verifyCodeIpErrorMap[key] = errorInfo
|
||||
}
|
||||
|
||||
if errorInfo.wrongTimes < defaultVerifyCodeIpLimit {
|
||||
errorInfo.wrongTimes++
|
||||
}
|
||||
|
||||
if errorInfo.wrongTimes >= defaultVerifyCodeIpLimit {
|
||||
errorInfo.lastWrongTime = time.Now().UTC()
|
||||
}
|
||||
|
||||
leftChances := defaultVerifyCodeIpLimit - errorInfo.wrongTimes
|
||||
if leftChances >= 0 {
|
||||
return fmt.Errorf(i18n.Translate(lang, "check:password or code is incorrect, you have %s remaining chances"), strconv.Itoa(leftChances))
|
||||
}
|
||||
|
||||
return fmt.Errorf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), defaultVerifyCodeIpFrozenMinute)
|
||||
}
|
||||
|
||||
func resetVerifyCodeIpErrorTimes(remoteAddr, dest string) {
|
||||
if remoteAddr == "" {
|
||||
return
|
||||
}
|
||||
|
||||
key := getVerifyCodeIpErrorKey(remoteAddr, dest)
|
||||
|
||||
verifyCodeIpErrorMapLock.Lock()
|
||||
defer verifyCodeIpErrorMapLock.Unlock()
|
||||
|
||||
delete(verifyCodeIpErrorMap, key)
|
||||
}
|
||||
|
||||
// CheckVerifyCodeWithLimitAndIp enforces both per-user and per-IP attempt limits for verification codes.
|
||||
// It is intended for security-sensitive flows like password reset.
|
||||
func CheckVerifyCodeWithLimitAndIp(user *User, remoteAddr, dest, code, lang string) error {
|
||||
if err := checkVerifyCodeIpErrorTimes(remoteAddr, dest, lang); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
if err := checkVerifyCodeErrorTimes(user, dest, lang); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
result, err := CheckVerificationCode(dest, code, lang)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch result.Code {
|
||||
case VerificationSuccess:
|
||||
resetVerifyCodeIpErrorTimes(remoteAddr, dest)
|
||||
if user != nil {
|
||||
resetVerifyCodeErrorTimes(user, dest)
|
||||
}
|
||||
return nil
|
||||
case wrongCodeError:
|
||||
ipErr := recordVerifyCodeIpErrorInfo(remoteAddr, dest, lang)
|
||||
if user != nil {
|
||||
// Keep existing user-level error semantics when user is known.
|
||||
return recordVerifyCodeErrorInfo(user, dest, lang)
|
||||
}
|
||||
return ipErr
|
||||
default:
|
||||
return errors.New(result.Msg)
|
||||
}
|
||||
}
|
||||
@@ -45,11 +45,6 @@ type Webhook struct {
|
||||
IsUserExtended bool `json:"isUserExtended"`
|
||||
SingleOrgOnly bool `json:"singleOrgOnly"`
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
|
||||
// Retry configuration
|
||||
MaxRetries int `xorm:"int default 3" json:"maxRetries"`
|
||||
RetryInterval int `xorm:"int default 60" json:"retryInterval"` // seconds
|
||||
UseExponentialBackoff bool `json:"useExponentialBackoff"`
|
||||
}
|
||||
|
||||
func GetWebhookCount(owner, organization, field, value string) (int64, error) {
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
// 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"
|
||||
"github.com/xorm-io/xorm"
|
||||
)
|
||||
|
||||
// WebhookEventStatus represents the delivery status of a webhook event
|
||||
type WebhookEventStatus string
|
||||
|
||||
const (
|
||||
WebhookEventStatusPending WebhookEventStatus = "pending"
|
||||
WebhookEventStatusSuccess WebhookEventStatus = "success"
|
||||
WebhookEventStatusFailed WebhookEventStatus = "failed"
|
||||
WebhookEventStatusRetrying WebhookEventStatus = "retrying"
|
||||
)
|
||||
|
||||
// WebhookEvent represents a webhook delivery event with retry and replay capability
|
||||
type WebhookEvent 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"`
|
||||
|
||||
WebhookName string `xorm:"varchar(200) index" json:"webhookName"`
|
||||
Organization string `xorm:"varchar(100) index" json:"organization"`
|
||||
EventType string `xorm:"varchar(100)" json:"eventType"`
|
||||
Status WebhookEventStatus `xorm:"varchar(50) index" json:"status"`
|
||||
|
||||
// Payload stores the event data (Record)
|
||||
Payload string `xorm:"mediumtext" json:"payload"`
|
||||
|
||||
// Extended user data if applicable
|
||||
ExtendedUser string `xorm:"mediumtext" json:"extendedUser"`
|
||||
|
||||
// Delivery tracking
|
||||
AttemptCount int `xorm:"int default 0" json:"attemptCount"`
|
||||
MaxRetries int `xorm:"int default 3" json:"maxRetries"`
|
||||
NextRetryTime string `xorm:"varchar(100)" json:"nextRetryTime"`
|
||||
|
||||
// Last delivery response
|
||||
LastStatusCode int `xorm:"int" json:"lastStatusCode"`
|
||||
LastResponse string `xorm:"mediumtext" json:"lastResponse"`
|
||||
LastError string `xorm:"mediumtext" json:"lastError"`
|
||||
}
|
||||
|
||||
func GetWebhookEvent(id string) (*WebhookEvent, error) {
|
||||
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getWebhookEvent(owner, name)
|
||||
}
|
||||
|
||||
func getWebhookEvent(owner string, name string) (*WebhookEvent, error) {
|
||||
if owner == "" || name == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
event := WebhookEvent{Owner: owner, Name: name}
|
||||
existed, err := ormer.Engine.Get(&event)
|
||||
if err != nil {
|
||||
return &event, err
|
||||
}
|
||||
|
||||
if existed {
|
||||
return &event, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func getWebhookEventSession(owner, organization, webhookName string, status WebhookEventStatus, offset, limit int, sortField, sortOrder string) *xorm.Session {
|
||||
session := ormer.Engine.Prepare()
|
||||
|
||||
if owner != "" {
|
||||
session = session.Where("owner = ?", owner)
|
||||
}
|
||||
if organization != "" {
|
||||
session = session.Where("organization = ?", organization)
|
||||
}
|
||||
if webhookName != "" {
|
||||
session = session.Where("webhook_name = ?", webhookName)
|
||||
}
|
||||
if status != "" {
|
||||
session = session.Where("status = ?", status)
|
||||
}
|
||||
|
||||
if offset > 0 {
|
||||
session = session.Limit(limit, offset)
|
||||
} else if limit > 0 {
|
||||
session = session.Limit(limit)
|
||||
}
|
||||
|
||||
if sortField == "" || sortOrder == "" {
|
||||
sortField = "created_time"
|
||||
}
|
||||
if sortOrder == "ascend" {
|
||||
session = session.Asc(util.SnakeString(sortField))
|
||||
} else {
|
||||
session = session.Desc(util.SnakeString(sortField))
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
func GetWebhookEvents(owner, organization, webhookName string, status WebhookEventStatus, offset, limit int, sortField, sortOrder string) ([]*WebhookEvent, error) {
|
||||
events := []*WebhookEvent{}
|
||||
session := getWebhookEventSession(owner, organization, webhookName, status, offset, limit, sortField, sortOrder)
|
||||
|
||||
err := session.Find(&events)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func GetWebhookEventCount(owner, organization, webhookName string, status WebhookEventStatus) (int64, error) {
|
||||
session := ormer.Engine.Where("1 = 1")
|
||||
|
||||
if owner != "" {
|
||||
session = session.Where("owner = ?", owner)
|
||||
}
|
||||
if organization != "" {
|
||||
session = session.Where("organization = ?", organization)
|
||||
}
|
||||
if webhookName != "" {
|
||||
session = session.Where("webhook_name = ?", webhookName)
|
||||
}
|
||||
if status != "" {
|
||||
session = session.Where("status = ?", status)
|
||||
}
|
||||
|
||||
return session.Count(&WebhookEvent{})
|
||||
}
|
||||
|
||||
func GetPendingWebhookEvents(limit int) ([]*WebhookEvent, error) {
|
||||
events := []*WebhookEvent{}
|
||||
currentTime := util.GetCurrentTime()
|
||||
|
||||
err := ormer.Engine.
|
||||
Where("status = ? OR status = ?", WebhookEventStatusPending, WebhookEventStatusRetrying).
|
||||
And("(next_retry_time = '' OR next_retry_time <= ?)", currentTime).
|
||||
Asc("created_time").
|
||||
Limit(limit).
|
||||
Find(&events)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func AddWebhookEvent(event *WebhookEvent) (bool, error) {
|
||||
if event.Name == "" {
|
||||
event.Name = util.GenerateId()
|
||||
}
|
||||
if event.CreatedTime == "" {
|
||||
event.CreatedTime = util.GetCurrentTime()
|
||||
}
|
||||
if event.UpdatedTime == "" {
|
||||
event.UpdatedTime = util.GetCurrentTime()
|
||||
}
|
||||
if event.Status == "" {
|
||||
event.Status = WebhookEventStatusPending
|
||||
}
|
||||
|
||||
affected, err := ormer.Engine.Insert(event)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func UpdateWebhookEvent(id string, event *WebhookEvent) (bool, error) {
|
||||
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
event.UpdatedTime = util.GetCurrentTime()
|
||||
|
||||
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(event)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func UpdateWebhookEventStatus(event *WebhookEvent, status WebhookEventStatus, statusCode int, response string, err error) (bool, error) {
|
||||
event.Status = status
|
||||
event.LastStatusCode = statusCode
|
||||
event.LastResponse = response
|
||||
event.UpdatedTime = util.GetCurrentTime()
|
||||
|
||||
if status != WebhookEventStatusRetrying {
|
||||
event.NextRetryTime = ""
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
event.LastError = err.Error()
|
||||
} else {
|
||||
event.LastError = ""
|
||||
}
|
||||
|
||||
affected, dbErr := ormer.Engine.ID(core.PK{event.Owner, event.Name}).
|
||||
Cols("status", "last_status_code", "last_response", "last_error", "updated_time", "attempt_count", "max_retries", "next_retry_time").
|
||||
Update(event)
|
||||
|
||||
if dbErr != nil {
|
||||
return false, dbErr
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func DeleteWebhookEvent(event *WebhookEvent) (bool, error) {
|
||||
affected, err := ormer.Engine.ID(core.PK{event.Owner, event.Name}).Delete(&WebhookEvent{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func (e *WebhookEvent) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", e.Owner, e.Name)
|
||||
}
|
||||
|
||||
// CreateWebhookEventFromRecord creates a webhook event from a record
|
||||
func CreateWebhookEventFromRecord(webhook *Webhook, record *Record, extendedUser *User) (*WebhookEvent, error) {
|
||||
maxRetries := webhook.MaxRetries
|
||||
if maxRetries <= 0 {
|
||||
maxRetries = 3
|
||||
}
|
||||
|
||||
event := &WebhookEvent{
|
||||
Owner: webhook.Owner,
|
||||
Name: util.GenerateId(),
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
UpdatedTime: util.GetCurrentTime(),
|
||||
WebhookName: webhook.GetId(),
|
||||
Organization: record.Organization,
|
||||
EventType: record.Action,
|
||||
Status: WebhookEventStatusPending,
|
||||
Payload: util.StructToJson(record),
|
||||
AttemptCount: 0,
|
||||
MaxRetries: maxRetries,
|
||||
}
|
||||
|
||||
if extendedUser != nil {
|
||||
event.ExtendedUser = util.StructToJson(extendedUser)
|
||||
}
|
||||
|
||||
_, err := AddWebhookEvent(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return event, nil
|
||||
}
|
||||
@@ -19,13 +19,12 @@ import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func sendWebhook(webhook *Webhook, record *Record, extendedUser *User) (int, string, error) {
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
client := &http.Client{}
|
||||
userMap := make(map[string]interface{})
|
||||
var body io.Reader
|
||||
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/core/logs"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
var (
|
||||
webhookWorkerMu sync.Mutex
|
||||
webhookWorkerRunning = false
|
||||
webhookWorkerStop chan struct{}
|
||||
webhookPollingInterval = 30 * time.Second // Configurable polling interval
|
||||
webhookBatchSize = 100 // Configurable batch size for processing events
|
||||
)
|
||||
|
||||
// StartWebhookDeliveryWorker starts the background worker for webhook delivery
|
||||
func StartWebhookDeliveryWorker() {
|
||||
webhookWorkerMu.Lock()
|
||||
defer webhookWorkerMu.Unlock()
|
||||
|
||||
if webhookWorkerRunning {
|
||||
return
|
||||
}
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
webhookWorkerStop = stopCh
|
||||
webhookWorkerRunning = true
|
||||
|
||||
util.SafeGoroutine(func() {
|
||||
ticker := time.NewTicker(webhookPollingInterval)
|
||||
defer ticker.Stop()
|
||||
defer func() {
|
||||
webhookWorkerMu.Lock()
|
||||
defer webhookWorkerMu.Unlock()
|
||||
|
||||
if webhookWorkerStop == stopCh {
|
||||
webhookWorkerRunning = false
|
||||
webhookWorkerStop = nil
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
processWebhookEvents()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// StopWebhookDeliveryWorker stops the background worker
|
||||
func StopWebhookDeliveryWorker() {
|
||||
webhookWorkerMu.Lock()
|
||||
defer webhookWorkerMu.Unlock()
|
||||
|
||||
if !webhookWorkerRunning {
|
||||
return
|
||||
}
|
||||
|
||||
if webhookWorkerStop == nil {
|
||||
webhookWorkerRunning = false
|
||||
return
|
||||
}
|
||||
|
||||
close(webhookWorkerStop)
|
||||
webhookWorkerStop = nil
|
||||
webhookWorkerRunning = false
|
||||
}
|
||||
|
||||
// processWebhookEvents processes pending webhook events
|
||||
func processWebhookEvents() {
|
||||
events, err := GetPendingWebhookEvents(webhookBatchSize)
|
||||
if err != nil {
|
||||
logs.Error(fmt.Sprintf("failed to get pending webhook events: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
deliverWebhookEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
// deliverWebhookEvent attempts to deliver a single webhook event
|
||||
func deliverWebhookEvent(event *WebhookEvent) {
|
||||
// Get the webhook configuration
|
||||
webhook, err := GetWebhook(event.WebhookName)
|
||||
if err != nil {
|
||||
logs.Error(fmt.Sprintf("failed to get webhook %s: %v", event.WebhookName, err))
|
||||
return
|
||||
}
|
||||
|
||||
if webhook == nil {
|
||||
// Webhook has been deleted, mark event as failed
|
||||
event.Status = WebhookEventStatusFailed
|
||||
event.LastError = "Webhook not found"
|
||||
UpdateWebhookEventStatus(event, WebhookEventStatusFailed, 0, "", fmt.Errorf("webhook not found"))
|
||||
return
|
||||
}
|
||||
|
||||
if !webhook.IsEnabled {
|
||||
// Disabled webhooks should finalize the event to avoid hot-looping forever.
|
||||
UpdateWebhookEventStatus(event, WebhookEventStatusFailed, 0, "", fmt.Errorf("webhook is disabled"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the record from payload
|
||||
var record Record
|
||||
err = json.Unmarshal([]byte(event.Payload), &record)
|
||||
if err != nil {
|
||||
event.Status = WebhookEventStatusFailed
|
||||
event.LastError = fmt.Sprintf("Invalid payload: %v", err)
|
||||
UpdateWebhookEventStatus(event, WebhookEventStatusFailed, 0, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse extended user if present
|
||||
var extendedUser *User
|
||||
if event.ExtendedUser != "" {
|
||||
extendedUser = &User{}
|
||||
err = json.Unmarshal([]byte(event.ExtendedUser), extendedUser)
|
||||
if err != nil {
|
||||
logs.Warning(fmt.Sprintf("failed to parse extended user for webhook event %s: %v", event.GetId(), err))
|
||||
extendedUser = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Increment attempt count
|
||||
event.AttemptCount++
|
||||
|
||||
// Attempt to send the webhook
|
||||
statusCode, respBody, err := sendWebhook(webhook, &record, extendedUser)
|
||||
|
||||
// Add webhook record for backward compatibility (only if non-200 status)
|
||||
if statusCode != 200 {
|
||||
addWebhookRecord(webhook, &record, statusCode, respBody, err)
|
||||
}
|
||||
|
||||
// Determine the result
|
||||
if err == nil && statusCode >= 200 && statusCode < 300 {
|
||||
// Success
|
||||
UpdateWebhookEventStatus(event, WebhookEventStatusSuccess, statusCode, respBody, nil)
|
||||
} else {
|
||||
// Failed - decide whether to retry
|
||||
maxRetries := event.MaxRetries
|
||||
if maxRetries <= 0 {
|
||||
maxRetries = webhook.MaxRetries
|
||||
}
|
||||
if maxRetries <= 0 {
|
||||
maxRetries = 3 // Default
|
||||
}
|
||||
event.MaxRetries = maxRetries
|
||||
|
||||
if event.AttemptCount >= maxRetries {
|
||||
// Max retries reached, mark as permanently failed
|
||||
UpdateWebhookEventStatus(event, WebhookEventStatusFailed, statusCode, respBody, err)
|
||||
} else {
|
||||
// Schedule retry
|
||||
retryInterval := webhook.RetryInterval
|
||||
if retryInterval <= 0 {
|
||||
retryInterval = 60 // Default 60 seconds
|
||||
}
|
||||
|
||||
nextRetryTime := calculateNextRetryTime(event.AttemptCount, retryInterval, webhook.UseExponentialBackoff)
|
||||
event.NextRetryTime = nextRetryTime
|
||||
event.Status = WebhookEventStatusRetrying
|
||||
|
||||
UpdateWebhookEventStatus(event, WebhookEventStatusRetrying, statusCode, respBody, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// calculateNextRetryTime calculates the next retry time based on attempt count and backoff strategy
|
||||
func calculateNextRetryTime(attemptCount int, baseInterval int, useExponentialBackoff bool) string {
|
||||
var delaySeconds int
|
||||
|
||||
if useExponentialBackoff {
|
||||
// Exponential backoff: baseInterval * 2^(attemptCount-1)
|
||||
// Cap attemptCount at 10 to prevent overflow
|
||||
cappedAttemptCount := attemptCount - 1
|
||||
if cappedAttemptCount > 10 {
|
||||
cappedAttemptCount = 10
|
||||
}
|
||||
|
||||
// Calculate delay with overflow protection
|
||||
delaySeconds = baseInterval * (1 << uint(cappedAttemptCount))
|
||||
|
||||
// Cap at 1 hour
|
||||
if delaySeconds > 3600 {
|
||||
delaySeconds = 3600
|
||||
}
|
||||
} else {
|
||||
// Fixed interval
|
||||
delaySeconds = baseInterval
|
||||
}
|
||||
|
||||
nextTime := time.Now().Add(time.Duration(delaySeconds) * time.Second)
|
||||
return nextTime.Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
|
||||
// ReplayWebhookEvent replays a failed or missed webhook event
|
||||
func ReplayWebhookEvent(eventId string) error {
|
||||
event, err := GetWebhookEvent(eventId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if event == nil {
|
||||
return fmt.Errorf("webhook event not found: %s", eventId)
|
||||
}
|
||||
|
||||
// Reset the event for replay
|
||||
event.Status = WebhookEventStatusPending
|
||||
event.AttemptCount = 0
|
||||
event.NextRetryTime = ""
|
||||
event.LastError = ""
|
||||
|
||||
_, err = UpdateWebhookEvent(event.GetId(), event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Immediately try to deliver
|
||||
deliverWebhookEvent(event)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -287,12 +287,6 @@ func ApiFilter(ctx *context.Context) {
|
||||
|
||||
isAllowed := authz.IsAllowed(subOwner, subName, method, urlPath, objOwner, objName, extraInfo)
|
||||
|
||||
if method != "GET" && !strings.HasSuffix(urlPath, "-entry") {
|
||||
util.SafeGoroutine(func() {
|
||||
writePermissionLog(objOwner, subOwner, subName, method, urlPath, isAllowed)
|
||||
})
|
||||
}
|
||||
|
||||
result := "deny"
|
||||
if isAllowed {
|
||||
result = "allow"
|
||||
@@ -330,31 +324,6 @@ func ApiFilter(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func writePermissionLog(objOwner, subOwner, subName, method, urlPath string, allowed bool) {
|
||||
providers, err := object.GetProvidersByCategory(objOwner, "Log")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
severity := "info"
|
||||
if !allowed {
|
||||
severity = "warning"
|
||||
}
|
||||
message := fmt.Sprintf("sub=%s/%s method=%s url=%s objOwner=%s allowed=%v", subOwner, subName, method, urlPath, objOwner, allowed)
|
||||
|
||||
for _, provider := range providers {
|
||||
// System Log is a pull-based collector; it does not accept Write calls.
|
||||
if provider.Type == "System Log" {
|
||||
continue
|
||||
}
|
||||
logProvider, err := object.GetLogProviderFromProvider(provider)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_ = logProvider.Write(severity, message)
|
||||
}
|
||||
}
|
||||
|
||||
func formatExtraInfo(extra map[string]interface{}) string {
|
||||
if extra == nil {
|
||||
return ""
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/core/logs"
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
@@ -111,6 +112,7 @@ func denyMcpRequest(ctx *context.Context) {
|
||||
|
||||
func getUsernameByClientIdSecret(ctx *context.Context) (string, error) {
|
||||
clientId, clientSecret, ok := ctx.Request.BasicAuth()
|
||||
fromBasicAuth := ok
|
||||
if !ok {
|
||||
clientId = ctx.Input.Query("clientId")
|
||||
clientSecret = ctx.Input.Query("clientSecret")
|
||||
@@ -125,10 +127,24 @@ func getUsernameByClientIdSecret(ctx *context.Context) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
if application == nil {
|
||||
if fromBasicAuth {
|
||||
// The Authorization: Basic header may come from an HTTP proxy or gateway
|
||||
// protecting Casdoor at the transport level. Silently ignore credentials
|
||||
// that don't match any Casdoor application instead of returning an error,
|
||||
// so that proxy-protected deployments continue to work normally.
|
||||
logs.Debug("getUsernameByClientIdSecret: Basic Auth clientId %q does not match any application; ignoring (possible HTTP proxy credentials)", clientId)
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("Application not found for client ID: %s", clientId)
|
||||
}
|
||||
|
||||
if application.ClientSecret != clientSecret {
|
||||
if fromBasicAuth {
|
||||
// Same reasoning: proxy credentials that happen to share a clientId with a
|
||||
// Casdoor application but have a different password should not block requests.
|
||||
logs.Warning("getUsernameByClientIdSecret: Basic Auth clientId %q matched application %q but secret did not match; ignoring (possible HTTP proxy credentials or misconfiguration)", clientId, application.Name)
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("Incorrect client secret for application: %s", application.Name)
|
||||
}
|
||||
|
||||
|
||||
@@ -126,32 +126,6 @@ func InitAPI() {
|
||||
web.Router("/api/delete-resource", &controllers.ApiController{}, "POST:DeleteResource")
|
||||
web.Router("/api/upload-resource", &controllers.ApiController{}, "POST:UploadResource")
|
||||
|
||||
web.Router("/api/get-agents", &controllers.ApiController{}, "GET:GetAgents")
|
||||
web.Router("/api/get-agent", &controllers.ApiController{}, "GET:GetAgent")
|
||||
web.Router("/api/update-agent", &controllers.ApiController{}, "POST:UpdateAgent")
|
||||
web.Router("/api/add-agent", &controllers.ApiController{}, "POST:AddAgent")
|
||||
web.Router("/api/delete-agent", &controllers.ApiController{}, "POST:DeleteAgent")
|
||||
|
||||
web.Router("/api/get-servers", &controllers.ApiController{}, "GET:GetServers")
|
||||
web.Router("/api/get-online-servers", &controllers.ApiController{}, "GET:GetOnlineServers")
|
||||
web.Router("/api/sync-intranet-servers", &controllers.ApiController{}, "POST:SyncIntranetServers")
|
||||
web.Router("/api/get-server", &controllers.ApiController{}, "GET:GetServer")
|
||||
web.Router("/api/update-server", &controllers.ApiController{}, "POST:UpdateServer")
|
||||
web.Router("/api/sync-mcp-tool", &controllers.ApiController{}, "POST:SyncMcpTool")
|
||||
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{}, "POST:ProxyServer")
|
||||
|
||||
web.Router("/api/get-entries", &controllers.ApiController{}, "GET:GetEntries")
|
||||
web.Router("/api/get-entry", &controllers.ApiController{}, "GET:GetEntry")
|
||||
web.Router("/api/update-entry", &controllers.ApiController{}, "POST:UpdateEntry")
|
||||
web.Router("/api/add-entry", &controllers.ApiController{}, "POST:AddEntry")
|
||||
web.Router("/api/delete-entry", &controllers.ApiController{}, "POST:DeleteEntry")
|
||||
|
||||
web.Router("/api/v1/traces", &controllers.ApiController{}, "POST:AddOtlpTrace")
|
||||
web.Router("/api/v1/metrics", &controllers.ApiController{}, "POST:AddOtlpMetrics")
|
||||
web.Router("/api/v1/logs", &controllers.ApiController{}, "POST:AddOtlpLogs")
|
||||
|
||||
web.Router("/api/get-global-sites", &controllers.ApiController{}, "GET:GetGlobalSites")
|
||||
web.Router("/api/get-sites", &controllers.ApiController{}, "GET:GetSites")
|
||||
web.Router("/api/get-site", &controllers.ApiController{}, "GET:GetSite")
|
||||
@@ -159,6 +133,13 @@ func InitAPI() {
|
||||
web.Router("/api/add-site", &controllers.ApiController{}, "POST:AddSite")
|
||||
web.Router("/api/delete-site", &controllers.ApiController{}, "POST:DeleteSite")
|
||||
|
||||
web.Router("/api/get-servers", &controllers.ApiController{}, "GET:GetServers")
|
||||
web.Router("/api/get-server", &controllers.ApiController{}, "GET:GetServer")
|
||||
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{}, "POST:ProxyServer")
|
||||
|
||||
web.Router("/api/get-rules", &controllers.ApiController{}, "GET:GetRules")
|
||||
web.Router("/api/get-rule", &controllers.ApiController{}, "GET:GetRule")
|
||||
web.Router("/api/add-rule", &controllers.ApiController{}, "POST:AddRule")
|
||||
@@ -246,7 +227,6 @@ func InitAPI() {
|
||||
web.Router("/api/update-product", &controllers.ApiController{}, "POST:UpdateProduct")
|
||||
web.Router("/api/add-product", &controllers.ApiController{}, "POST:AddProduct")
|
||||
web.Router("/api/delete-product", &controllers.ApiController{}, "POST:DeleteProduct")
|
||||
web.Router("/api/buy-product", &controllers.ApiController{}, "POST:BuyProduct")
|
||||
|
||||
web.Router("/api/get-orders", &controllers.ApiController{}, "GET:GetOrders")
|
||||
web.Router("/api/get-user-orders", &controllers.ApiController{}, "GET:GetUserOrders")
|
||||
@@ -318,12 +298,6 @@ func InitAPI() {
|
||||
web.Router("/api/add-webhook", &controllers.ApiController{}, "POST:AddWebhook")
|
||||
web.Router("/api/delete-webhook", &controllers.ApiController{}, "POST:DeleteWebhook")
|
||||
|
||||
// Webhook event routes
|
||||
web.Router("/api/get-webhook-events", &controllers.ApiController{}, "GET:GetWebhookEvents")
|
||||
web.Router("/api/get-webhook-event-detail", &controllers.ApiController{}, "GET:GetWebhookEvent")
|
||||
web.Router("/api/replay-webhook-event", &controllers.ApiController{}, "POST:ReplayWebhookEvent")
|
||||
web.Router("/api/delete-webhook-event", &controllers.ApiController{}, "POST:DeleteWebhookEvent")
|
||||
|
||||
web.Router("/api/get-tickets", &controllers.ApiController{}, "GET:GetTickets")
|
||||
web.Router("/api/get-ticket", &controllers.ApiController{}, "GET:GetTicket")
|
||||
web.Router("/api/update-ticket", &controllers.ApiController{}, "POST:UpdateTicket")
|
||||
|
||||
@@ -1138,70 +1138,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/buy-product": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Product API"
|
||||
],
|
||||
"description": "buy product using the deprecated compatibility endpoint, prefer place-order plus pay-order for new integrations",
|
||||
"operationId": "ApiController.BuyProduct",
|
||||
"deprecated": true,
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "id",
|
||||
"description": "The id ( owner/name ) of the product",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "providerName",
|
||||
"description": "The name of the provider",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "pricingName",
|
||||
"description": "The name of the pricing (for subscription)",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "planName",
|
||||
"description": "The name of the plan (for subscription)",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "userName",
|
||||
"description": "The username to buy product for (admin only)",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "paymentEnv",
|
||||
"description": "The payment environment",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "customPrice",
|
||||
"description": "Custom price for recharge products",
|
||||
"type": "number"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The Response object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/controllers.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/delete-adapter": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -10552,4 +10488,4 @@
|
||||
"in": "header"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -734,49 +734,6 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/object.Userinfo'
|
||||
/api/buy-product:
|
||||
post:
|
||||
tags:
|
||||
- Product API
|
||||
description: buy product using the deprecated compatibility endpoint, prefer place-order plus pay-order for new integrations
|
||||
operationId: ApiController.BuyProduct
|
||||
deprecated: true
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id ( owner/name ) of the product
|
||||
required: true
|
||||
type: string
|
||||
- in: query
|
||||
name: providerName
|
||||
description: The name of the provider
|
||||
required: true
|
||||
type: string
|
||||
- in: query
|
||||
name: pricingName
|
||||
description: The name of the pricing (for subscription)
|
||||
type: string
|
||||
- in: query
|
||||
name: planName
|
||||
description: The name of the plan (for subscription)
|
||||
type: string
|
||||
- in: query
|
||||
name: userName
|
||||
description: The username to buy product for (admin only)
|
||||
type: string
|
||||
- in: query
|
||||
name: paymentEnv
|
||||
description: The payment environment
|
||||
type: string
|
||||
- in: query
|
||||
name: customPrice
|
||||
description: Custom price for recharge products
|
||||
type: number
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/delete-adapter:
|
||||
post:
|
||||
tags:
|
||||
|
||||
@@ -14,13 +14,7 @@
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/thanhpk/randstr"
|
||||
)
|
||||
import "github.com/thanhpk/randstr"
|
||||
|
||||
func GenerateClientId() string {
|
||||
return randstr.Hex(10)
|
||||
@@ -33,57 +27,3 @@ func GenerateClientSecret() string {
|
||||
func GeneratePasswordSalt() string {
|
||||
return randstr.Hex(10)
|
||||
}
|
||||
|
||||
// RandomIntn returns a cryptographically secure random int in [0, n).
|
||||
func RandomIntn(n int) int {
|
||||
val, err := rand.Int(rand.Reader, big.NewInt(int64(n)))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return int(val.Int64())
|
||||
}
|
||||
|
||||
// GenerateUUID returns a random UUID v4 string.
|
||||
func GenerateUUID() string {
|
||||
return uuid.NewString()
|
||||
}
|
||||
|
||||
// RandomStringFromCharset returns a cryptographically secure random string
|
||||
// of the given length drawn from charset.
|
||||
func RandomStringFromCharset(charset string, length int) string {
|
||||
result := make([]byte, length)
|
||||
for i := range result {
|
||||
result[i] = charset[RandomIntn(len(charset))]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func GetRandomName() string {
|
||||
return RandomStringFromCharset("0123456789abcdefghijklmnopqrstuvwxyz", 6)
|
||||
}
|
||||
|
||||
func generateRandomString(length int) (string, error) {
|
||||
return RandomStringFromCharset("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", length), nil
|
||||
}
|
||||
|
||||
func GenerateTwoUniqueRandomStrings() (string, string, error) {
|
||||
len1 := 16 + int(big.NewInt(17).Int64())
|
||||
len2 := 16 + int(big.NewInt(17).Int64())
|
||||
|
||||
str1, err := generateRandomString(len1)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
str2, err := generateRandomString(len2)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
for str1 == str2 {
|
||||
str2, err = generateRandomString(len2)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
return str1, str2, nil
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -28,6 +30,7 @@ import (
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/nyaruka/phonenumbers"
|
||||
)
|
||||
|
||||
@@ -164,7 +167,7 @@ func GetSharedOrgFromApp(rawName string) (name string, organization string) {
|
||||
}
|
||||
|
||||
func GenerateId() string {
|
||||
return GenerateUUID()
|
||||
return uuid.NewString()
|
||||
}
|
||||
|
||||
func GenerateTimeId() string {
|
||||
@@ -172,7 +175,7 @@ func GenerateTimeId() string {
|
||||
tm := time.Unix(timestamp, 0)
|
||||
t := tm.Format("20060102_150405")
|
||||
|
||||
random := GenerateUUID()[0:7]
|
||||
random := uuid.NewString()[0:7]
|
||||
|
||||
res := fmt.Sprintf("%s_%s", t, random)
|
||||
return res
|
||||
@@ -186,6 +189,16 @@ func GenerateSimpleTimeId() string {
|
||||
return t
|
||||
}
|
||||
|
||||
func GetRandomName() string {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
const charset = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
result := make([]byte, 6)
|
||||
for i := range result {
|
||||
result[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func GetId(owner, name string) string {
|
||||
return fmt.Sprintf("%s/%s", owner, name)
|
||||
}
|
||||
@@ -342,7 +355,7 @@ func GetValueFromDataSourceName(key string, dataSourceName string) string {
|
||||
func GetUsernameFromEmail(email string) string {
|
||||
tokens := strings.Split(email, "@")
|
||||
if len(tokens) == 0 {
|
||||
return GenerateUUID()
|
||||
return uuid.NewString()
|
||||
} else {
|
||||
return tokens[0]
|
||||
}
|
||||
@@ -381,3 +394,37 @@ func StringToInterfaceArray2d(arrays [][]string) [][]interface{} {
|
||||
}
|
||||
return interfaceArrays
|
||||
}
|
||||
|
||||
func generateRandomString(length int) (string, error) {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
var c byte
|
||||
index := rand.Intn(len(charset))
|
||||
c = charset[index]
|
||||
b[i] = c
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func GenerateTwoUniqueRandomStrings() (string, string, error) {
|
||||
len1 := 16 + int(big.NewInt(17).Int64())
|
||||
len2 := 16 + int(big.NewInt(17).Int64())
|
||||
|
||||
str1, err := generateRandomString(len1)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
str2, err := generateRandomString(len2)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
for str1 == str2 {
|
||||
str2, err = generateRandomString(len2)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
return str1, str2, nil
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ package util
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
@@ -25,20 +24,14 @@ import (
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/shirou/gopsutil/v4/cpu"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/shirou/gopsutil/v4/mem"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
)
|
||||
|
||||
type SystemInfo struct {
|
||||
CpuUsage []float64 `json:"cpuUsage"`
|
||||
MemoryUsed uint64 `json:"memoryUsed"`
|
||||
MemoryTotal uint64 `json:"memoryTotal"`
|
||||
DiskUsed uint64 `json:"diskUsed"`
|
||||
DiskTotal uint64 `json:"diskTotal"`
|
||||
NetworkSent uint64 `json:"networkSent"`
|
||||
NetworkRecv uint64 `json:"networkRecv"`
|
||||
NetworkTotal uint64 `json:"networkTotal"`
|
||||
CpuUsage []float64 `json:"cpuUsage"`
|
||||
MemoryUsed uint64 `json:"memoryUsed"`
|
||||
MemoryTotal uint64 `json:"memoryTotal"`
|
||||
}
|
||||
|
||||
type VersionInfo struct {
|
||||
@@ -76,56 +69,6 @@ func getMemoryUsage() (uint64, uint64, error) {
|
||||
return memInfo.RSS, virtualMem.Total, nil
|
||||
}
|
||||
|
||||
// getDiskUsage gets disk usage for Casdoor's data directory
|
||||
func getDiskUsage() (uint64, uint64, error) {
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
rootPath := path.Dir(path.Dir(filename))
|
||||
dataPath := filepath.Join(rootPath, "data")
|
||||
|
||||
var size uint64
|
||||
err := filepath.Walk(dataPath, func(_ string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if !info.IsDir() {
|
||||
size += uint64(info.Size())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
diskStat, err := disk.Usage(dataPath)
|
||||
if err != nil {
|
||||
diskStat, err = disk.Usage("/")
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
return size, diskStat.Total, nil
|
||||
}
|
||||
|
||||
// getNetworkUsage gets Casdoor process's own I/O usage
|
||||
func getNetworkUsage() (uint64, uint64, uint64, error) {
|
||||
proc, err := process.NewProcess(int32(os.Getpid()))
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
ioCounters, err := proc.IOCounters()
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
bytesSent := ioCounters.WriteBytes
|
||||
bytesRecv := ioCounters.ReadBytes
|
||||
bytesTotal := bytesSent + bytesRecv
|
||||
|
||||
return bytesSent, bytesRecv, bytesTotal, nil
|
||||
}
|
||||
|
||||
func GetSystemInfo() (*SystemInfo, error) {
|
||||
cpuUsage, err := getCpuUsage()
|
||||
if err != nil {
|
||||
@@ -137,25 +80,10 @@ func GetSystemInfo() (*SystemInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
diskUsed, diskTotal, err := getDiskUsage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
networkSent, networkRecv, networkTotal, err := getNetworkUsage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &SystemInfo{
|
||||
CpuUsage: cpuUsage,
|
||||
MemoryUsed: memoryUsed,
|
||||
MemoryTotal: memoryTotal,
|
||||
DiskUsed: diskUsed,
|
||||
DiskTotal: diskTotal,
|
||||
NetworkSent: networkSent,
|
||||
NetworkRecv: networkRecv,
|
||||
NetworkTotal: networkTotal,
|
||||
CpuUsage: cpuUsage,
|
||||
MemoryUsed: memoryUsed,
|
||||
MemoryTotal: memoryTotal,
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -42,18 +42,6 @@ func TestGetMemoryUsage(t *testing.T) {
|
||||
t.Log(used, total)
|
||||
}
|
||||
|
||||
func TestGetDiskUsage(t *testing.T) {
|
||||
used, total, err := getDiskUsage()
|
||||
assert.Nil(t, err)
|
||||
t.Log(used, total)
|
||||
}
|
||||
|
||||
func TestGetNetworkUsage(t *testing.T) {
|
||||
sent, recv, total, err := getNetworkUsage()
|
||||
assert.Nil(t, err)
|
||||
t.Log(sent, recv, total)
|
||||
}
|
||||
|
||||
func TestGetVersionInfo(t *testing.T) {
|
||||
versionInfo, err := GetVersionInfo()
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
// 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, Input, Row, Select} from "antd";
|
||||
import {LinkOutlined} from "@ant-design/icons";
|
||||
import * as AgentBackend from "./backend/AgentBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
class AgentEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
agentName: props.match.params.agentName,
|
||||
owner: props.match.params.organizationName,
|
||||
agent: null,
|
||||
organizations: [],
|
||||
applications: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getAgent();
|
||||
this.getOrganizations();
|
||||
this.getApplications(this.state.owner);
|
||||
}
|
||||
|
||||
getAgent() {
|
||||
AgentBackend.getAgent(this.state.agent?.owner || this.state.owner, this.state.agentName)
|
||||
.then((res) => {
|
||||
if (res.data === null) {
|
||||
this.props.history.push("/404");
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
agent: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to get")}: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
if (Setting.isAdminUser(this.props.account)) {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
organizations: res.data || [],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getApplications(owner) {
|
||||
ApplicationBackend.getApplicationsByOrganization("admin", owner)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
applications: res.data || [],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateAgentField(key, value) {
|
||||
const agent = this.state.agent;
|
||||
if (key === "owner" && agent.owner !== value) {
|
||||
agent.application = "";
|
||||
this.getApplications(value);
|
||||
}
|
||||
|
||||
agent[key] = value;
|
||||
this.setState({
|
||||
agent: agent,
|
||||
});
|
||||
}
|
||||
|
||||
submitAgentEdit(willExit) {
|
||||
const agent = Setting.deepCopy(this.state.agent);
|
||||
AgentBackend.updateAgent(this.state.owner, this.state.agentName, agent)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully modified"));
|
||||
if (willExit) {
|
||||
this.props.history.push("/agents");
|
||||
} else {
|
||||
this.setState({
|
||||
mode: "edit",
|
||||
owner: agent.owner,
|
||||
agentName: agent.name,
|
||||
}, () => {this.getAgent();});
|
||||
this.props.history.push(`/agents/${agent.owner}/${agent.name}`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to update")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteAgent() {
|
||||
AgentBackend.deleteAgent(this.state.agent)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
|
||||
this.props.history.push("/agents");
|
||||
} 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}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderAgent() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{this.state.mode === "add" ? i18next.t("agent:New Agent") : i18next.t("agent:Edit Agent")}
|
||||
<Button onClick={() => this.submitAgentEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitAgentEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteAgent()}>{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.agent.owner} onChange={(value => {this.updateAgentField("owner", 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}>
|
||||
{i18next.t("general:Name")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.agent.name} onChange={e => {
|
||||
this.updateAgentField("name", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Display name")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.agent.displayName} onChange={e => {
|
||||
this.updateAgentField("displayName", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Listening URL"), i18next.t("general:Listening URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.agent.url} onChange={e => {
|
||||
this.updateAgentField("url", e.target.value);
|
||||
}} />
|
||||
</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.agent.token} onChange={e => {
|
||||
this.updateAgentField("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"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.agent.application} onChange={(value => {this.updateAgentField("application", value);})}>
|
||||
{
|
||||
this.state.applications.map((application, index) => <Option key={index} value={application.name}>{application.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.agent === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.renderAgent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AgentEditPage;
|
||||
@@ -1,219 +0,0 @@
|
||||
// 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 AgentBackend from "./backend/AgentBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
|
||||
class AgentListPage extends BaseListPage {
|
||||
newAgent() {
|
||||
const randomName = Setting.getRandomName();
|
||||
const owner = Setting.getRequestOrganization(this.props.account);
|
||||
return {
|
||||
owner: owner,
|
||||
name: `agent_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Agent - ${randomName}`,
|
||||
url: "",
|
||||
token: "",
|
||||
application: "",
|
||||
};
|
||||
}
|
||||
|
||||
addAgent() {
|
||||
const newAgent = this.newAgent();
|
||||
AgentBackend.addAgent(newAgent)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push({pathname: `/agents/${newAgent.owner}/${newAgent.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}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteAgent(i) {
|
||||
AgentBackend.deleteAgent(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}`);
|
||||
});
|
||||
}
|
||||
|
||||
fetch = (params = {}) => {
|
||||
const field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
if (!params.pagination) {
|
||||
params.pagination = {current: 1, pageSize: 10};
|
||||
}
|
||||
|
||||
this.setState({loading: true});
|
||||
AgentBackend.getAgents(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 {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to get")}: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
renderTable(agents) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "160px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("name"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/agents/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Organization"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
width: "130px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("owner"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Created time"),
|
||||
dataIndex: "createdTime",
|
||||
key: "createdTime",
|
||||
width: "180px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Display name"),
|
||||
dataIndex: "displayName",
|
||||
key: "displayName",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("displayName"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Listening URL"),
|
||||
dataIndex: "url",
|
||||
key: "url",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("url"),
|
||||
render: (text) => {
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<a target="_blank" rel="noreferrer" href={text}>
|
||||
{Setting.getShortText(text, 40)}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Application"),
|
||||
dataIndex: "application",
|
||||
key: "application",
|
||||
width: "140px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("application"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "op",
|
||||
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(`/agents/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<PopconfirmModal title={i18next.t("general:Sure to delete") + `: ${record.name} ?`} onConfirm={() => this.deleteAgent(index)}>
|
||||
</PopconfirmModal>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const filteredColumns = Setting.filterTableColumns(columns, this.props.formItems ?? this.state.formItems);
|
||||
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 (
|
||||
<Table
|
||||
scroll={{x: "max-content"}}
|
||||
dataSource={agents}
|
||||
columns={filteredColumns}
|
||||
rowKey={record => `${record.owner}/${record.name}`}
|
||||
pagination={{...this.state.pagination, ...paginationProps}}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
size="middle"
|
||||
bordered
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Agents")}
|
||||
<Button type="primary" size="small" onClick={() => this.addAgent()}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AgentListPage;
|
||||
@@ -176,10 +176,9 @@ class App extends Component {
|
||||
"/organizations", "/groups", "/users", "/invitations", // User Management
|
||||
"/applications", "/providers", "/resources", "/certs", "/keys", // Identity
|
||||
"/roles", "/permissions", "/models", "/adapters", "/enforcers", // Authorization
|
||||
"/agents", "/servers", "/server-store", "/entries", "/sites", "/rules", // LLM AI
|
||||
"/sessions", "/records", "/tokens", "/verifications", // Logging & Auditing
|
||||
"/products", "/orders", "/payments", "/plans", "/pricings", "/subscriptions", "/transactions", // Business
|
||||
"/sysinfo", "/forms", "/syncers", "/webhooks", "/webhook-events", "/tickets", "/swagger", // Admin
|
||||
"/sysinfo", "/forms", "/syncers", "/webhooks", "/tickets", "/swagger", // Admin
|
||||
];
|
||||
|
||||
const count = navItems.filter(item => validMenuItems.includes(item)).length;
|
||||
@@ -218,20 +217,6 @@ class App extends Component {
|
||||
}
|
||||
} else if (uri.includes("/keys")) {
|
||||
return "/keys";
|
||||
} else if (uri.includes("/agents") || uri.includes("/servers") || uri.includes("/entries") || uri.includes("/sites") || uri.includes("/rules")) {
|
||||
if (uri.includes("/agents")) {
|
||||
return "/agents";
|
||||
} else if (uri.includes("/servers")) {
|
||||
return "/servers";
|
||||
} else if (uri.includes("/server-store")) {
|
||||
return "/server-store";
|
||||
} else if (uri.includes("/entries")) {
|
||||
return "/entries";
|
||||
} else if (uri.includes("/sites")) {
|
||||
return "/sites";
|
||||
} else if (uri.includes("/rules")) {
|
||||
return "/rules";
|
||||
}
|
||||
} else if (uri.includes("/roles") || uri.includes("/permissions") || uri.includes("/models") || uri.includes("/adapters") || uri.includes("/enforcers")) {
|
||||
if (uri.includes("/roles")) {
|
||||
return "/roles";
|
||||
@@ -270,16 +255,14 @@ class App extends Component {
|
||||
} else if (uri.includes("/transactions")) {
|
||||
return "/transactions";
|
||||
}
|
||||
} else if (uri.includes("/sysinfo") || uri.includes("/forms") || uri.includes("/syncers") || uri.includes("/webhooks") || uri.includes("/webhook-events") || uri.includes("/tickets")) {
|
||||
} else if (uri.includes("/sysinfo") || uri.includes("/forms") || uri.includes("/syncers") || uri.includes("/webhooks") || uri.includes("/tickets")) {
|
||||
if (uri.includes("/sysinfo")) {
|
||||
return "/sysinfo";
|
||||
} else if (uri.includes("/forms")) {
|
||||
return "/forms";
|
||||
} else if (uri.includes("/syncers")) {
|
||||
return "/syncers";
|
||||
} else if (uri.includes("/webhook-events")) {
|
||||
return "/webhook-events";
|
||||
} else if (uri.includes("/webhooks") || uri.includes("/webhook-events")) {
|
||||
} else if (uri.includes("/webhooks")) {
|
||||
return "/webhooks";
|
||||
} else if (uri.includes("/tickets")) {
|
||||
return "/tickets";
|
||||
@@ -314,15 +297,13 @@ class App extends Component {
|
||||
this.setState({selectedMenuKey: "/orgs"});
|
||||
} 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("/agents") || uri.includes("/servers") || uri.includes("/server-store") || uri.includes("/entries") || uri.includes("/sites") || uri.includes("/rules")) {
|
||||
this.setState({selectedMenuKey: "/gateway"});
|
||||
} else if (uri.includes("/roles") || uri.includes("/permissions") || uri.includes("/models") || uri.includes("/adapters") || uri.includes("/enforcers")) {
|
||||
this.setState({selectedMenuKey: "/auth"});
|
||||
} else if (uri.includes("/records") || uri.includes("/tokens") || uri.includes("/sessions") || uri.includes("/verifications")) {
|
||||
this.setState({selectedMenuKey: "/logs"});
|
||||
} else if (uri.includes("/product-store") || uri.includes("/products") || uri.includes("/orders") || uri.includes("/payments") || uri.includes("/plans") || uri.includes("/pricings") || uri.includes("/subscriptions") || uri.includes("/transactions")) {
|
||||
this.setState({selectedMenuKey: "/business"});
|
||||
} else if (uri.includes("/sysinfo") || uri.includes("/forms") || uri.includes("/syncers") || uri.includes("/webhooks") || uri.includes("/webhook-events") || uri.includes("/tickets")) {
|
||||
} else if (uri.includes("/sysinfo") || uri.includes("/forms") || uri.includes("/syncers") || uri.includes("/webhooks") || uri.includes("/tickets")) {
|
||||
this.setState({selectedMenuKey: "/admin"});
|
||||
} else if (uri.includes("/signup")) {
|
||||
this.setState({selectedMenuKey: "/signup"});
|
||||
|
||||
@@ -594,20 +594,6 @@ class ApplicationEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.application.organization !== "built-in" ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Enable guest signin"), i18next.t("application:Enable guest signin - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.application.enableGuestSignin} onChange={checked => {
|
||||
this.updateApplicationField("enableGuestSignin", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Enable exclusive signin"), i18next.t("application:Enable exclusive signin - Tooltip"))} :
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
import * as Cookie from "cookie";
|
||||
|
||||
export let DefaultApplication = "app-built-in";
|
||||
export const DefaultApplication = "app-built-in";
|
||||
|
||||
export let ShowGithubCorner = false;
|
||||
export let IsDemoMode = false;
|
||||
@@ -38,7 +38,7 @@ export const CustomFooter = null;
|
||||
export let AiAssistantUrl = "https://ai.casbin.com";
|
||||
|
||||
// Maximum number of navbar items before switching from flat to grouped menu
|
||||
export let MaxItemsForFlatMenu = 7;
|
||||
export const MaxItemsForFlatMenu = 7;
|
||||
|
||||
// setConfig updates the frontend configuration from backend
|
||||
export function setConfig(config) {
|
||||
@@ -63,12 +63,6 @@ export function setConfig(config) {
|
||||
if (config.aiAssistantUrl !== undefined) {
|
||||
AiAssistantUrl = config.aiAssistantUrl;
|
||||
}
|
||||
if (config.defaultApplication !== undefined) {
|
||||
DefaultApplication = config.defaultApplication;
|
||||
}
|
||||
if (config.maxItemsForFlatMenu !== undefined) {
|
||||
MaxItemsForFlatMenu = config.maxItemsForFlatMenu;
|
||||
}
|
||||
}
|
||||
|
||||
export function initConfigFromCookie() {
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
// 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, Card, Col, Input, Row, Select} from "antd";
|
||||
import * as EntryBackend from "./backend/EntryBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import EntryMessageViewer from "./EntryMessageViewer";
|
||||
|
||||
const {Option} = Select;
|
||||
class EntryEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
entryName: props.match.params.entryName,
|
||||
owner: props.match.params.organizationName,
|
||||
entry: null,
|
||||
organizations: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getEntry();
|
||||
this.getOrganizations();
|
||||
}
|
||||
|
||||
getEntry() {
|
||||
EntryBackend.getEntry(this.state.entry?.owner || this.state.owner, this.state.entryName)
|
||||
.then((res) => {
|
||||
if (res.data === null) {
|
||||
this.props.history.push("/404");
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
entry: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to get")}: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
if (Setting.isAdminUser(this.props.account)) {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
organizations: res.data || [],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateEntryField(key, value) {
|
||||
const entry = this.state.entry;
|
||||
entry[key] = value;
|
||||
this.setState({
|
||||
entry: entry,
|
||||
});
|
||||
}
|
||||
|
||||
submitEntryEdit(willExit) {
|
||||
const entry = Setting.deepCopy(this.state.entry);
|
||||
EntryBackend.updateEntry(this.state.owner, this.state.entryName, entry)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully modified"));
|
||||
if (willExit) {
|
||||
this.props.history.push("/entries");
|
||||
} else {
|
||||
this.setState({
|
||||
mode: "edit",
|
||||
owner: entry.owner,
|
||||
entryName: entry.name,
|
||||
}, () => {this.getEntry();});
|
||||
this.props.history.push(`/entries/${entry.owner}/${entry.name}`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to update")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteEntry() {
|
||||
EntryBackend.deleteEntry(this.state.entry)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
|
||||
this.props.history.push("/entries");
|
||||
} 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}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderEntry() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{this.state.mode === "add" ? i18next.t("entry:New Entry") : i18next.t("entry:Edit Entry")}
|
||||
<Button onClick={() => this.submitEntryEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitEntryEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteEntry()}>{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.entry.owner} onChange={(value => {this.updateEntryField("owner", 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}>
|
||||
{i18next.t("general:Name")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.entry.name} onChange={e => {
|
||||
this.updateEntryField("name", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Display name")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.entry.displayName} onChange={e => {
|
||||
this.updateEntryField("displayName", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Provider")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
{this.state.entry.provider ? (
|
||||
<Link to={`/providers/${this.state.entry.owner}/${this.state.entry.provider}`}>
|
||||
{this.state.entry.provider}
|
||||
</Link>
|
||||
) : null}
|
||||
</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"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
{this.state.entry.application ? (
|
||||
<Link to={`/applications/${this.state.entry.owner}/${this.state.entry.application}`}>
|
||||
{this.state.entry.application}
|
||||
</Link>
|
||||
) : null}
|
||||
</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} >
|
||||
<Input disabled value={this.state.entry.type ?? ""} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Client IP")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled value={this.state.entry.clientIp ?? ""} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:User agent")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled value={this.state.entry.userAgent ?? ""} />
|
||||
</Col>
|
||||
</Row>
|
||||
<EntryMessageViewer entry={this.state.entry} labelSpan={(Setting.isMobile()) ? 22 : 2} contentSpan={22} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.entry === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.renderEntry()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default EntryEditPage;
|
||||
@@ -1,261 +0,0 @@
|
||||
// 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, Popover, Table} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as EntryBackend from "./backend/EntryBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
import Editor from "./common/Editor";
|
||||
|
||||
class EntryListPage extends BaseListPage {
|
||||
newEntry() {
|
||||
const randomHex = Math.random().toString(16).slice(2, 18);
|
||||
const owner = Setting.getRequestOrganization(this.props.account);
|
||||
return {
|
||||
owner: owner,
|
||||
name: randomHex,
|
||||
createdTime: moment().format(),
|
||||
displayName: randomHex,
|
||||
provider: "",
|
||||
application: "",
|
||||
type: "",
|
||||
clientIp: "",
|
||||
userAgent: "",
|
||||
message: "",
|
||||
};
|
||||
}
|
||||
|
||||
addEntry() {
|
||||
const newEntry = this.newEntry();
|
||||
EntryBackend.addEntry(newEntry)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push({pathname: `/entries/${newEntry.owner}/${newEntry.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}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteEntry(i) {
|
||||
EntryBackend.deleteEntry(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}`);
|
||||
});
|
||||
}
|
||||
|
||||
fetch = (params = {}) => {
|
||||
const field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
if (!params.pagination) {
|
||||
params.pagination = {current: 1, pageSize: 10};
|
||||
}
|
||||
|
||||
this.setState({loading: true});
|
||||
EntryBackend.getEntries(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 {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to get")}: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
renderTable(entries) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Organization"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
width: "130px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("owner"),
|
||||
render: (text) => {
|
||||
return (
|
||||
<Link to={`/organizations/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "160px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("name"),
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Link to={`/entries/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Created time"),
|
||||
dataIndex: "createdTime",
|
||||
key: "createdTime",
|
||||
width: "180px",
|
||||
sorter: true,
|
||||
render: (text) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Provider"),
|
||||
dataIndex: "provider",
|
||||
key: "provider",
|
||||
width: "160px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("provider"),
|
||||
render: (text, record) => {
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Link to={`/providers/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: "140px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("type"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Client IP"),
|
||||
dataIndex: "clientIp",
|
||||
key: "clientIp",
|
||||
width: "140px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("clientIp", (row, highlightContent) => (
|
||||
<a target="_blank" rel="noreferrer" href={`https://db-ip.com/${row.text}`}>
|
||||
{highlightContent}
|
||||
</a>
|
||||
)),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:User agent"),
|
||||
dataIndex: "userAgent",
|
||||
key: "userAgent",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("userAgent"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("payment:Message"),
|
||||
dataIndex: "message",
|
||||
key: "message",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("message"),
|
||||
render: (text) => {
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Popover placement="topRight" content={() => (
|
||||
<Editor value={text} readOnly={true} />
|
||||
)} title="" trigger="hover">
|
||||
{Setting.getShortText(text, 60)}
|
||||
</Popover>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "op",
|
||||
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(`/entries/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<PopconfirmModal title={i18next.t("general:Sure to delete") + `: ${record.name} ?`} onConfirm={() => this.deleteEntry(index)}>
|
||||
</PopconfirmModal>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const filteredColumns = Setting.filterTableColumns(columns, this.props.formItems ?? this.state.formItems);
|
||||
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 (
|
||||
<Table
|
||||
scroll={{x: "max-content"}}
|
||||
dataSource={entries}
|
||||
columns={filteredColumns}
|
||||
rowKey={record => `${record.owner}/${record.name}`}
|
||||
pagination={{...this.state.pagination, ...paginationProps}}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
size="middle"
|
||||
bordered
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Entries")}
|
||||
<Button type="primary" size="small" onClick={() => this.addEntry()}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default EntryListPage;
|
||||
@@ -1,596 +0,0 @@
|
||||
// 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 {Alert, Button, Col, Descriptions, Drawer, Row, Table} from "antd";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import Editor from "./common/Editor";
|
||||
|
||||
class EntryMessageViewer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
traceSpanDrawerVisible: false,
|
||||
selectedTraceSpan: null,
|
||||
};
|
||||
}
|
||||
|
||||
getEditorMaxWidth() {
|
||||
return Setting.isMobile() ? window.innerWidth - 60 : 560;
|
||||
}
|
||||
|
||||
getLabelSpan() {
|
||||
return this.props.labelSpan ?? (Setting.isMobile() ? 22 : 2);
|
||||
}
|
||||
|
||||
getContentSpan() {
|
||||
return this.props.contentSpan ?? 22;
|
||||
}
|
||||
|
||||
formatJsonValue(value) {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(value), null, 2);
|
||||
} catch (e) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
formatAnyValue(value) {
|
||||
if (value === undefined || value === null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (value.stringValue !== undefined) {
|
||||
return value.stringValue;
|
||||
}
|
||||
|
||||
if (value.boolValue !== undefined) {
|
||||
return `${value.boolValue}`;
|
||||
}
|
||||
|
||||
if (value.intValue !== undefined) {
|
||||
return `${value.intValue}`;
|
||||
}
|
||||
|
||||
if (value.doubleValue !== undefined) {
|
||||
return `${value.doubleValue}`;
|
||||
}
|
||||
|
||||
if (value.bytesValue !== undefined) {
|
||||
return value.bytesValue;
|
||||
}
|
||||
|
||||
if (Array.isArray(value.arrayValue?.values)) {
|
||||
return value.arrayValue.values.map(item => this.formatAnyValue(item)).join(", ");
|
||||
}
|
||||
|
||||
if (Array.isArray(value.kvlistValue?.values)) {
|
||||
return value.kvlistValue.values.map(item => `${item?.key || "-"}=${this.formatAnyValue(item?.value)}`).join(", ");
|
||||
}
|
||||
|
||||
return this.formatJsonValue(value);
|
||||
}
|
||||
|
||||
getAnyValueType(value) {
|
||||
if (value === undefined || value === null) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
if (value.stringValue !== undefined) {
|
||||
return "string";
|
||||
}
|
||||
|
||||
if (value.boolValue !== undefined) {
|
||||
return "bool";
|
||||
}
|
||||
|
||||
if (value.intValue !== undefined) {
|
||||
return "int";
|
||||
}
|
||||
|
||||
if (value.doubleValue !== undefined) {
|
||||
return "double";
|
||||
}
|
||||
|
||||
if (value.bytesValue !== undefined) {
|
||||
return "bytes";
|
||||
}
|
||||
|
||||
if (Array.isArray(value.arrayValue?.values)) {
|
||||
return "array";
|
||||
}
|
||||
|
||||
if (Array.isArray(value.kvlistValue?.values)) {
|
||||
return "map";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
getAttributeValue(attributes, key) {
|
||||
const attribute = attributes.find(item => item?.key === key);
|
||||
return attribute ? this.formatAnyValue(attribute.value) : "";
|
||||
}
|
||||
|
||||
renderTraceAttributeTable(attributes) {
|
||||
const rows = Array.isArray(attributes) ? attributes.map((attribute, index) => ({
|
||||
key: `${attribute?.key || "attribute"}-${index}`,
|
||||
name: attribute?.key || "-",
|
||||
type: this.getAnyValueType(attribute?.value),
|
||||
value: this.formatAnyValue(attribute?.value) || "-",
|
||||
})) : [];
|
||||
|
||||
if (rows.length === 0) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("user:Keys"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: 220,
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: i18next.t("user:Values"),
|
||||
dataIndex: "value",
|
||||
key: "value",
|
||||
render: value => (
|
||||
<div style={{whiteSpace: "pre-wrap", wordBreak: "break-word"}}>
|
||||
{value}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
scroll={{x: "max-content"}}
|
||||
size="small"
|
||||
bordered
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
rowKey="key"
|
||||
pagination={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
normalizeIntegerString(value) {
|
||||
const text = `${value ?? ""}`.trim();
|
||||
if (!/^\d+$/.test(text)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return text.replace(/^0+(?=\d)/, "");
|
||||
}
|
||||
|
||||
subtractIntegerStrings(minuend, subtrahend) {
|
||||
const left = this.normalizeIntegerString(minuend);
|
||||
const right = this.normalizeIntegerString(subtrahend);
|
||||
if (!left || !right) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (left.length < right.length || (left.length === right.length && left < right)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let borrow = 0;
|
||||
let result = "";
|
||||
|
||||
for (let i = 0; i < left.length; i++) {
|
||||
const leftDigit = Number(left[left.length - 1 - i]);
|
||||
const rightDigit = Number(right[right.length - 1 - i] || 0);
|
||||
let digit = leftDigit - borrow - rightDigit;
|
||||
if (digit < 0) {
|
||||
digit += 10;
|
||||
borrow = 1;
|
||||
} else {
|
||||
borrow = 0;
|
||||
}
|
||||
|
||||
result = `${digit}${result}`;
|
||||
}
|
||||
|
||||
return result.replace(/^0+(?=\d)/, "");
|
||||
}
|
||||
|
||||
getTraceData() {
|
||||
if (this.props.entry?.type !== "trace") {
|
||||
return {spans: [], error: ""};
|
||||
}
|
||||
|
||||
const message = this.props.entry?.message?.trim();
|
||||
if (!message) {
|
||||
return {spans: [], error: ""};
|
||||
}
|
||||
|
||||
try {
|
||||
const trace = JSON.parse(message);
|
||||
return {
|
||||
spans: this.flattenTraceSpans(trace),
|
||||
error: "",
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
spans: [],
|
||||
error: e.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
flattenTraceSpans(trace) {
|
||||
const spans = [];
|
||||
const resourceSpans = Array.isArray(trace?.resourceSpans) ? trace.resourceSpans : [];
|
||||
|
||||
resourceSpans.forEach((resourceSpan, resourceIndex) => {
|
||||
const resource = resourceSpan?.resource ?? {};
|
||||
const resourceAttributes = Array.isArray(resource.attributes) ? resource.attributes : [];
|
||||
const serviceName = this.getAttributeValue(resourceAttributes, "service.name");
|
||||
const scopeSpans = Array.isArray(resourceSpan?.scopeSpans) ? resourceSpan.scopeSpans : [];
|
||||
|
||||
scopeSpans.forEach((scopeSpan, scopeIndex) => {
|
||||
const scope = scopeSpan?.scope ?? {};
|
||||
const scopeSchemaUrl = scopeSpan?.schemaUrl ?? "";
|
||||
const innerSpans = Array.isArray(scopeSpan?.spans) ? scopeSpan.spans : [];
|
||||
|
||||
innerSpans.forEach((span, spanIndex) => {
|
||||
spans.push({
|
||||
key: `${resourceIndex}-${scopeIndex}-${spanIndex}-${span?.spanId ?? span?.name ?? "span"}`,
|
||||
resource,
|
||||
resourceAttributes,
|
||||
resourceSchemaUrl: resourceSpan?.schemaUrl ?? "",
|
||||
scope,
|
||||
scopeSchemaUrl,
|
||||
serviceName,
|
||||
span,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
formatTraceTimestamp(unixNano) {
|
||||
if (!unixNano) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
const normalized = this.normalizeIntegerString(unixNano);
|
||||
if (!normalized) {
|
||||
return `${unixNano}`;
|
||||
}
|
||||
|
||||
const padded = normalized.padStart(9, "0");
|
||||
const milliseconds = Number(padded.slice(0, -6) || "0");
|
||||
const nanoseconds = padded.slice(-9);
|
||||
const date = new Date(milliseconds);
|
||||
if (!Number.isFinite(milliseconds) || Number.isNaN(date.getTime())) {
|
||||
return `${unixNano}`;
|
||||
}
|
||||
|
||||
return `${Setting.getFormattedDate(date.toISOString())}.${nanoseconds}`;
|
||||
}
|
||||
|
||||
getSpanDuration(span) {
|
||||
if (!span?.startTimeUnixNano || !span?.endTimeUnixNano) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
const duration = this.subtractIntegerStrings(span.endTimeUnixNano, span.startTimeUnixNano);
|
||||
if (!duration) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
const durationNumber = Number(duration);
|
||||
if (!Number.isFinite(durationNumber)) {
|
||||
return `${duration} ns`;
|
||||
}
|
||||
|
||||
if (durationNumber >= 1e9) {
|
||||
return `${(durationNumber / 1e9).toFixed(3)} s`;
|
||||
}
|
||||
|
||||
if (durationNumber >= 1e6) {
|
||||
return `${(durationNumber / 1e6).toFixed(3)} ms`;
|
||||
}
|
||||
|
||||
if (durationNumber >= 1e3) {
|
||||
return `${(durationNumber / 1e3).toFixed(3)} us`;
|
||||
}
|
||||
|
||||
return `${durationNumber} ns`;
|
||||
}
|
||||
|
||||
getSpanStatus(span) {
|
||||
const code = span?.status?.code ?? "";
|
||||
const message = span?.status?.message ?? "";
|
||||
|
||||
if (code && message) {
|
||||
return `${code}: ${message}`;
|
||||
}
|
||||
|
||||
return code || message || "-";
|
||||
}
|
||||
|
||||
getScopeName(scope) {
|
||||
if (!scope?.name) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
return scope.version ? `${scope.name}@${scope.version}` : scope.name;
|
||||
}
|
||||
|
||||
openTraceSpanDrawer(traceSpan) {
|
||||
this.setState({
|
||||
traceSpanDrawerVisible: true,
|
||||
selectedTraceSpan: traceSpan,
|
||||
});
|
||||
}
|
||||
|
||||
closeTraceSpanDrawer = () => {
|
||||
this.setState({
|
||||
traceSpanDrawerVisible: false,
|
||||
selectedTraceSpan: null,
|
||||
});
|
||||
};
|
||||
|
||||
renderJsonEditor(value) {
|
||||
const formattedValue = this.formatJsonValue(value);
|
||||
if (!formattedValue) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
return (
|
||||
<Editor
|
||||
value={formattedValue}
|
||||
lang="json"
|
||||
fillHeight
|
||||
fillWidth
|
||||
maxWidth={this.getEditorMaxWidth()}
|
||||
dark
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderMessageEditor() {
|
||||
return (
|
||||
<Editor
|
||||
value={this.formatJsonValue(this.props.entry?.message) || ""}
|
||||
lang="json"
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderTraceSpans() {
|
||||
if (this.props.entry?.type !== "trace") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {spans, error} = this.getTraceData();
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: ["span", "name"],
|
||||
key: "name",
|
||||
width: 220,
|
||||
render: (text, record) => (
|
||||
<Button type="link" style={{padding: 0}} onClick={() => this.openTraceSpanDrawer(record)}>
|
||||
{text || record.span?.spanId || "-"}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18next.t("entry:Service", {defaultValue: "Service"}),
|
||||
dataIndex: "serviceName",
|
||||
key: "serviceName",
|
||||
width: 180,
|
||||
render: value => value || "-",
|
||||
},
|
||||
{
|
||||
title: i18next.t("entry:Span ID", {defaultValue: "Span ID"}),
|
||||
dataIndex: ["span", "spanId"],
|
||||
key: "spanId",
|
||||
width: 180,
|
||||
render: value => value || "-",
|
||||
},
|
||||
{
|
||||
title: i18next.t("entry:Start time", {defaultValue: "Start time"}),
|
||||
dataIndex: ["span", "startTimeUnixNano"],
|
||||
key: "startTimeUnixNano",
|
||||
width: 220,
|
||||
render: value => this.formatTraceTimestamp(value),
|
||||
},
|
||||
{
|
||||
title: i18next.t("entry:Duration", {defaultValue: "Duration"}),
|
||||
key: "duration",
|
||||
width: 120,
|
||||
render: (_, record) => this.getSpanDuration(record.span),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
key: "action",
|
||||
width: 100,
|
||||
render: (_, record) => (
|
||||
<Button type="link" onClick={() => this.openTraceSpanDrawer(record)}>
|
||||
{i18next.t("general:View")}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={this.getLabelSpan()}>
|
||||
{i18next.t("entry:Trace spans", {defaultValue: "Trace spans"})}:
|
||||
</Col>
|
||||
<Col span={this.getContentSpan()} >
|
||||
{error ? (
|
||||
<Alert
|
||||
message={`${i18next.t("entry:Failed to parse trace message", {defaultValue: "Failed to parse trace message"})}: ${error}`}
|
||||
type="warning"
|
||||
showIcon
|
||||
/>
|
||||
) : (
|
||||
<Table
|
||||
scroll={{x: "max-content"}}
|
||||
size="small"
|
||||
bordered
|
||||
columns={columns}
|
||||
dataSource={spans}
|
||||
rowKey="key"
|
||||
onRow={record => ({
|
||||
onClick: () => this.openTraceSpanDrawer(record),
|
||||
style: {cursor: "pointer"},
|
||||
})}
|
||||
pagination={spans.length > 10 ? {pageSize: 10, hideOnSinglePage: true} : false}
|
||||
locale={{emptyText: i18next.t("entry:No spans", {defaultValue: "No spans"})}}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
{this.renderTraceSpanDrawer()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderTraceSpanDrawer() {
|
||||
const traceSpan = this.state.selectedTraceSpan;
|
||||
const span = traceSpan?.span;
|
||||
if (!traceSpan) {
|
||||
return (
|
||||
<Drawer
|
||||
title={i18next.t("entry:Span detail", {defaultValue: "Span detail"})}
|
||||
width={Setting.isMobile() ? "100%" : 760}
|
||||
placement="right"
|
||||
destroyOnClose
|
||||
onClose={this.closeTraceSpanDrawer}
|
||||
open={this.state.traceSpanDrawerVisible}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={`${i18next.t("entry:Span detail", {defaultValue: "Span detail"})}: ${span?.name || span?.spanId || "-"}`}
|
||||
width={Setting.isMobile() ? "100%" : 760}
|
||||
placement="right"
|
||||
destroyOnClose
|
||||
onClose={this.closeTraceSpanDrawer}
|
||||
open={this.state.traceSpanDrawerVisible}
|
||||
>
|
||||
<Descriptions
|
||||
bordered
|
||||
size="small"
|
||||
column={1}
|
||||
layout={Setting.isMobile() ? "vertical" : "horizontal"}
|
||||
style={{padding: "12px", height: "100%", overflowY: "auto"}}
|
||||
>
|
||||
<Descriptions.Item label={i18next.t("general:Name")}>
|
||||
{span?.name || "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("entry:Service", {defaultValue: "Service"})}>
|
||||
{traceSpan.serviceName || "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("provider:Scope", {defaultValue: "Scope"})}>
|
||||
{this.getScopeName(traceSpan.scope)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("general:Type")}>
|
||||
{span?.kind || "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("entry:Trace ID", {defaultValue: "Trace ID"})}>
|
||||
{span?.traceId || "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("entry:Span ID", {defaultValue: "Span ID"})}>
|
||||
{span?.spanId || "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("entry:Parent Span ID", {defaultValue: "Parent Span ID"})}>
|
||||
{span?.parentSpanId || "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("general:Status")}>
|
||||
{this.getSpanStatus(span)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("entry:Start time", {defaultValue: "Start time"})}>
|
||||
{this.formatTraceTimestamp(span?.startTimeUnixNano)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("subscription:End time", {defaultValue: "End time"})}>
|
||||
{this.formatTraceTimestamp(span?.endTimeUnixNano)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("entry:Duration", {defaultValue: "Duration"})}>
|
||||
{this.getSpanDuration(span)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("entry:Resource schema URL", {defaultValue: "Resource schema URL"})}>
|
||||
{traceSpan.resourceSchemaUrl || "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("entry:Scope schema URL", {defaultValue: "Scope schema URL"})}>
|
||||
{traceSpan.scopeSchemaUrl || "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("entry:Resource attributes", {defaultValue: "Resource attributes"})}>
|
||||
{this.renderTraceAttributeTable(traceSpan.resourceAttributes)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("entry:Span attributes", {defaultValue: "Span attributes"})}>
|
||||
{this.renderTraceAttributeTable(span?.attributes)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("webhook:Events", {defaultValue: "Events"})}>
|
||||
{this.renderJsonEditor(span?.events)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("entry:Links", {defaultValue: "Links"})}>
|
||||
{this.renderJsonEditor(span?.links)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("entry:Raw span", {defaultValue: "Raw span"})}>
|
||||
{this.renderJsonEditor(span)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
{this.renderTraceSpans()}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={this.getLabelSpan()}>
|
||||
{i18next.t("payment:Message")}:
|
||||
</Col>
|
||||
<Col span={this.getContentSpan()} >
|
||||
{this.renderMessageEditor()}
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default EntryMessageViewer;
|
||||
@@ -19,7 +19,6 @@ import * as InvitationBackend from "./backend/InvitationBackend";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import * as Conf from "./Conf";
|
||||
import i18next from "i18next";
|
||||
import copy from "copy-to-clipboard";
|
||||
import * as GroupBackend from "./backend/GroupBackend";
|
||||
@@ -112,7 +111,7 @@ class InvitationEditPage extends React.Component {
|
||||
copySignupLink() {
|
||||
let defaultApplication;
|
||||
if (this.state.invitation.owner === "built-in") {
|
||||
defaultApplication = Conf.DefaultApplication;
|
||||
defaultApplication = "app-built-in";
|
||||
} else {
|
||||
const selectedOrganization = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner);
|
||||
defaultApplication = selectedOrganization.defaultApplication;
|
||||
|
||||
@@ -85,7 +85,6 @@ import FormEditPage from "./FormEditPage";
|
||||
import SyncerListPage from "./SyncerListPage";
|
||||
import SyncerEditPage from "./SyncerEditPage";
|
||||
import WebhookListPage from "./WebhookListPage";
|
||||
import WebhookEventListPage from "./WebhookEventListPage";
|
||||
import WebhookEditPage from "./WebhookEditPage";
|
||||
import LdapEditPage from "./LdapEditPage";
|
||||
import LdapSyncPage from "./LdapSyncPage";
|
||||
@@ -107,17 +106,12 @@ import TicketListPage from "./TicketListPage";
|
||||
import TicketEditPage from "./TicketEditPage";
|
||||
import * as Cookie from "cookie";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import AgentListPage from "./AgentListPage";
|
||||
import AgentEditPage from "./AgentEditPage";
|
||||
import ServerListPage from "./ServerListPage";
|
||||
import ServerStorePage from "./ServerStorePage";
|
||||
import ServerEditPage from "./ServerEditPage";
|
||||
import EntryListPage from "./EntryListPage";
|
||||
import EntryEditPage from "./EntryEditPage";
|
||||
import SiteListPage from "./SiteListPage";
|
||||
import SiteEditPage from "./SiteEditPage";
|
||||
import RuleListPage from "./RuleListPage";
|
||||
import ServerListPage from "./ServerListPage";
|
||||
import ServerEditPage from "./ServerEditPage";
|
||||
import RuleEditPage from "./RuleEditPage";
|
||||
import RuleListPage from "./RuleListPage";
|
||||
|
||||
function ManagementPage(props) {
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
@@ -338,6 +332,8 @@ function ManagementPage(props) {
|
||||
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} />, [
|
||||
@@ -354,12 +350,10 @@ function ManagementPage(props) {
|
||||
}
|
||||
})));
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sites">{i18next.t("general:LLM AI")}</Link>, "/gateway", <CheckCircleTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/agents">{i18next.t("general:Agents")}</Link>, "/agents"),
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sites">{i18next.t("general:Gateway")}</Link>, "/gateway", <CheckCircleTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/servers">{i18next.t("general:MCP Servers")}</Link>, "/servers"),
|
||||
Setting.getItem(<Link to="/server-store">{i18next.t("general:MCP Store")}</Link>, "/server-store"),
|
||||
Setting.getItem(<Link to="/entries">{i18next.t("general:Entries")}</Link>, "/entries"),
|
||||
Setting.getItem(<Link to="/sites">{i18next.t("general:Sites")}</Link>, "/sites"),
|
||||
Setting.getItem(<Link to="/certs">{i18next.t("general:Certs")}</Link>, "/certs"),
|
||||
Setting.getItem(<Link to="/rules">{i18next.t("general:Rules")}</Link>, "/rules"),
|
||||
]));
|
||||
|
||||
@@ -388,7 +382,6 @@ function ManagementPage(props) {
|
||||
Setting.getItem(<Link to="/forms">{i18next.t("general:Forms")}</Link>, "/forms"),
|
||||
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
|
||||
Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks"),
|
||||
Setting.getItem(<Link to="/webhook-events">{i18next.t("general:Webhook Events")}</Link>, "/webhook-events"),
|
||||
Setting.getItem(<Link to="/tickets">{i18next.t("general:Tickets")}</Link>, "/tickets"),
|
||||
Setting.getItem(<a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>{i18next.t("general:Swagger")}</a>, "/swagger")]));
|
||||
} else {
|
||||
@@ -396,7 +389,6 @@ function ManagementPage(props) {
|
||||
Setting.getItem(<Link to="/forms">{i18next.t("general:Forms")}</Link>, "/forms"),
|
||||
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
|
||||
Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks"),
|
||||
Setting.getItem(<Link to="/webhook-events">{i18next.t("general:Webhook Events")}</Link>, "/webhook-events"),
|
||||
Setting.getItem(<Link to="/tickets">{i18next.t("general:Tickets")}</Link>, "/tickets")]));
|
||||
}
|
||||
|
||||
@@ -504,13 +496,8 @@ function ManagementPage(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="/agents" render={(props) => renderLoginIfNotLoggedIn(<AgentListPage account={account} {...props} />)} />
|
||||
<Route exact path="/agents/:organizationName/:agentName" render={(props) => renderLoginIfNotLoggedIn(<AgentEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/servers" render={(props) => renderLoginIfNotLoggedIn(<ServerListPage account={account} {...props} />)} />
|
||||
<Route exact path="/server-store" render={(props) => renderLoginIfNotLoggedIn(<ServerStorePage account={account} {...props} />)} />
|
||||
<Route exact path="/servers/:organizationName/:serverName" render={(props) => renderLoginIfNotLoggedIn(<ServerEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/entries" render={(props) => renderLoginIfNotLoggedIn(<EntryListPage account={account} {...props} />)} />
|
||||
<Route exact path="/entries/:organizationName/:entryName" render={(props) => renderLoginIfNotLoggedIn(<EntryEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/sites" render={(props) => renderLoginIfNotLoggedIn(<SiteListPage account={account} {...props} />)} />
|
||||
<Route exact path="/sites/:organizationName/:siteName" render={(props) => renderLoginIfNotLoggedIn(<SiteEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/rules" render={(props) => renderLoginIfNotLoggedIn(<RuleListPage account={account} {...props} />)} />
|
||||
@@ -554,7 +541,6 @@ function ManagementPage(props) {
|
||||
<Route exact path="/transactions" render={(props) => renderLoginIfNotLoggedIn(<TransactionListPage account={account} {...props} />)} />
|
||||
<Route exact path="/transactions/:organizationName/:transactionName" render={(props) => renderLoginIfNotLoggedIn(<TransactionEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/webhooks" render={(props) => renderLoginIfNotLoggedIn(<WebhookListPage account={account} {...props} />)} />
|
||||
<Route exact path="/webhook-events" render={(props) => renderLoginIfNotLoggedIn(<WebhookEventListPage account={account} {...props} />)} />
|
||||
<Route exact path="/webhooks/:webhookName" render={(props) => renderLoginIfNotLoggedIn(<WebhookEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/tickets" render={(props) => renderLoginIfNotLoggedIn(<TicketListPage account={account} {...props} />)} />
|
||||
<Route exact path="/tickets/:organizationName/:ticketName" render={(props) => renderLoginIfNotLoggedIn(<TicketEditPage account={account} {...props} />)} />
|
||||
|
||||
@@ -17,7 +17,6 @@ import {Link} from "react-router-dom";
|
||||
import {Button, Modal, Switch, Table, Upload} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as Conf from "./Conf";
|
||||
import * as PermissionBackend from "./backend/PermissionBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
@@ -39,7 +38,7 @@ class PermissionListPage extends BaseListPage {
|
||||
roles: [],
|
||||
domains: [],
|
||||
resourceType: "Application",
|
||||
resources: [Conf.DefaultApplication],
|
||||
resources: ["app-built-in"],
|
||||
actions: ["Read"],
|
||||
effect: "Allow",
|
||||
isEnabled: true,
|
||||
|
||||
@@ -32,36 +32,10 @@ import {renderWeb3ProviderFields} from "./provider/Web3ProviderFields";
|
||||
import {renderStorageProviderFields} from "./provider/StorageProviderFields";
|
||||
import {renderFaceIdProviderFields} from "./provider/FaceIDProviderFields";
|
||||
import {renderIDVerificationProviderFields} from "./provider/IDVerificationProviderFields";
|
||||
import {renderLogProviderFields} from "./provider/LogProviderFields";
|
||||
|
||||
const {Option} = Select;
|
||||
const {TextArea} = Input;
|
||||
|
||||
function isDefaultProviderName(name) {
|
||||
return /^provider_[a-z0-9]+$/.test(name);
|
||||
}
|
||||
|
||||
function isDefaultProviderDisplayName(displayName) {
|
||||
return /^New Provider - [a-z0-9]+$/.test(displayName);
|
||||
}
|
||||
|
||||
function getAutoProviderName(category, type, subType) {
|
||||
const catSlug = category.toLowerCase().replace(/[\s-]+/g, "_").replace(/[^a-z0-9_]/g, "");
|
||||
const typeSlug = type.toLowerCase().replace(/[\s-]+/g, "_").replace(/[^a-z0-9_]/g, "");
|
||||
if (subType) {
|
||||
const subTypeSlug = subType.toLowerCase().replace(/[\s-]+/g, "_").replace(/[^a-z0-9_]/g, "");
|
||||
return `provider_${catSlug}_${typeSlug}_${subTypeSlug}`;
|
||||
}
|
||||
return `provider_${catSlug}_${typeSlug}`;
|
||||
}
|
||||
|
||||
function getAutoProviderDisplayName(category, type, subType) {
|
||||
if (subType) {
|
||||
return `${category} ${type} ${subType}`;
|
||||
}
|
||||
return `${category} ${type}`;
|
||||
}
|
||||
|
||||
const defaultUserMapping = {
|
||||
id: "id",
|
||||
username: "username",
|
||||
@@ -102,8 +76,6 @@ class ProviderEditPage extends React.Component {
|
||||
certs: [],
|
||||
organizations: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
nameNotUserEdited: false,
|
||||
displayNameNotUserEdited: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -142,8 +114,6 @@ class ProviderEditPage extends React.Component {
|
||||
}
|
||||
this.setState({
|
||||
provider: provider,
|
||||
nameNotUserEdited: isDefaultProviderName(provider.name),
|
||||
displayNameNotUserEdited: isDefaultProviderDisplayName(provider.displayName),
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
@@ -484,11 +454,7 @@ class ProviderEditPage extends React.Component {
|
||||
}
|
||||
|
||||
getProviderSubTypeOptions(type) {
|
||||
if (type === "Agent") {
|
||||
return ([
|
||||
{id: "OpenClaw", name: "OpenClaw"},
|
||||
]);
|
||||
} else if (type === "WeCom" || type === "Infoflow") {
|
||||
if (type === "WeCom" || type === "Infoflow") {
|
||||
return (
|
||||
[
|
||||
{id: "Internal", name: i18next.t("provider:Internal")},
|
||||
@@ -682,7 +648,6 @@ class ProviderEditPage extends React.Component {
|
||||
<Col span={22} >
|
||||
<Input value={this.state.provider.name} onChange={e => {
|
||||
this.updateProviderField("name", e.target.value);
|
||||
this.setState({nameNotUserEdited: false});
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -693,7 +658,6 @@ class ProviderEditPage extends React.Component {
|
||||
<Col span={22} >
|
||||
<Input value={this.state.provider.displayName} onChange={e => {
|
||||
this.updateProviderField("displayName", e.target.value);
|
||||
this.setState({displayNameNotUserEdited: false});
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -717,13 +681,10 @@ class ProviderEditPage extends React.Component {
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.category} onChange={(value => {
|
||||
this.updateProviderField("category", value);
|
||||
let defaultType = "";
|
||||
if (value === "OAuth") {
|
||||
defaultType = "Google";
|
||||
this.updateProviderField("type", defaultType);
|
||||
this.updateProviderField("type", "Google");
|
||||
} else if (value === "Email") {
|
||||
defaultType = "Default";
|
||||
this.updateProviderField("type", defaultType);
|
||||
this.updateProviderField("type", "Default");
|
||||
this.updateProviderField("host", "smtp.example.com");
|
||||
this.updateProviderField("port", 465);
|
||||
this.updateProviderField("sslMode", "Auto");
|
||||
@@ -732,52 +693,28 @@ class ProviderEditPage extends React.Component {
|
||||
this.updateProviderField("metadata", Setting.getDefaultInvitationHtmlEmailContent());
|
||||
this.updateProviderField("receiver", this.props.account.email);
|
||||
} else if (value === "SMS") {
|
||||
defaultType = "Twilio SMS";
|
||||
this.updateProviderField("type", defaultType);
|
||||
this.updateProviderField("type", "Twilio SMS");
|
||||
} else if (value === "Storage") {
|
||||
defaultType = "AWS S3";
|
||||
this.updateProviderField("type", defaultType);
|
||||
this.updateProviderField("type", "AWS S3");
|
||||
} else if (value === "SAML") {
|
||||
defaultType = "Keycloak";
|
||||
this.updateProviderField("type", defaultType);
|
||||
this.updateProviderField("type", "Keycloak");
|
||||
} else if (value === "Payment") {
|
||||
defaultType = "PayPal";
|
||||
this.updateProviderField("type", defaultType);
|
||||
this.updateProviderField("type", "PayPal");
|
||||
} else if (value === "Captcha") {
|
||||
defaultType = "Default";
|
||||
this.updateProviderField("type", defaultType);
|
||||
this.updateProviderField("type", "Default");
|
||||
} else if (value === "Web3") {
|
||||
defaultType = "MetaMask";
|
||||
this.updateProviderField("type", defaultType);
|
||||
this.updateProviderField("type", "MetaMask");
|
||||
} else if (value === "Notification") {
|
||||
defaultType = "Telegram";
|
||||
this.updateProviderField("type", defaultType);
|
||||
this.updateProviderField("type", "Telegram");
|
||||
} else if (value === "Face ID") {
|
||||
defaultType = "Alibaba Cloud Facebody";
|
||||
this.updateProviderField("type", defaultType);
|
||||
this.updateProviderField("type", "Alibaba Cloud Facebody");
|
||||
} else if (value === "MFA") {
|
||||
defaultType = "RADIUS";
|
||||
this.updateProviderField("type", defaultType);
|
||||
this.updateProviderField("type", "RADIUS");
|
||||
this.updateProviderField("host", "");
|
||||
this.updateProviderField("port", 1812);
|
||||
} else if (value === "ID Verification") {
|
||||
defaultType = "Jumio";
|
||||
this.updateProviderField("type", defaultType);
|
||||
this.updateProviderField("type", "Jumio");
|
||||
this.updateProviderField("endpoint", "");
|
||||
} else if (value === "Log") {
|
||||
defaultType = "Casdoor Permission Log";
|
||||
this.updateProviderField("type", defaultType);
|
||||
this.updateProviderField("host", "");
|
||||
this.updateProviderField("port", 0);
|
||||
this.updateProviderField("title", "");
|
||||
}
|
||||
if (defaultType) {
|
||||
if (this.state.nameNotUserEdited) {
|
||||
this.updateProviderField("name", getAutoProviderName(value, defaultType, ""));
|
||||
}
|
||||
if (this.state.displayNameNotUserEdited) {
|
||||
this.updateProviderField("displayName", getAutoProviderDisplayName(value, defaultType, ""));
|
||||
}
|
||||
}
|
||||
})}>
|
||||
{
|
||||
@@ -785,7 +722,6 @@ class ProviderEditPage extends React.Component {
|
||||
{id: "Captcha", name: "Captcha"},
|
||||
{id: "Email", name: "Email"},
|
||||
{id: "ID Verification", name: "ID Verification"},
|
||||
{id: "Log", name: "Log"},
|
||||
{id: "MFA", name: "MFA"},
|
||||
{id: "Notification", name: "Notification"},
|
||||
{id: "OAuth", name: "OAuth"},
|
||||
@@ -827,12 +763,6 @@ class ProviderEditPage extends React.Component {
|
||||
this.updateProviderField("method", "GET");
|
||||
this.updateProviderField("title", "");
|
||||
}
|
||||
if (this.state.nameNotUserEdited) {
|
||||
this.updateProviderField("name", getAutoProviderName(this.state.provider.category, value, ""));
|
||||
}
|
||||
if (this.state.displayNameNotUserEdited) {
|
||||
this.updateProviderField("displayName", getAutoProviderDisplayName(this.state.provider.category, value, ""));
|
||||
}
|
||||
})}>
|
||||
{
|
||||
Setting.getProviderTypeOptions(this.state.provider.category)
|
||||
@@ -846,7 +776,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.provider.type !== "WeCom" && this.state.provider.type !== "Infoflow" && this.state.provider.type !== "WeChat" && this.state.provider.type !== "Agent" ? null : (
|
||||
this.state.provider.type !== "WeCom" && this.state.provider.type !== "Infoflow" && this.state.provider.type !== "WeChat" ? null : (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
@@ -855,12 +785,6 @@ class ProviderEditPage extends React.Component {
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.subType} onChange={value => {
|
||||
this.updateProviderField("subType", value);
|
||||
if (this.state.nameNotUserEdited) {
|
||||
this.updateProviderField("name", getAutoProviderName(this.state.provider.category, this.state.provider.type, value));
|
||||
}
|
||||
if (this.state.displayNameNotUserEdited) {
|
||||
this.updateProviderField("displayName", getAutoProviderDisplayName(this.state.provider.category, this.state.provider.type, value));
|
||||
}
|
||||
}}>
|
||||
{
|
||||
this.getProviderSubTypeOptions(this.state.provider.type).map((providerSubType, index) => <Option key={index} value={providerSubType.id}>{providerSubType.name}</Option>)
|
||||
@@ -927,7 +851,6 @@ class ProviderEditPage extends React.Component {
|
||||
(this.state.provider.category === "Captcha" && this.state.provider.type === "Default") ||
|
||||
(this.state.provider.category === "Web3") ||
|
||||
(this.state.provider.category === "MFA") ||
|
||||
(this.state.provider.category === "Log") ||
|
||||
(this.state.provider.category === "Storage" && this.state.provider.type === "Local File System") ||
|
||||
(this.state.provider.category === "SMS" && this.state.provider.type === "Custom HTTP SMS") ||
|
||||
(this.state.provider.category === "Email" && this.state.provider.type === "Custom HTTP Email") ||
|
||||
@@ -1019,9 +942,6 @@ class ProviderEditPage extends React.Component {
|
||||
) : this.state.provider.category === "MFA" ? renderMfaProviderFields(
|
||||
this.state.provider,
|
||||
this.updateProviderField.bind(this)
|
||||
) : this.state.provider.category === "Log" ? renderLogProviderFields(
|
||||
this.state.provider,
|
||||
this.updateProviderField.bind(this)
|
||||
) : this.state.provider.category === "SAML" ? renderSamlProviderFields(
|
||||
this.state.provider,
|
||||
this.updateProviderField.bind(this),
|
||||
@@ -1055,18 +975,16 @@ class ProviderEditPage extends React.Component {
|
||||
this.state.provider,
|
||||
this.updateProviderField.bind(this)
|
||||
) : null}
|
||||
{this.state.provider.category !== "Log" && (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Provider URL"), i18next.t("provider:Provider URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.provider.providerUrl} onChange={e => {
|
||||
this.updateProviderField("providerUrl", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Provider URL"), i18next.t("provider:Provider URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.provider.providerUrl} onChange={e => {
|
||||
this.updateProviderField("providerUrl", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.provider.category === "Captcha" ? renderCaptchaProviderFields(
|
||||
this.state.provider,
|
||||
|
||||
@@ -51,7 +51,7 @@ class ProviderListPage extends BaseListPage {
|
||||
enableSignUp: true,
|
||||
host: "",
|
||||
port: 0,
|
||||
providerUrl: "",
|
||||
providerUrl: "https://github.com/organizations/xxx/settings/applications/1234567",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -120,38 +120,6 @@ class ServerEditPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
syncMcpTool() {
|
||||
const server = Setting.deepCopy(this.state.server);
|
||||
ServerBackend.syncMcpTool(this.state.owner, this.state.serverName, server)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully modified"));
|
||||
this.getServer();
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to update")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
clearMcpTool() {
|
||||
const server = Setting.deepCopy(this.state.server);
|
||||
ServerBackend.syncMcpTool(this.state.owner, this.state.serverName, server, true)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully modified"));
|
||||
this.getServer();
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to update")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteServer() {
|
||||
ServerBackend.deleteServer(this.state.server)
|
||||
.then((res) => {
|
||||
@@ -246,8 +214,6 @@ class ServerEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("general:Tool"), i18next.t("general:Tool - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
{this.state.mode !== "add" ? <Button type="primary" style={{marginBottom: "5px"}} onClick={() => this.syncMcpTool()}>{i18next.t("general:Sync")}</Button> : null}
|
||||
{this.state.mode !== "add" ? <Button style={{marginBottom: "5px", marginLeft: "10px"}} onClick={() => this.clearMcpTool()}>{i18next.t("general:Clear")}</Button> : null}
|
||||
<ToolTable
|
||||
tools={this.state.server?.tools || []}
|
||||
onUpdateTable={(value) => {this.updateServerField("tools", value);}}
|
||||
|
||||
@@ -21,25 +21,8 @@ import * as ServerBackend from "./backend/ServerBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
import ScanServerModal from "./common/modal/ScanServerModal";
|
||||
|
||||
class ServerListPage extends BaseListPage {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
...this.state,
|
||||
scanLoading: false,
|
||||
scanResult: null,
|
||||
scanServers: [],
|
||||
showScanModal: false,
|
||||
scanFilters: {
|
||||
cidrs: ["127.0.0.1/32"],
|
||||
ports: ["1-65535"],
|
||||
paths: ["/", "/mcp", "/sse", "/mcp/sse"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
newServer() {
|
||||
const randomName = Setting.getRandomName();
|
||||
const owner = Setting.getRequestOrganization(this.props.account);
|
||||
@@ -116,93 +99,6 @@ class ServerListPage extends BaseListPage {
|
||||
});
|
||||
};
|
||||
|
||||
scanIntranetServers = (scanRequest) => {
|
||||
this.setState({scanLoading: true});
|
||||
ServerBackend.syncIntranetServers(scanRequest)
|
||||
.then((res) => {
|
||||
this.setState({scanLoading: false});
|
||||
if (res.status === "ok") {
|
||||
const scanResult = res.data ?? {};
|
||||
const scanServers = scanResult.servers ?? [];
|
||||
this.setState({scanResult: scanResult, scanServers: scanServers});
|
||||
Setting.showMessage("success", `${i18next.t("general:Successfully got")}: ${scanServers.length} server(s)`);
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to get")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({scanLoading: false});
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
};
|
||||
|
||||
openScanModal = () => {
|
||||
this.setState({showScanModal: true});
|
||||
};
|
||||
|
||||
closeScanModal = () => {
|
||||
if (this.state.scanLoading) {
|
||||
return;
|
||||
}
|
||||
this.setState({showScanModal: false});
|
||||
};
|
||||
|
||||
submitScan = () => {
|
||||
const cidr = this.state.scanFilters.cidrs
|
||||
.map(item => item.trim())
|
||||
.filter(item => item !== "");
|
||||
const ports = this.state.scanFilters.ports
|
||||
.map(item => `${item}`.trim())
|
||||
.filter(item => item !== "");
|
||||
const paths = this.state.scanFilters.paths
|
||||
.map(item => item.trim())
|
||||
.filter(item => item !== "");
|
||||
|
||||
if (cidr.length === 0) {
|
||||
Setting.showMessage("error", i18next.t("server:Please select at least one IP range"));
|
||||
return;
|
||||
}
|
||||
if (ports.length === 0) {
|
||||
Setting.showMessage("error", i18next.t("server:Please select at least one port"));
|
||||
return;
|
||||
}
|
||||
|
||||
const invalidPort = ports.find(item => !/^\d+$|^\d+\s*-\s*\d+$/.test(item));
|
||||
if (invalidPort !== undefined) {
|
||||
Setting.showMessage("error", `Invalid port expression: ${invalidPort}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.scanIntranetServers({cidr: cidr, ports: ports, paths: paths});
|
||||
};
|
||||
|
||||
addScannedServer = (scanServer) => {
|
||||
const owner = Setting.getRequestOrganization(this.props.account);
|
||||
const randomName = Setting.getRandomName();
|
||||
const newServer = {
|
||||
owner: owner,
|
||||
name: `server_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
displayName: `Scanned MCP ${scanServer.host}:${scanServer.port}`,
|
||||
url: scanServer.url,
|
||||
application: "",
|
||||
};
|
||||
|
||||
ServerBackend.addServer(newServer)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully added"));
|
||||
const {pagination} = this.state;
|
||||
this.fetch({pagination});
|
||||
} 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}`);
|
||||
});
|
||||
};
|
||||
|
||||
renderTable(servers) {
|
||||
const columns = [
|
||||
{
|
||||
@@ -298,40 +194,23 @@ class ServerListPage extends BaseListPage {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
scroll={{x: "max-content"}}
|
||||
dataSource={servers}
|
||||
columns={filteredColumns}
|
||||
rowKey={record => `${record.owner}/${record.name}`}
|
||||
pagination={{...this.state.pagination, ...paginationProps}}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
size="middle"
|
||||
bordered
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("server:Edit MCP Server")}
|
||||
<Button type="primary" size="small" onClick={() => this.addServer()}>{i18next.t("general:Add")}</Button>
|
||||
|
||||
<Button size="small" onClick={this.openScanModal}>{i18next.t("server:Scan server")}</Button>
|
||||
|
||||
<Button size="small" onClick={() => this.props.history.push("/server-store")}>{i18next.t("general:MCP Store")}</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<ScanServerModal
|
||||
open={this.state.showScanModal}
|
||||
loading={this.state.scanLoading}
|
||||
scanFilters={this.state.scanFilters}
|
||||
scanResult={this.state.scanResult}
|
||||
scanServers={this.state.scanServers}
|
||||
onSubmit={this.submitScan}
|
||||
onCancel={this.closeScanModal}
|
||||
onChangeScanFilters={(patch) => this.setState(prevState => ({scanFilters: {...prevState.scanFilters, ...patch}}))}
|
||||
onAddScannedServer={this.addScannedServer}
|
||||
/>
|
||||
</>
|
||||
<Table
|
||||
scroll={{x: "max-content"}}
|
||||
dataSource={servers}
|
||||
columns={filteredColumns}
|
||||
rowKey={record => `${record.owner}/${record.name}`}
|
||||
pagination={{...this.state.pagination, ...paginationProps}}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
size="middle"
|
||||
bordered
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("server:Edit MCP Server")}
|
||||
<Button type="primary" size="small" onClick={() => this.addServer()}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
// 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, Empty, Input, Row, Select, Spin, Tag, Typography} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as ServerBackend from "./backend/ServerBackend";
|
||||
import i18next from "i18next";
|
||||
|
||||
const {Text, Title} = Typography;
|
||||
|
||||
class ServerStorePage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
onlineListLoading: false,
|
||||
onlineServerList: [],
|
||||
creatingOnlineServerId: "",
|
||||
onlineNameFilter: "",
|
||||
onlineCategoryFilter: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchOnlineServers();
|
||||
}
|
||||
|
||||
fetchOnlineServers = () => {
|
||||
this.setState({
|
||||
onlineListLoading: true,
|
||||
onlineNameFilter: "",
|
||||
onlineCategoryFilter: [],
|
||||
});
|
||||
|
||||
ServerBackend.getOnlineServers()
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const onlineServerList = this.normalizeOnlineServers(this.getOnlineServersFromResponse(res.data));
|
||||
this.setState({
|
||||
onlineServerList: onlineServerList,
|
||||
onlineListLoading: false,
|
||||
});
|
||||
} else {
|
||||
this.setState({onlineListLoading: false});
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to get")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({onlineListLoading: false});
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
};
|
||||
|
||||
getOnlineServerName = (onlineServer) => {
|
||||
const source = onlineServer.id || onlineServer.name || `server_${Setting.getRandomName()}`;
|
||||
const normalized = String(source).toLowerCase().replace(/[^a-z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
||||
return normalized || `server_${Setting.getRandomName()}`;
|
||||
};
|
||||
|
||||
createServerFromOnline = (onlineServer) => {
|
||||
const owner = Setting.getRequestOrganization(this.props.account);
|
||||
const serverName = this.getOnlineServerName(onlineServer);
|
||||
const serverUrl = onlineServer.endpoint;
|
||||
|
||||
if (!serverUrl) {
|
||||
Setting.showMessage("error", i18next.t("server:Production endpoint is empty"));
|
||||
return;
|
||||
}
|
||||
|
||||
const randomName = Setting.getRandomName();
|
||||
const newServer = {
|
||||
owner: owner,
|
||||
name: serverName + randomName,
|
||||
createdTime: moment().format(),
|
||||
displayName: onlineServer.name || serverName,
|
||||
url: serverUrl,
|
||||
application: "",
|
||||
};
|
||||
|
||||
this.setState({creatingOnlineServerId: onlineServer.id});
|
||||
ServerBackend.addServer(newServer)
|
||||
.then((res) => {
|
||||
this.setState({creatingOnlineServerId: ""});
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push({pathname: `/servers/${newServer.owner}/${newServer.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 => {
|
||||
this.setState({creatingOnlineServerId: ""});
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
};
|
||||
|
||||
normalizeOnlineServers = (onlineServers) => {
|
||||
return onlineServers.map((server, index) => {
|
||||
const categoriesRaw = [server?.category].filter((category) => typeof category === "string" && category.trim() !== "");
|
||||
|
||||
return {
|
||||
id: server.id ?? `${server.name ?? "server"}-${index}`,
|
||||
name: server.name ?? "",
|
||||
nameText: (server.name ?? "").toLowerCase(),
|
||||
categoriesRaw: categoriesRaw,
|
||||
categoriesLower: categoriesRaw.map((category) => category.toLowerCase()),
|
||||
endpoint: server.endpoints?.production ?? server.endpoint ?? "",
|
||||
description: server.description ?? "",
|
||||
website: server?.maintainer?.website ?? server?.website,
|
||||
};
|
||||
}).filter(server => server.endpoint.startsWith("http"));
|
||||
};
|
||||
|
||||
getWebsiteUrl = (website) => {
|
||||
if (!website) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return /^https?:\/\//i.test(website) ? website : `https://${website}`;
|
||||
};
|
||||
|
||||
getOnlineServersFromResponse = (data) => {
|
||||
if (Array.isArray(data?.servers)) {
|
||||
return data.servers;
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (Array.isArray(data?.data)) {
|
||||
return data.data;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
getOnlineCategoryOptions = () => {
|
||||
const categories = this.state.onlineServerList.flatMap((server) => server.categoriesRaw || []);
|
||||
return [...new Set(categories)].sort((a, b) => a.localeCompare(b)).map((category) => ({label: category, value: category.toLowerCase()}));
|
||||
};
|
||||
|
||||
getFilteredOnlineServers = () => {
|
||||
const nameFilter = this.state.onlineNameFilter.trim().toLowerCase();
|
||||
const categoryFilter = this.state.onlineCategoryFilter;
|
||||
|
||||
return this.state.onlineServerList.filter((server) => {
|
||||
const nameMatched = !nameFilter || server.nameText.includes(nameFilter);
|
||||
const categoryMatched = categoryFilter.length === 0 || categoryFilter.some((category) => server.categoriesLower.includes(category));
|
||||
return nameMatched && categoryMatched;
|
||||
});
|
||||
};
|
||||
|
||||
renderServerCard = (server) => {
|
||||
return (
|
||||
<Col xs={24} sm={12} md={8} lg={6} key={server.id} style={{marginBottom: "16px"}}>
|
||||
<Card
|
||||
title={server.name || "-"}
|
||||
hoverable
|
||||
style={{height: "100%"}}
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
loading={this.state.creatingOnlineServerId === server.id}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
this.createServerFromOnline(server);
|
||||
}}
|
||||
>
|
||||
{i18next.t("general:Add")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div style={{minHeight: "48px", marginBottom: "8px"}}>
|
||||
<Text type="secondary">{server.description || "-"}</Text>
|
||||
</div>
|
||||
<div style={{marginBottom: "8px"}}>
|
||||
<Text strong>{i18next.t("general:Url")}: </Text>
|
||||
{server.website ? (
|
||||
<a target="_blank" rel="noreferrer" href={this.getWebsiteUrl(server.endpoint)}>{server.endpoint}</a>
|
||||
) : (
|
||||
<Text>-</Text>
|
||||
)}
|
||||
</div>
|
||||
<div style={{marginBottom: "8px"}}>
|
||||
<Text strong>{i18next.t("general:Website")}: </Text>
|
||||
{server.website ? (
|
||||
<a target="_blank" rel="noreferrer" href={this.getWebsiteUrl(server.website)}>{server.website}</a>
|
||||
) : (
|
||||
<Text>-</Text>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{(server.categoriesRaw || []).map((category) => <Tag key={`${server.id}-${category}`}>{category}</Tag>)}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const filteredServers = this.getFilteredOnlineServers();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{display: "flex", gap: "8px", marginBottom: "12px"}}>
|
||||
<Input
|
||||
allowClear
|
||||
placeholder={i18next.t("general:Name")}
|
||||
value={this.state.onlineNameFilter}
|
||||
onChange={(e) => this.setState({onlineNameFilter: e.target.value})}
|
||||
/>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
placeholder={i18next.t("general:Category")}
|
||||
value={this.state.onlineCategoryFilter}
|
||||
onChange={(values) => this.setState({onlineCategoryFilter: values})}
|
||||
options={this.getOnlineCategoryOptions()}
|
||||
style={{minWidth: "260px"}}
|
||||
/>
|
||||
<Button onClick={() => this.setState({onlineNameFilter: "", onlineCategoryFilter: []})}>
|
||||
{i18next.t("general:Clear")}
|
||||
</Button>
|
||||
<Button onClick={this.fetchOnlineServers}>
|
||||
{i18next.t("general:Refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
<Title level={4} style={{marginBottom: "12px"}}>{i18next.t("general:MCP Store")}</Title>
|
||||
{this.state.onlineListLoading ? (
|
||||
<div style={{textAlign: "center", padding: "36px 0"}}>
|
||||
<Spin />
|
||||
</div>
|
||||
) : filteredServers.length === 0 ? (
|
||||
<Empty description={i18next.t("general:No data")} />
|
||||
) : (
|
||||
<Row gutter={16}>
|
||||
{filteredServers.map((server) => this.renderServerCard(server))}
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ServerStorePage;
|
||||
@@ -455,24 +455,6 @@ export const OtherProviderInfo = {
|
||||
url: "https://www.aliyun.com/product/idverification",
|
||||
},
|
||||
},
|
||||
Log: {
|
||||
"Casdoor Permission Log": {
|
||||
logo: `${StaticBaseUrl}/img/social_default.png`,
|
||||
url: "https://casdoor.org",
|
||||
},
|
||||
"System Log": {
|
||||
logo: `${StaticBaseUrl}/img/social_default.png`,
|
||||
url: "https://en.wikipedia.org/wiki/Syslog",
|
||||
},
|
||||
"Agent": {
|
||||
logo: `${StaticBaseUrl}/img/social_default.png`,
|
||||
url: "",
|
||||
},
|
||||
"SELinux Log": {
|
||||
logo: `${StaticBaseUrl}/img/social_default.png`,
|
||||
url: "https://github.com/SELinuxProject/selinux",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const UserFields = ["owner", "name", "password", "display_name", "id", "type", "email", "phone", "country_code",
|
||||
@@ -1437,13 +1419,6 @@ export function getProviderTypeOptions(category) {
|
||||
{id: "Jumio", name: "Jumio"},
|
||||
{id: "Alibaba Cloud", name: "Alibaba Cloud"},
|
||||
]);
|
||||
} else if (category === "Log") {
|
||||
return ([
|
||||
{id: "Casdoor Permission Log", name: "Casdoor Permission Log"},
|
||||
{id: "System Log", name: "System Log"},
|
||||
{id: "Agent", name: "Agent"},
|
||||
{id: "SELinux Log", name: "SELinux Log"},
|
||||
]);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class SystemInfo extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
systemInfo: {cpuUsage: [], memoryUsed: 0, memoryTotal: 0, diskUsed: 0, diskTotal: 0, networkSent: 0, networkRecv: 0, networkTotal: 0},
|
||||
systemInfo: {cpuUsage: [], memoryUsed: 0, memoryTotal: 0},
|
||||
versionInfo: {},
|
||||
prometheusInfo: {apiThroughput: [], apiLatency: [], totalThroughput: 0},
|
||||
intervalId: null,
|
||||
@@ -166,25 +166,6 @@ class SystemInfo extends React.Component {
|
||||
<br /> <br />
|
||||
<Progress type="circle" percent={memPercent} strokeColor={getProgressColor(memPercent)} format={p => `${p}%`} />
|
||||
</div>;
|
||||
|
||||
const diskUi = this.state.systemInfo.diskTotal <= 0 ? i18next.t("general:Failed to get") :
|
||||
<div>
|
||||
{Setting.getFriendlyFileSize(this.state.systemInfo.diskUsed)} / {Setting.getFriendlyFileSize(this.state.systemInfo.diskTotal)}
|
||||
<br /> <br />
|
||||
<Progress type="circle" percent={Number((Number(this.state.systemInfo.diskUsed) / Number(this.state.systemInfo.diskTotal) * 100).toFixed(2))} />
|
||||
</div>;
|
||||
|
||||
const networkUi = this.state.systemInfo.networkTotal === undefined || this.state.systemInfo.networkTotal === null ? i18next.t("general:Failed to get") :
|
||||
<div>
|
||||
{i18next.t("system:Sent")}: {Setting.getFriendlyFileSize(this.state.systemInfo.networkSent)}
|
||||
<br />
|
||||
{i18next.t("system:Received")}: {Setting.getFriendlyFileSize(this.state.systemInfo.networkRecv)}
|
||||
<br /> <br />
|
||||
<div style={{fontSize: "16px", fontWeight: "600", color: "rgba(0, 0, 0, 0.85)"}}>
|
||||
{i18next.t("system:Total Throughput")}: {Setting.getFriendlyFileSize(this.state.systemInfo.networkTotal)}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
const latencyUi = this.state.prometheusInfo?.apiLatency === null || this.state.prometheusInfo?.apiLatency?.length <= 0 ? <Spin size="large" /> :
|
||||
<PrometheusInfoTable prometheusInfo={this.state.prometheusInfo} table={"latency"} />;
|
||||
const throughputUi = this.state.prometheusInfo?.apiThroughput === null || this.state.prometheusInfo?.apiThroughput?.length <= 0 ? <Spin size="large" /> :
|
||||
@@ -212,16 +193,6 @@ class SystemInfo extends React.Component {
|
||||
{this.state.loading ? <Spin size="large" /> : memUi}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card id="disk-card" title={i18next.t("system:Disk Usage")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
||||
{this.state.loading ? <Spin size="large" /> : diskUi}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card id="network-card" title={i18next.t("system:Network Usage")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
||||
{this.state.loading ? <Spin size="large" /> : networkUi}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Card id="latency-card" title={i18next.t("system:API Latency")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
||||
{this.state.loading ? <Spin size="large" /> : latencyUi}
|
||||
@@ -273,16 +244,6 @@ class SystemInfo extends React.Component {
|
||||
{this.state.loading ? <Spin size="large" /> : memUi}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Card title={i18next.t("system:Disk Usage")} bordered={true} style={{textAlign: "center", width: "100%"}}>
|
||||
{this.state.loading ? <Spin size="large" /> : diskUi}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Card title={i18next.t("system:Network Usage")} bordered={true} style={{textAlign: "center", width: "100%"}}>
|
||||
{this.state.loading ? <Spin size="large" /> : networkUi}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Card title={i18next.t("system:About Casdoor")} bordered={true} style={{textAlign: "center"}}>
|
||||
<div>{i18next.t("system:An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS")}</div>
|
||||
|
||||
@@ -17,7 +17,6 @@ import {Link} from "react-router-dom";
|
||||
import {Button, Table} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as Conf from "./Conf";
|
||||
import * as TokenBackend from "./backend/TokenBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
@@ -31,7 +30,7 @@ class TokenListPage extends BaseListPage {
|
||||
owner: "admin", // this.props.account.tokenname,
|
||||
name: `token_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
application: Conf.DefaultApplication,
|
||||
application: "app-built-in",
|
||||
organization: organizationName,
|
||||
user: "admin",
|
||||
accessToken: "",
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import i18next from "i18next";
|
||||
import * as Setting from "./Setting";
|
||||
import * as Conf from "./Conf";
|
||||
import {Button, Table} from "antd";
|
||||
import React from "react";
|
||||
import * as TransactionBackend from "./backend/TransactionBackend";
|
||||
@@ -28,7 +27,7 @@ class TransactionListPage extends BaseListPage {
|
||||
return {
|
||||
owner: organizationName,
|
||||
createdTime: moment().format(),
|
||||
application: Conf.DefaultApplication,
|
||||
application: "app-built-in",
|
||||
domain: "https://ai-admin.casibase.com",
|
||||
category: "",
|
||||
type: "chat_id",
|
||||
|
||||
@@ -1,371 +0,0 @@
|
||||
// Copyright 2021 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, Descriptions, Drawer, Result, Table, Tag, Tooltip} from "antd";
|
||||
import i18next from "i18next";
|
||||
import * as Setting from "./Setting";
|
||||
import * as WebhookEventBackend from "./backend/WebhookEventBackend";
|
||||
import Editor from "./common/Editor";
|
||||
|
||||
class WebhookEventListPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
data: [],
|
||||
loading: false,
|
||||
replayingId: "",
|
||||
isAuthorized: true,
|
||||
statusFilter: "",
|
||||
sortField: "",
|
||||
sortOrder: "",
|
||||
detailShow: false,
|
||||
detailRecord: null,
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
showQuickJumper: true,
|
||||
showSizeChanger: true,
|
||||
total: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener("storageOrganizationChanged", this.handleOrganizationChange);
|
||||
this.fetchWebhookEvents(this.state.pagination);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("storageOrganizationChanged", this.handleOrganizationChange);
|
||||
}
|
||||
|
||||
handleOrganizationChange = () => {
|
||||
const pagination = {
|
||||
...this.state.pagination,
|
||||
current: 1,
|
||||
};
|
||||
this.fetchWebhookEvents(pagination, this.state.statusFilter, this.state.sortField, this.state.sortOrder);
|
||||
};
|
||||
|
||||
getStatusTag = (status) => {
|
||||
const statusConfig = {
|
||||
pending: {color: "gold", text: i18next.t("webhook:Pending")},
|
||||
success: {color: "green", text: i18next.t("webhook:Success")},
|
||||
failed: {color: "red", text: i18next.t("webhook:Failed")},
|
||||
retrying: {color: "blue", text: i18next.t("webhook:Retrying")},
|
||||
};
|
||||
|
||||
const config = statusConfig[status] || {color: "default", text: status || i18next.t("webhook:Unknown")};
|
||||
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
};
|
||||
|
||||
getWebhookLink = (webhookName) => {
|
||||
if (!webhookName) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
const shortName = Setting.getShortName(webhookName);
|
||||
|
||||
return (
|
||||
<Tooltip title={webhookName}>
|
||||
<Link to={`/webhooks/${encodeURIComponent(shortName)}`}>
|
||||
{shortName}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
getOrganizationFilter = () => {
|
||||
if (!this.props.account) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account);
|
||||
};
|
||||
|
||||
fetchWebhookEvents = (pagination = this.state.pagination, statusFilter = this.state.statusFilter, sortField = this.state.sortField, sortOrder = this.state.sortOrder) => {
|
||||
this.setState({loading: true});
|
||||
|
||||
WebhookEventBackend.getWebhookEvents("", this.getOrganizationFilter(), pagination.current, pagination.pageSize, "", statusFilter, sortField, sortOrder)
|
||||
.then((res) => {
|
||||
this.setState({loading: false});
|
||||
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
data: res.data || [],
|
||||
statusFilter,
|
||||
sortField,
|
||||
sortOrder,
|
||||
pagination: {
|
||||
...pagination,
|
||||
total: res.data2 ?? 0,
|
||||
},
|
||||
});
|
||||
} else if (Setting.isResponseDenied(res)) {
|
||||
this.setState({isAuthorized: false});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setState({loading: false});
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
};
|
||||
|
||||
replayWebhookEvent = (event) => {
|
||||
const eventId = `${event.owner}/${event.name}`;
|
||||
this.setState({replayingId: eventId});
|
||||
|
||||
WebhookEventBackend.replayWebhookEvent(eventId)
|
||||
.then((res) => {
|
||||
this.setState({replayingId: ""});
|
||||
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", typeof res.data === "string" ? res.data : i18next.t("webhook:Webhook event replay triggered"));
|
||||
this.fetchWebhookEvents(this.state.pagination, this.state.statusFilter, this.state.sortField, this.state.sortOrder);
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("webhook:Failed to replay webhook event")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setState({replayingId: ""});
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
};
|
||||
|
||||
handleTableChange = (pagination, filters, sorter) => {
|
||||
const statusFilter = Array.isArray(filters?.status) ? (filters.status[0] ?? "") : (filters?.status ?? "");
|
||||
const sortField = Array.isArray(sorter) ? "" : sorter?.field ?? "";
|
||||
const sortOrder = Array.isArray(sorter) ? "" : sorter?.order ?? "";
|
||||
const nextPagination = statusFilter !== this.state.statusFilter ? {
|
||||
...pagination,
|
||||
current: 1,
|
||||
} : pagination;
|
||||
|
||||
this.fetchWebhookEvents(nextPagination, statusFilter, sortField, sortOrder);
|
||||
};
|
||||
|
||||
openDetailDrawer = (record) => {
|
||||
this.setState({
|
||||
detailRecord: record,
|
||||
detailShow: true,
|
||||
});
|
||||
};
|
||||
|
||||
closeDetailDrawer = () => {
|
||||
this.setState({
|
||||
detailShow: false,
|
||||
detailRecord: null,
|
||||
});
|
||||
};
|
||||
|
||||
getEditorMaxWidth = () => {
|
||||
return Setting.isMobile() ? window.innerWidth - 80 : 520;
|
||||
};
|
||||
|
||||
jsonStrFormatter = (str) => {
|
||||
if (!str) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(str), null, 2);
|
||||
} catch (e) {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
||||
getDetailField = (field) => {
|
||||
return this.state.detailRecord ? this.state.detailRecord[field] ?? "" : "";
|
||||
};
|
||||
|
||||
renderTable = () => {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("webhook:Webhook Name"),
|
||||
dataIndex: "webhookName",
|
||||
key: "webhookName",
|
||||
width: 220,
|
||||
render: (text) => this.getWebhookLink(text),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Organization"),
|
||||
dataIndex: "organization",
|
||||
key: "organization",
|
||||
width: 160,
|
||||
render: (text) => text ? <Link to={`/organizations/${text}`}>{text}</Link> : "-",
|
||||
},
|
||||
{
|
||||
title: i18next.t("webhook:Status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: 140,
|
||||
filters: [
|
||||
{text: i18next.t("webhook:Pending"), value: "pending"},
|
||||
{text: i18next.t("webhook:Success"), value: "success"},
|
||||
{text: i18next.t("webhook:Failed"), value: "failed"},
|
||||
{text: i18next.t("webhook:Retrying"), value: "retrying"},
|
||||
],
|
||||
filterMultiple: false,
|
||||
filteredValue: this.state.statusFilter ? [this.state.statusFilter] : null,
|
||||
render: (text) => this.getStatusTag(text),
|
||||
},
|
||||
{
|
||||
title: i18next.t("webhook:Attempt Count"),
|
||||
dataIndex: "attemptCount",
|
||||
key: "attemptCount",
|
||||
width: 140,
|
||||
sorter: true,
|
||||
sortOrder: this.state.sortField === "attemptCount" ? this.state.sortOrder : null,
|
||||
},
|
||||
{
|
||||
title: i18next.t("webhook:Next Retry Time"),
|
||||
dataIndex: "nextRetryTime",
|
||||
key: "nextRetryTime",
|
||||
width: 180,
|
||||
sorter: true,
|
||||
sortOrder: this.state.sortField === "nextRetryTime" ? this.state.sortOrder : null,
|
||||
render: (text) => text ? Setting.getFormattedDate(text) : "-",
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "action",
|
||||
key: "action",
|
||||
width: 180,
|
||||
fixed: Setting.isMobile() ? false : "right",
|
||||
render: (_, record) => {
|
||||
const eventId = `${record.owner}/${record.name}`;
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="link"
|
||||
style={{paddingLeft: 0}}
|
||||
onClick={() => this.openDetailDrawer(record)}
|
||||
>
|
||||
{i18next.t("general:View")}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={this.state.replayingId === eventId}
|
||||
onClick={() => this.replayWebhookEvent(record)}
|
||||
>
|
||||
{i18next.t("webhook:Replay")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
rowKey={(record) => `${record.owner}/${record.name}`}
|
||||
columns={columns}
|
||||
dataSource={this.state.data}
|
||||
loading={this.state.loading}
|
||||
pagination={{
|
||||
...this.state.pagination,
|
||||
showTotal: (total) => i18next.t("general:{total} in total").replace("{total}", total),
|
||||
}}
|
||||
scroll={{x: "max-content"}}
|
||||
size="middle"
|
||||
bordered
|
||||
title={() => i18next.t("webhook:Webhook Event Logs")}
|
||||
onChange={this.handleTableChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.state.isAuthorized) {
|
||||
return (
|
||||
<Result
|
||||
status="403"
|
||||
title={`403 ${i18next.t("general:Unauthorized")}`}
|
||||
subTitle={i18next.t("general:Sorry, you do not have permission to access this page or logged in status invalid.")}
|
||||
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.renderTable()}
|
||||
<Drawer
|
||||
title={i18next.t("webhook:Webhook Event Detail")}
|
||||
width={Setting.isMobile() ? "100%" : 720}
|
||||
placement="right"
|
||||
destroyOnClose
|
||||
onClose={this.closeDetailDrawer}
|
||||
open={this.state.detailShow}
|
||||
>
|
||||
<Descriptions
|
||||
bordered
|
||||
size="small"
|
||||
column={1}
|
||||
layout={Setting.isMobile() ? "vertical" : "horizontal"}
|
||||
style={{padding: "12px", height: "100%", overflowY: "auto"}}
|
||||
>
|
||||
<Descriptions.Item label={i18next.t("webhook:Webhook Name")}>
|
||||
{this.getDetailField("webhookName") ? this.getWebhookLink(this.getDetailField("webhookName")) : "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("general:Organization")}>
|
||||
{this.getDetailField("organization") ? (
|
||||
<Link to={`/organizations/${this.getDetailField("organization")}`}>
|
||||
{this.getDetailField("organization")}
|
||||
</Link>
|
||||
) : "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("webhook:Status")}>
|
||||
{this.getStatusTag(this.getDetailField("status"))}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("webhook:Attempt Count")}>
|
||||
{this.getDetailField("attemptCount") || 0}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("webhook:Next Retry Time")}>
|
||||
{this.getDetailField("nextRetryTime") ? Setting.getFormattedDate(this.getDetailField("nextRetryTime")) : "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("webhook:Payload")}>
|
||||
<Editor
|
||||
value={this.jsonStrFormatter(this.getDetailField("payload"))}
|
||||
lang="json"
|
||||
fillHeight
|
||||
fillWidth
|
||||
maxWidth={this.getEditorMaxWidth()}
|
||||
dark
|
||||
readOnly
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("webhook:Last Error")}>
|
||||
<Editor
|
||||
value={this.getDetailField("lastError") || "-"}
|
||||
fillHeight
|
||||
fillWidth
|
||||
maxWidth={this.getEditorMaxWidth()}
|
||||
dark
|
||||
readOnly
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WebhookEventListPage;
|
||||
@@ -698,11 +698,7 @@ class LoginPage extends React.Component {
|
||||
return (
|
||||
<div key={resultItemKey} className="login-languages">
|
||||
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
|
||||
<LanguageSelect
|
||||
languages={application.organizationObj.languages}
|
||||
mode={signinItem.rule}
|
||||
onClick={key => {this.setState({userLang: key});}}
|
||||
/>
|
||||
<LanguageSelect languages={application.organizationObj.languages} onClick={key => {this.setState({userLang: key});}} />
|
||||
</div>
|
||||
);
|
||||
} else if (signinItem.name === "Signin methods") {
|
||||
|
||||
@@ -18,7 +18,6 @@ import {withRouter} from "react-router-dom";
|
||||
import * as AuthBackend from "./AuthBackend";
|
||||
import * as Util from "./Util";
|
||||
import * as Setting from "../Setting";
|
||||
import * as Conf from "../Conf";
|
||||
import i18next from "i18next";
|
||||
import {authConfig} from "./Auth";
|
||||
import {renderLoginPanel} from "../Setting";
|
||||
@@ -54,7 +53,7 @@ class SamlCallback extends React.Component {
|
||||
|
||||
const messages = atob(relayState).split("&");
|
||||
const clientId = messages[0] === "" ? "" : messages[0];
|
||||
const application = messages[0] === "" ? Conf.DefaultApplication : "";
|
||||
const application = messages[0] === "" ? "app-built-in" : "";
|
||||
const state = messages[1];
|
||||
const providerName = messages[2];
|
||||
const redirectUri = messages[3];
|
||||
|
||||
@@ -254,11 +254,6 @@ class SignupPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
getLanguageSelectorMode(application) {
|
||||
const languagesItem = application.signinItems?.find((item) => item.name === "Languages");
|
||||
return languagesItem?.rule;
|
||||
}
|
||||
|
||||
onFinish(values) {
|
||||
const application = this.getApplicationObj();
|
||||
|
||||
@@ -1015,11 +1010,7 @@ class SignupPage extends React.Component {
|
||||
{
|
||||
Setting.renderLogo(application)
|
||||
}
|
||||
<LanguageSelect
|
||||
languages={application.organizationObj.languages}
|
||||
mode={this.getLanguageSelectorMode(application)}
|
||||
style={{top: "55px", right: "5px", position: "absolute"}}
|
||||
/>
|
||||
<LanguageSelect languages={application.organizationObj.languages} style={{top: "55px", right: "5px", position: "absolute"}} />
|
||||
{
|
||||
this.renderForm(application)
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
// 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 getAgents(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-agents?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getAgent(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-agent?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function updateAgent(owner, name, agent) {
|
||||
const newAgent = Setting.deepCopy(agent);
|
||||
return fetch(`${Setting.ServerUrl}/api/update-agent?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newAgent),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addAgent(agent) {
|
||||
const newAgent = Setting.deepCopy(agent);
|
||||
return fetch(`${Setting.ServerUrl}/api/add-agent`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newAgent),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function deleteAgent(agent) {
|
||||
const newAgent = Setting.deepCopy(agent);
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-agent`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newAgent),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// 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 getEntries(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-entries?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getEntry(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-entry?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function updateEntry(owner, name, entry) {
|
||||
const newEntry = Setting.deepCopy(entry);
|
||||
return fetch(`${Setting.ServerUrl}/api/update-entry?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newEntry),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addEntry(entry) {
|
||||
const newEntry = Setting.deepCopy(entry);
|
||||
return fetch(`${Setting.ServerUrl}/api/add-entry`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newEntry),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function deleteEntry(entry) {
|
||||
const newEntry = Setting.deepCopy(entry);
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-entry`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newEntry),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
import * as Setting from "../Setting";
|
||||
import * as Conf from "../Conf";
|
||||
|
||||
export function getResources(owner, user, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-resources?owner=${owner}&user=${user}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
@@ -72,7 +71,7 @@ export function deleteResource(resource, provider = "") {
|
||||
}
|
||||
|
||||
export function uploadResource(owner, user, tag, parent, fullFilePath, file, provider = "") {
|
||||
const application = Conf.DefaultApplication;
|
||||
const application = "app-built-in";
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
return fetch(`${Setting.ServerUrl}/api/upload-resource?owner=${owner}&user=${user}&application=${application}&tag=${tag}&parent=${parent}&fullFilePath=${encodeURIComponent(fullFilePath)}&provider=${provider}`, {
|
||||
|
||||
@@ -21,13 +21,6 @@ export function getServers(owner, page = "", pageSize = "", field = "", value =
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getOnlineServers() {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-online-servers`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getServer(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-server?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "GET",
|
||||
@@ -44,15 +37,6 @@ export function updateServer(owner, name, server) {
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function syncMcpTool(owner, name, server, isCleared = false) {
|
||||
const newServer = Setting.deepCopy(server);
|
||||
return fetch(`${Setting.ServerUrl}/api/sync-mcp-tool?id=${owner}/${encodeURIComponent(name)}&isCleared=${isCleared ? "1" : "0"}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newServer),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addServer(server) {
|
||||
const newServer = Setting.deepCopy(server);
|
||||
return fetch(`${Setting.ServerUrl}/api/add-server`, {
|
||||
@@ -70,11 +54,3 @@ export function deleteServer(server) {
|
||||
body: JSON.stringify(newServer),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function syncIntranetServers(scanRequest) {
|
||||
return fetch(`${Setting.ServerUrl}/api/sync-intranet-servers`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(scanRequest),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
// 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 getWebhookEvents(owner = "", organization = "", page = "", pageSize = "", webhookName = "", status = "", sortField = "", sortOrder = "") {
|
||||
const params = new URLSearchParams({
|
||||
owner,
|
||||
organization,
|
||||
pageSize,
|
||||
p: page,
|
||||
webhookName,
|
||||
status,
|
||||
sortField,
|
||||
sortOrder,
|
||||
});
|
||||
|
||||
return fetch(`${Setting.ServerUrl}/api/get-webhook-events?${params.toString()}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function replayWebhookEvent(eventId) {
|
||||
return fetch(`${Setting.ServerUrl}/api/replay-webhook-event?id=${encodeURIComponent(eventId)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user