forked from casdoor/casdoor
Compare commits
369 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c38a99973 | |||
| 7c26dbb7d0 | |||
| 61bc75b12e | |||
| 18a8694d28 | |||
| 8478543c6b | |||
|
|
25d8595e66 | ||
|
|
3aafa91937 | ||
|
|
0077839549 | ||
|
|
e1ee2ddee8 | ||
|
|
b93be2d3e2 | ||
|
|
77b56a2e40 | ||
|
|
c0591f316e | ||
|
|
6749d46561 | ||
|
|
a4a50f182b | ||
|
|
221d10a172 | ||
|
|
5c051ba03d | ||
|
|
c16f4d2fb5 | ||
|
|
fe185f880c | ||
|
|
b3bed1992b | ||
|
|
be38d178fd | ||
|
|
3eb164e149 | ||
|
|
6c3cd8a74b | ||
|
|
c5ab4eec59 | ||
|
|
e8170884d7 | ||
|
|
729b21e8ae | ||
|
|
bed67a1ff2 | ||
|
|
df5f5def31 | ||
|
|
76c56e9b2d | ||
|
|
f46e229d5b | ||
|
|
112be9714b | ||
|
|
9d85362a24 | ||
|
|
37e2f13d99 | ||
|
|
f35398ea5c | ||
|
|
5a5470d5a3 | ||
|
|
948fc017e1 | ||
|
|
c63184fc67 | ||
|
|
f5f4032b3b | ||
|
|
7006041fa9 | ||
|
|
d7bc2bf052 | ||
|
|
29eeb03f85 | ||
|
|
14b4b557f9 | ||
|
|
49d35ac161 | ||
|
|
5ed9158368 | ||
|
|
2bb728ad7d | ||
|
|
f4665df477 | ||
|
|
12bbecb69d | ||
|
|
a5079cd0c5 | ||
|
|
e361044f86 | ||
|
|
91cdf56636 | ||
|
|
10daed237e | ||
|
|
315a6bb040 | ||
|
|
cef6b85389 | ||
|
|
14a802f2c5 | ||
|
|
40d1f63cd6 | ||
|
|
85c91c50d3 | ||
|
|
0e5f810f2f | ||
|
|
e9c2ec0d6c | ||
|
|
2a8ac578da | ||
|
|
31ce1512df | ||
|
|
bac824cb4f | ||
|
|
1637ca1dfb | ||
|
|
c7ad2052c9 | ||
|
|
117bf608ea | ||
|
|
13e0af4b0a | ||
|
|
e8a0b268dc | ||
|
|
2762390c32 | ||
|
|
a69c4454ca | ||
|
|
c76d0d17ed | ||
|
|
e10706cb6d | ||
|
|
d92b856868 | ||
|
|
d14674e60e | ||
|
|
284dde292a | ||
|
|
ea56cfec2b | ||
|
|
82d7f241bb | ||
|
|
56ac5cd221 | ||
|
|
203a61cfef | ||
|
|
b9500a27d9 | ||
|
|
c979a05c25 | ||
|
|
1e7a2d8dad | ||
|
|
f6a3fb9455 | ||
|
|
9030a06792 | ||
|
|
fffb26deb9 | ||
|
|
fab57364db | ||
|
|
e73cfe8b40 | ||
|
|
facc1ec203 | ||
|
|
6cb9978475 | ||
|
|
f75cee76ae | ||
|
|
c92e553e9b | ||
|
|
a824fc0f3c | ||
|
|
98dea3a15a | ||
|
|
c0d3fdf812 | ||
|
|
1c60a4ddfa | ||
|
|
ac43fb9cac | ||
|
|
2f7e6c1cc2 | ||
|
|
28b76cce76 | ||
|
|
319896267e | ||
|
|
a3698024bc | ||
|
|
8ffca95c59 | ||
|
|
4f68432349 | ||
|
|
17a52da2b8 | ||
|
|
5140053083 | ||
|
|
9b86530763 | ||
|
|
84f289ddc4 | ||
|
|
23cdb279e6 | ||
|
|
ea2408a7d2 | ||
|
|
4ccb28571b | ||
|
|
1439031780 | ||
|
|
2ebe3f1d5d | ||
|
|
0ff862dbc5 | ||
|
|
bb11511029 | ||
|
|
18979caea4 | ||
|
|
a61575f9d1 | ||
|
|
863d86d55f | ||
|
|
b690ee4ea3 | ||
|
|
5b58d8bf16 | ||
|
|
e5d2feb73d | ||
|
|
96359f78c5 | ||
|
|
6f18f67138 | ||
|
|
9038d8ab5b | ||
|
|
b396a69ed7 | ||
|
|
189277f9a2 | ||
|
|
655777f0f1 | ||
|
|
fb0b93873c | ||
|
|
f5af87683d | ||
|
|
df47f5785c | ||
|
|
4879926977 | ||
|
|
7148c9db85 | ||
|
|
29dccbe32f | ||
|
|
65755d3b28 | ||
|
|
239e8bd694 | ||
|
|
d23e8b205b | ||
|
|
1260db8c27 | ||
|
|
1506a5c895 | ||
|
|
7b5f4aefab | ||
|
|
75bc8e6b0d | ||
|
|
5965e75610 | ||
|
|
899c2546cf | ||
|
|
95defad3b1 | ||
|
|
6a263cb5cb | ||
|
|
54d6a59cb6 | ||
|
|
2693c07b3c | ||
|
|
2895c72d32 | ||
|
|
f6129b09c8 | ||
|
|
0bbbb48af1 | ||
|
|
34a8b252d5 | ||
|
|
c756e56f74 | ||
|
|
dbc2a676ba | ||
|
|
74e6b73e7b | ||
|
|
07de8a40d6 | ||
|
|
c6a6ec8869 | ||
|
|
394b3e1372 | ||
|
|
fa93d4eb8b | ||
|
|
47a5fc8b09 | ||
|
|
c1acb7a432 | ||
|
|
c10b2c162f | ||
|
|
41ec8ba44f | ||
|
|
7df722a103 | ||
|
|
04b1ca1157 | ||
|
|
b0fecefeb7 | ||
|
|
167d24fb1f | ||
|
|
dc58ac0503 | ||
|
|
038d021797 | ||
|
|
7ba660fd7f | ||
|
|
b1c31a4a9d | ||
|
|
90d7add503 | ||
|
|
c961e75ad3 | ||
|
|
547189a034 | ||
|
|
be725eda74 | ||
|
|
0765b352c9 | ||
|
|
a2a8b582d9 | ||
|
|
0973652be4 | ||
|
|
fef75715bf | ||
|
|
4f78d56e31 | ||
|
|
712bc756bc | ||
|
|
1c9952e3d9 | ||
|
|
bbaa28133f | ||
|
|
baef7680ea | ||
|
|
d15b66177c | ||
|
|
5ce6bac529 | ||
|
|
0621f35665 | ||
|
|
1ac2490419 | ||
|
|
8c50ada494 | ||
|
|
22da90576e | ||
|
|
b00404cb3a | ||
|
|
2ed27f4f0a | ||
|
|
bf538d5260 | ||
|
|
13ee5fd150 | ||
|
|
04cdd5a012 | ||
|
|
7b4873734b | ||
|
|
8d2290944a | ||
|
|
6a2bba1627 | ||
|
|
07554bbbe5 | ||
|
|
a050403ee5 | ||
|
|
118eb0af80 | ||
|
|
c16aebe642 | ||
|
|
3b8e7c9da2 | ||
|
|
4d5de767b0 | ||
|
|
54bf8eae5c | ||
|
|
1731b74fa0 | ||
|
|
6e1e5dd569 | ||
|
|
b183359daf | ||
|
|
3cb9df3723 | ||
|
|
9d1e5c10d0 | ||
|
|
ef84c4b0b4 | ||
|
|
5a108bd921 | ||
|
|
ac671ec1ee | ||
|
|
7814caf2ab | ||
|
|
f966f4a0f9 | ||
|
|
a4b1a068a8 | ||
|
|
362797678d | ||
|
|
7879e1bf09 | ||
|
|
c246f102c9 | ||
|
|
37d1c4910c | ||
|
|
3bcde7cb7c | ||
|
|
6a90d21941 | ||
|
|
80b4c0b1a7 | ||
|
|
eb5a422026 | ||
|
|
f7bd70e0a3 | ||
|
|
5e7dbe4b56 | ||
|
|
bd1fca2f32 | ||
|
|
3d4cc42f1f | ||
|
|
1836cab44d | ||
|
|
75b18635f7 | ||
|
|
47cd44c7ce | ||
|
|
090ca97dcd | ||
|
|
bed01b31f1 | ||
|
|
c8f8f88d85 | ||
|
|
7acb303995 | ||
|
|
2607f8d3e5 | ||
|
|
481db33e58 | ||
|
|
f556c7e11f | ||
|
|
f590992f28 | ||
|
|
80f9db0fa2 | ||
|
|
0748661d2a | ||
|
|
83552ed143 | ||
|
|
8cb8541f96 | ||
|
|
5b646a726c | ||
|
|
19b9586670 | ||
|
|
73f8d19c5f | ||
|
|
04da531df3 | ||
|
|
d97558051d | ||
|
|
ac55355290 | ||
|
|
a2da380be4 | ||
|
|
ecf8039c5d | ||
|
|
0a6948034c | ||
|
|
442f8fb19e | ||
|
|
b771add9e3 | ||
|
|
df8e9fceea | ||
|
|
d674f0c33d | ||
|
|
1e1b5273d9 | ||
|
|
cf5e88915c | ||
|
|
c8973e6c9e | ||
|
|
87ea451561 | ||
|
|
8f32779b42 | ||
|
|
aba471b4e8 | ||
|
|
72b70c3b03 | ||
|
|
a1c56894c7 | ||
|
|
a9ae9394c7 | ||
|
|
5f0fa5f23e | ||
|
|
f99aa047a9 | ||
|
|
1d22b7ebd0 | ||
|
|
d147053329 | ||
|
|
0f8cd92be4 | ||
|
|
7ea6f1296d | ||
|
|
db8c649f5e | ||
|
|
a06d003589 | ||
|
|
33298e44d4 | ||
|
|
f4d86f8d92 | ||
|
|
af4337a1ae | ||
|
|
81e650df65 | ||
|
|
fcea1e4c07 | ||
|
|
639a8a47b1 | ||
|
|
43f61d4426 | ||
|
|
e90cdb8a74 | ||
|
|
bfe8955250 | ||
|
|
36b9c4602a | ||
|
|
18117833e1 | ||
|
|
78dde97b64 | ||
|
|
3a06c66057 | ||
|
|
aa59901400 | ||
|
|
8e03b2d97c | ||
|
|
d1da9499e8 | ||
|
|
2e7673c015 | ||
|
|
2d1ace427e | ||
|
|
039c12afa3 | ||
|
|
4236160fa7 | ||
|
|
071b5ddec0 | ||
|
|
f46b92d225 | ||
|
|
cc7eb4664c | ||
|
|
1567723e2b | ||
|
|
074253f45e | ||
|
|
23c86e9018 | ||
|
|
f088827a50 | ||
|
|
663815fefe | ||
|
|
0d003d347e | ||
|
|
7d495ca5f2 | ||
|
|
f89495b35c | ||
|
|
4a3aefc5f5 | ||
|
|
15646b23ff | ||
|
|
4b663a437f | ||
|
|
9fb90fbb95 | ||
|
|
65eeaef8a7 | ||
|
|
ecf8e2eb32 | ||
|
|
e49e678d16 | ||
|
|
623ee23285 | ||
|
|
0901a1d5a0 | ||
|
|
58ff2fe69c | ||
|
|
737f44a059 | ||
|
|
32cef8e828 | ||
|
|
9e854abc77 | ||
|
|
9b3343d3db | ||
|
|
5b71725c94 | ||
|
|
59b6854ccc | ||
|
|
0daf67c52c | ||
|
|
4b612269ea | ||
|
|
f438d39720 | ||
|
|
f8df200dbf | ||
|
|
cb1b3b767e | ||
|
|
3bec49f16c | ||
|
|
e28344f0e7 | ||
|
|
93fefed6e8 | ||
|
|
ea9abb2f29 | ||
|
|
337a8c357b | ||
|
|
d8cebfbf04 | ||
|
|
91d5039155 | ||
|
|
5996ee8695 | ||
|
|
8c9331932b | ||
|
|
db594e2096 | ||
|
|
b46b79ee44 | ||
|
|
b9dbbca716 | ||
|
|
313cf6d480 | ||
|
|
0548597d04 | ||
|
|
eb8e26748f | ||
|
|
516a23ab1b | ||
|
|
9887d80e55 | ||
|
|
13dd4337a6 | ||
|
|
36c69a6da1 | ||
|
|
3f4a60096a | ||
|
|
b6240fa356 | ||
|
|
d61f06b053 | ||
|
|
6fe785b6a4 | ||
|
|
cccddea67e | ||
|
|
83b8c5477a | ||
|
|
ac0e069f71 | ||
|
|
4b25e56048 | ||
|
|
39740e3d6c | ||
|
|
87c5bf3855 | ||
|
|
c4a28acbd8 | ||
|
|
ee26b896f6 | ||
|
|
4a8cb9535e | ||
|
|
387a22d5f8 | ||
|
|
36cadded1c | ||
|
|
7d130392d9 | ||
|
|
f82c90b901 | ||
|
|
1a08d6514e | ||
|
|
4d5bf09b36 | ||
|
|
f050deada7 | ||
|
|
dee94666e0 | ||
|
|
b84b7d787b | ||
|
|
d425183137 | ||
|
|
ff7fcd277c | ||
|
|
ed5c0b2713 | ||
|
|
eb60e43192 | ||
|
|
d0170532e6 | ||
|
|
7ddb87cdf8 | ||
|
|
fac45f5ac7 | ||
|
|
266d361244 | ||
|
|
b454ab1931 | ||
|
|
ff39b6f186 |
31
.gitea/workflows/build.yml
Normal file
31
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Build & Push Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- custom
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: gromlab.ru
|
||||
username: ${{ secrets.CR_USER }}
|
||||
password: ${{ secrets.CR_TOKEN }}
|
||||
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
target: STANDARD
|
||||
push: true
|
||||
tags: |
|
||||
gromlab.ru/gromov/casdoor:latest
|
||||
gromlab.ru/gromov/casdoor:${{ github.sha }}
|
||||
152
.github/workflows/build.yml
vendored
152
.github/workflows/build.yml
vendored
@@ -1,5 +1,8 @@
|
||||
name: Build
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.25.8"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -7,7 +10,6 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
go-tests:
|
||||
name: Running Go tests
|
||||
runs-on: ubuntu-latest
|
||||
@@ -24,7 +26,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.23'
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache-dependency-path: ./go.mod
|
||||
- name: Tests
|
||||
run: |
|
||||
@@ -34,26 +36,32 @@ 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
|
||||
- name: Upload build artifacts
|
||||
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: frontend-build-${{ github.run_id }}
|
||||
path: ./web/build
|
||||
|
||||
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: '1.23'
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache-dependency-path: ./go.mod
|
||||
- run: go version
|
||||
- name: Build
|
||||
@@ -64,27 +72,28 @@ 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: '1.23'
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: false
|
||||
|
||||
# gen a dummy config file
|
||||
- run: touch dummy.yml
|
||||
- name: Sync vendor tree
|
||||
run: go mod vendor
|
||||
|
||||
# CI and local `make lint` both use the repo's gofumpt-only golangci-lint config.
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
uses: golangci/golangci-lint-action@v9.2.0
|
||||
with:
|
||||
version: latest
|
||||
args: --disable-all -c dummy.yml -E=gofumpt --max-same-issues=0 --timeout 5m --modules-download-mode=mod
|
||||
version: v2.11.4
|
||||
args: --config .golangci.yml ./...
|
||||
|
||||
e2e:
|
||||
name: e2e-test
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ go-tests ]
|
||||
needs: [go-tests]
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
@@ -98,7 +107,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.23'
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache-dependency-path: ./go.mod
|
||||
- name: start backend
|
||||
run: nohup go run ./main.go > /tmp/backend.log 2>&1 &
|
||||
@@ -123,7 +132,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
|
||||
@@ -131,7 +140,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
|
||||
|
||||
@@ -146,39 +155,100 @@ jobs:
|
||||
name: cypress-videos
|
||||
path: ./web/cypress/videos
|
||||
|
||||
release-and-push:
|
||||
name: Release And Push
|
||||
tag-release:
|
||||
name: Create Tag
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
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 }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create Tag with Semantic Release
|
||||
id: semantic
|
||||
uses: cycjimmy/semantic-release-action@v4
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
github-release:
|
||||
name: GitHub Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && needs.tag-release.outputs.new-release-published == 'true'
|
||||
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:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Download frontend build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: frontend-build-${{ github.run_id }}
|
||||
path: ./web/build
|
||||
|
||||
- name: Prepare Go caches
|
||||
run: |
|
||||
echo "GOMODCACHE=$RUNNER_TEMP/gomod" >> $GITHUB_ENV
|
||||
echo "GOCACHE=$RUNNER_TEMP/gocache" >> $GITHUB_ENV
|
||||
go clean -cache -modcache -testcache -fuzzcache
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: "~> v2"
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
docker-release:
|
||||
name: Docker Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && needs.tag-release.outputs.new-release-published == 'true'
|
||||
needs: [tag-release]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: -1
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Fetch Previous version
|
||||
id: get-previous-tag
|
||||
uses: actions-ecosystem/action-get-latest-tag@v1.6.0
|
||||
|
||||
- name: Release
|
||||
run: yarn global add semantic-release@17.4.4 && semantic-release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Fetch Current version
|
||||
id: get-current-tag
|
||||
uses: actions-ecosystem/action-get-latest-tag@v1.6.0
|
||||
|
||||
- name: Decide Should_Push Or Not
|
||||
id: should_push
|
||||
run: |
|
||||
old_version=${{steps.get-previous-tag.outputs.tag}}
|
||||
new_version=${{steps.get-current-tag.outputs.tag }}
|
||||
new_version=${{ needs.tag-release.outputs.new-release-version }}
|
||||
|
||||
old_array=(${old_version//\./ })
|
||||
new_array=(${new_version//\./ })
|
||||
@@ -217,7 +287,7 @@ jobs:
|
||||
target: STANDARD
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: casbin/casdoor:${{steps.get-current-tag.outputs.tag }},casbin/casdoor:latest
|
||||
tags: casbin/casdoor:${{ needs.tag-release.outputs.new-release-version }},casbin/casdoor:latest
|
||||
|
||||
- name: Push All In One Version to Docker Hub
|
||||
uses: docker/build-push-action@v3
|
||||
@@ -227,21 +297,21 @@ jobs:
|
||||
target: ALLINONE
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: casbin/casdoor-all-in-one:${{steps.get-current-tag.outputs.tag }},casbin/casdoor-all-in-one:latest
|
||||
tags: casbin/casdoor-all-in-one:${{ needs.tag-release.outputs.new-release-version }},casbin/casdoor-all-in-one:latest
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
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
|
||||
if: steps.should_push.outputs.push=='true'
|
||||
run: |
|
||||
# Set the appVersion and version of the chart to the current tag
|
||||
sed -i "s/appVersion: .*/appVersion: ${{steps.get-current-tag.outputs.tag }}/g" ./charts/casdoor/Chart.yaml
|
||||
sed -i "s/version: .*/version: ${{steps.get-current-tag.outputs.tag }}/g" ./charts/casdoor/Chart.yaml
|
||||
sed -i "s/appVersion: .*/appVersion: ${{ needs.tag-release.outputs.new-release-version }}/g" ./charts/casdoor/Chart.yaml
|
||||
sed -i "s/version: .*/version: ${{ needs.tag-release.outputs.new-release-version }}/g" ./charts/casdoor/Chart.yaml
|
||||
|
||||
REGISTRY=oci://registry-1.docker.io/casbin
|
||||
cd charts/casdoor
|
||||
@@ -255,6 +325,6 @@ jobs:
|
||||
git config --global user.name "casbin-bot"
|
||||
git config --global user.email "bot@casbin.org"
|
||||
git add Chart.yaml index.yaml
|
||||
git commit -m "chore(helm): bump helm charts appVersion to ${{steps.get-current-tag.outputs.tag }}"
|
||||
git tag ${{steps.get-current-tag.outputs.tag }}
|
||||
git commit -m "chore(helm): bump helm charts appVersion to ${{ needs.tag-release.outputs.new-release-version }}"
|
||||
git tag ${{ needs.tag-release.outputs.new-release-version }}
|
||||
git push origin HEAD:master --follow-tags
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,6 +20,7 @@ bin/
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/settings.json
|
||||
.claude
|
||||
|
||||
tmp/
|
||||
tmpFiles/
|
||||
|
||||
@@ -1,42 +1,26 @@
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- deadcode
|
||||
- dupl
|
||||
- errcheck
|
||||
- goconst
|
||||
- gocyclo
|
||||
- gofmt
|
||||
- goimports
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- lll
|
||||
- misspell
|
||||
- nakedret
|
||||
- prealloc
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- revive
|
||||
- exportloopref
|
||||
version: "2"
|
||||
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
|
||||
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:
|
||||
enable:
|
||||
- gofumpt
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
55
.goreleaser.yaml
Normal file
55
.goreleaser.yaml
Normal file
@@ -0,0 +1,55 @@
|
||||
# This is an example .goreleaser.yml file with some sensible defaults.
|
||||
# Make sure to check the documentation at https://goreleaser.com
|
||||
|
||||
# The lines below are called `modelines`. See `:help modeline`
|
||||
# Feel free to remove those if you don't want/need to use them.
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
# You may remove this if you don't use go modules.
|
||||
- go mod tidy
|
||||
# you may remove this if you don't need go generate
|
||||
#- go generate ./...
|
||||
- go test -v -run TestGetVersionInfo ./util/system_test.go ./util/system.go ./util/variable.go
|
||||
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
# this name template makes the OS and Arch compatible with the results of `uname`.
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_
|
||||
{{- title .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
# use zip for windows archives
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- src: 'web/build'
|
||||
dst: './web/build'
|
||||
- src: 'conf/app.conf'
|
||||
dst: './conf/app.conf'
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
35
Dockerfile
35
Dockerfile
@@ -1,14 +1,25 @@
|
||||
FROM --platform=$BUILDPLATFORM node:18.19.0 AS FRONT
|
||||
FROM --platform=$BUILDPLATFORM node:20.20.1 AS FRONT
|
||||
WORKDIR /web
|
||||
|
||||
# Copy only dependency files first for better caching
|
||||
COPY ./web/package.json ./web/yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile --network-timeout 1000000
|
||||
|
||||
# Copy source files and build
|
||||
COPY ./web .
|
||||
RUN yarn install --frozen-lockfile --network-timeout 1000000 && NODE_OPTIONS="--max-old-space-size=4096" yarn run build
|
||||
RUN NODE_OPTIONS="--max-old-space-size=4096" yarn run build
|
||||
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.23.12 AS BACK
|
||||
FROM --platform=$BUILDPLATFORM golang:1.25.8 AS BACK
|
||||
WORKDIR /go/src/casdoor
|
||||
|
||||
# Copy only go.mod and go.sum first for dependency caching
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source files
|
||||
COPY . .
|
||||
|
||||
RUN ./build.sh
|
||||
RUN go test -v -run TestGetVersionInfo ./util/system_test.go ./util/system.go > version_info.txt
|
||||
|
||||
FROM alpine:latest AS STANDARD
|
||||
LABEL MAINTAINER="https://casdoor.org/"
|
||||
@@ -34,35 +45,25 @@ WORKDIR /
|
||||
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/server_${BUILDX_ARCH} ./server
|
||||
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/swagger ./swagger
|
||||
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/conf/app.conf ./conf/app.conf
|
||||
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/version_info.txt ./go/src/casdoor/version_info.txt
|
||||
COPY --from=FRONT --chown=$USER:$USER /web/build ./web/build
|
||||
|
||||
ENTRYPOINT ["/server"]
|
||||
|
||||
|
||||
FROM debian:latest AS db
|
||||
RUN apt update \
|
||||
&& apt install -y \
|
||||
mariadb-server \
|
||||
mariadb-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
FROM db AS ALLINONE
|
||||
FROM debian:latest AS ALLINONE
|
||||
LABEL MAINTAINER="https://casdoor.org/"
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ENV BUILDX_ARCH="${TARGETOS:-linux}_${TARGETARCH:-amd64}"
|
||||
|
||||
RUN apt update
|
||||
RUN apt install -y ca-certificates && update-ca-certificates
|
||||
RUN apt install -y ca-certificates lsof && update-ca-certificates
|
||||
|
||||
WORKDIR /
|
||||
COPY --from=BACK /go/src/casdoor/server_${BUILDX_ARCH} ./server
|
||||
COPY --from=BACK /go/src/casdoor/swagger ./swagger
|
||||
COPY --from=BACK /go/src/casdoor/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
COPY --from=BACK /go/src/casdoor/conf/app.conf ./conf/app.conf
|
||||
COPY --from=BACK /go/src/casdoor/version_info.txt ./go/src/casdoor/version_info.txt
|
||||
COPY --from=FRONT /web/build ./web/build
|
||||
|
||||
ENTRYPOINT ["/bin/bash"]
|
||||
|
||||
8
Makefile
8
Makefile
@@ -90,12 +90,12 @@ deps: ## Run dependencies for local development
|
||||
docker compose up -d db
|
||||
|
||||
lint-install: ## Install golangci-lint
|
||||
@# 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
|
||||
@# 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
|
||||
|
||||
lint: ## Run golangci-lint
|
||||
lint: vendor ## Run golangci-lint
|
||||
@echo "---lint---"
|
||||
golangci-lint run --modules-download-mode=vendor ./...
|
||||
golangci-lint run ./...
|
||||
|
||||
##@ Deployment
|
||||
|
||||
|
||||
16
README.md
16
README.md
@@ -1,5 +1,5 @@
|
||||
<h1 align="center" style="border-bottom: none;">📦⚡️ Casdoor</h1>
|
||||
<h3 align="center">An open-source UI-first Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML, CAS, LDAP, SCIM, WebAuthn, TOTP, MFA and RADIUS</h3>
|
||||
<h3 align="center">An open-source AI-first Identity and Access Management (IAM) /AI MCP gateway and auth server with web UI supporting MCP, A2A, OAuth 2.1, OIDC, SAML, CAS, LDAP, SCIM, WebAuthn, TOTP, MFA, Face ID, Google Workspace, Azure AD</h3>
|
||||
<p align="center">
|
||||
<a href="#badge">
|
||||
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
|
||||
@@ -42,20 +42,6 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<sup>Sponsored by</sup>
|
||||
<br>
|
||||
<a href="https://stytch.com/docs?utm_source=oss-sponsorship&utm_medium=paid_sponsorship&utm_campaign=casbin">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.casbin.org/img/stytch-white.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://cdn.casbin.org/img/stytch-charcoal.png">
|
||||
<img src="https://cdn.casbin.org/img/stytch-charcoal.png" width="275">
|
||||
</picture>
|
||||
</a><br/>
|
||||
<a href="https://stytch.com/docs?utm_source=oss-sponsorship&utm_medium=paid_sponsorship&utm_campaign=casbin"><b>Build auth with fraud prevention, faster.</b><br/> Try Stytch for API-first authentication, user & org management, multi-tenant SSO, MFA, device fingerprinting, and more.</a>
|
||||
<br>
|
||||
</p>
|
||||
|
||||
## Online demo
|
||||
|
||||
- Read-only site: https://door.casdoor.com (any modification operation will fail)
|
||||
|
||||
@@ -59,6 +59,7 @@ p, *, *, GET, /api/get-qrcode, *, *
|
||||
p, *, *, GET, /api/get-webhook-event, *, *
|
||||
p, *, *, GET, /api/get-captcha-status, *, *
|
||||
p, *, *, *, /api/login/oauth, *, *
|
||||
p, *, *, POST, /api/oauth/register, *, *
|
||||
p, *, *, GET, /api/get-application, *, *
|
||||
p, *, *, GET, /api/get-organization-applications, *, *
|
||||
p, *, *, GET, /api/get-user, *, *
|
||||
@@ -67,22 +68,37 @@ p, *, *, POST, /api/upload-users, *, *
|
||||
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, *, *
|
||||
p, *, *, GET, /api/get-payment, *, *
|
||||
p, *, *, POST, /api/update-payment, *, *
|
||||
p, *, *, POST, /api/invoice-payment, *, *
|
||||
p, *, *, POST, /api/notify-payment, *, *
|
||||
p, *, *, POST, /api/place-order, *, *
|
||||
p, *, *, POST, /api/cancel-order, *, *
|
||||
p, *, *, POST, /api/pay-order, *, *
|
||||
p, *, *, POST, /api/unlink, *, *
|
||||
p, *, *, POST, /api/set-password, *, *
|
||||
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, *, *
|
||||
p, *, *, GET, /.well-known/oauth-authorization-server, *, *
|
||||
p, *, *, GET, /.well-known/oauth-protected-resource, *, *
|
||||
p, *, *, GET, /.well-known/webfinger, *, *
|
||||
p, *, *, *, /.well-known/jwks, *, *
|
||||
p, *, *, GET, /.well-known/:application/openid-configuration, *, *
|
||||
p, *, *, GET, /.well-known/:application/oauth-authorization-server, *, *
|
||||
p, *, *, GET, /.well-known/:application/oauth-protected-resource, *, *
|
||||
p, *, *, GET, /.well-known/:application/webfinger, *, *
|
||||
p, *, *, *, /.well-known/:application/jwks, *, *
|
||||
p, *, *, GET, /api/get-saml-login, *, *
|
||||
@@ -110,6 +126,7 @@ p, *, *, GET, /api/run-casbin-command, *, *
|
||||
p, *, *, POST, /api/refresh-engines, *, *
|
||||
p, *, *, GET, /api/get-invitation-info, *, *
|
||||
p, *, *, GET, /api/faceid-signin-begin, *, *
|
||||
p, *, *, GET, /api/kerberos-login, *, *
|
||||
`
|
||||
|
||||
sa := stringadapter.NewAdapter(ruleText)
|
||||
@@ -129,7 +146,15 @@ p, *, *, GET, /api/faceid-signin-begin, *, *
|
||||
}
|
||||
}
|
||||
|
||||
func IsAllowed(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
|
||||
func IsAllowed(subOwner string, subName string, method string, urlPath string, objOwner string, objName string, extraInfo map[string]interface{}) bool {
|
||||
if urlPath == "/api/mcp" {
|
||||
if detailPath, ok := extraInfo["detailPathUrl"].(string); ok {
|
||||
if detailPath == "initialize" || detailPath == "notifications/initialized" || detailPath == "ping" || detailPath == "tools/list" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if conf.IsDemoMode() {
|
||||
if !isAllowedInDemoMode(subOwner, subName, method, urlPath, objOwner, objName) {
|
||||
return false
|
||||
@@ -154,7 +179,7 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
|
||||
return true
|
||||
}
|
||||
|
||||
if user.IsAdmin && (subOwner == objOwner || (objOwner == "admin")) {
|
||||
if user.IsAdmin && subOwner == objOwner {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
107
certificate/account.go
Normal file
107
certificate/account.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// Copyright 2021 The casbin 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 certificate
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
|
||||
"github.com/casbin/lego/v4/acme"
|
||||
"github.com/casbin/lego/v4/certcrypto"
|
||||
"github.com/casbin/lego/v4/lego"
|
||||
"github.com/casbin/lego/v4/registration"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
Email string
|
||||
Registration *registration.Resource
|
||||
Key crypto.PrivateKey
|
||||
}
|
||||
|
||||
/** Implementation of the registration.User interface **/
|
||||
|
||||
// GetEmail returns the email address for the account.
|
||||
func (a *Account) GetEmail() string {
|
||||
return a.Email
|
||||
}
|
||||
|
||||
// GetPrivateKey returns the private RSA account key.
|
||||
func (a *Account) GetPrivateKey() crypto.PrivateKey {
|
||||
return a.Key
|
||||
}
|
||||
|
||||
// GetRegistration returns the server registration.
|
||||
func (a *Account) GetRegistration() *registration.Resource {
|
||||
return a.Registration
|
||||
}
|
||||
|
||||
func getLegoClientAndAccount(email string, privateKey string, devMode bool) (*lego.Client, *Account, error) {
|
||||
key, err := decodeEccKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
account := &Account{
|
||||
Email: email,
|
||||
Key: key,
|
||||
}
|
||||
|
||||
config := lego.NewConfig(account)
|
||||
if devMode {
|
||||
config.CADirURL = lego.LEDirectoryStaging
|
||||
} else {
|
||||
config.CADirURL = lego.LEDirectoryProduction
|
||||
}
|
||||
|
||||
config.Certificate.KeyType = certcrypto.RSA2048
|
||||
config.HTTPClient = proxy.ProxyHttpClient
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return client, account, err
|
||||
}
|
||||
|
||||
// GetAcmeClient Incoming an email ,a privatekey and a Boolean value that controls the opening of the test environment
|
||||
// When this function is started for the first time, it will initialize the account-related configuration,
|
||||
// After initializing the configuration, It will try to obtain an account based on the private key,
|
||||
// if it fails, it will create an account based on the private key.
|
||||
// This account will be used during the running of the program
|
||||
func GetAcmeClient(email string, privateKey string, devMode bool) (*lego.Client, error) {
|
||||
// Create a user. New accounts need an email and private key to start.
|
||||
client, account, err := getLegoClientAndAccount(email, privateKey, devMode)
|
||||
|
||||
// try to obtain an account based on the private key
|
||||
account.Registration, err = client.Registration.ResolveAccountByKey()
|
||||
if err != nil {
|
||||
acmeError, ok := err.(*acme.ProblemDetails)
|
||||
if !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if acmeError.Type != "urn:ietf:params:acme:error:accountDoesNotExist" {
|
||||
return nil, acmeError
|
||||
}
|
||||
|
||||
// Failed to get account, so create an account based on the private key.
|
||||
account.Registration, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
47
certificate/account_test.go
Normal file
47
certificate/account_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2021 The casbin 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 !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetClient(t *testing.T) {
|
||||
err := web.LoadAppConfig("ini", "../conf/app.conf")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
proxy.InitHttpClient()
|
||||
|
||||
eccKey := util.ReadStringFromPath("acme_account.key")
|
||||
println(eccKey)
|
||||
|
||||
client, err := GetAcmeClient("acme2@casbin.org", eccKey, false)
|
||||
assert.Nil(t, err)
|
||||
pem, key, err := ObtainCertificateAli(client, "casbin.com", accessKeyId, accessKeySecret)
|
||||
assert.Nil(t, err)
|
||||
println(pem)
|
||||
println()
|
||||
println(key)
|
||||
}
|
||||
20
certificate/conf.go
Normal file
20
certificate/conf.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright 2021 The casbin 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 certificate
|
||||
|
||||
var (
|
||||
accessKeyId = ""
|
||||
accessKeySecret = ""
|
||||
)
|
||||
151
certificate/dns.go
Normal file
151
certificate/dns.go
Normal file
@@ -0,0 +1,151 @@
|
||||
// Copyright 2021 The casbin 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 certificate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/casbin/lego/v4/certificate"
|
||||
"github.com/casbin/lego/v4/challenge/dns01"
|
||||
"github.com/casbin/lego/v4/cmd"
|
||||
"github.com/casbin/lego/v4/lego"
|
||||
"github.com/casbin/lego/v4/providers/dns/alidns"
|
||||
"github.com/casbin/lego/v4/providers/dns/godaddy"
|
||||
)
|
||||
|
||||
type AliConf struct {
|
||||
Domains []string // The domain names for which you want to apply for a certificate
|
||||
AccessKey string // Aliyun account's AccessKey, if this is not empty, Secret is required.
|
||||
Secret string
|
||||
RAMRole string // Use Ramrole to control aliyun account
|
||||
SecurityToken string // Optional
|
||||
Path string // The path to store cert file
|
||||
Timeout int // Maximum waiting time for certificate application, in minutes
|
||||
}
|
||||
|
||||
type GodaddyConf struct {
|
||||
Domains []string // The domain names for which you want to apply for a certificate
|
||||
APIKey string // GoDaddy account's API Key
|
||||
APISecret string
|
||||
Path string // The path to store cert file
|
||||
Timeout int // Maximum waiting time for certificate application, in minutes
|
||||
}
|
||||
|
||||
// getCert Verify domain ownership, then obtain a certificate, and finally store it locally.
|
||||
// Need to pass in an AliConf struct, some parameters are required, other parameters can be left blank
|
||||
func getAliCert(client *lego.Client, conf AliConf) (string, string, error) {
|
||||
if conf.Timeout <= 0 {
|
||||
conf.Timeout = 3
|
||||
}
|
||||
|
||||
config := alidns.NewDefaultConfig()
|
||||
config.PropagationTimeout = time.Duration(conf.Timeout) * time.Minute
|
||||
config.APIKey = conf.AccessKey
|
||||
config.SecretKey = conf.Secret
|
||||
config.RAMRole = conf.RAMRole
|
||||
config.SecurityToken = conf.SecurityToken
|
||||
|
||||
dnsProvider, err := alidns.NewDNSProvider(config)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Choose a local DNS service provider to increase the authentication speed
|
||||
servers := []string{"223.5.5.5:53"}
|
||||
err = client.Challenge.SetDNS01Provider(dnsProvider, dns01.CondOption(len(servers) > 0, dns01.AddRecursiveNameservers(dns01.ParseNameservers(servers))), dns01.DisableCompletePropagationRequirement())
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Obtain the certificate
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: conf.Domains,
|
||||
Bundle: true,
|
||||
}
|
||||
cert, err := client.Certificate.Obtain(request)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return string(cert.Certificate), string(cert.PrivateKey), nil
|
||||
}
|
||||
|
||||
func getGoDaddyCert(client *lego.Client, conf GodaddyConf) (string, string, error) {
|
||||
if conf.Timeout <= 0 {
|
||||
conf.Timeout = 3
|
||||
}
|
||||
|
||||
config := godaddy.NewDefaultConfig()
|
||||
config.PropagationTimeout = time.Duration(conf.Timeout) * time.Minute
|
||||
config.PollingInterval = time.Duration(conf.Timeout) * time.Minute / 9
|
||||
config.APIKey = conf.APIKey
|
||||
config.APISecret = conf.APISecret
|
||||
|
||||
dnsProvider, err := godaddy.NewDNSProvider(config)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Choose a local DNS service provider to increase the authentication speed
|
||||
servers := []string{"223.5.5.5:53"}
|
||||
err = client.Challenge.SetDNS01Provider(dnsProvider, dns01.CondOption(len(servers) > 0, dns01.AddRecursiveNameservers(dns01.ParseNameservers(servers))), dns01.DisableCompletePropagationRequirement())
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Obtain the certificate
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: conf.Domains,
|
||||
Bundle: true,
|
||||
}
|
||||
cert, err := client.Certificate.Obtain(request)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return string(cert.Certificate), string(cert.PrivateKey), nil
|
||||
}
|
||||
|
||||
func ObtainCertificateAli(client *lego.Client, domain string, accessKey string, accessSecret string) (string, string, error) {
|
||||
conf := AliConf{
|
||||
Domains: []string{fmt.Sprintf("*.%s", domain), domain},
|
||||
AccessKey: accessKey,
|
||||
Secret: accessSecret,
|
||||
RAMRole: "",
|
||||
SecurityToken: "",
|
||||
Path: "",
|
||||
Timeout: 3,
|
||||
}
|
||||
return getAliCert(client, conf)
|
||||
}
|
||||
|
||||
func ObtainCertificateGoDaddy(client *lego.Client, domain string, accessKey string, accessSecret string) (string, string, error) {
|
||||
conf := GodaddyConf{
|
||||
Domains: []string{fmt.Sprintf("*.%s", domain), domain},
|
||||
APIKey: accessKey,
|
||||
APISecret: accessSecret,
|
||||
Path: "",
|
||||
Timeout: 3,
|
||||
}
|
||||
return getGoDaddyCert(client, conf)
|
||||
}
|
||||
|
||||
func SaveCert(path, filename string, cert *certificate.Resource) {
|
||||
// Store the certificate file locally
|
||||
certsStorage := cmd.NewCertificatesStorageLib(path, filename, true)
|
||||
certsStorage.CreateRootFolder()
|
||||
certsStorage.SaveResource(cert)
|
||||
}
|
||||
55
certificate/ecc.go
Normal file
55
certificate/ecc.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright 2021 The casbin 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 certificate
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// generateEccKey generates a public and private key pair.(NIST P-256)
|
||||
func generateEccKey() (*ecdsa.PrivateKey, error) {
|
||||
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
}
|
||||
|
||||
// encodeEccKey Return the input private key object as string type private key
|
||||
func encodeEccKey(privateKey *ecdsa.PrivateKey) (string, error) {
|
||||
x509Encoded, err := x509.MarshalECPrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pemEncoded := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: x509Encoded})
|
||||
return string(pemEncoded), nil
|
||||
}
|
||||
|
||||
// decodeEccKey Return the entered private key string as a private key object that can be used
|
||||
func decodeEccKey(pemEncoded string) (*ecdsa.PrivateKey, error) {
|
||||
block, _ := pem.Decode([]byte(pemEncoded))
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("invalid PEM-encoded EC private key")
|
||||
}
|
||||
x509Encoded := block.Bytes
|
||||
privateKey, err := x509.ParseECPrivateKey(x509Encoded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return privateKey, nil
|
||||
}
|
||||
34
certificate/ecc_test.go
Normal file
34
certificate/ecc_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright 2021 The casbin 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 !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGenerateEccKey(t *testing.T) {
|
||||
eccKey, err := generateEccKey()
|
||||
assert.Nil(t, err)
|
||||
eccKeyStr, err := encodeEccKey(eccKey)
|
||||
assert.Nil(t, err)
|
||||
println(eccKeyStr)
|
||||
util.WriteStringToPath(eccKeyStr, "acme_account.key")
|
||||
}
|
||||
@@ -1,37 +1,37 @@
|
||||
appname = casdoor
|
||||
httpport = 8000
|
||||
runmode = dev
|
||||
copyrequestbody = true
|
||||
driverName = mysql
|
||||
dataSourceName = root:123456@tcp(localhost:3306)/
|
||||
dbName = casdoor
|
||||
tableNamePrefix =
|
||||
showSql = false
|
||||
redisEndpoint =
|
||||
defaultStorageProvider =
|
||||
isCloudIntranet = false
|
||||
authState = "casdoor"
|
||||
socks5Proxy = "127.0.0.1:10808"
|
||||
verificationCodeTimeout = 10
|
||||
initScore = 0
|
||||
logPostOnly = true
|
||||
isUsernameLowered = false
|
||||
origin =
|
||||
originFrontend =
|
||||
staticBaseUrl = "https://cdn.casbin.org"
|
||||
isDemoMode = false
|
||||
batchSize = 100
|
||||
enableErrorMask = false
|
||||
enableGzip = true
|
||||
inactiveTimeoutMinutes =
|
||||
ldapServerPort = 389
|
||||
ldapsCertId = ""
|
||||
ldapsServerPort = 636
|
||||
radiusServerPort = 1812
|
||||
radiusDefaultOrganization = "built-in"
|
||||
radiusSecret = "secret"
|
||||
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
|
||||
logConfig = {"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
|
||||
initDataNewOnly = false
|
||||
initDataFile = "./init_data.json"
|
||||
frontendBaseDir = "../cc_0"
|
||||
appname = casdoor
|
||||
httpport = 8000
|
||||
runmode = dev
|
||||
copyrequestbody = true
|
||||
driverName = postgres
|
||||
dataSourceName = "user=casdoor password=casdoor_dev host=localhost port=5434 sslmode=disable dbname=casdoor"
|
||||
dbName = casdoor
|
||||
tableNamePrefix =
|
||||
showSql = false
|
||||
redisEndpoint =
|
||||
defaultStorageProvider =
|
||||
isCloudIntranet = false
|
||||
authState = "casdoor"
|
||||
socks5Proxy = ""
|
||||
verificationCodeTimeout = 10
|
||||
initScore = 0
|
||||
logPostOnly = true
|
||||
isUsernameLowered = false
|
||||
origin = "http://localhost:8000"
|
||||
originFrontend = "http://localhost:7001"
|
||||
staticBaseUrl = "https://cdn.casbin.org"
|
||||
isDemoMode = false
|
||||
batchSize = 100
|
||||
showGithubCorner = false
|
||||
forceLanguage = ""
|
||||
defaultLanguage = "ru"
|
||||
enableErrorMask = false
|
||||
enableGzip = true
|
||||
ldapServerPort = 389
|
||||
ldapsServerPort = 636
|
||||
radiusServerPort = 1812
|
||||
radiusDefaultOrganization = "built-in"
|
||||
radiusSecret = "secret"
|
||||
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
|
||||
logConfig = {"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
|
||||
initDataNewOnly = false
|
||||
initDataFile = "./init_data.json"
|
||||
|
||||
43
conf/app.conf.orig
Normal file
43
conf/app.conf.orig
Normal file
@@ -0,0 +1,43 @@
|
||||
appname = casdoor
|
||||
httpport = 8000
|
||||
runmode = dev
|
||||
copyrequestbody = true
|
||||
driverName = mysql
|
||||
dataSourceName = root:123456@tcp(localhost:3306)/
|
||||
dbName = casdoor
|
||||
tableNamePrefix =
|
||||
showSql = false
|
||||
redisEndpoint =
|
||||
defaultStorageProvider =
|
||||
isCloudIntranet = false
|
||||
authState = "casdoor"
|
||||
socks5Proxy = "127.0.0.1:10808"
|
||||
verificationCodeTimeout = 10
|
||||
initScore = 0
|
||||
logPostOnly = true
|
||||
isUsernameLowered = false
|
||||
origin =
|
||||
originFrontend =
|
||||
staticBaseUrl = "https://cdn.casbin.org"
|
||||
isDemoMode = false
|
||||
batchSize = 100
|
||||
showGithubCorner = false
|
||||
forceLanguage = ""
|
||||
defaultLanguage = "en"
|
||||
aiAssistantUrl = "https://ai.casbin.com"
|
||||
defaultApplication = "app-built-in"
|
||||
maxItemsForFlatMenu = 7
|
||||
enableErrorMask = false
|
||||
enableGzip = true
|
||||
inactiveTimeoutMinutes =
|
||||
ldapServerPort = 389
|
||||
ldapsCertId = ""
|
||||
ldapsServerPort = 636
|
||||
radiusServerPort = 1812
|
||||
radiusDefaultOrganization = "built-in"
|
||||
radiusSecret = "secret"
|
||||
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
|
||||
logConfig = {"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
|
||||
initDataNewOnly = false
|
||||
initDataFile = "./init_data.json"
|
||||
frontendBaseDir = "../cc_0"
|
||||
37
conf/app.dev.conf
Normal file
37
conf/app.dev.conf
Normal file
@@ -0,0 +1,37 @@
|
||||
appname = casdoor
|
||||
httpport = 8000
|
||||
runmode = dev
|
||||
copyrequestbody = true
|
||||
driverName = postgres
|
||||
dataSourceName = "user=casdoor password=casdoor_dev host=localhost port=5434 sslmode=disable dbname=casdoor"
|
||||
dbName = casdoor
|
||||
tableNamePrefix =
|
||||
showSql = false
|
||||
redisEndpoint =
|
||||
defaultStorageProvider =
|
||||
isCloudIntranet = false
|
||||
authState = "casdoor"
|
||||
socks5Proxy = ""
|
||||
verificationCodeTimeout = 10
|
||||
initScore = 0
|
||||
logPostOnly = true
|
||||
isUsernameLowered = false
|
||||
origin = "http://localhost:8000"
|
||||
originFrontend = "http://localhost:7001"
|
||||
staticBaseUrl = "https://cdn.casbin.org"
|
||||
isDemoMode = false
|
||||
batchSize = 100
|
||||
showGithubCorner = false
|
||||
forceLanguage = ""
|
||||
defaultLanguage = "ru"
|
||||
enableErrorMask = false
|
||||
enableGzip = true
|
||||
ldapServerPort = 389
|
||||
ldapsServerPort = 636
|
||||
radiusServerPort = 1812
|
||||
radiusDefaultOrganization = "built-in"
|
||||
radiusSecret = "secret"
|
||||
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
|
||||
logConfig = {"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
|
||||
initDataNewOnly = false
|
||||
initDataFile = "./init_data.json"
|
||||
13
conf/conf.go
13
conf/conf.go
@@ -15,21 +15,25 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego"
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
//go:embed waf.conf
|
||||
var WafConf string
|
||||
|
||||
func init() {
|
||||
// this array contains the beego configuration items that may be modified via env
|
||||
presetConfigItems := []string{"httpport", "appname"}
|
||||
for _, key := range presetConfigItems {
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
err := beego.AppConfig.Set(key, value)
|
||||
err := web.AppConfig.Set(key, value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -42,12 +46,13 @@ func GetConfigString(key string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
res := beego.AppConfig.String(key)
|
||||
res, _ := web.AppConfig.String(key)
|
||||
if res == "" {
|
||||
if key == "staticBaseUrl" {
|
||||
res = "https://cdn.casbin.org"
|
||||
} else if key == "logConfig" {
|
||||
res = fmt.Sprintf("{\"filename\": \"logs/%s.log\", \"maxdays\":99999, \"perm\":\"0770\"}", beego.AppConfig.String("appname"))
|
||||
appname, _ := web.AppConfig.String("appname")
|
||||
res = fmt.Sprintf("{\"filename\": \"logs/%s.log\", \"maxdays\":99999, \"perm\":\"0770\"}", appname)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ package conf
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego"
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
type Quota struct {
|
||||
@@ -34,7 +34,7 @@ func init() {
|
||||
}
|
||||
|
||||
func initQuota() {
|
||||
res := beego.AppConfig.String("quota")
|
||||
res, _ := web.AppConfig.String("quota")
|
||||
if res != "" {
|
||||
err := json.Unmarshal([]byte(res), quota)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/beego/beego"
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestGetConfString(t *testing.T) {
|
||||
os.Setenv("appname", "casbin")
|
||||
os.Setenv("key", "value")
|
||||
|
||||
err := beego.LoadAppConfig("ini", "app.conf")
|
||||
err := web.LoadAppConfig("ini", "app.conf")
|
||||
assert.Nil(t, err)
|
||||
|
||||
for _, scenery := range scenarios {
|
||||
@@ -62,7 +62,7 @@ func TestGetConfInt(t *testing.T) {
|
||||
// do some set up job
|
||||
os.Setenv("httpport", "8001")
|
||||
|
||||
err := beego.LoadAppConfig("ini", "app.conf")
|
||||
err := web.LoadAppConfig("ini", "app.conf")
|
||||
assert.Nil(t, err)
|
||||
|
||||
for _, scenery := range scenarios {
|
||||
@@ -83,7 +83,7 @@ func TestGetConfBool(t *testing.T) {
|
||||
{"Should be return false", "copyrequestbody", true},
|
||||
}
|
||||
|
||||
err := beego.LoadAppConfig("ini", "app.conf")
|
||||
err := web.LoadAppConfig("ini", "app.conf")
|
||||
assert.Nil(t, err)
|
||||
for _, scenery := range scenarios {
|
||||
t.Run(scenery.description, func(t *testing.T) {
|
||||
@@ -102,7 +102,7 @@ func TestGetConfigQuota(t *testing.T) {
|
||||
{"default", &Quota{-1, -1, -1, -1}},
|
||||
}
|
||||
|
||||
err := beego.LoadAppConfig("ini", "app.conf")
|
||||
err := web.LoadAppConfig("ini", "app.conf")
|
||||
assert.Nil(t, err)
|
||||
for _, scenery := range scenarios {
|
||||
quota := GetConfigQuota()
|
||||
@@ -118,7 +118,7 @@ func TestGetConfigLogs(t *testing.T) {
|
||||
{"Default log config", `{"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}`},
|
||||
}
|
||||
|
||||
err := beego.LoadAppConfig("ini", "app.conf")
|
||||
err := web.LoadAppConfig("ini", "app.conf")
|
||||
assert.Nil(t, err)
|
||||
for _, scenery := range scenarios {
|
||||
quota := GetConfigString("logConfig")
|
||||
|
||||
246
conf/waf.conf
Normal file
246
conf/waf.conf
Normal file
@@ -0,0 +1,246 @@
|
||||
# -- Rule engine initialization ----------------------------------------------
|
||||
|
||||
# Enable Coraza, attaching it to every transaction. Use detection
|
||||
# only to start with, because that minimises the chances of post-installation
|
||||
# disruption.
|
||||
#
|
||||
SecRuleEngine DetectionOnly
|
||||
|
||||
|
||||
# -- Request body handling ---------------------------------------------------
|
||||
|
||||
# Allow Coraza to access request bodies. If you don't, Coraza
|
||||
# won't be able to see any POST parameters, which opens a large security
|
||||
# hole for attackers to exploit.
|
||||
#
|
||||
SecRequestBodyAccess On
|
||||
|
||||
# Enable XML request body parser.
|
||||
# Initiate XML Processor in case of xml content-type
|
||||
#
|
||||
SecRule REQUEST_HEADERS:Content-Type "^(?:application(?:/soap\+|/)|text/)xml" \
|
||||
"id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML"
|
||||
|
||||
# Enable JSON request body parser.
|
||||
# Initiate JSON Processor in case of JSON content-type; change accordingly
|
||||
# if your application does not use 'application/json'
|
||||
#
|
||||
SecRule REQUEST_HEADERS:Content-Type "^application/json" \
|
||||
"id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
|
||||
|
||||
# Sample rule to enable JSON request body parser for more subtypes.
|
||||
# Uncomment or adapt this rule if you want to engage the JSON
|
||||
# Processor for "+json" subtypes
|
||||
#
|
||||
#SecRule REQUEST_HEADERS:Content-Type "^application/[a-z0-9.-]+[+]json" \
|
||||
# "id:'200006',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
|
||||
|
||||
# Maximum request body size we will accept for buffering. If you support
|
||||
# file uploads then the value given on the first line has to be as large
|
||||
# as the largest file you are willing to accept. The second value refers
|
||||
# to the size of data, with files excluded. You want to keep that value as
|
||||
# low as practical.
|
||||
#
|
||||
SecRequestBodyLimit 13107200
|
||||
|
||||
SecRequestBodyInMemoryLimit 131072
|
||||
|
||||
# SecRequestBodyNoFilesLimit is currently not supported by Coraza
|
||||
# SecRequestBodyNoFilesLimit 131072
|
||||
|
||||
# What to do if the request body size is above our configured limit.
|
||||
# Keep in mind that this setting will automatically be set to ProcessPartial
|
||||
# when SecRuleEngine is set to DetectionOnly mode in order to minimize
|
||||
# disruptions when initially deploying Coraza.
|
||||
#
|
||||
SecRequestBodyLimitAction Reject
|
||||
|
||||
# Verify that we've correctly processed the request body.
|
||||
# As a rule of thumb, when failing to process a request body
|
||||
# you should reject the request (when deployed in blocking mode)
|
||||
# or log a high-severity alert (when deployed in detection-only mode).
|
||||
#
|
||||
SecRule REQBODY_ERROR "!@eq 0" \
|
||||
"id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2"
|
||||
|
||||
# By default be strict with what we accept in the multipart/form-data
|
||||
# request body. If the rule below proves to be too strict for your
|
||||
# environment consider changing it to detection-only. You are encouraged
|
||||
# _not_ to remove it altogether.
|
||||
#
|
||||
SecRule MULTIPART_STRICT_ERROR "!@eq 0" \
|
||||
"id:'200003',phase:2,t:none,log,deny,status:400, \
|
||||
msg:'Multipart request body failed strict validation: \
|
||||
PE %{REQBODY_PROCESSOR_ERROR}, \
|
||||
BQ %{MULTIPART_BOUNDARY_QUOTED}, \
|
||||
BW %{MULTIPART_BOUNDARY_WHITESPACE}, \
|
||||
DB %{MULTIPART_DATA_BEFORE}, \
|
||||
DA %{MULTIPART_DATA_AFTER}, \
|
||||
HF %{MULTIPART_HEADER_FOLDING}, \
|
||||
LF %{MULTIPART_LF_LINE}, \
|
||||
SM %{MULTIPART_MISSING_SEMICOLON}, \
|
||||
IQ %{MULTIPART_INVALID_QUOTING}, \
|
||||
IP %{MULTIPART_INVALID_PART}, \
|
||||
IH %{MULTIPART_INVALID_HEADER_FOLDING}, \
|
||||
FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'"
|
||||
|
||||
# Did we see anything that might be a boundary?
|
||||
#
|
||||
# Here is a short description about the Coraza Multipart parser: the
|
||||
# parser returns with value 0, if all "boundary-like" line matches with
|
||||
# the boundary string which given in MIME header. In any other cases it returns
|
||||
# with different value, eg. 1 or 2.
|
||||
#
|
||||
# The RFC 1341 descript the multipart content-type and its syntax must contains
|
||||
# only three mandatory lines (above the content):
|
||||
# * Content-Type: multipart/mixed; boundary=BOUNDARY_STRING
|
||||
# * --BOUNDARY_STRING
|
||||
# * --BOUNDARY_STRING--
|
||||
#
|
||||
# First line indicates, that this is a multipart content, second shows that
|
||||
# here starts a part of the multipart content, third shows the end of content.
|
||||
#
|
||||
# If there are any other lines, which starts with "--", then it should be
|
||||
# another boundary id - or not.
|
||||
#
|
||||
# After 3.0.3, there are two kinds of types of boundary errors: strict and permissive.
|
||||
#
|
||||
# If multipart content contains the three necessary lines with correct order, but
|
||||
# there are one or more lines with "--", then parser returns with value 2 (non-zero).
|
||||
#
|
||||
# If some of the necessary lines (usually the start or end) misses, or the order
|
||||
# is wrong, then parser returns with value 1 (also a non-zero).
|
||||
#
|
||||
# You can choose, which one is what you need. The example below contains the
|
||||
# 'strict' mode, which means if there are any lines with start of "--", then
|
||||
# Coraza blocked the content. But the next, commented example contains
|
||||
# the 'permissive' mode, then you check only if the necessary lines exists in
|
||||
# correct order. Whit this, you can enable to upload PEM files (eg "----BEGIN.."),
|
||||
# or other text files, which contains eg. HTTP headers.
|
||||
#
|
||||
# The difference is only the operator - in strict mode (first) the content blocked
|
||||
# in case of any non-zero value. In permissive mode (second, commented) the
|
||||
# content blocked only if the value is explicit 1. If it 0 or 2, the content will
|
||||
# allowed.
|
||||
#
|
||||
|
||||
#
|
||||
# See #1747 and #1924 for further information on the possible values for
|
||||
# MULTIPART_UNMATCHED_BOUNDARY.
|
||||
#
|
||||
SecRule MULTIPART_UNMATCHED_BOUNDARY "@eq 1" \
|
||||
"id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'"
|
||||
|
||||
# Some internal errors will set flags in TX and we will need to look for these.
|
||||
# All of these are prefixed with "MSC_". The following flags currently exist:
|
||||
#
|
||||
# COR_PCRE_LIMITS_EXCEEDED: PCRE match limits were exceeded.
|
||||
#
|
||||
SecRule TX:/^COR_/ "!@streq 0" \
|
||||
"id:'200005',phase:2,t:none,deny,msg:'Coraza internal error flagged: %{MATCHED_VAR_NAME}'"
|
||||
|
||||
|
||||
# -- Response body handling --------------------------------------------------
|
||||
|
||||
# Allow Coraza to access response bodies.
|
||||
# You should have this directive enabled in order to identify errors
|
||||
# and data leakage issues.
|
||||
#
|
||||
# Do keep in mind that enabling this directive does increases both
|
||||
# memory consumption and response latency.
|
||||
#
|
||||
SecResponseBodyAccess On
|
||||
|
||||
# Which response MIME types do you want to inspect? You should adjust the
|
||||
# configuration below to catch documents but avoid static files
|
||||
# (e.g., images and archives).
|
||||
#
|
||||
SecResponseBodyMimeType text/plain text/html text/xml
|
||||
|
||||
# Buffer response bodies of up to 512 KB in length.
|
||||
SecResponseBodyLimit 524288
|
||||
|
||||
# What happens when we encounter a response body larger than the configured
|
||||
# limit? By default, we process what we have and let the rest through.
|
||||
# That's somewhat less secure, but does not break any legitimate pages.
|
||||
#
|
||||
SecResponseBodyLimitAction ProcessPartial
|
||||
|
||||
|
||||
# -- Filesystem configuration ------------------------------------------------
|
||||
|
||||
# The location where Coraza will keep its persistent data. This default setting
|
||||
# is chosen due to all systems have /tmp available however, it
|
||||
# too should be updated to a place that other users can't access.
|
||||
#
|
||||
SecDataDir /tmp/
|
||||
|
||||
|
||||
# -- File uploads handling configuration -------------------------------------
|
||||
|
||||
# The location where Coraza stores intercepted uploaded files. This
|
||||
# location must be private to Coraza. You don't want other users on
|
||||
# the server to access the files, do you?
|
||||
#
|
||||
#SecUploadDir /opt/coraza/var/upload/
|
||||
|
||||
# By default, only keep the files that were determined to be unusual
|
||||
# in some way (by an external inspection script). For this to work you
|
||||
# will also need at least one file inspection rule.
|
||||
#
|
||||
#SecUploadKeepFiles RelevantOnly
|
||||
|
||||
# Uploaded files are by default created with permissions that do not allow
|
||||
# any other user to access them. You may need to relax that if you want to
|
||||
# interface Coraza to an external program (e.g., an anti-virus).
|
||||
#
|
||||
#SecUploadFileMode 0600
|
||||
|
||||
|
||||
# -- Debug log configuration -------------------------------------------------
|
||||
|
||||
# Default debug log path
|
||||
# Debug levels:
|
||||
# 0: No logging (least verbose)
|
||||
# 1: Error
|
||||
# 2: Warn
|
||||
# 3: Info
|
||||
# 4-8: Debug
|
||||
# 9: Trace (most verbose)
|
||||
# Most logging has not been implemented because it will be replaced with
|
||||
# advanced rule profiling options
|
||||
#SecDebugLog /opt/coraza/var/log/debug.log
|
||||
#SecDebugLogLevel 3
|
||||
|
||||
|
||||
# -- Audit log configuration -------------------------------------------------
|
||||
|
||||
# Log the transactions that are marked by a rule, as well as those that
|
||||
# trigger a server error (determined by a 5xx or 4xx, excluding 404,
|
||||
# level response status codes).
|
||||
#
|
||||
SecAuditEngine RelevantOnly
|
||||
SecAuditLogRelevantStatus "^(?:(5|4)(0|1)[0-9])$"
|
||||
|
||||
# Log everything we know about a transaction.
|
||||
SecAuditLogParts ABIJDEFHZ
|
||||
|
||||
# Use a single file for logging. This is much easier to look at, but
|
||||
# assumes that you will use the audit log only occasionally.
|
||||
#
|
||||
SecAuditLogType Serial
|
||||
|
||||
|
||||
# -- Miscellaneous -----------------------------------------------------------
|
||||
|
||||
# Use the most commonly used application/x-www-form-urlencoded parameter
|
||||
# separator. There's probably only one application somewhere that uses
|
||||
# something else so don't expect to change this value.
|
||||
#
|
||||
SecArgumentSeparator &
|
||||
|
||||
# Settle on version 0 (zero) cookies, as that is what most applications
|
||||
# use. Using an incorrect cookie version may open your installation to
|
||||
# evasion attacks (against the rules that examine named cookies).
|
||||
#
|
||||
SecCookieFormat 0
|
||||
49
conf/web_config.go
Normal file
49
conf/web_config.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// 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 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"`
|
||||
}
|
||||
|
||||
func GetWebConfig() *WebConfig {
|
||||
config := &WebConfig{}
|
||||
|
||||
config.ShowGithubCorner = GetConfigBool("showGithubCorner")
|
||||
config.ForceLanguage = GetLanguage(GetConfigString("forceLanguage"))
|
||||
config.DefaultLanguage = GetLanguage(GetConfigString("defaultLanguage"))
|
||||
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
|
||||
}
|
||||
@@ -15,11 +15,13 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/core/logs"
|
||||
"github.com/casdoor/casdoor/form"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@@ -80,11 +82,6 @@ type LaravelResponse struct {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /signup [post]
|
||||
func (c *ApiController) Signup() {
|
||||
if c.GetSessionUsername() != "" {
|
||||
c.ResponseError(c.T("account:Please sign out first"), c.GetSessionUsername())
|
||||
return
|
||||
}
|
||||
|
||||
var authForm form.AuthForm
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &authForm)
|
||||
if err != nil {
|
||||
@@ -316,6 +313,40 @@ func (c *ApiController) Signup() {
|
||||
userId := user.GetId()
|
||||
util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId)
|
||||
|
||||
// Check if this is an OAuth flow and automatically generate code
|
||||
clientId := c.Ctx.Input.Query("clientId")
|
||||
responseType := c.Ctx.Input.Query("responseType")
|
||||
redirectUri := c.Ctx.Input.Query("redirectUri")
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
state := c.Ctx.Input.Query("state")
|
||||
nonce := c.Ctx.Input.Query("nonce")
|
||||
codeChallenge := c.Ctx.Input.Query("code_challenge")
|
||||
|
||||
// If OAuth parameters are present, generate OAuth code and return it
|
||||
if clientId != "" && responseType == ResponseTypeCode {
|
||||
consentRequired, err := object.CheckConsentRequired(user, application, scope)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if consentRequired {
|
||||
c.ResponseOk(map[string]bool{"required": true})
|
||||
return
|
||||
}
|
||||
|
||||
code, err := object.GetOAuthCode(userId, clientId, "", "password", responseType, redirectUri, scope, state, nonce, codeChallenge, "", c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
resp := codeToResponse(code)
|
||||
c.Data["json"] = resp
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(userId)
|
||||
}
|
||||
|
||||
@@ -343,22 +374,21 @@ func (c *ApiController) Logout() {
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve application and token before clearing the session
|
||||
application := c.GetSessionApplication()
|
||||
sessionToken := c.GetSessionToken()
|
||||
|
||||
c.ClearUserSession()
|
||||
c.ClearTokenSession()
|
||||
owner, username, err := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
_, err = object.DeleteSessionId(util.GetSessionId(owner, username, object.CasdoorApplication), c.Ctx.Input.CruSession.SessionID())
|
||||
if err != nil {
|
||||
|
||||
if err := c.deleteUserSession(user); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
util.LogInfo(c.Ctx, "API: [%s] logged out", user)
|
||||
// Propagate logout to external Custom OAuth2 providers
|
||||
object.InvokeCustomProviderLogout(application, sessionToken)
|
||||
|
||||
application := c.GetSessionApplication()
|
||||
if application == nil || application.Name == "app-built-in" || application.HomepageUrl == "" {
|
||||
c.ResponseOk(user)
|
||||
return
|
||||
@@ -386,7 +416,7 @@ func (c *ApiController) Logout() {
|
||||
return
|
||||
}
|
||||
if application == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist")), token.Application)
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), token.Application))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -396,20 +426,15 @@ func (c *ApiController) Logout() {
|
||||
|
||||
c.ClearUserSession()
|
||||
c.ClearTokenSession()
|
||||
|
||||
// TODO https://github.com/casdoor/casdoor/pull/1494#discussion_r1095675265
|
||||
owner, username, err := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if err != nil {
|
||||
if err := c.deleteUserSession(user); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_, err = object.DeleteSessionId(util.GetSessionId(owner, username, object.CasdoorApplication), c.Ctx.Input.CruSession.SessionID())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
util.LogInfo(c.Ctx, "API: [%s] logged out", user)
|
||||
// Propagate logout to external Custom OAuth2 providers
|
||||
object.InvokeCustomProviderLogout(application, accessToken)
|
||||
|
||||
if redirectUri == "" {
|
||||
c.ResponseOk()
|
||||
@@ -436,7 +461,8 @@ func (c *ApiController) Logout() {
|
||||
// SsoLogout
|
||||
// @Title SsoLogout
|
||||
// @Tag Login API
|
||||
// @Description logout the current user from all applications
|
||||
// @Description logout the current user from all applications or current session only
|
||||
// @Param logoutAll query string false "Whether to logout from all sessions. Accepted values: 'true', '1', or empty (default: true). Any other value means false."
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /sso-logout [get,post]
|
||||
func (c *ApiController) SsoLogout() {
|
||||
@@ -447,6 +473,15 @@ func (c *ApiController) SsoLogout() {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user wants to logout from all sessions or just current session
|
||||
// Default is true for backward compatibility
|
||||
logoutAll := c.Ctx.Input.Query("logoutAll")
|
||||
logoutAllSessions := logoutAll == "" || logoutAll == "true" || logoutAll == "1"
|
||||
|
||||
// Retrieve application and token before clearing the session
|
||||
ssoApplication := c.GetSessionApplication()
|
||||
ssoSessionToken := c.GetSessionToken()
|
||||
|
||||
c.ClearUserSession()
|
||||
c.ClearTokenSession()
|
||||
owner, username, err := util.GetOwnerAndNameFromIdWithError(user)
|
||||
@@ -454,37 +489,64 @@ func (c *ApiController) SsoLogout() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
_, err = object.DeleteSessionId(util.GetSessionId(owner, username, object.CasdoorApplication), c.Ctx.Input.CruSession.SessionID())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_, err = object.ExpireTokenByUser(owner, username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
sessions, err := object.GetUserSessions(owner, username)
|
||||
|
||||
currentSessionId := c.Ctx.Input.CruSession.SessionID(context.Background())
|
||||
_, err = object.DeleteSessionId(util.GetSessionId(owner, username, object.CasdoorApplication), currentSessionId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var tokens []*object.Token
|
||||
var sessionIds []string
|
||||
for _, session := range sessions {
|
||||
sessionIds = append(sessionIds, session.SessionId...)
|
||||
}
|
||||
object.DeleteBeegoSession(sessionIds)
|
||||
|
||||
_, err = object.DeleteAllUserSessions(owner, username)
|
||||
// Get tokens for notification (needed for both session-level and full logout)
|
||||
// This enables subsystems to identify and invalidate corresponding access tokens
|
||||
// Note: Tokens must be retrieved BEFORE expiration to include their hashes in the notification
|
||||
tokens, err = object.GetTokensByUser(owner, username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if logoutAllSessions {
|
||||
// Logout from all sessions: expire all tokens and delete all sessions
|
||||
_, err = object.ExpireTokenByUser(owner, username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
sessions, err := object.GetUserSessions(owner, username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, session := range sessions {
|
||||
sessionIds = append(sessionIds, session.SessionId...)
|
||||
}
|
||||
object.DeleteBeegoSession(sessionIds)
|
||||
|
||||
_, err = object.DeleteAllUserSessions(owner, username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
util.LogInfo(c.Ctx, "API: [%s] logged out from all applications", user)
|
||||
} else {
|
||||
// Logout from current session only
|
||||
sessionIds = []string{currentSessionId}
|
||||
|
||||
// Only delete the current session's Beego session
|
||||
object.DeleteBeegoSession(sessionIds)
|
||||
|
||||
util.LogInfo(c.Ctx, "API: [%s] logged out from current session", user)
|
||||
}
|
||||
|
||||
// Send SSO logout notifications to all notification providers in the user's signup application
|
||||
// Now includes session-level information for targeted logout
|
||||
userObj, err := object.GetUser(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -492,14 +554,15 @@ func (c *ApiController) SsoLogout() {
|
||||
}
|
||||
|
||||
if userObj != nil {
|
||||
err = object.SendSsoLogoutNotifications(userObj)
|
||||
err = object.SendSsoLogoutNotifications(userObj, sessionIds, tokens)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
util.LogInfo(c.Ctx, "API: [%s] logged out from all applications", user)
|
||||
// Propagate logout to external Custom OAuth2 providers
|
||||
object.InvokeCustomProviderLogout(ssoApplication, ssoSessionToken)
|
||||
|
||||
c.ResponseOk()
|
||||
}
|
||||
@@ -512,12 +575,17 @@ func (c *ApiController) SsoLogout() {
|
||||
// @router /get-account [get]
|
||||
func (c *ApiController) GetAccount() {
|
||||
var err error
|
||||
err = util.AppendWebConfigCookie(c.Ctx)
|
||||
if err != nil {
|
||||
logs.Error("AppendWebConfigCookie failed in GetAccount, error: %s", err)
|
||||
}
|
||||
|
||||
user, ok := c.RequireSignedInUser()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
managedAccounts := c.Input().Get("managedAccounts")
|
||||
managedAccounts := c.Ctx.Input.Query("managedAccounts")
|
||||
if managedAccounts == "1" {
|
||||
user, err = object.ExtendManagedAccountsWithUser(user)
|
||||
if err != nil {
|
||||
@@ -635,9 +703,54 @@ func (c *ApiController) GetUserinfo2() {
|
||||
// @router /get-captcha [get]
|
||||
// @Success 200 {object} object.Userinfo The Response object
|
||||
func (c *ApiController) GetCaptcha() {
|
||||
applicationId := c.Input().Get("applicationId")
|
||||
isCurrentProvider := c.Input().Get("isCurrentProvider")
|
||||
applicationId := c.Ctx.Input.Query("applicationId")
|
||||
isCurrentProvider := c.Ctx.Input.Query("isCurrentProvider")
|
||||
|
||||
// When isCurrentProvider == "true", the frontend passes a provider ID instead of an application ID.
|
||||
// In that case, skip application lookup and rule evaluation, and just return the provider config.
|
||||
shouldSkipCaptcha := false
|
||||
|
||||
if isCurrentProvider != "true" {
|
||||
application, err := object.GetApplication(applicationId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), applicationId))
|
||||
return
|
||||
}
|
||||
|
||||
// Check the CAPTCHA rule to determine if CAPTCHA should be shown
|
||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||
|
||||
// For Internet-Only rule, we can determine on the backend if CAPTCHA should be shown
|
||||
// For other rules (Dynamic, Always), we need to return the CAPTCHA config
|
||||
for _, providerItem := range application.Providers {
|
||||
if providerItem.Provider == nil || providerItem.Provider.Category != "Captcha" {
|
||||
continue
|
||||
}
|
||||
|
||||
// For "None" rule, skip CAPTCHA
|
||||
if providerItem.Rule == "None" || providerItem.Rule == "" {
|
||||
shouldSkipCaptcha = true
|
||||
} else if providerItem.Rule == "Internet-Only" {
|
||||
// For Internet-Only rule, check if the client is from intranet
|
||||
if !util.IsInternetIp(clientIp) {
|
||||
// Client is from intranet, skip CAPTCHA
|
||||
shouldSkipCaptcha = true
|
||||
}
|
||||
}
|
||||
|
||||
break // Only check the first CAPTCHA provider
|
||||
}
|
||||
|
||||
if shouldSkipCaptcha {
|
||||
c.ResponseOk(Captcha{Type: "none"})
|
||||
return
|
||||
}
|
||||
}
|
||||
captchaProvider, err := object.GetCaptchaProviderByApplication(applicationId, isCurrentProvider, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -671,3 +784,24 @@ func (c *ApiController) GetCaptcha() {
|
||||
|
||||
c.ResponseOk(Captcha{Type: "none"})
|
||||
}
|
||||
|
||||
func (c *ApiController) deleteUserSession(user string) error {
|
||||
owner, username, err := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Casdoor session ID derived from owner, username, and application
|
||||
sessionId := util.GetSessionId(owner, username, object.CasdoorApplication)
|
||||
|
||||
// Explicitly get the Beego session ID from the context
|
||||
beegoSessionId := c.Ctx.Input.CruSession.SessionID(context.Background())
|
||||
|
||||
_, err = object.DeleteSessionId(sessionId, beegoSessionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
util.LogInfo(c.Ctx, "API: [%s] logged out", user)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Adapter The Response object
|
||||
// @router /get-adapters [get]
|
||||
func (c *ApiController) GetAdapters() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
adapters, err := object.GetAdapters(owner)
|
||||
@@ -54,7 +54,7 @@ func (c *ApiController) GetAdapters() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
adapters, err := object.GetPaginationAdapters(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -73,7 +73,7 @@ func (c *ApiController) GetAdapters() {
|
||||
// @Success 200 {object} object.Adapter The Response object
|
||||
// @router /get-adapter [get]
|
||||
func (c *ApiController) GetAdapter() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
adapter, err := object.GetAdapter(id)
|
||||
if err != nil {
|
||||
@@ -93,7 +93,7 @@ func (c *ApiController) GetAdapter() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-adapter [post]
|
||||
func (c *ApiController) UpdateAdapter() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var adapter object.Adapter
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &adapter)
|
||||
|
||||
149
controllers/agent.go
Normal file
149
controllers/agent.go
Normal file
@@ -0,0 +1,149 @@
|
||||
// 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()
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -32,14 +32,14 @@ import (
|
||||
// @router /get-applications [get]
|
||||
func (c *ApiController) GetApplications() {
|
||||
userId := c.GetSessionUsername()
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
organization := c.Input().Get("organization")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
organization := c.Ctx.Input.Query("organization")
|
||||
var err error
|
||||
if limit == "" || page == "" {
|
||||
var applications []*object.Application
|
||||
@@ -61,7 +61,7 @@ func (c *ApiController) GetApplications() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
application, err := object.GetPaginationApplications(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -82,7 +82,7 @@ func (c *ApiController) GetApplications() {
|
||||
// @router /get-application [get]
|
||||
func (c *ApiController) GetApplication() {
|
||||
userId := c.GetSessionUsername()
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
application, err := object.GetApplication(id)
|
||||
if err != nil {
|
||||
@@ -90,7 +90,7 @@ func (c *ApiController) GetApplication() {
|
||||
return
|
||||
}
|
||||
|
||||
if c.Input().Get("withKey") != "" && application != nil && application.Cert != "" {
|
||||
if c.Ctx.Input.Query("withKey") != "" && application != nil && application.Cert != "" {
|
||||
cert, err := object.GetCert(util.GetId(application.Owner, application.Cert))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -125,7 +125,7 @@ func (c *ApiController) GetApplication() {
|
||||
// @router /get-user-application [get]
|
||||
func (c *ApiController) GetUserApplication() {
|
||||
userId := c.GetSessionUsername()
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
user, err := object.GetUser(id)
|
||||
if err != nil {
|
||||
@@ -159,14 +159,14 @@ func (c *ApiController) GetUserApplication() {
|
||||
// @router /get-organization-applications [get]
|
||||
func (c *ApiController) GetOrganizationApplications() {
|
||||
userId := c.GetSessionUsername()
|
||||
organization := c.Input().Get("organization")
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
organization := c.Ctx.Input.Query("organization")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if organization == "" {
|
||||
c.ResponseError(c.T("general:Missing parameter") + ": organization")
|
||||
@@ -196,7 +196,7 @@ func (c *ApiController) GetOrganizationApplications() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
applications, err := object.GetPaginationOrganizationApplications(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -223,7 +223,7 @@ func (c *ApiController) GetOrganizationApplications() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-application [post]
|
||||
func (c *ApiController) UpdateApplication() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var application object.Application
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &application)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
@@ -27,7 +28,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego"
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/casdoor/casdoor/captcha"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/form"
|
||||
@@ -36,7 +37,6 @@ import (
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
@@ -99,7 +99,8 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
// check user's tag
|
||||
if !user.IsGlobalAdmin() && !user.IsAdmin && len(application.Tags) > 0 {
|
||||
// only users with the tag that is listed in the application tags can login
|
||||
if !util.InSlice(application.Tags, user.Tag) {
|
||||
// supports comma-separated tags in user.Tag (e.g., "default-policy,project-admin")
|
||||
if !util.HasTagInSlice(application.Tags, user.Tag) {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:User's tag: %s is not listed in the application's tags"), user.Tag))
|
||||
return
|
||||
}
|
||||
@@ -151,20 +152,34 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
|
||||
resp = &Response{Status: "ok", Msg: "", Data: userId, Data3: user.NeedUpdatePassword}
|
||||
} else if form.Type == ResponseTypeCode {
|
||||
clientId := c.Input().Get("clientId")
|
||||
responseType := c.Input().Get("responseType")
|
||||
redirectUri := c.Input().Get("redirectUri")
|
||||
scope := c.Input().Get("scope")
|
||||
state := c.Input().Get("state")
|
||||
nonce := c.Input().Get("nonce")
|
||||
challengeMethod := c.Input().Get("code_challenge_method")
|
||||
codeChallenge := c.Input().Get("code_challenge")
|
||||
clientId := c.Ctx.Input.Query("clientId")
|
||||
responseType := c.Ctx.Input.Query("responseType")
|
||||
redirectUri := c.Ctx.Input.Query("redirectUri")
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
state := c.Ctx.Input.Query("state")
|
||||
nonce := c.Ctx.Input.Query("nonce")
|
||||
challengeMethod := c.Ctx.Input.Query("code_challenge_method")
|
||||
codeChallenge := c.Ctx.Input.Query("code_challenge")
|
||||
resource := c.Ctx.Input.Query("resource")
|
||||
|
||||
if challengeMethod != "S256" && challengeMethod != "null" && challengeMethod != "" {
|
||||
c.ResponseError(c.T("auth:Challenge method should be S256"))
|
||||
return
|
||||
}
|
||||
code, err := object.GetOAuthCode(userId, clientId, form.Provider, form.SigninMethod, responseType, redirectUri, scope, state, nonce, codeChallenge, c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||
|
||||
consentRequired, err := object.CheckConsentRequired(user, application, scope)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if consentRequired {
|
||||
resp = &Response{Status: "ok", Data: map[string]bool{"required": true}}
|
||||
resp.Data3 = user.NeedUpdatePassword
|
||||
return
|
||||
}
|
||||
|
||||
code, err := object.GetOAuthCode(userId, clientId, form.Provider, form.SigninMethod, responseType, redirectUri, scope, state, nonce, codeChallenge, resource, c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
@@ -180,12 +195,17 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
if !object.IsGrantTypeValid(form.Type, application.GrantTypes) {
|
||||
resp = &Response{Status: "error", Msg: fmt.Sprintf("error: grant_type: %s is not supported in this application", form.Type), Data: ""}
|
||||
} else {
|
||||
scope := c.Input().Get("scope")
|
||||
nonce := c.Input().Get("nonce")
|
||||
token, _ := object.GetTokenByUser(application, user, scope, nonce, c.Ctx.Request.Host)
|
||||
resp = tokenToResponse(token)
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
nonce := c.Ctx.Input.Query("nonce")
|
||||
expandedScope, valid := object.IsScopeValidAndExpand(scope, application)
|
||||
if !valid {
|
||||
resp = &Response{Status: "error", Msg: "error: invalid_scope", Data: ""}
|
||||
} else {
|
||||
token, _ := object.GetTokenByUser(application, user, expandedScope, nonce, c.Ctx.Request.Host)
|
||||
resp = tokenToResponse(token)
|
||||
|
||||
resp.Data3 = user.NeedUpdatePassword
|
||||
resp.Data3 = user.NeedUpdatePassword
|
||||
}
|
||||
}
|
||||
} else if form.Type == ResponseTypeDevice {
|
||||
authCache, ok := object.DeviceAuthMap.LoadAndDelete(form.UserCode)
|
||||
@@ -227,7 +247,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
}
|
||||
} else if form.Type == ResponseTypeCas {
|
||||
// not oauth but CAS SSO protocol
|
||||
service := c.Input().Get("service")
|
||||
service := c.Ctx.Input.Query("service")
|
||||
resp = wrapErrorResponse(nil)
|
||||
if service != "" {
|
||||
st, err := object.GenerateCasToken(userId, service)
|
||||
@@ -246,9 +266,18 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
resp = wrapErrorResponse(fmt.Errorf("unknown response type: %s", form.Type))
|
||||
}
|
||||
|
||||
// if user did not check auto signin
|
||||
if resp.Status == "ok" && !form.AutoSignin {
|
||||
c.setExpireForSession()
|
||||
// For all successful logins, set the session expiration; if auto signin is not checked, cap it at 24 hours.
|
||||
if resp.Status == "ok" {
|
||||
expireInHours := application.CookieExpireInHours
|
||||
|
||||
if expireInHours == 0 {
|
||||
expireInHours = 720
|
||||
}
|
||||
|
||||
if !form.AutoSignin && expireInHours > 24 {
|
||||
expireInHours = 24
|
||||
}
|
||||
c.setExpireForSession(expireInHours)
|
||||
}
|
||||
|
||||
if application.EnableExclusiveSignin {
|
||||
@@ -260,7 +289,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
|
||||
for _, session := range sessions {
|
||||
for _, sid := range session.SessionId {
|
||||
err := beego.GlobalSessions.GetProvider().SessionDestroy(sid)
|
||||
err := web.GlobalSessions.GetProvider().SessionDestroy(context.Background(), sid)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
@@ -274,9 +303,9 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
Owner: user.Owner,
|
||||
Name: user.Name,
|
||||
Application: application.Name,
|
||||
SessionId: []string{c.Ctx.Input.CruSession.SessionID()},
|
||||
SessionId: []string{c.Ctx.Input.CruSession.SessionID(context.Background())},
|
||||
|
||||
ExclusiveSignin: true,
|
||||
ExclusiveSignin: application.EnableExclusiveSignin,
|
||||
})
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
@@ -299,14 +328,14 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /get-app-login [get]
|
||||
func (c *ApiController) GetApplicationLogin() {
|
||||
clientId := c.Input().Get("clientId")
|
||||
responseType := c.Input().Get("responseType")
|
||||
redirectUri := c.Input().Get("redirectUri")
|
||||
scope := c.Input().Get("scope")
|
||||
state := c.Input().Get("state")
|
||||
id := c.Input().Get("id")
|
||||
loginType := c.Input().Get("type")
|
||||
userCode := c.Input().Get("userCode")
|
||||
clientId := c.Ctx.Input.Query("clientId")
|
||||
responseType := c.Ctx.Input.Query("responseType")
|
||||
redirectUri := c.Ctx.Input.Query("redirectUri")
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
state := c.Ctx.Input.Query("state")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
loginType := c.Ctx.Input.Query("type")
|
||||
userCode := c.Ctx.Input.Query("userCode")
|
||||
|
||||
var application *object.Application
|
||||
var msg string
|
||||
@@ -417,7 +446,7 @@ func checkMfaEnable(c *ApiController, user *object.User, organization *object.Or
|
||||
}
|
||||
if len(mfaAllowList) >= 1 {
|
||||
c.SetSession("verificationCodeType", verificationType)
|
||||
c.Ctx.Input.CruSession.SessionRelease(c.Ctx.ResponseWriter)
|
||||
c.Ctx.Input.CruSession.SessionRelease(context.Background(), c.Ctx.ResponseWriter)
|
||||
c.ResponseOk(object.NextMfa, mfaAllowList)
|
||||
return true
|
||||
}
|
||||
@@ -426,6 +455,55 @@ func checkMfaEnable(c *ApiController, user *object.User, organization *object.Or
|
||||
return false
|
||||
}
|
||||
|
||||
func getExistUserByBindingRule(providerItem *object.ProviderItem, application *object.Application, userInfo *idp.UserInfo) (user *object.User, err error) {
|
||||
if providerItem.BindingRule == nil {
|
||||
providerItem.BindingRule = &[]string{"Email", "Phone", "Name"}
|
||||
}
|
||||
if len(*providerItem.BindingRule) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for _, rule := range *providerItem.BindingRule {
|
||||
// Find existing user with Email
|
||||
if rule == "Email" {
|
||||
user, err = object.GetUserByField(application.Organization, "email", userInfo.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user != nil {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Find existing user with phone number
|
||||
if rule == "Phone" {
|
||||
user, err = object.GetUserByField(application.Organization, "phone", userInfo.Phone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user != nil {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find existing user by username (case-insensitive)
|
||||
// This allows OAuth providers (e.g., Wecom) to automatically associate with
|
||||
// existing users when usernames match, particularly useful for enterprise
|
||||
// scenarios where signup is disabled and users already exist in Casdoor
|
||||
if rule == "Name" {
|
||||
user, err = object.GetUserByFields(application.Organization, userInfo.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user != nil {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Login ...
|
||||
// @Title Login
|
||||
// @Tag Login API
|
||||
@@ -454,13 +532,6 @@ func (c *ApiController) Login() {
|
||||
verificationType := ""
|
||||
|
||||
if authForm.Username != "" {
|
||||
if authForm.Type == ResponseTypeLogin {
|
||||
if c.GetSessionUsername() != "" {
|
||||
c.ResponseError(c.T("account:Please sign out first"), c.GetSessionUsername())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var user *object.User
|
||||
if authForm.SigninMethod == "Face ID" {
|
||||
if user, err = object.GetUserByFields(authForm.Organization, authForm.Username); err != nil {
|
||||
@@ -734,7 +805,12 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
|
||||
// OAuth
|
||||
idpInfo := object.FromProviderToIdpInfo(c.Ctx, provider)
|
||||
idpInfo, err := object.FromProviderToIdpInfo(c.Ctx, provider)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
idpInfo.CodeVerifier = authForm.CodeVerifier
|
||||
var idProvider idp.IdProvider
|
||||
idProvider, err = idp.GetIdProvider(idpInfo, authForm.RedirectUri)
|
||||
if err != nil {
|
||||
@@ -779,7 +855,7 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
if !reg.MatchString(userInfo.Email) {
|
||||
c.ResponseError(fmt.Sprintf(c.T("check:Email is invalid")))
|
||||
c.ResponseError(c.T("check:Email is invalid"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -819,36 +895,10 @@ func (c *ApiController) Login() {
|
||||
c.Ctx.Input.SetParam("recordUserId", user.GetId())
|
||||
} else if provider.Category == "OAuth" || provider.Category == "Web3" || provider.Category == "SAML" {
|
||||
// Sign up via OAuth
|
||||
if application.EnableLinkWithEmail {
|
||||
if userInfo.Email != "" {
|
||||
// Find existing user with Email
|
||||
user, err = object.GetUserByField(application.Organization, "email", userInfo.Email)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil && userInfo.Phone != "" {
|
||||
// Find existing user with phone number
|
||||
user, err = object.GetUserByField(application.Organization, "phone", userInfo.Phone)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find existing user by username (case-insensitive)
|
||||
// This allows OAuth providers (e.g., Wecom) to automatically associate with
|
||||
// existing users when usernames match, particularly useful for enterprise
|
||||
// scenarios where signup is disabled and users already exist in Casdoor
|
||||
if user == nil && userInfo.Username != "" {
|
||||
user, err = object.GetUserByFields(application.Organization, userInfo.Username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
user, err = getExistUserByBindingRule(providerItem, application, userInfo)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
@@ -862,10 +912,16 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
if application.IsSignupItemRequired("Invitation code") {
|
||||
c.ResponseError(c.T("check:Invitation code cannot be blank"))
|
||||
// Check and validate invitation code
|
||||
invitation, msg := object.CheckInvitationCode(application, organization, &authForm, c.GetAcceptLanguage())
|
||||
if msg != "" {
|
||||
c.ResponseError(msg)
|
||||
return
|
||||
}
|
||||
invitationName := ""
|
||||
if invitation != nil {
|
||||
invitationName = invitation.Name
|
||||
}
|
||||
|
||||
// Handle UseEmailAsUsername for OAuth and Web3
|
||||
if organization.UseEmailAsUsername && userInfo.Email != "" {
|
||||
@@ -881,14 +937,7 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
|
||||
if tmpUser != nil {
|
||||
var uid uuid.UUID
|
||||
uid, err = uuid.NewRandom()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
uidStr := strings.Split(uid.String(), "-")
|
||||
uidStr := strings.Split(util.GenerateUUID(), "-")
|
||||
userInfo.Username = fmt.Sprintf("%s_%s", userInfo.Username, uidStr[1])
|
||||
}
|
||||
|
||||
@@ -932,12 +981,19 @@ func (c *ApiController) Login() {
|
||||
IsDeleted: false,
|
||||
SignupApplication: application.Name,
|
||||
Properties: properties,
|
||||
Invitation: invitationName,
|
||||
InvitationCode: authForm.InvitationCode,
|
||||
RegisterType: "Application Signup",
|
||||
RegisterSource: fmt.Sprintf("%s/%s", application.Organization, application.Name),
|
||||
}
|
||||
|
||||
if providerItem.SignupGroup != "" {
|
||||
// Set group from invitation code if available, otherwise use provider's signup group or application's default group
|
||||
if invitation != nil && invitation.SignupGroup != "" {
|
||||
user.Groups = []string{invitation.SignupGroup}
|
||||
} else if providerItem.SignupGroup != "" {
|
||||
user.Groups = []string{providerItem.SignupGroup}
|
||||
} else if application.DefaultGroup != "" {
|
||||
user.Groups = []string{application.DefaultGroup}
|
||||
}
|
||||
|
||||
var affected bool
|
||||
@@ -951,6 +1007,16 @@ func (c *ApiController) Login() {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:Failed to create user, user information is invalid: %s"), util.StructToJson(user)))
|
||||
return
|
||||
}
|
||||
|
||||
// Increment invitation usage count
|
||||
if invitation != nil {
|
||||
invitation.UsedCount += 1
|
||||
_, err = object.UpdateInvitation(invitation.GetId(), invitation, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sync info from 3rd-party if possible
|
||||
@@ -1154,8 +1220,8 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
|
||||
func (c *ApiController) GetSamlLogin() {
|
||||
providerId := c.Input().Get("id")
|
||||
relayState := c.Input().Get("relayState")
|
||||
providerId := c.Ctx.Input.Query("id")
|
||||
relayState := c.Ctx.Input.Query("relayState")
|
||||
authURL, method, err := object.GenerateSamlRequest(providerId, relayState, c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -1165,8 +1231,8 @@ func (c *ApiController) GetSamlLogin() {
|
||||
}
|
||||
|
||||
func (c *ApiController) HandleSamlLogin() {
|
||||
relayState := c.Input().Get("RelayState")
|
||||
samlResponse := c.Input().Get("SAMLResponse")
|
||||
relayState := c.Ctx.Input.Query("RelayState")
|
||||
samlResponse := c.Ctx.Input.Query("SAMLResponse")
|
||||
decode, err := base64.StdEncoding.DecodeString(relayState)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -1198,9 +1264,9 @@ func (c *ApiController) HandleOfficialAccountEvent() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
signature := c.Input().Get("signature")
|
||||
timestamp := c.Input().Get("timestamp")
|
||||
nonce := c.Input().Get("nonce")
|
||||
signature := c.Ctx.Input.Query("signature")
|
||||
timestamp := c.Ctx.Input.Query("timestamp")
|
||||
nonce := c.Ctx.Input.Query("nonce")
|
||||
var data struct {
|
||||
MsgType string `xml:"MsgType"`
|
||||
Event string `xml:"Event"`
|
||||
@@ -1258,7 +1324,7 @@ func (c *ApiController) HandleOfficialAccountEvent() {
|
||||
// @Param ticket query string true "The eventId of QRCode"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
func (c *ApiController) GetWebhookEventType() {
|
||||
ticket := c.Input().Get("ticket")
|
||||
ticket := c.Ctx.Input.Query("ticket")
|
||||
|
||||
idp.Lock.RLock()
|
||||
_, ok := idp.WechatCacheMap[ticket]
|
||||
@@ -1278,7 +1344,7 @@ func (c *ApiController) GetWebhookEventType() {
|
||||
// @Param id query string true "The id ( owner/name ) of provider"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
func (c *ApiController) GetQRCode() {
|
||||
providerId := c.Input().Get("id")
|
||||
providerId := c.Ctx.Input.Query("id")
|
||||
provider, err := object.GetProvider(providerId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -1306,9 +1372,9 @@ func (c *ApiController) GetQRCode() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /get-captcha-status [get]
|
||||
func (c *ApiController) GetCaptchaStatus() {
|
||||
organization := c.Input().Get("organization")
|
||||
userId := c.Input().Get("userId")
|
||||
applicationName := c.Input().Get("application")
|
||||
organization := c.Ctx.Input.Query("organization")
|
||||
userId := c.Ctx.Input.Query("userId")
|
||||
applicationName := c.Ctx.Input.Query("application")
|
||||
|
||||
application, err := object.GetApplication(fmt.Sprintf("admin/%s", applicationName))
|
||||
if err != nil {
|
||||
@@ -1340,7 +1406,7 @@ func (c *ApiController) Callback() {
|
||||
code := c.GetString("code")
|
||||
state := c.GetString("state")
|
||||
|
||||
frontendCallbackUrl := fmt.Sprintf("/callback?code=%s&state=%s", code, state)
|
||||
frontendCallbackUrl := fmt.Sprintf("/callback?code=%s&state=%s", url.QueryEscape(code), url.QueryEscape(state))
|
||||
c.Ctx.Redirect(http.StatusFound, frontendCallbackUrl)
|
||||
}
|
||||
|
||||
@@ -1351,8 +1417,8 @@ func (c *ApiController) Callback() {
|
||||
// @router /device-auth [post]
|
||||
// @Success 200 {object} object.DeviceAuthResponse The Response object
|
||||
func (c *ApiController) DeviceAuth() {
|
||||
clientId := c.Input().Get("client_id")
|
||||
scope := c.Input().Get("scope")
|
||||
clientId := c.Ctx.Input.Query("client_id")
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
application, err := object.GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
c.Data["json"] = object.TokenError{
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego"
|
||||
"github.com/beego/beego/logs"
|
||||
"github.com/beego/beego/v2/core/logs"
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/casdoor/casdoor/mcpself"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -27,7 +29,7 @@ import (
|
||||
// ApiController
|
||||
// controller for handlers under /api uri
|
||||
type ApiController struct {
|
||||
beego.Controller
|
||||
web.Controller
|
||||
}
|
||||
|
||||
// RootController
|
||||
@@ -104,6 +106,13 @@ func (c *ApiController) getCurrentUser() *object.User {
|
||||
|
||||
// GetSessionUsername ...
|
||||
func (c *ApiController) GetSessionUsername() string {
|
||||
// prefer username stored in Beego context by ApiFilter
|
||||
if ctxUser := c.Ctx.Input.GetData("currentUserId"); ctxUser != nil {
|
||||
if username, ok := ctxUser.(string); ok {
|
||||
return username
|
||||
}
|
||||
}
|
||||
|
||||
// check if user session expired
|
||||
sessionData := c.GetSessionData()
|
||||
|
||||
@@ -168,6 +177,7 @@ func (c *ApiController) GetSessionApplication() *object.Application {
|
||||
func (c *ApiController) ClearUserSession() {
|
||||
c.SetSessionUsername("")
|
||||
c.SetSessionData(nil)
|
||||
_ = c.SessionRegenerateID()
|
||||
}
|
||||
|
||||
func (c *ApiController) ClearTokenSession() {
|
||||
@@ -236,16 +246,19 @@ func (c *ApiController) setMfaUserSession(userId string) {
|
||||
}
|
||||
|
||||
func (c *ApiController) getMfaUserSession() string {
|
||||
userId := c.Ctx.Input.CruSession.Get(object.MfaSessionUserId)
|
||||
userId := c.Ctx.Input.CruSession.Get(context.Background(), object.MfaSessionUserId)
|
||||
if userId == nil {
|
||||
return ""
|
||||
}
|
||||
return userId.(string)
|
||||
}
|
||||
|
||||
func (c *ApiController) setExpireForSession() {
|
||||
func (c *ApiController) setExpireForSession(cookieExpireInHours int64) {
|
||||
timestamp := time.Now().Unix()
|
||||
timestamp += 3600 * 24
|
||||
if cookieExpireInHours == 0 {
|
||||
cookieExpireInHours = 720
|
||||
}
|
||||
timestamp += 3600 * cookieExpireInHours
|
||||
c.SetSessionData(&SessionData{
|
||||
ExpireTime: timestamp,
|
||||
})
|
||||
@@ -279,3 +292,14 @@ func (c *ApiController) Finish() {
|
||||
}
|
||||
c.Controller.Finish()
|
||||
}
|
||||
|
||||
func (c *ApiController) McpResponseError(id interface{}, code int, message string, data interface{}) {
|
||||
resp := mcpself.BuildMcpResponse(id, nil, &mcpself.McpError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
})
|
||||
c.Ctx.Output.Header("Content-Type", "application/json")
|
||||
c.Data["json"] = resp
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -41,8 +41,8 @@ func queryUnescape(service string) string {
|
||||
}
|
||||
|
||||
func (c *RootController) CasValidate() {
|
||||
ticket := c.Input().Get("ticket")
|
||||
service := c.Input().Get("service")
|
||||
ticket := c.Ctx.Input.Query("ticket")
|
||||
service := c.Ctx.Input.Query("service")
|
||||
c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
|
||||
if service == "" || ticket == "" {
|
||||
c.Ctx.Output.Body([]byte("no\n"))
|
||||
@@ -60,8 +60,8 @@ func (c *RootController) CasValidate() {
|
||||
}
|
||||
|
||||
func (c *RootController) CasServiceValidate() {
|
||||
ticket := c.Input().Get("ticket")
|
||||
format := c.Input().Get("format")
|
||||
ticket := c.Ctx.Input.Query("ticket")
|
||||
format := c.Ctx.Input.Query("format")
|
||||
if !strings.HasPrefix(ticket, "ST") {
|
||||
c.sendCasAuthenticationResponseErr(InvalidTicket, fmt.Sprintf("Ticket %s not recognized", ticket), format)
|
||||
}
|
||||
@@ -75,8 +75,8 @@ func (c *RootController) CasProxyValidate() {
|
||||
}
|
||||
|
||||
func (c *RootController) CasP3ServiceValidate() {
|
||||
ticket := c.Input().Get("ticket")
|
||||
format := c.Input().Get("format")
|
||||
ticket := c.Ctx.Input.Query("ticket")
|
||||
format := c.Ctx.Input.Query("format")
|
||||
if !strings.HasPrefix(ticket, "ST") {
|
||||
c.sendCasAuthenticationResponseErr(InvalidTicket, fmt.Sprintf("Ticket %s not recognized", ticket), format)
|
||||
}
|
||||
@@ -84,10 +84,10 @@ func (c *RootController) CasP3ServiceValidate() {
|
||||
}
|
||||
|
||||
func (c *RootController) CasP3ProxyValidate() {
|
||||
ticket := c.Input().Get("ticket")
|
||||
format := c.Input().Get("format")
|
||||
service := c.Input().Get("service")
|
||||
pgtUrl := c.Input().Get("pgtUrl")
|
||||
ticket := c.Ctx.Input.Query("ticket")
|
||||
format := c.Ctx.Input.Query("format")
|
||||
service := c.Ctx.Input.Query("service")
|
||||
pgtUrl := c.Ctx.Input.Query("pgtUrl")
|
||||
|
||||
serviceResponse := object.CasServiceResponse{
|
||||
Xmlns: "http://www.yale.edu/tp/cas",
|
||||
@@ -161,9 +161,9 @@ func (c *RootController) CasP3ProxyValidate() {
|
||||
}
|
||||
|
||||
func (c *RootController) CasProxy() {
|
||||
pgt := c.Input().Get("pgt")
|
||||
targetService := c.Input().Get("targetService")
|
||||
format := c.Input().Get("format")
|
||||
pgt := c.Ctx.Input.Query("pgt")
|
||||
targetService := c.Ctx.Input.Query("targetService")
|
||||
format := c.Ctx.Input.Query("format")
|
||||
if pgt == "" || targetService == "" {
|
||||
c.sendCasProxyResponseErr(InvalidRequest, "pgt and targetService must exist", format)
|
||||
return
|
||||
@@ -200,7 +200,7 @@ func (c *RootController) CasProxy() {
|
||||
|
||||
func (c *RootController) SamlValidate() {
|
||||
c.Ctx.Output.Header("Content-Type", "text/xml; charset=utf-8")
|
||||
target := c.Input().Get("TARGET")
|
||||
target := c.Ctx.Input.Query("TARGET")
|
||||
body := c.Ctx.Input.RequestBody
|
||||
envelopRequest := struct {
|
||||
XMLName xml.Name `xml:"Envelope"`
|
||||
|
||||
@@ -34,11 +34,11 @@ import (
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /enforce [post]
|
||||
func (c *ApiController) Enforce() {
|
||||
permissionId := c.Input().Get("permissionId")
|
||||
modelId := c.Input().Get("modelId")
|
||||
resourceId := c.Input().Get("resourceId")
|
||||
enforcerId := c.Input().Get("enforcerId")
|
||||
owner := c.Input().Get("owner")
|
||||
permissionId := c.Ctx.Input.Query("permissionId")
|
||||
modelId := c.Ctx.Input.Query("modelId")
|
||||
resourceId := c.Ctx.Input.Query("resourceId")
|
||||
enforcerId := c.Ctx.Input.Query("enforcerId")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
|
||||
params := []string{permissionId, modelId, resourceId, enforcerId, owner}
|
||||
nonEmpty := 0
|
||||
@@ -57,7 +57,9 @@ func (c *ApiController) Enforce() {
|
||||
return
|
||||
}
|
||||
|
||||
var request []string
|
||||
// Accept both plain string arrays (["alice","data1","read"]) and mixed arrays
|
||||
// with JSON objects ([{"DivisionGuid":"x"}, "resource", "read"]) for ABAC support.
|
||||
var request []interface{}
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &request)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -74,8 +76,8 @@ func (c *ApiController) Enforce() {
|
||||
res := []bool{}
|
||||
keyRes := []string{}
|
||||
|
||||
// type transformation
|
||||
interfaceRequest := util.StringToInterfaceArray(request)
|
||||
// Convert elements: JSON-object strings and maps become anonymous structs for ABAC.
|
||||
interfaceRequest := util.InterfaceToEnforceArray(request)
|
||||
|
||||
enforceResult, err := enforcer.Enforce(interfaceRequest...)
|
||||
if err != nil {
|
||||
@@ -180,10 +182,10 @@ func (c *ApiController) Enforce() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /batch-enforce [post]
|
||||
func (c *ApiController) BatchEnforce() {
|
||||
permissionId := c.Input().Get("permissionId")
|
||||
modelId := c.Input().Get("modelId")
|
||||
enforcerId := c.Input().Get("enforcerId")
|
||||
owner := c.Input().Get("owner")
|
||||
permissionId := c.Ctx.Input.Query("permissionId")
|
||||
modelId := c.Ctx.Input.Query("modelId")
|
||||
enforcerId := c.Ctx.Input.Query("enforcerId")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
|
||||
params := []string{permissionId, modelId, enforcerId, owner}
|
||||
nonEmpty := 0
|
||||
@@ -197,7 +199,8 @@ func (c *ApiController) BatchEnforce() {
|
||||
return
|
||||
}
|
||||
|
||||
var requests [][]string
|
||||
// Accept both string arrays and mixed arrays with JSON objects for ABAC support.
|
||||
var requests [][]interface{}
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &requests)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -214,8 +217,8 @@ func (c *ApiController) BatchEnforce() {
|
||||
res := [][]bool{}
|
||||
keyRes := []string{}
|
||||
|
||||
// type transformation
|
||||
interfaceRequests := util.StringToInterfaceArray2d(requests)
|
||||
// Convert elements: JSON-object strings and maps become anonymous structs for ABAC.
|
||||
interfaceRequests := util.InterfaceToEnforceArray2d(requests)
|
||||
|
||||
enforceResult, err := enforcer.BatchEnforce(interfaceRequests)
|
||||
if err != nil {
|
||||
@@ -303,8 +306,15 @@ func (c *ApiController) BatchEnforce() {
|
||||
c.ResponseOk(res, keyRes)
|
||||
}
|
||||
|
||||
// GetAllObjects
|
||||
// @Title GetAllObjects
|
||||
// @Tag Enforcer API
|
||||
// @Description Get all objects for a user (Casbin API)
|
||||
// @Param userId query string false "user id like built-in/admin"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /get-all-objects [get]
|
||||
func (c *ApiController) GetAllObjects() {
|
||||
userId := c.Input().Get("userId")
|
||||
userId := c.Ctx.Input.Query("userId")
|
||||
if userId == "" {
|
||||
userId = c.GetSessionUsername()
|
||||
if userId == "" {
|
||||
@@ -322,8 +332,15 @@ func (c *ApiController) GetAllObjects() {
|
||||
c.ResponseOk(objects)
|
||||
}
|
||||
|
||||
// GetAllActions
|
||||
// @Title GetAllActions
|
||||
// @Tag Enforcer API
|
||||
// @Description Get all actions for a user (Casbin API)
|
||||
// @Param userId query string false "user id like built-in/admin"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /get-all-actions [get]
|
||||
func (c *ApiController) GetAllActions() {
|
||||
userId := c.Input().Get("userId")
|
||||
userId := c.Ctx.Input.Query("userId")
|
||||
if userId == "" {
|
||||
userId = c.GetSessionUsername()
|
||||
if userId == "" {
|
||||
@@ -341,8 +358,15 @@ func (c *ApiController) GetAllActions() {
|
||||
c.ResponseOk(actions)
|
||||
}
|
||||
|
||||
// GetAllRoles
|
||||
// @Title GetAllRoles
|
||||
// @Tag Enforcer API
|
||||
// @Description Get all roles for a user (Casbin API)
|
||||
// @Param userId query string false "user id like built-in/admin"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /get-all-roles [get]
|
||||
func (c *ApiController) GetAllRoles() {
|
||||
userId := c.Input().Get("userId")
|
||||
userId := c.Ctx.Input.Query("userId")
|
||||
if userId == "" {
|
||||
userId = c.GetSessionUsername()
|
||||
if userId == "" {
|
||||
|
||||
@@ -26,6 +26,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
)
|
||||
|
||||
type CLIVersionInfo struct {
|
||||
@@ -164,13 +166,18 @@ 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
|
||||
}
|
||||
|
||||
language := c.Input().Get("language")
|
||||
argString := c.Input().Get("args")
|
||||
language := c.Ctx.Input.Query("language")
|
||||
argString := c.Ctx.Input.Query("args")
|
||||
|
||||
if language == "" {
|
||||
language = "go"
|
||||
@@ -262,10 +269,10 @@ func (c *ApiController) RunCasbinCommand() {
|
||||
// @Param hash string The SHA-256 hash string
|
||||
// @Return error Returns error if validation fails, nil if successful
|
||||
func validateIdentifier(c *ApiController) error {
|
||||
language := c.Input().Get("language")
|
||||
args := c.Input().Get("args")
|
||||
hash := c.Input().Get("m")
|
||||
timestamp := c.Input().Get("t")
|
||||
language := c.Ctx.Input.Query("language")
|
||||
args := c.Ctx.Input.Query("args")
|
||||
hash := c.Ctx.Input.Query("m")
|
||||
timestamp := c.Ctx.Input.Query("t")
|
||||
|
||||
if hash == "" || timestamp == "" || language == "" || args == "" {
|
||||
return fmt.Errorf("invalid identifier")
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Cert The Response object
|
||||
// @router /get-certs [get]
|
||||
func (c *ApiController) GetCerts() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
certs, err := object.GetMaskedCerts(object.GetCerts(owner))
|
||||
@@ -54,7 +54,7 @@ func (c *ApiController) GetCerts() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
certs, err := object.GetMaskedCerts(object.GetPaginationCerts(owner, paginator.Offset(), limit, field, value, sortField, sortOrder))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -72,12 +72,12 @@ func (c *ApiController) GetCerts() {
|
||||
// @Success 200 {array} object.Cert The Response object
|
||||
// @router /get-global-certs [get]
|
||||
func (c *ApiController) GetGlobalCerts() {
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
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 == "" {
|
||||
certs, err := object.GetMaskedCerts(object.GetGlobalCerts())
|
||||
@@ -95,7 +95,7 @@ func (c *ApiController) GetGlobalCerts() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
certs, err := object.GetMaskedCerts(object.GetPaginationGlobalCerts(paginator.Offset(), limit, field, value, sortField, sortOrder))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -114,7 +114,7 @@ func (c *ApiController) GetGlobalCerts() {
|
||||
// @Success 200 {object} object.Cert The Response object
|
||||
// @router /get-cert [get]
|
||||
func (c *ApiController) GetCert() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
cert, err := object.GetCert(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -133,7 +133,7 @@ func (c *ApiController) GetCert() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-cert [post]
|
||||
func (c *ApiController) UpdateCert() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var cert object.Cert
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &cert)
|
||||
@@ -183,3 +183,40 @@ func (c *ApiController) DeleteCert() {
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteCert(&cert))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateCertDomainExpire
|
||||
// @Title UpdateCertDomainExpire
|
||||
// @Tag Cert API
|
||||
// @Description update cert domain expire time
|
||||
// @Param id query string true "The ID of the cert"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-cert-domain-expire [post]
|
||||
func (c *ApiController) UpdateCertDomainExpire() {
|
||||
if _, ok := c.RequireSignedIn(); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Ctx.Input.Query("id")
|
||||
cert, err := object.GetCert(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
domainExpireTime, err := object.GetDomainExpireTime(cert.Name)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if domainExpireTime == "" {
|
||||
c.ResponseError("Failed to determine domain expiration time for domain " + cert.Name +
|
||||
". Please verify that the domain is valid, publicly resolvable, and has a retrievable expiration date, " +
|
||||
"or update the domain expiration time manually.")
|
||||
return
|
||||
}
|
||||
cert.DomainExpireTime = domainExpireTime
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateCert(id, cert))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego"
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -446,13 +447,13 @@ func downloadCLI() error {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /refresh-engines [post]
|
||||
func (c *ApiController) RefreshEngines() {
|
||||
if !beego.AppConfig.DefaultBool("isDemoMode", false) {
|
||||
c.ResponseError("refresh engines is only available in demo mode")
|
||||
if !conf.IsDemoMode() && !c.IsAdmin() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
hash := c.Input().Get("m")
|
||||
timestamp := c.Input().Get("t")
|
||||
hash := c.Ctx.Input.Query("m")
|
||||
timestamp := c.Ctx.Input.Query("t")
|
||||
|
||||
if hash == "" || timestamp == "" {
|
||||
c.ResponseError("invalid identifier")
|
||||
@@ -498,7 +499,7 @@ func (c *ApiController) RefreshEngines() {
|
||||
// @Title ScheduleCLIUpdater
|
||||
// @Description Start periodic CLI update scheduler
|
||||
func ScheduleCLIUpdater() {
|
||||
if !beego.AppConfig.DefaultBool("isDemoMode", false) {
|
||||
if !web.AppConfig.DefaultBool("isDemoMode", false) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -526,7 +527,7 @@ func DownloadCLI() error {
|
||||
// @Title InitCLIDownloader
|
||||
// @Description Initialize CLI downloader and start update scheduler
|
||||
func InitCLIDownloader() {
|
||||
if !beego.AppConfig.DefaultBool("isDemoMode", false) {
|
||||
if !web.AppConfig.DefaultBool("isDemoMode", false) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
226
controllers/consent.go
Normal file
226
controllers/consent.go
Normal file
@@ -0,0 +1,226 @@
|
||||
// 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/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
// RevokeConsent revokes a consent record
|
||||
// @Title RevokeConsent
|
||||
// @Tag Consent API
|
||||
// @Description revoke a consent record
|
||||
// @Param body body object.ConsentRecord true "The consent object"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /revoke-consent [post]
|
||||
func (c *ApiController) RevokeConsent() {
|
||||
userId := c.GetSessionUsername()
|
||||
if userId == "" {
|
||||
c.ResponseError(c.T("general:Please login first"))
|
||||
return
|
||||
}
|
||||
|
||||
var consent object.ConsentRecord
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &consent)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that consent.Application is not empty
|
||||
if consent.Application == "" {
|
||||
c.ResponseError(c.T("general:Application cannot be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that GrantedScopes is not empty when scope-specific revoke is requested
|
||||
if len(consent.GrantedScopes) == 0 {
|
||||
c.ResponseError(c.T("general:Granted scopes cannot be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
userObj, err := object.GetUser(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if userObj == nil {
|
||||
c.ResponseError(c.T("general:The user doesn't exist"))
|
||||
return
|
||||
}
|
||||
|
||||
newScopes := []object.ConsentRecord{}
|
||||
for _, record := range userObj.ApplicationScopes {
|
||||
if record.Application != consent.Application {
|
||||
// skip other applications
|
||||
newScopes = append(newScopes, record)
|
||||
continue
|
||||
}
|
||||
// revoke specified scopes
|
||||
revokeSet := make(map[string]bool)
|
||||
for _, s := range consent.GrantedScopes {
|
||||
revokeSet[s] = true
|
||||
}
|
||||
remaining := []string{}
|
||||
for _, s := range record.GrantedScopes {
|
||||
if !revokeSet[s] {
|
||||
remaining = append(remaining, s)
|
||||
}
|
||||
}
|
||||
if len(remaining) > 0 {
|
||||
// still have remaining scopes, keep the record and update
|
||||
record.GrantedScopes = remaining
|
||||
newScopes = append(newScopes, record)
|
||||
}
|
||||
// otherwise the application authorization is revoked, delete the whole record
|
||||
}
|
||||
userObj.ApplicationScopes = newScopes
|
||||
success, err := object.UpdateUser(userObj.GetId(), userObj, nil, false)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.ResponseOk(success)
|
||||
}
|
||||
|
||||
// GrantConsent grants consent for an OAuth application and returns authorization code
|
||||
// @Title GrantConsent
|
||||
// @Tag Consent API
|
||||
// @Description grant consent for an OAuth application and get authorization code
|
||||
// @Param body body object.ConsentRecord true "The consent object with OAuth parameters"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /grant-consent [post]
|
||||
func (c *ApiController) GrantConsent() {
|
||||
userId := c.GetSessionUsername()
|
||||
if userId == "" {
|
||||
c.ResponseError(c.T("general:Please login first"))
|
||||
return
|
||||
}
|
||||
|
||||
var request struct {
|
||||
Application string `json:"application"`
|
||||
Scopes []string `json:"grantedScopes"`
|
||||
ClientId string `json:"clientId"`
|
||||
Provider string `json:"provider"`
|
||||
SigninMethod string `json:"signinMethod"`
|
||||
ResponseType string `json:"responseType"`
|
||||
RedirectUri string `json:"redirectUri"`
|
||||
Scope string `json:"scope"`
|
||||
State string `json:"state"`
|
||||
Nonce string `json:"nonce"`
|
||||
Challenge string `json:"challenge"`
|
||||
Resource string `json:"resource"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &request)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Validate application by clientId
|
||||
application, err := object.GetApplicationByClientId(request.ClientId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if application == nil {
|
||||
c.ResponseError(c.T("general:Invalid client_id"))
|
||||
return
|
||||
}
|
||||
|
||||
// Verify that request.Application matches the application's actual ID
|
||||
if request.Application != application.GetId() {
|
||||
c.ResponseError(c.T("general:Invalid application"))
|
||||
return
|
||||
}
|
||||
|
||||
// Update user's ApplicationScopes
|
||||
userObj, err := object.GetUser(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if userObj == nil {
|
||||
c.ResponseError(c.T("general:User not found"))
|
||||
return
|
||||
}
|
||||
|
||||
appId := application.GetId()
|
||||
found := false
|
||||
// Insert new scope into existing applicationScopes
|
||||
for i, record := range userObj.ApplicationScopes {
|
||||
if record.Application == appId {
|
||||
existing := make(map[string]bool)
|
||||
for _, s := range userObj.ApplicationScopes[i].GrantedScopes {
|
||||
existing[s] = true
|
||||
}
|
||||
for _, s := range request.Scopes {
|
||||
if !existing[s] {
|
||||
userObj.ApplicationScopes[i].GrantedScopes = append(userObj.ApplicationScopes[i].GrantedScopes, s)
|
||||
existing[s] = true
|
||||
}
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// create a new applicationScopes if not found
|
||||
if !found {
|
||||
uniqueScopes := []string{}
|
||||
existing := make(map[string]bool)
|
||||
for _, s := range request.Scopes {
|
||||
if !existing[s] {
|
||||
uniqueScopes = append(uniqueScopes, s)
|
||||
existing[s] = true
|
||||
}
|
||||
}
|
||||
userObj.ApplicationScopes = append(userObj.ApplicationScopes, object.ConsentRecord{
|
||||
Application: appId,
|
||||
GrantedScopes: uniqueScopes,
|
||||
})
|
||||
}
|
||||
|
||||
_, err = object.UpdateUser(userObj.GetId(), userObj, []string{"application_scopes"}, false)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Now get the OAuth code
|
||||
code, err := object.GetOAuthCode(
|
||||
userId,
|
||||
request.ClientId,
|
||||
request.Provider,
|
||||
request.SigninMethod,
|
||||
request.ResponseType,
|
||||
request.RedirectUri,
|
||||
request.Scope,
|
||||
request.State,
|
||||
request.Nonce,
|
||||
request.Challenge,
|
||||
request.Resource,
|
||||
c.Ctx.Request.Host,
|
||||
c.GetAcceptLanguage(),
|
||||
)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(code.Code)
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
xormadapter "github.com/casdoor/xorm-adapter/v3"
|
||||
@@ -32,13 +32,13 @@ import (
|
||||
// @Success 200 {array} object.Enforcer
|
||||
// @router /get-enforcers [get]
|
||||
func (c *ApiController) GetEnforcers() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
enforcers, err := object.GetEnforcers(owner)
|
||||
@@ -56,7 +56,7 @@ func (c *ApiController) GetEnforcers() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
enforcers, err := object.GetPaginationEnforcers(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -75,8 +75,8 @@ func (c *ApiController) GetEnforcers() {
|
||||
// @Success 200 {object} object.Enforcer
|
||||
// @router /get-enforcer [get]
|
||||
func (c *ApiController) GetEnforcer() {
|
||||
id := c.Input().Get("id")
|
||||
loadModelCfg := c.Input().Get("loadModelCfg")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
loadModelCfg := c.Ctx.Input.Query("loadModelCfg")
|
||||
|
||||
enforcer, err := object.GetEnforcer(id)
|
||||
if err != nil {
|
||||
@@ -105,7 +105,7 @@ func (c *ApiController) GetEnforcer() {
|
||||
// @Success 200 {object} object.Enforcer
|
||||
// @router /update-enforcer [post]
|
||||
func (c *ApiController) UpdateEnforcer() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
enforcer := object.Enforcer{}
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &enforcer)
|
||||
@@ -165,8 +165,8 @@ func (c *ApiController) DeleteEnforcer() {
|
||||
// @Success 200 {array} xormadapter.CasbinRule
|
||||
// @router /get-policies [get]
|
||||
func (c *ApiController) GetPolicies() {
|
||||
id := c.Input().Get("id")
|
||||
adapterId := c.Input().Get("adapterId")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
adapterId := c.Ctx.Input.Query("adapterId")
|
||||
|
||||
if adapterId != "" {
|
||||
adapter, err := object.GetAdapter(adapterId)
|
||||
@@ -207,7 +207,7 @@ func (c *ApiController) GetPolicies() {
|
||||
// @Success 200 {array} xormadapter.CasbinRule
|
||||
// @router /get-filtered-policies [post]
|
||||
func (c *ApiController) GetFilteredPolicies() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var filters []object.Filter
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &filters)
|
||||
@@ -234,7 +234,7 @@ func (c *ApiController) GetFilteredPolicies() {
|
||||
// @Success 200 {object} Response
|
||||
// @router /update-policy [post]
|
||||
func (c *ApiController) UpdatePolicy() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var policies []xormadapter.CasbinRule
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &policies)
|
||||
@@ -261,7 +261,7 @@ func (c *ApiController) UpdatePolicy() {
|
||||
// @Success 200 {object} Response
|
||||
// @router /add-policy [post]
|
||||
func (c *ApiController) AddPolicy() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var policy xormadapter.CasbinRule
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &policy)
|
||||
@@ -288,7 +288,7 @@ func (c *ApiController) AddPolicy() {
|
||||
// @Success 200 {object} Response
|
||||
// @router /remove-policy [post]
|
||||
func (c *ApiController) RemovePolicy() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var policy xormadapter.CasbinRule
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &policy)
|
||||
|
||||
168
controllers/entry.go
Normal file
168
controllers/entry.go
Normal file
@@ -0,0 +1,168 @@
|
||||
// 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)
|
||||
}
|
||||
|
||||
// GetOpenClawSessionGraph
|
||||
// @Title GetOpenClawSessionGraph
|
||||
// @Tag Entry API
|
||||
// @Description get OpenClaw session graph
|
||||
// @Param id query string true "The id ( owner/name ) of the entry"
|
||||
// @Success 200 {object} object.OpenClawSessionGraph The Response object
|
||||
// @router /get-openclaw-session-graph [get]
|
||||
func (c *ApiController) GetOpenClawSessionGraph() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
graph, err := object.GetOpenClawSessionGraph(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(graph)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
148
controllers/entry_opentelemetry.go
Normal file
148
controllers/entry_opentelemetry.go
Normal file
@@ -0,0 +1,148 @@
|
||||
// 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)
|
||||
}
|
||||
78
controllers/entry_util.go
Normal file
78
controllers/entry_util.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
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
|
||||
}
|
||||
@@ -33,8 +33,8 @@ import (
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /faceid-signin-begin [get]
|
||||
func (c *ApiController) FaceIDSigninBegin() {
|
||||
userOwner := c.Input().Get("owner")
|
||||
userName := c.Input().Get("name")
|
||||
userOwner := c.Ctx.Input.Query("owner")
|
||||
userName := c.Ctx.Input.Query("name")
|
||||
|
||||
user, err := object.GetUserByFields(userOwner, userName)
|
||||
if err != nil {
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -46,13 +46,13 @@ func (c *ApiController) GetGlobalForms() {
|
||||
// @Success 200 {array} object.Form The Response object
|
||||
// @router /get-forms [get]
|
||||
func (c *ApiController) GetForms() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
forms, err := object.GetForms(owner)
|
||||
@@ -70,7 +70,7 @@ func (c *ApiController) GetForms() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
forms, err := object.GetPaginationForms(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -88,7 +88,7 @@ func (c *ApiController) GetForms() {
|
||||
// @Success 200 {object} object.Form The Response object
|
||||
// @router /get-form [get]
|
||||
func (c *ApiController) GetForm() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
form, err := object.GetForm(id)
|
||||
if err != nil {
|
||||
@@ -108,7 +108,7 @@ func (c *ApiController) GetForm() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-form [post]
|
||||
func (c *ApiController) UpdateForm() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var form object.Form
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
|
||||
|
||||
@@ -23,7 +23,7 @@ import "github.com/casdoor/casdoor/object"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /get-dashboard [get]
|
||||
func (c *ApiController) GetDashboard() {
|
||||
owner := c.Input().Get("owner")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
|
||||
data, err := object.GetDashboard(owner)
|
||||
if err != nil {
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,14 +30,14 @@ import (
|
||||
// @Success 200 {array} object.Group The Response object
|
||||
// @router /get-groups [get]
|
||||
func (c *ApiController) GetGroups() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
withTree := c.Input().Get("withTree")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
withTree := c.Ctx.Input.Query("withTree")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
groups, err := object.GetGroups(owner)
|
||||
@@ -66,7 +66,7 @@ func (c *ApiController) GetGroups() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
groups, err := object.GetPaginationGroups(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -109,7 +109,7 @@ func (c *ApiController) GetGroups() {
|
||||
// @Success 200 {object} object.Group The Response object
|
||||
// @router /get-group [get]
|
||||
func (c *ApiController) GetGroup() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
group, err := object.GetGroup(id)
|
||||
if err != nil {
|
||||
@@ -135,7 +135,7 @@ func (c *ApiController) GetGroup() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-group [post]
|
||||
func (c *ApiController) UpdateGroup() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var group object.Group
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &group)
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -32,13 +32,13 @@ import (
|
||||
// @Success 200 {array} object.Invitation The Response object
|
||||
// @router /get-invitations [get]
|
||||
func (c *ApiController) GetInvitations() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
invitations, err := object.GetInvitations(owner)
|
||||
@@ -56,7 +56,7 @@ func (c *ApiController) GetInvitations() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
invitations, err := object.GetPaginationInvitations(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -75,7 +75,7 @@ func (c *ApiController) GetInvitations() {
|
||||
// @Success 200 {object} object.Invitation The Response object
|
||||
// @router /get-invitation [get]
|
||||
func (c *ApiController) GetInvitation() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
invitation, err := object.GetInvitation(id)
|
||||
if err != nil {
|
||||
@@ -94,8 +94,8 @@ func (c *ApiController) GetInvitation() {
|
||||
// @Success 200 {object} object.Invitation The Response object
|
||||
// @router /get-invitation-info [get]
|
||||
func (c *ApiController) GetInvitationCodeInfo() {
|
||||
code := c.Input().Get("code")
|
||||
applicationId := c.Input().Get("applicationId")
|
||||
code := c.Ctx.Input.Query("code")
|
||||
applicationId := c.Ctx.Input.Query("applicationId")
|
||||
|
||||
application, err := object.GetApplication(applicationId)
|
||||
if err != nil {
|
||||
@@ -103,7 +103,7 @@ func (c *ApiController) GetInvitationCodeInfo() {
|
||||
return
|
||||
}
|
||||
if application == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The application: %s does not exist"), applicationId))
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), applicationId))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ func (c *ApiController) GetInvitationCodeInfo() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-invitation [post]
|
||||
func (c *ApiController) UpdateInvitation() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var invitation object.Invitation
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &invitation)
|
||||
@@ -184,7 +184,7 @@ func (c *ApiController) DeleteInvitation() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /verify-invitation [get]
|
||||
func (c *ApiController) VerifyInvitation() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
payment, attachInfo, err := object.VerifyInvitation(id)
|
||||
if err != nil {
|
||||
@@ -204,7 +204,7 @@ func (c *ApiController) VerifyInvitation() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /send-invitation [post]
|
||||
func (c *ApiController) SendInvitation() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var destinations []string
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &destinations)
|
||||
@@ -230,14 +230,23 @@ func (c *ApiController) SendInvitation() {
|
||||
return
|
||||
}
|
||||
if organization == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The organization: %s does not exist"), invitation.Owner))
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The organization: %s does not exist"), invitation.Owner))
|
||||
return
|
||||
}
|
||||
|
||||
application, err := object.GetApplicationByOrganizationName(invitation.Owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
var application *object.Application
|
||||
if invitation.Application != "" {
|
||||
application, err = object.GetApplication(fmt.Sprintf("admin/%s-org-%s", invitation.Application, invitation.Owner))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
application, err = object.GetApplicationByOrganizationName(invitation.Owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
@@ -245,6 +254,10 @@ func (c *ApiController) SendInvitation() {
|
||||
return
|
||||
}
|
||||
|
||||
if application.IsShared {
|
||||
application.Name = fmt.Sprintf("%s-org-%s", application.Name, invitation.Owner)
|
||||
}
|
||||
|
||||
provider, err := application.GetEmailProvider("Invitation")
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
|
||||
105
controllers/kerberos.go
Normal file
105
controllers/kerberos.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// 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"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/form"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// KerberosLogin
|
||||
// @Title KerberosLogin
|
||||
// @Tag Login API
|
||||
// @Description Kerberos/SPNEGO login via Integrated Windows Authentication
|
||||
// @Param application query string true "application name"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /kerberos-login [get]
|
||||
func (c *ApiController) KerberosLogin() {
|
||||
applicationName := c.Ctx.Input.Query("application")
|
||||
if applicationName == "" {
|
||||
c.ResponseError(c.T("general:Missing parameter") + ": application")
|
||||
return
|
||||
}
|
||||
|
||||
application, err := object.GetApplication(fmt.Sprintf("admin/%s", applicationName))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if application == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), applicationName))
|
||||
return
|
||||
}
|
||||
|
||||
organization, err := object.GetOrganization(util.GetId("admin", application.Organization))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if organization == nil {
|
||||
c.ResponseError(fmt.Sprintf("The organization: %s does not exist", application.Organization))
|
||||
return
|
||||
}
|
||||
|
||||
if organization.KerberosRealm == "" || organization.KerberosKeytab == "" {
|
||||
c.ResponseError("Kerberos is not configured for this organization")
|
||||
return
|
||||
}
|
||||
|
||||
authHeader := c.Ctx.Input.Header("Authorization")
|
||||
if authHeader == "" || !strings.HasPrefix(authHeader, "Negotiate ") {
|
||||
c.Ctx.Output.Header("WWW-Authenticate", "Negotiate")
|
||||
c.Ctx.Output.SetStatus(401)
|
||||
c.Ctx.Output.Body([]byte("Kerberos authentication required"))
|
||||
return
|
||||
}
|
||||
|
||||
spnegoToken := strings.TrimPrefix(authHeader, "Negotiate ")
|
||||
|
||||
kerberosUsername, err := object.ValidateKerberosToken(organization, spnegoToken)
|
||||
if err != nil {
|
||||
c.Ctx.Output.Header("WWW-Authenticate", "Negotiate")
|
||||
c.ResponseError(fmt.Sprintf("Kerberos authentication failed: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := object.GetUserByKerberosName(organization.Name, kerberosUsername)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), kerberosUsername))
|
||||
return
|
||||
}
|
||||
|
||||
application.OrganizationObj = organization
|
||||
|
||||
authForm := &form.AuthForm{
|
||||
Type: "code",
|
||||
Application: applicationName,
|
||||
Organization: organization.Name,
|
||||
}
|
||||
|
||||
resp := c.HandleLoggedIn(application, user, authForm)
|
||||
if resp != nil {
|
||||
c.Data["json"] = resp
|
||||
c.ServeJSON()
|
||||
}
|
||||
}
|
||||
222
controllers/key.go
Normal file
222
controllers/key.go
Normal file
@@ -0,0 +1,222 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// GetKeys
|
||||
// @Title GetKeys
|
||||
// @Tag Key API
|
||||
// @Description get keys
|
||||
// @Param owner query string true "The owner of keys"
|
||||
// @Success 200 {array} object.Key The Response object
|
||||
// @router /get-keys [get]
|
||||
func (c *ApiController) GetKeys() {
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
keys, err := object.GetKeys(owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
maskedKeys, err := object.GetMaskedKeys(keys, true, nil)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(maskedKeys)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
count, err := object.GetKeyCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
keys, err := object.GetPaginationKeys(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
maskedKeys, err := object.GetMaskedKeys(keys, true, nil)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(maskedKeys, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
// GetGlobalKeys
|
||||
// @Title GetGlobalKeys
|
||||
// @Tag Key API
|
||||
// @Description get global keys
|
||||
// @Success 200 {array} object.Key The Response object
|
||||
// @router /get-global-keys [get]
|
||||
func (c *ApiController) GetGlobalKeys() {
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
keys, err := object.GetGlobalKeys()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
maskedKeys, err := object.GetMaskedKeys(keys, true, nil)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(maskedKeys)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
count, err := object.GetGlobalKeyCount(field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
keys, err := object.GetPaginationGlobalKeys(paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
maskedKeys, err := object.GetMaskedKeys(keys, true, nil)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(maskedKeys, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
// GetKey
|
||||
// @Title GetKey
|
||||
// @Tag Key API
|
||||
// @Description get key
|
||||
// @Param id query string true "The id ( owner/name ) of the key"
|
||||
// @Success 200 {object} object.Key The Response object
|
||||
// @router /get-key [get]
|
||||
func (c *ApiController) GetKey() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
key, err := object.GetKey(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(key)
|
||||
}
|
||||
|
||||
// UpdateKey
|
||||
// @Title UpdateKey
|
||||
// @Tag Key API
|
||||
// @Description update key
|
||||
// @Param id query string true "The id ( owner/name ) of the key"
|
||||
// @Param body body object.Key true "The details of the key"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-key [post]
|
||||
func (c *ApiController) UpdateKey() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
oldKey, err := object.GetKey(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if oldKey == nil {
|
||||
c.Data["json"] = wrapActionResponse(false)
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
var key object.Key
|
||||
err = json.Unmarshal(c.Ctx.Input.RequestBody, &key)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !c.IsGlobalAdmin() && oldKey.Owner != key.Owner {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateKey(id, &key))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddKey
|
||||
// @Title AddKey
|
||||
// @Tag Key API
|
||||
// @Description add key
|
||||
// @Param body body object.Key true "The details of the key"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-key [post]
|
||||
func (c *ApiController) AddKey() {
|
||||
var key object.Key
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &key)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddKey(&key))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteKey
|
||||
// @Title DeleteKey
|
||||
// @Tag Key API
|
||||
// @Description delete key
|
||||
// @Param body body object.Key true "The details of the key"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-key [post]
|
||||
func (c *ApiController) DeleteKey() {
|
||||
var key object.Key
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &key)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteKey(&key))
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -46,7 +46,7 @@ type LdapSyncResp struct {
|
||||
// @Success 200 {object} controllers.LdapResp The Response object
|
||||
// @router /get-ldap-users [get]
|
||||
func (c *ApiController) GetLdapUsers() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
_, ldapId, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
@@ -114,7 +114,7 @@ func (c *ApiController) GetLdapUsers() {
|
||||
// @Success 200 {array} object.Ldap The Response object
|
||||
// @router /get-ldaps [get]
|
||||
func (c *ApiController) GetLdaps() {
|
||||
owner := c.Input().Get("owner")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
|
||||
c.ResponseOk(object.GetMaskedLdaps(object.GetLdaps(owner)))
|
||||
}
|
||||
@@ -127,7 +127,7 @@ func (c *ApiController) GetLdaps() {
|
||||
// @Success 200 {object} object.Ldap The Response object
|
||||
// @router /get-ldap [get]
|
||||
func (c *ApiController) GetLdap() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
if util.IsStringsEmpty(id) {
|
||||
c.ResponseError(c.T("general:Missing parameter"))
|
||||
@@ -266,7 +266,7 @@ func (c *ApiController) DeleteLdap() {
|
||||
// @Success 200 {object} controllers.LdapSyncResp The Response object
|
||||
// @router /sync-ldap-users [post]
|
||||
func (c *ApiController) SyncLdapUsers() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
owner, ldapId, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
|
||||
112
controllers/mcp_server.go
Normal file
112
controllers/mcp_server.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"github.com/casdoor/casdoor/mcpself"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// ProxyServer
|
||||
// @Title ProxyServer
|
||||
// @Tag Server API
|
||||
// @Description proxy request to the upstream MCP server by Server URL
|
||||
// @Param owner path string true "The owner name of the server"
|
||||
// @Param name path string true "The name of the server"
|
||||
// @Success 200 {object} mcp.McpResponse The Response object
|
||||
// @router /server/:owner/:name [get,post]
|
||||
func (c *ApiController) ProxyServer() {
|
||||
owner := c.Ctx.Input.Param(":owner")
|
||||
name := c.Ctx.Input.Param(":name")
|
||||
|
||||
var mcpReq *mcpself.McpRequest
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &mcpReq)
|
||||
if err != nil {
|
||||
c.McpResponseError(1, -32700, "Parse error", err.Error())
|
||||
return
|
||||
}
|
||||
if util.IsStringsEmpty(owner, name) {
|
||||
c.McpResponseError(1, -32600, "invalid server identifier", nil)
|
||||
return
|
||||
}
|
||||
|
||||
server, err := object.GetServer(util.GetId(owner, name))
|
||||
if err != nil {
|
||||
c.McpResponseError(mcpReq.ID, -32600, "server not found", err.Error())
|
||||
return
|
||||
}
|
||||
if server == nil {
|
||||
c.McpResponseError(mcpReq.ID, -32600, "server not found", nil)
|
||||
return
|
||||
}
|
||||
if server.Url == "" {
|
||||
c.McpResponseError(mcpReq.ID, -32600, "server URL is empty", nil)
|
||||
return
|
||||
}
|
||||
|
||||
targetUrl, err := url.Parse(server.Url)
|
||||
if err != nil || !targetUrl.IsAbs() || targetUrl.Host == "" {
|
||||
c.McpResponseError(mcpReq.ID, -32600, "server URL is invalid", nil)
|
||||
return
|
||||
}
|
||||
if targetUrl.Scheme != "http" && targetUrl.Scheme != "https" {
|
||||
c.McpResponseError(mcpReq.ID, -32600, "server URL scheme is invalid", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if mcpReq.Method == "tools/call" {
|
||||
var params mcpself.McpCallToolParams
|
||||
err = json.Unmarshal(mcpReq.Params, ¶ms)
|
||||
if err != nil {
|
||||
c.McpResponseError(mcpReq.ID, -32600, "Invalid request", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, tool := range server.Tools {
|
||||
if tool.Name == params.Name && !tool.IsAllowed {
|
||||
c.McpResponseError(mcpReq.ID, -32600, "tool is forbidden", nil)
|
||||
return
|
||||
} else if tool.Name == params.Name {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(targetUrl)
|
||||
proxy.ErrorHandler = func(writer http.ResponseWriter, request *http.Request, proxyErr error) {
|
||||
c.Ctx.Output.SetStatus(http.StatusBadGateway)
|
||||
c.McpResponseError(mcpReq.ID, -32603, "failed to proxy server request: %s", proxyErr.Error())
|
||||
}
|
||||
proxy.Director = func(request *http.Request) {
|
||||
request.URL.Scheme = targetUrl.Scheme
|
||||
request.URL.Host = targetUrl.Host
|
||||
request.Host = targetUrl.Host
|
||||
request.URL.Path = targetUrl.Path
|
||||
request.URL.RawPath = ""
|
||||
request.URL.RawQuery = targetUrl.RawQuery
|
||||
|
||||
if server.Token != "" {
|
||||
request.Header.Set("Authorization", "Bearer "+server.Token)
|
||||
}
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)
|
||||
}
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MfaSetupInitiate
|
||||
@@ -64,13 +63,20 @@ func (c *ApiController) MfaSetupInitiate() {
|
||||
return
|
||||
}
|
||||
|
||||
mfaProps, err := MfaUtil.Initiate(user.GetId())
|
||||
issuer := ""
|
||||
if organization != nil && organization.DisplayName != "" {
|
||||
issuer = organization.DisplayName
|
||||
} else if organization != nil {
|
||||
issuer = organization.Name
|
||||
}
|
||||
|
||||
mfaProps, err := MfaUtil.Initiate(user.GetId(), issuer)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
recoveryCode := uuid.NewString()
|
||||
recoveryCode := util.GenerateUUID()
|
||||
mfaProps.RecoveryCodes = []string{recoveryCode}
|
||||
mfaProps.MfaRememberInHours = organization.MfaRememberInHours
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Model The Response object
|
||||
// @router /get-models [get]
|
||||
func (c *ApiController) GetModels() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
models, err := object.GetModels(owner)
|
||||
@@ -54,7 +54,7 @@ func (c *ApiController) GetModels() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
models, err := object.GetPaginationModels(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -73,7 +73,7 @@ func (c *ApiController) GetModels() {
|
||||
// @Success 200 {object} object.Model The Response object
|
||||
// @router /get-model [get]
|
||||
func (c *ApiController) GetModel() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
model, err := object.GetModel(id)
|
||||
if err != nil {
|
||||
@@ -93,7 +93,7 @@ func (c *ApiController) GetModel() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-model [post]
|
||||
func (c *ApiController) UpdateModel() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var model object.Model
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &model)
|
||||
|
||||
74
controllers/oauth_dcr.go
Normal file
74
controllers/oauth_dcr.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
// DynamicClientRegister
|
||||
// @Title DynamicClientRegister
|
||||
// @Tag OAuth API
|
||||
// @Description Register a new OAuth 2.0 client dynamically (RFC 7591)
|
||||
// @Param organization query string false "The organization name (defaults to built-in)"
|
||||
// @Param body body object.DynamicClientRegistrationRequest true "Client registration request"
|
||||
// @Success 201 {object} object.DynamicClientRegistrationResponse
|
||||
// @Failure 400 {object} object.DcrError
|
||||
// @router /api/oauth/register [post]
|
||||
func (c *ApiController) DynamicClientRegister() {
|
||||
var req object.DynamicClientRegistrationRequest
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &req)
|
||||
if err != nil {
|
||||
c.Ctx.Output.Status = http.StatusBadRequest
|
||||
c.Data["json"] = object.DcrError{
|
||||
Error: "invalid_client_metadata",
|
||||
ErrorDescription: "invalid request body: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// Get organization from query parameter or default to built-in
|
||||
organization := c.Ctx.Input.Query("organization")
|
||||
if organization == "" {
|
||||
organization = "built-in"
|
||||
}
|
||||
|
||||
// Register the client
|
||||
response, dcrErr, err := object.RegisterDynamicClient(&req, organization)
|
||||
if err != nil {
|
||||
c.Ctx.Output.Status = http.StatusInternalServerError
|
||||
c.Data["json"] = object.DcrError{
|
||||
Error: "server_error",
|
||||
ErrorDescription: err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if dcrErr != nil {
|
||||
c.Ctx.Output.Status = http.StatusBadRequest
|
||||
c.Data["json"] = dcrErr
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// Return 201 Created
|
||||
c.Ctx.Output.Status = http.StatusCreated
|
||||
c.Data["json"] = response
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,16 +30,35 @@ import (
|
||||
// @Success 200 {array} object.Order The Response object
|
||||
// @router /get-orders [get]
|
||||
func (c *ApiController) GetOrders() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
orders, err := object.GetOrders(owner)
|
||||
var orders []*object.Order
|
||||
var err error
|
||||
|
||||
if c.IsAdmin() {
|
||||
// If field is "user", filter by that user even for admins
|
||||
if field == "user" && value != "" {
|
||||
orders, err = object.GetUserOrders(owner, value)
|
||||
} else {
|
||||
orders, err = object.GetOrders(owner)
|
||||
}
|
||||
} else {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
orders, err = object.GetUserOrders(owner, userName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -48,13 +67,23 @@ func (c *ApiController) GetOrders() {
|
||||
c.ResponseOk(orders)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
if !c.IsAdmin() {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
field = "user"
|
||||
value = userName
|
||||
}
|
||||
count, err := object.GetOrderCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
orders, err := object.GetPaginationOrders(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -74,8 +103,8 @@ func (c *ApiController) GetOrders() {
|
||||
// @Success 200 {array} object.Order The Response object
|
||||
// @router /get-user-orders [get]
|
||||
func (c *ApiController) GetUserOrders() {
|
||||
owner := c.Input().Get("owner")
|
||||
user := c.Input().Get("user")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
user := c.Ctx.Input.Query("user")
|
||||
|
||||
orders, err := object.GetUserOrders(owner, user)
|
||||
if err != nil {
|
||||
@@ -94,7 +123,7 @@ func (c *ApiController) GetUserOrders() {
|
||||
// @Success 200 {object} object.Order The Response object
|
||||
// @router /get-order [get]
|
||||
func (c *ApiController) GetOrder() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
order, err := object.GetOrder(id)
|
||||
if err != nil {
|
||||
@@ -114,7 +143,7 @@ func (c *ApiController) GetOrder() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-order [post]
|
||||
func (c *ApiController) UpdateOrder() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var order object.Order
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &order)
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@@ -34,30 +34,21 @@ import (
|
||||
// @Success 200 {object} object.Order The Response object
|
||||
// @router /place-order [post]
|
||||
func (c *ApiController) PlaceOrder() {
|
||||
productId := c.Input().Get("productId")
|
||||
pricingName := c.Input().Get("pricingName")
|
||||
planName := c.Input().Get("planName")
|
||||
customPriceStr := c.Input().Get("customPrice")
|
||||
paidUserName := c.Input().Get("userName")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
paidUserName := c.Ctx.Input.Query("userName")
|
||||
|
||||
if productId == "" {
|
||||
c.ResponseError(c.T("general:ProductId is required"))
|
||||
var req struct {
|
||||
ProductInfos []object.ProductInfo `json:"productInfos"`
|
||||
}
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &req)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var customPrice float64
|
||||
if customPriceStr != "" {
|
||||
var err error
|
||||
customPrice, err = strconv.ParseFloat(customPriceStr, 64)
|
||||
if err != nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:Invalid customPrice: %s"), customPriceStr))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
owner, _, err := util.GetOwnerAndNameFromIdWithError(productId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
productInfos := req.ProductInfos
|
||||
if len(productInfos) == 0 {
|
||||
c.ResponseError(c.T("product:Product list cannot be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,7 +80,7 @@ func (c *ApiController) PlaceOrder() {
|
||||
return
|
||||
}
|
||||
|
||||
order, err := object.PlaceOrder(productId, user, pricingName, planName, customPrice)
|
||||
order, err := object.PlaceOrder(owner, productInfos, user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -107,10 +98,10 @@ func (c *ApiController) PlaceOrder() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /pay-order [post]
|
||||
func (c *ApiController) PayOrder() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
host := c.Ctx.Request.Host
|
||||
providerName := c.Input().Get("providerName")
|
||||
paymentEnv := c.Input().Get("paymentEnv")
|
||||
providerName := c.Ctx.Input.Query("providerName")
|
||||
paymentEnv := c.Ctx.Input.Query("paymentEnv")
|
||||
|
||||
order, err := object.GetOrder(id)
|
||||
if err != nil {
|
||||
@@ -129,7 +120,7 @@ func (c *ApiController) PayOrder() {
|
||||
return
|
||||
}
|
||||
|
||||
payment, attachInfo, err := object.PayOrder(providerName, host, paymentEnv, order)
|
||||
payment, attachInfo, err := object.PayOrder(providerName, host, paymentEnv, order, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -146,7 +137,7 @@ func (c *ApiController) PayOrder() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /cancel-order [post]
|
||||
func (c *ApiController) CancelOrder() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
order, err := object.GetOrder(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,14 +30,14 @@ import (
|
||||
// @Success 200 {array} object.Organization The Response object
|
||||
// @router /get-organizations [get]
|
||||
func (c *ApiController) GetOrganizations() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
organizationName := c.Input().Get("organizationName")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
organizationName := c.Ctx.Input.Query("organizationName")
|
||||
|
||||
isGlobalAdmin := c.IsGlobalAdmin()
|
||||
if limit == "" || page == "" {
|
||||
@@ -71,7 +71,7 @@ func (c *ApiController) GetOrganizations() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
organizations, err := object.GetMaskedOrganizations(object.GetPaginationOrganizations(owner, organizationName, paginator.Offset(), limit, field, value, sortField, sortOrder))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -91,7 +91,7 @@ func (c *ApiController) GetOrganizations() {
|
||||
// @Success 200 {object} object.Organization The Response object
|
||||
// @router /get-organization [get]
|
||||
func (c *ApiController) GetOrganization() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
organization, err := object.GetMaskedOrganization(object.GetOrganization(id))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -114,7 +114,7 @@ func (c *ApiController) GetOrganization() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-organization [post]
|
||||
func (c *ApiController) UpdateOrganization() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var organization object.Organization
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &organization)
|
||||
@@ -205,7 +205,7 @@ func (c *ApiController) DeleteOrganization() {
|
||||
// @router /get-default-application [get]
|
||||
func (c *ApiController) GetDefaultApplication() {
|
||||
userId := c.GetSessionUsername()
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
application, err := object.GetDefaultApplication(id)
|
||||
if err != nil {
|
||||
@@ -225,7 +225,7 @@ func (c *ApiController) GetDefaultApplication() {
|
||||
// @Success 200 {array} object.Organization The Response object
|
||||
// @router /get-organization-names [get]
|
||||
func (c *ApiController) GetOrganizationNames() {
|
||||
owner := c.Input().Get("owner")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
organizationNames, err := object.GetOrganizationsByFields(owner, []string{"name", "display_name"}...)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,16 +30,35 @@ import (
|
||||
// @Success 200 {array} object.Payment The Response object
|
||||
// @router /get-payments [get]
|
||||
func (c *ApiController) GetPayments() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
payments, err := object.GetPayments(owner)
|
||||
var payments []*object.Payment
|
||||
var err error
|
||||
|
||||
if c.IsAdmin() {
|
||||
// If field is "user", filter by that user even for admins
|
||||
if field == "user" && value != "" {
|
||||
payments, err = object.GetUserPayments(owner, value)
|
||||
} else {
|
||||
payments, err = object.GetPayments(owner)
|
||||
}
|
||||
} else {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
payments, err = object.GetUserPayments(owner, userName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -48,13 +67,23 @@ func (c *ApiController) GetPayments() {
|
||||
c.ResponseOk(payments)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
if !c.IsAdmin() {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
field = "user"
|
||||
value = userName
|
||||
}
|
||||
count, err := object.GetPaymentCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
payments, err := object.GetPaginationPayments(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -75,8 +104,8 @@ func (c *ApiController) GetPayments() {
|
||||
// @Success 200 {array} object.Payment The Response object
|
||||
// @router /get-user-payments [get]
|
||||
func (c *ApiController) GetUserPayments() {
|
||||
owner := c.Input().Get("owner")
|
||||
user := c.Input().Get("user")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
user := c.Ctx.Input.Query("user")
|
||||
|
||||
payments, err := object.GetUserPayments(owner, user)
|
||||
if err != nil {
|
||||
@@ -95,7 +124,7 @@ func (c *ApiController) GetUserPayments() {
|
||||
// @Success 200 {object} object.Payment The Response object
|
||||
// @router /get-payment [get]
|
||||
func (c *ApiController) GetPayment() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
payment, err := object.GetPayment(id)
|
||||
if err != nil {
|
||||
@@ -115,7 +144,7 @@ func (c *ApiController) GetPayment() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-payment [post]
|
||||
func (c *ApiController) UpdatePayment() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var payment object.Payment
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &payment)
|
||||
@@ -179,7 +208,7 @@ func (c *ApiController) NotifyPayment() {
|
||||
|
||||
body := c.Ctx.Input.RequestBody
|
||||
|
||||
payment, err := object.NotifyPayment(body, owner, paymentName)
|
||||
payment, err := object.NotifyPayment(body, owner, paymentName, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -196,7 +225,7 @@ func (c *ApiController) NotifyPayment() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /invoice-payment [post]
|
||||
func (c *ApiController) InvoicePayment() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
payment, err := object.GetPayment(id)
|
||||
if err != nil {
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Permission The Response object
|
||||
// @router /get-permissions [get]
|
||||
func (c *ApiController) GetPermissions() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
permissions, err := object.GetPermissions(owner)
|
||||
@@ -54,7 +54,7 @@ func (c *ApiController) GetPermissions() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
permissions, err := object.GetPaginationPermissions(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -94,7 +94,7 @@ func (c *ApiController) GetPermissionsBySubmitter() {
|
||||
// @Success 200 {array} object.Permission The Response object
|
||||
// @router /get-permissions-by-role [get]
|
||||
func (c *ApiController) GetPermissionsByRole() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
permissions, err := object.GetPermissionsByRole(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -112,7 +112,7 @@ func (c *ApiController) GetPermissionsByRole() {
|
||||
// @Success 200 {object} object.Permission The Response object
|
||||
// @router /get-permission [get]
|
||||
func (c *ApiController) GetPermission() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
permission, err := object.GetPermission(id)
|
||||
if err != nil {
|
||||
@@ -132,7 +132,7 @@ func (c *ApiController) GetPermission() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-permission [post]
|
||||
func (c *ApiController) UpdatePermission() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var permission object.Permission
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &permission)
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Plan The Response object
|
||||
// @router /get-plans [get]
|
||||
func (c *ApiController) GetPlans() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
plans, err := object.GetPlans(owner)
|
||||
@@ -54,7 +54,7 @@ func (c *ApiController) GetPlans() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
plan, err := object.GetPaginatedPlans(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -74,8 +74,8 @@ func (c *ApiController) GetPlans() {
|
||||
// @Success 200 {object} object.Plan The Response object
|
||||
// @router /get-plan [get]
|
||||
func (c *ApiController) GetPlan() {
|
||||
id := c.Input().Get("id")
|
||||
includeOption := c.Input().Get("includeOption") == "true"
|
||||
id := c.Ctx.Input.Query("id")
|
||||
includeOption := c.Ctx.Input.Query("includeOption") == "true"
|
||||
|
||||
plan, err := object.GetPlan(id)
|
||||
if err != nil {
|
||||
@@ -107,7 +107,7 @@ func (c *ApiController) GetPlan() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-plan [post]
|
||||
func (c *ApiController) UpdatePlan() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
owner := util.GetOwnerFromId(id)
|
||||
var plan object.Plan
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plan)
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Pricing The Response object
|
||||
// @router /get-pricings [get]
|
||||
func (c *ApiController) GetPricings() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
pricings, err := object.GetPricings(owner)
|
||||
@@ -54,7 +54,7 @@ func (c *ApiController) GetPricings() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
pricing, err := object.GetPaginatedPricings(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -73,7 +73,7 @@ func (c *ApiController) GetPricings() {
|
||||
// @Success 200 {object} object.Pricing The Response object
|
||||
// @router /get-pricing [get]
|
||||
func (c *ApiController) GetPricing() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
pricing, err := object.GetPricing(id)
|
||||
if err != nil {
|
||||
@@ -93,7 +93,7 @@ func (c *ApiController) GetPricing() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-pricing [post]
|
||||
func (c *ApiController) UpdatePricing() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var pricing object.Pricing
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &pricing)
|
||||
|
||||
@@ -16,8 +16,10 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +32,13 @@ import (
|
||||
// @Success 200 {array} object.Product The Response object
|
||||
// @router /get-products [get]
|
||||
func (c *ApiController) GetProducts() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
products, err := object.GetProducts(owner)
|
||||
@@ -54,7 +56,7 @@ func (c *ApiController) GetProducts() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
products, err := object.GetPaginationProducts(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -73,7 +75,7 @@ func (c *ApiController) GetProducts() {
|
||||
// @Success 200 {object} object.Product The Response object
|
||||
// @router /get-product [get]
|
||||
func (c *ApiController) GetProduct() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
product, err := object.GetProduct(id)
|
||||
if err != nil {
|
||||
@@ -99,7 +101,7 @@ func (c *ApiController) GetProduct() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-product [post]
|
||||
func (c *ApiController) UpdateProduct() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var product object.Product
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &product)
|
||||
@@ -149,3 +151,78 @@ 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)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Provider The Response object
|
||||
// @router /get-providers [get]
|
||||
func (c *ApiController) GetProviders() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
ok, isMaskEnabled := c.IsMaskedEnabled()
|
||||
if !ok {
|
||||
@@ -59,7 +59,7 @@ func (c *ApiController) GetProviders() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
paginationProviders, err := object.GetPaginationProviders(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -78,12 +78,12 @@ func (c *ApiController) GetProviders() {
|
||||
// @Success 200 {array} object.Provider The Response object
|
||||
// @router /get-global-providers [get]
|
||||
func (c *ApiController) GetGlobalProviders() {
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
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")
|
||||
|
||||
ok, isMaskEnabled := c.IsMaskedEnabled()
|
||||
if !ok {
|
||||
@@ -106,7 +106,7 @@ func (c *ApiController) GetGlobalProviders() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
paginationGlobalProviders, err := object.GetPaginationGlobalProviders(paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -126,7 +126,7 @@ func (c *ApiController) GetGlobalProviders() {
|
||||
// @Success 200 {object} object.Provider The Response object
|
||||
// @router /get-provider [get]
|
||||
func (c *ApiController) GetProvider() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
ok, isMaskEnabled := c.IsMaskedEnabled()
|
||||
if !ok {
|
||||
@@ -164,7 +164,7 @@ func (c *ApiController) requireProviderPermission(provider *object.Provider) boo
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-provider [post]
|
||||
func (c *ApiController) UpdateProvider() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var provider object.Provider
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &provider)
|
||||
|
||||
@@ -17,9 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/casvisor/casvisor-go-sdk/casvisorsdk"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -38,13 +36,13 @@ func (c *ApiController) GetRecords() {
|
||||
return
|
||||
}
|
||||
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
organizationName := c.Input().Get("organizationName")
|
||||
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")
|
||||
organizationName := c.Ctx.Input.Query("organizationName")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
records, err := object.GetRecords()
|
||||
@@ -59,14 +57,14 @@ func (c *ApiController) GetRecords() {
|
||||
if c.IsGlobalAdmin() && organizationName != "" {
|
||||
organization = organizationName
|
||||
}
|
||||
filterRecord := &casvisorsdk.Record{Organization: organization}
|
||||
filterRecord := &object.Record{Organization: organization}
|
||||
count, err := object.GetRecordCount(field, value, filterRecord)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
records, err := object.GetPaginationRecords(paginator.Offset(), limit, field, value, sortField, sortOrder, filterRecord)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -92,7 +90,7 @@ func (c *ApiController) GetRecordsByFilter() {
|
||||
|
||||
body := string(c.Ctx.Input.RequestBody)
|
||||
|
||||
record := &casvisorsdk.Record{}
|
||||
record := &object.Record{}
|
||||
err := util.JsonToStruct(body, record)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -116,7 +114,7 @@ func (c *ApiController) GetRecordsByFilter() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-record [post]
|
||||
func (c *ApiController) AddRecord() {
|
||||
var record casvisorsdk.Record
|
||||
var record object.Record
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &record)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -44,14 +44,14 @@ import (
|
||||
// @Success 200 {array} object.Resource The Response object
|
||||
// @router /get-resources [get]
|
||||
func (c *ApiController) GetResources() {
|
||||
owner := c.Input().Get("owner")
|
||||
user := c.Input().Get("user")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
user := c.Ctx.Input.Query("user")
|
||||
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")
|
||||
|
||||
isOrgAdmin, ok := c.IsOrgAdmin()
|
||||
if !ok {
|
||||
@@ -93,7 +93,7 @@ func (c *ApiController) GetResources() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
resources, err := object.GetPaginationResources(owner, user, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -112,7 +112,7 @@ func (c *ApiController) GetResources() {
|
||||
// @Success 200 {object} object.Resource The Response object
|
||||
// @router /get-resource [get]
|
||||
func (c *ApiController) GetResource() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
resource, err := object.GetResource(id)
|
||||
if err != nil {
|
||||
@@ -132,7 +132,7 @@ func (c *ApiController) GetResource() {
|
||||
// @Success 200 {object} controllers.Response Success or error
|
||||
// @router /update-resource [post]
|
||||
func (c *ApiController) UpdateResource() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var resource object.Resource
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &resource)
|
||||
@@ -178,9 +178,11 @@ func (c *ApiController) DeleteResource() {
|
||||
}
|
||||
|
||||
if resource.Provider != "" {
|
||||
c.Input().Set("provider", resource.Provider)
|
||||
inputs, _ := c.Input()
|
||||
inputs.Set("provider", resource.Provider)
|
||||
}
|
||||
c.Input().Set("fullFilePath", resource.Name)
|
||||
inputs, _ := c.Input()
|
||||
inputs.Set("fullFilePath", resource.Name)
|
||||
provider, err := c.GetProviderFromContext("Storage")
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -188,7 +190,7 @@ func (c *ApiController) DeleteResource() {
|
||||
}
|
||||
_, resource.Name = refineFullFilePath(resource.Name)
|
||||
|
||||
tag := c.Input().Get("tag")
|
||||
tag := c.Ctx.Input.Query("tag")
|
||||
if tag == "Direct" {
|
||||
resource.Name = path.Join(provider.PathPrefix, resource.Name)
|
||||
}
|
||||
@@ -218,14 +220,14 @@ func (c *ApiController) DeleteResource() {
|
||||
// @Success 200 {object} object.Resource FileUrl, objectKey
|
||||
// @router /upload-resource [post]
|
||||
func (c *ApiController) UploadResource() {
|
||||
owner := c.Input().Get("owner")
|
||||
username := c.Input().Get("user")
|
||||
application := c.Input().Get("application")
|
||||
tag := c.Input().Get("tag")
|
||||
parent := c.Input().Get("parent")
|
||||
fullFilePath := c.Input().Get("fullFilePath")
|
||||
createdTime := c.Input().Get("createdTime")
|
||||
description := c.Input().Get("description")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
username := c.Ctx.Input.Query("user")
|
||||
application := c.Ctx.Input.Query("application")
|
||||
tag := c.Ctx.Input.Query("tag")
|
||||
parent := c.Ctx.Input.Query("parent")
|
||||
fullFilePath := c.Ctx.Input.Query("fullFilePath")
|
||||
createdTime := c.Ctx.Input.Query("createdTime")
|
||||
description := c.Ctx.Input.Query("description")
|
||||
|
||||
file, header, err := c.GetFile("file")
|
||||
if err != nil {
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Role The Response object
|
||||
// @router /get-roles [get]
|
||||
func (c *ApiController) GetRoles() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
roles, err := object.GetRoles(owner)
|
||||
@@ -54,7 +54,7 @@ func (c *ApiController) GetRoles() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
roles, err := object.GetPaginationRoles(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -73,7 +73,7 @@ func (c *ApiController) GetRoles() {
|
||||
// @Success 200 {object} object.Role The Response object
|
||||
// @router /get-role [get]
|
||||
func (c *ApiController) GetRole() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
role, err := object.GetRole(id)
|
||||
if err != nil {
|
||||
@@ -93,7 +93,7 @@ func (c *ApiController) GetRole() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-role [post]
|
||||
func (c *ApiController) UpdateRole() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var role object.Role
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &role)
|
||||
|
||||
229
controllers/rule.go
Normal file
229
controllers/rule.go
Normal file
@@ -0,0 +1,229 @@
|
||||
// Copyright 2023 The casbin 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"
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/hsluoyz/modsecurity-go/seclang/parser"
|
||||
)
|
||||
|
||||
// GetRules
|
||||
// @Title GetRules
|
||||
// @Tag Rule API
|
||||
// @Description get rules
|
||||
// @Param owner query string true "The owner of rules"
|
||||
// @Success 200 {array} object.Rule The Response object
|
||||
// @router /get-rules [get]
|
||||
func (c *ApiController) GetRules() {
|
||||
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 == "" {
|
||||
rules, err := object.GetRules(owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(rules)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
count, err := object.GetRuleCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
rules, err := object.GetPaginationRules(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(rules, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
// GetRule
|
||||
// @Title GetRule
|
||||
// @Tag Rule API
|
||||
// @Description get rule
|
||||
// @Param id query string true "The id ( owner/name ) of the rule"
|
||||
// @Success 200 {object} object.Rule The Response object
|
||||
// @router /get-rule [get]
|
||||
func (c *ApiController) GetRule() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
rule, err := object.GetRule(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(rule)
|
||||
}
|
||||
|
||||
// AddRule
|
||||
// @Title AddRule
|
||||
// @Tag Rule API
|
||||
// @Description add rule
|
||||
// @Param body body object.Rule true "The details of the rule"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-rule [post]
|
||||
func (c *ApiController) AddRule() {
|
||||
currentTime := util.GetCurrentTime()
|
||||
rule := object.Rule{
|
||||
CreatedTime: currentTime,
|
||||
UpdatedTime: currentTime,
|
||||
}
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &rule)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
err = checkExpressions(rule.Expressions, rule.Type)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = wrapActionResponse(object.AddRule(&rule))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateRule
|
||||
// @Title UpdateRule
|
||||
// @Tag Rule API
|
||||
// @Description update rule
|
||||
// @Param id query string true "The id ( owner/name ) of the rule"
|
||||
// @Param body body object.Rule true "The details of the rule"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-rule [post]
|
||||
func (c *ApiController) UpdateRule() {
|
||||
var rule object.Rule
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &rule)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = checkExpressions(rule.Expressions, rule.Type)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Ctx.Input.Query("id")
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateRule(id, &rule))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteRule
|
||||
// @Title DeleteRule
|
||||
// @Tag Rule API
|
||||
// @Description delete rule
|
||||
// @Param body body object.Rule true "The details of the rule"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-rule [post]
|
||||
func (c *ApiController) DeleteRule() {
|
||||
var rule object.Rule
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &rule)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteRule(&rule))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func checkExpressions(expressions []*object.Expression, ruleType string) error {
|
||||
values := make([]string, len(expressions))
|
||||
for i, expression := range expressions {
|
||||
values[i] = expression.Value
|
||||
}
|
||||
switch ruleType {
|
||||
case "WAF":
|
||||
return checkWafRule(values)
|
||||
case "IP":
|
||||
return checkIpRule(values)
|
||||
case "IP Rate Limiting":
|
||||
return checkIpRateRule(expressions)
|
||||
case "Compound":
|
||||
return checkCompoundRules(values)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkWafRule(rules []string) error {
|
||||
for _, rule := range rules {
|
||||
scanner := parser.NewSecLangScannerFromString(rule)
|
||||
_, err := scanner.AllDirective()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkIpRule(ipLists []string) error {
|
||||
for _, ipList := range ipLists {
|
||||
for _, ip := range strings.Split(ipList, ",") {
|
||||
_, _, err := net.ParseCIDR(ip)
|
||||
if net.ParseIP(ip) == nil && err != nil {
|
||||
return errors.New("Invalid IP address: " + ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkIpRateRule(expressions []*object.Expression) error {
|
||||
if len(expressions) != 1 {
|
||||
return errors.New("IP Rate Limiting rule must have exactly one expression")
|
||||
}
|
||||
expression := expressions[0]
|
||||
_, err := util.ParseIntWithError(expression.Operator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = util.ParseIntWithError(expression.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkCompoundRules(rules []string) error {
|
||||
_, err := object.GetRulesByRuleIds(rules)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -17,13 +17,14 @@ package controllers
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
func (c *ApiController) GetSamlMeta() {
|
||||
host := c.Ctx.Request.Host
|
||||
paramApp := c.Input().Get("application")
|
||||
paramApp := c.Ctx.Input.Query("application")
|
||||
application, err := object.GetApplication(paramApp)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -57,11 +58,12 @@ func (c *ApiController) HandleSamlRedirect() {
|
||||
owner := c.Ctx.Input.Param(":owner")
|
||||
application := c.Ctx.Input.Param(":application")
|
||||
|
||||
relayState := c.Input().Get("RelayState")
|
||||
samlRequest := c.Input().Get("SAMLRequest")
|
||||
username := c.Input().Get("username")
|
||||
loginHint := c.Input().Get("login_hint")
|
||||
relayState := c.Ctx.Input.Query("RelayState")
|
||||
samlRequest := c.Ctx.Input.Query("SAMLRequest")
|
||||
username := c.Ctx.Input.Query("username")
|
||||
loginHint := c.Ctx.Input.Query("login_hint")
|
||||
|
||||
relayState = url.QueryEscape(relayState)
|
||||
targetURL := object.GetSamlRedirectAddress(owner, application, relayState, samlRequest, host, username, loginHint)
|
||||
|
||||
c.Redirect(targetURL, http.StatusSeeOther)
|
||||
|
||||
173
controllers/server.go
Normal file
173
controllers/server.go
Normal file
@@ -0,0 +1,173 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// GetServers
|
||||
// @Title GetServers
|
||||
// @Tag Server API
|
||||
// @Description get servers
|
||||
// @Param owner query string true "The owner of servers"
|
||||
// @Success 200 {array} object.Server The Response object
|
||||
// @router /get-servers [get]
|
||||
func (c *ApiController) GetServers() {
|
||||
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 == "" {
|
||||
servers, err := object.GetServers(owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.ResponseOk(servers)
|
||||
return
|
||||
}
|
||||
|
||||
limitInt := util.ParseInt(limit)
|
||||
count, err := object.GetServerCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limitInt, count)
|
||||
servers, err := object.GetPaginationServers(owner, paginator.Offset(), limitInt, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(servers, paginator.Nums())
|
||||
}
|
||||
|
||||
// GetServer
|
||||
// @Title GetServer
|
||||
// @Tag Server API
|
||||
// @Description get server
|
||||
// @Param id query string true "The id ( owner/name ) of the server"
|
||||
// @Success 200 {object} object.Server The Response object
|
||||
// @router /get-server [get]
|
||||
func (c *ApiController) GetServer() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
server, err := object.GetServer(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(server)
|
||||
}
|
||||
|
||||
// UpdateServer
|
||||
// @Title UpdateServer
|
||||
// @Tag Server API
|
||||
// @Description update server
|
||||
// @Param id query string true "The id ( owner/name ) of the server"
|
||||
// @Param body body object.Server true "The details of the server"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-server [post]
|
||||
func (c *ApiController) UpdateServer() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
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.UpdateServer(id, &server))
|
||||
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
|
||||
// @Description add server
|
||||
// @Param body body object.Server true "The details of the server"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-server [post]
|
||||
func (c *ApiController) AddServer() {
|
||||
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.AddServer(&server))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteServer
|
||||
// @Title DeleteServer
|
||||
// @Tag Server API
|
||||
// @Description delete server
|
||||
// @Param body body object.Server true "The details of the server"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-server [post]
|
||||
func (c *ApiController) DeleteServer() {
|
||||
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.DeleteServer(&server))
|
||||
c.ServeJSON()
|
||||
}
|
||||
56
controllers/server_online.go
Normal file
56
controllers/server_online.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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)
|
||||
}
|
||||
170
controllers/server_sync.go
Normal file
170
controllers/server_sync.go
Normal file
@@ -0,0 +1,170 @@
|
||||
// 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()
|
||||
}
|
||||
@@ -15,9 +15,11 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +32,13 @@ import (
|
||||
// @Success 200 {array} string The Response object
|
||||
// @router /get-sessions [get]
|
||||
func (c *ApiController) GetSessions() {
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Input().Get("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")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
sessions, err := object.GetSessions(owner)
|
||||
@@ -53,7 +55,7 @@ func (c *ApiController) GetSessions() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
sessions, err := object.GetPaginationSessions(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -68,11 +70,11 @@ func (c *ApiController) GetSessions() {
|
||||
// @Title GetSingleSession
|
||||
// @Tag Session API
|
||||
// @Description Get session for one user in one application.
|
||||
// @Param sessionPkId query string true "The id(organization/user/application) of session"
|
||||
// @Param sessionPkId query string true "The session ID in format: organization/user/application (e.g., built-in/admin/app-built-in)"
|
||||
// @Success 200 {array} string The Response object
|
||||
// @router /get-session [get]
|
||||
func (c *ApiController) GetSingleSession() {
|
||||
id := c.Input().Get("sessionPkId")
|
||||
id := c.Ctx.Input.Query("sessionPkId")
|
||||
|
||||
session, err := object.GetSingleSession(id)
|
||||
if err != nil {
|
||||
@@ -87,8 +89,8 @@ func (c *ApiController) GetSingleSession() {
|
||||
// @Title UpdateSession
|
||||
// @Tag Session API
|
||||
// @Description Update session for one user in one application.
|
||||
// @Param id query string true "The id(organization/user/application) of session"
|
||||
// @Success 200 {array} string The Response object
|
||||
// @Param body body object.Session true "The session object to update"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-session [post]
|
||||
func (c *ApiController) UpdateSession() {
|
||||
var session object.Session
|
||||
@@ -106,9 +108,8 @@ func (c *ApiController) UpdateSession() {
|
||||
// @Title AddSession
|
||||
// @Tag Session API
|
||||
// @Description Add session for one user in one application. If there are other existing sessions, join the session into the list.
|
||||
// @Param id query string true "The id(organization/user/application) of session"
|
||||
// @Param sessionId query string true "sessionId to be added"
|
||||
// @Success 200 {array} string The Response object
|
||||
// @Param body body object.Session true "The session object to add"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-session [post]
|
||||
func (c *ApiController) AddSession() {
|
||||
var session object.Session
|
||||
@@ -126,8 +127,8 @@ func (c *ApiController) AddSession() {
|
||||
// @Title DeleteSession
|
||||
// @Tag Session API
|
||||
// @Description Delete session for one user in one application.
|
||||
// @Param id query string true "The id(organization/user/application) of session"
|
||||
// @Success 200 {array} string The Response object
|
||||
// @Param body body object.Session true "The session object to delete"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-session [post]
|
||||
func (c *ApiController) DeleteSession() {
|
||||
var session object.Session
|
||||
@@ -137,7 +138,21 @@ func (c *ApiController) DeleteSession() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteSession(util.GetSessionId(session.Owner, session.Name, session.Application)))
|
||||
curSessionId := c.Ctx.Input.CruSession.SessionID(context.Background())
|
||||
|
||||
sessionId := c.Ctx.Input.Query("sessionId")
|
||||
if curSessionId == sessionId && sessionId != "" {
|
||||
c.ResponseError(fmt.Sprintf(c.T("session:session id %s is the current session and cannot be deleted"), curSessionId))
|
||||
return
|
||||
}
|
||||
|
||||
if sessionId != "" {
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteSessionId(util.GetSessionId(session.Owner, session.Name, session.Application), sessionId))
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteSession(util.GetSessionId(session.Owner, session.Name, session.Application), curSessionId))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -145,13 +160,13 @@ func (c *ApiController) DeleteSession() {
|
||||
// @Title IsSessionDuplicated
|
||||
// @Tag Session API
|
||||
// @Description Check if there are other different sessions for one user in one application.
|
||||
// @Param sessionPkId query string true "The id(organization/user/application) of session"
|
||||
// @Param sessionId query string true "sessionId to be checked"
|
||||
// @Param sessionPkId query string true "The session ID in format: organization/user/application (e.g., built-in/admin/app-built-in)"
|
||||
// @Param sessionId query string true "The specific session ID to check"
|
||||
// @Success 200 {array} string The Response object
|
||||
// @router /is-session-duplicated [get]
|
||||
func (c *ApiController) IsSessionDuplicated() {
|
||||
id := c.Input().Get("sessionPkId")
|
||||
sessionId := c.Input().Get("sessionId")
|
||||
id := c.Ctx.Input.Query("sessionPkId")
|
||||
sessionId := c.Ctx.Input.Query("sessionId")
|
||||
|
||||
isUserSessionDuplicated, err := object.IsSessionDuplicated(id, sessionId)
|
||||
if err != nil {
|
||||
|
||||
165
controllers/site.go
Normal file
165
controllers/site.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// Copyright 2023 The casbin 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"
|
||||
)
|
||||
|
||||
// GetGlobalSites
|
||||
// @Title GetGlobalSites
|
||||
// @Tag Site API
|
||||
// @Description get global sites
|
||||
// @Success 200 {array} object.Site The Response object
|
||||
// @router /get-global-sites [get]
|
||||
func (c *ApiController) GetGlobalSites() {
|
||||
sites, err := object.GetGlobalSites()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(object.GetMaskedSites(sites, util.GetHostname()))
|
||||
}
|
||||
|
||||
// GetSites
|
||||
// @Title GetSites
|
||||
// @Tag Site API
|
||||
// @Description get sites
|
||||
// @Param owner query string true "The owner of sites"
|
||||
// @Success 200 {array} object.Site The Response object
|
||||
// @router /get-sites [get]
|
||||
func (c *ApiController) GetSites() {
|
||||
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 == "" {
|
||||
sites, err := object.GetSites(owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.ResponseOk(object.GetMaskedSites(sites, util.GetHostname()))
|
||||
return
|
||||
}
|
||||
|
||||
limitInt := util.ParseInt(limit)
|
||||
count, err := object.GetSiteCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limitInt, count)
|
||||
sites, err := object.GetPaginationSites(owner, paginator.Offset(), limitInt, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(object.GetMaskedSites(sites, util.GetHostname()), paginator.Nums())
|
||||
}
|
||||
|
||||
// GetSite
|
||||
// @Title GetSite
|
||||
// @Tag Site API
|
||||
// @Description get site
|
||||
// @Param id query string true "The id ( owner/name ) of the site"
|
||||
// @Success 200 {object} object.Site The Response object
|
||||
// @router /get-site [get]
|
||||
func (c *ApiController) GetSite() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
site, err := object.GetSite(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(object.GetMaskedSite(site, util.GetHostname()))
|
||||
}
|
||||
|
||||
// UpdateSite
|
||||
// @Title UpdateSite
|
||||
// @Tag Site API
|
||||
// @Description update site
|
||||
// @Param id query string true "The id ( owner/name ) of the site"
|
||||
// @Param body body object.Site true "The details of the site"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-site [post]
|
||||
func (c *ApiController) UpdateSite() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var site object.Site
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &site)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateSite(id, &site))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddSite
|
||||
// @Title AddSite
|
||||
// @Tag Site API
|
||||
// @Description add site
|
||||
// @Param body body object.Site true "The details of the site"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-site [post]
|
||||
func (c *ApiController) AddSite() {
|
||||
var site object.Site
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &site)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddSite(&site))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteSite
|
||||
// @Title DeleteSite
|
||||
// @Tag Site API
|
||||
// @Description delete site
|
||||
// @Param body body object.Site true "The details of the site"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-site [post]
|
||||
func (c *ApiController) DeleteSite() {
|
||||
var site object.Site
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &site)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteSite(&site))
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -16,8 +16,9 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,16 +31,35 @@ import (
|
||||
// @Success 200 {array} object.Subscription The Response object
|
||||
// @router /get-subscriptions [get]
|
||||
func (c *ApiController) GetSubscriptions() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
subscriptions, err := object.GetSubscriptions(owner)
|
||||
var subscriptions []*object.Subscription
|
||||
var err error
|
||||
|
||||
if c.IsAdmin() {
|
||||
// If field is "user", filter by that user even for admins
|
||||
if field == "user" && value != "" {
|
||||
subscriptions, err = object.GetSubscriptionsByUser(owner, value)
|
||||
} else {
|
||||
subscriptions, err = object.GetSubscriptions(owner)
|
||||
}
|
||||
} else {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
subscriptions, err = object.GetSubscriptionsByUser(owner, userName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -48,13 +68,23 @@ func (c *ApiController) GetSubscriptions() {
|
||||
c.ResponseOk(subscriptions)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
if !c.IsAdmin() {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
field = "user"
|
||||
value = userName
|
||||
}
|
||||
count, err := object.GetSubscriptionCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
subscription, err := object.GetPaginationSubscriptions(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -73,7 +103,7 @@ func (c *ApiController) GetSubscriptions() {
|
||||
// @Success 200 {object} object.Subscription The Response object
|
||||
// @router /get-subscription [get]
|
||||
func (c *ApiController) GetSubscription() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
subscription, err := object.GetSubscription(id)
|
||||
if err != nil {
|
||||
@@ -93,7 +123,7 @@ func (c *ApiController) GetSubscription() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-subscription [post]
|
||||
func (c *ApiController) UpdateSubscription() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var subscription object.Subscription
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &subscription)
|
||||
@@ -121,6 +151,26 @@ func (c *ApiController) AddSubscription() {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if plan restricts user to one subscription
|
||||
if subscription.Plan != "" {
|
||||
plan, err := object.GetPlan(util.GetId(subscription.Owner, subscription.Plan))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if plan != nil && plan.IsExclusive {
|
||||
hasSubscription, err := object.HasActiveSubscriptionForPlan(subscription.Owner, subscription.User, subscription.Plan)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if hasSubscription {
|
||||
c.ResponseError(fmt.Sprintf("User already has an active subscription for plan: %s", subscription.Plan))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddSubscription(&subscription))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -31,14 +31,14 @@ import (
|
||||
// @Success 200 {array} object.Syncer The Response object
|
||||
// @router /get-syncers [get]
|
||||
func (c *ApiController) GetSyncers() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
organization := c.Input().Get("organization")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
organization := c.Ctx.Input.Query("organization")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
syncers, err := object.GetMaskedSyncers(object.GetOrganizationSyncers(owner, organization))
|
||||
@@ -56,7 +56,7 @@ func (c *ApiController) GetSyncers() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
syncers, err := object.GetMaskedSyncers(object.GetPaginationSyncers(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -75,7 +75,7 @@ func (c *ApiController) GetSyncers() {
|
||||
// @Success 200 {object} object.Syncer The Response object
|
||||
// @router /get-syncer [get]
|
||||
func (c *ApiController) GetSyncer() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
syncer, err := object.GetMaskedSyncer(object.GetSyncer(id))
|
||||
if err != nil {
|
||||
@@ -95,7 +95,7 @@ func (c *ApiController) GetSyncer() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-syncer [post]
|
||||
func (c *ApiController) UpdateSyncer() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var syncer object.Syncer
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &syncer)
|
||||
@@ -154,7 +154,7 @@ func (c *ApiController) DeleteSyncer() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /run-syncer [get]
|
||||
func (c *ApiController) RunSyncer() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
syncer, err := object.GetSyncer(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/go-git/go-git/v5"
|
||||
)
|
||||
|
||||
// GetSystemInfo
|
||||
@@ -46,10 +49,10 @@ func (c *ApiController) GetSystemInfo() {
|
||||
// @Success 200 {object} util.VersionInfo The Response object
|
||||
// @router /get-version-info [get]
|
||||
func (c *ApiController) GetVersionInfo() {
|
||||
errInfo := ""
|
||||
versionInfo, err := util.GetVersionInfo()
|
||||
if err != nil {
|
||||
errInfo = "Git error: " + err.Error()
|
||||
if err != nil && !errors.Is(err, git.ErrRepositoryNotExists) {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if versionInfo.Version != "" {
|
||||
@@ -57,14 +60,7 @@ func (c *ApiController) GetVersionInfo() {
|
||||
return
|
||||
}
|
||||
|
||||
versionInfo, err = util.GetVersionInfoFromFile()
|
||||
if err != nil {
|
||||
errInfo = errInfo + ", File error: " + err.Error()
|
||||
c.ResponseError(errInfo)
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(versionInfo)
|
||||
c.ResponseOk(util.GetBuiltInVersionInfo())
|
||||
}
|
||||
|
||||
// Health
|
||||
|
||||
271
controllers/ticket.go
Normal file
271
controllers/ticket.go
Normal file
@@ -0,0 +1,271 @@
|
||||
// Copyright 2024 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"
|
||||
)
|
||||
|
||||
// GetTickets
|
||||
// @Title GetTickets
|
||||
// @Tag Ticket API
|
||||
// @Description get tickets
|
||||
// @Param owner query string true "The owner of tickets"
|
||||
// @Success 200 {array} object.Ticket The Response object
|
||||
// @router /get-tickets [get]
|
||||
func (c *ApiController) GetTickets() {
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
user := c.getCurrentUser()
|
||||
isAdmin := c.IsAdmin()
|
||||
|
||||
var tickets []*object.Ticket
|
||||
var err error
|
||||
|
||||
if limit == "" || page == "" {
|
||||
if isAdmin {
|
||||
tickets, err = object.GetTickets(owner)
|
||||
} else {
|
||||
tickets, err = object.GetUserTickets(owner, user.GetId())
|
||||
}
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(tickets)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
var count int64
|
||||
|
||||
if isAdmin {
|
||||
count, err = object.GetTicketCount(owner, field, value)
|
||||
} else {
|
||||
// For non-admin users, only show their own tickets
|
||||
tickets, err = object.GetUserTickets(owner, user.GetId())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
count = int64(len(tickets))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
|
||||
if isAdmin {
|
||||
tickets, err = object.GetPaginationTickets(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(tickets, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
// GetTicket
|
||||
// @Title GetTicket
|
||||
// @Tag Ticket API
|
||||
// @Description get ticket
|
||||
// @Param id query string true "The id ( owner/name ) of the ticket"
|
||||
// @Success 200 {object} object.Ticket The Response object
|
||||
// @router /get-ticket [get]
|
||||
func (c *ApiController) GetTicket() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
ticket, err := object.GetTicket(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Check permission: user can only view their own tickets unless they are admin
|
||||
user := c.getCurrentUser()
|
||||
isAdmin := c.IsAdmin()
|
||||
|
||||
if ticket != nil && !isAdmin && ticket.User != user.GetId() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(ticket)
|
||||
}
|
||||
|
||||
// UpdateTicket
|
||||
// @Title UpdateTicket
|
||||
// @Tag Ticket API
|
||||
// @Description update ticket
|
||||
// @Param id query string true "The id ( owner/name ) of the ticket"
|
||||
// @Param body body object.Ticket true "The details of the ticket"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-ticket [post]
|
||||
func (c *ApiController) UpdateTicket() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var ticket object.Ticket
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ticket)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Check permission
|
||||
user := c.getCurrentUser()
|
||||
isAdmin := c.IsAdmin()
|
||||
|
||||
existingTicket, err := object.GetTicket(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if existingTicket == nil {
|
||||
c.ResponseError(c.T("ticket:Ticket not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// Normal users can only close their own tickets
|
||||
if !isAdmin {
|
||||
if existingTicket.User != user.GetId() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
// Normal users can only change state to "Closed"
|
||||
if ticket.State != "Closed" && ticket.State != existingTicket.State {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
// Preserve original fields that users shouldn't modify
|
||||
ticket.Owner = existingTicket.Owner
|
||||
ticket.Name = existingTicket.Name
|
||||
ticket.User = existingTicket.User
|
||||
ticket.CreatedTime = existingTicket.CreatedTime
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateTicket(id, &ticket))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddTicket
|
||||
// @Title AddTicket
|
||||
// @Tag Ticket API
|
||||
// @Description add ticket
|
||||
// @Param body body object.Ticket true "The details of the ticket"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-ticket [post]
|
||||
func (c *ApiController) AddTicket() {
|
||||
var ticket object.Ticket
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ticket)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set the user field to the current user
|
||||
user := c.getCurrentUser()
|
||||
ticket.User = user.GetId()
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddTicket(&ticket))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteTicket
|
||||
// @Title DeleteTicket
|
||||
// @Tag Ticket API
|
||||
// @Description delete ticket
|
||||
// @Param body body object.Ticket true "The details of the ticket"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-ticket [post]
|
||||
func (c *ApiController) DeleteTicket() {
|
||||
var ticket object.Ticket
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ticket)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Only admins can delete tickets
|
||||
if !c.IsAdmin() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteTicket(&ticket))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddTicketMessage
|
||||
// @Title AddTicketMessage
|
||||
// @Tag Ticket API
|
||||
// @Description add a message to a ticket
|
||||
// @Param id query string true "The id ( owner/name ) of the ticket"
|
||||
// @Param body body object.TicketMessage true "The message to add"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-ticket-message [post]
|
||||
func (c *ApiController) AddTicketMessage() {
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var message object.TicketMessage
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &message)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Check permission
|
||||
user := c.getCurrentUser()
|
||||
isAdmin := c.IsAdmin()
|
||||
|
||||
ticket, err := object.GetTicket(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if ticket == nil {
|
||||
c.ResponseError(c.T("ticket:Ticket not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// Users can only add messages to their own tickets, admins can add to any ticket
|
||||
if !isAdmin && ticket.User != user.GetId() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
// Set the author and admin flag
|
||||
message.Author = user.GetId()
|
||||
message.IsAdmin = isAdmin
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddTicketMessage(id, &message))
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -28,20 +28,20 @@ import (
|
||||
// @Title GetTokens
|
||||
// @Tag Token API
|
||||
// @Description get tokens
|
||||
// @Param owner query string true "The owner of tokens"
|
||||
// @Param owner query string true "The organization name (e.g., built-in)"
|
||||
// @Param pageSize query string true "The size of each page"
|
||||
// @Param p query string true "The number of the page"
|
||||
// @Success 200 {array} object.Token The Response object
|
||||
// @router /get-tokens [get]
|
||||
func (c *ApiController) GetTokens() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
organization := c.Input().Get("organization")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
organization := c.Ctx.Input.Query("organization")
|
||||
if limit == "" || page == "" {
|
||||
token, err := object.GetTokens(owner, organization)
|
||||
if err != nil {
|
||||
@@ -58,7 +58,7 @@ func (c *ApiController) GetTokens() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
tokens, err := object.GetPaginationTokens(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -73,11 +73,11 @@ func (c *ApiController) GetTokens() {
|
||||
// @Title GetToken
|
||||
// @Tag Token API
|
||||
// @Description get token
|
||||
// @Param id query string true "The id ( owner/name ) of token"
|
||||
// @Param id query string true "The token ID in format: organization/token-name (e.g., built-in/token-123456)"
|
||||
// @Success 200 {object} object.Token The Response object
|
||||
// @router /get-token [get]
|
||||
func (c *ApiController) GetToken() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
token, err := object.GetToken(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -91,12 +91,12 @@ func (c *ApiController) GetToken() {
|
||||
// @Title UpdateToken
|
||||
// @Tag Token API
|
||||
// @Description update token
|
||||
// @Param id query string true "The id ( owner/name ) of token"
|
||||
// @Param id query string true "The token ID in format: organization/token-name (e.g., built-in/token-123456)"
|
||||
// @Param body body object.Token true "Details of the token"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-token [post]
|
||||
func (c *ApiController) UpdateToken() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var token object.Token
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &token)
|
||||
@@ -160,19 +160,26 @@ func (c *ApiController) DeleteToken() {
|
||||
// @Success 401 {object} object.TokenError The Response object
|
||||
// @router /login/oauth/access_token [post]
|
||||
func (c *ApiController) GetOAuthToken() {
|
||||
clientId := c.Input().Get("client_id")
|
||||
clientSecret := c.Input().Get("client_secret")
|
||||
grantType := c.Input().Get("grant_type")
|
||||
code := c.Input().Get("code")
|
||||
verifier := c.Input().Get("code_verifier")
|
||||
scope := c.Input().Get("scope")
|
||||
nonce := c.Input().Get("nonce")
|
||||
username := c.Input().Get("username")
|
||||
password := c.Input().Get("password")
|
||||
tag := c.Input().Get("tag")
|
||||
avatar := c.Input().Get("avatar")
|
||||
refreshToken := c.Input().Get("refresh_token")
|
||||
deviceCode := c.Input().Get("device_code")
|
||||
clientId := c.Ctx.Input.Query("client_id")
|
||||
clientSecret := c.Ctx.Input.Query("client_secret")
|
||||
assertion := c.Ctx.Input.Query("assertion")
|
||||
clientAssertion := c.Ctx.Input.Query("client_assertion")
|
||||
clientAssertionType := c.Ctx.Input.Query("client_assertion_type")
|
||||
grantType := c.Ctx.Input.Query("grant_type")
|
||||
code := c.Ctx.Input.Query("code")
|
||||
verifier := c.Ctx.Input.Query("code_verifier")
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
nonce := c.Ctx.Input.Query("nonce")
|
||||
username := c.Ctx.Input.Query("username")
|
||||
password := c.Ctx.Input.Query("password")
|
||||
tag := c.Ctx.Input.Query("tag")
|
||||
avatar := c.Ctx.Input.Query("avatar")
|
||||
refreshToken := c.Ctx.Input.Query("refresh_token")
|
||||
deviceCode := c.Ctx.Input.Query("device_code")
|
||||
subjectToken := c.Ctx.Input.Query("subject_token")
|
||||
subjectTokenType := c.Ctx.Input.Query("subject_token_type")
|
||||
audience := c.Ctx.Input.Query("audience")
|
||||
resource := c.Ctx.Input.Query("resource")
|
||||
|
||||
if clientId == "" && clientSecret == "" {
|
||||
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
|
||||
@@ -189,6 +196,12 @@ func (c *ApiController) GetOAuthToken() {
|
||||
if clientSecret == "" {
|
||||
clientSecret = tokenRequest.ClientSecret
|
||||
}
|
||||
if clientAssertion == "" {
|
||||
clientAssertion = tokenRequest.ClientAssertion
|
||||
}
|
||||
if clientAssertionType == "" {
|
||||
clientAssertionType = tokenRequest.ClientAssertionType
|
||||
}
|
||||
if grantType == "" {
|
||||
grantType = tokenRequest.GrantType
|
||||
}
|
||||
@@ -219,9 +232,28 @@ func (c *ApiController) GetOAuthToken() {
|
||||
if refreshToken == "" {
|
||||
refreshToken = tokenRequest.RefreshToken
|
||||
}
|
||||
if subjectToken == "" {
|
||||
subjectToken = tokenRequest.SubjectToken
|
||||
}
|
||||
if subjectTokenType == "" {
|
||||
subjectTokenType = tokenRequest.SubjectTokenType
|
||||
}
|
||||
if audience == "" {
|
||||
audience = tokenRequest.Audience
|
||||
}
|
||||
if resource == "" {
|
||||
resource = tokenRequest.Resource
|
||||
}
|
||||
if assertion == "" {
|
||||
assertion = tokenRequest.Assertion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract DPoP proof header (RFC 9449). Empty string when DPoP is not used.
|
||||
dpopProof := c.Ctx.Request.Header.Get("DPoP")
|
||||
|
||||
host := c.Ctx.Request.Host
|
||||
if deviceCode != "" {
|
||||
deviceAuthCache, ok := object.DeviceAuthMap.Load(deviceCode)
|
||||
if !ok {
|
||||
@@ -262,8 +294,7 @@ func (c *ApiController) GetOAuthToken() {
|
||||
username = deviceAuthCacheCast.UserName
|
||||
}
|
||||
|
||||
host := c.Ctx.Request.Host
|
||||
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage())
|
||||
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage(), subjectToken, subjectTokenType, assertion, clientAssertion, clientAssertionType, audience, resource, dpopProof)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -288,11 +319,11 @@ func (c *ApiController) GetOAuthToken() {
|
||||
// @Success 401 {object} object.TokenError The Response object
|
||||
// @router /login/oauth/refresh_token [post]
|
||||
func (c *ApiController) RefreshToken() {
|
||||
grantType := c.Input().Get("grant_type")
|
||||
refreshToken := c.Input().Get("refresh_token")
|
||||
scope := c.Input().Get("scope")
|
||||
clientId := c.Input().Get("client_id")
|
||||
clientSecret := c.Input().Get("client_secret")
|
||||
grantType := c.Ctx.Input.Query("grant_type")
|
||||
refreshToken := c.Ctx.Input.Query("refresh_token")
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
clientId := c.Ctx.Input.Query("client_id")
|
||||
clientSecret := c.Ctx.Input.Query("client_secret")
|
||||
host := c.Ctx.Request.Host
|
||||
|
||||
if clientId == "" {
|
||||
@@ -307,7 +338,13 @@ func (c *ApiController) RefreshToken() {
|
||||
}
|
||||
}
|
||||
|
||||
refreshToken2, err := object.RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
|
||||
ok, application, clientId, _, err := c.ValidateOAuth(true)
|
||||
if err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
dpopProof := c.Ctx.Request.Header.Get("DPoP")
|
||||
refreshToken2, err := object.RefreshToken(application, grantType, refreshToken, scope, clientId, clientSecret, host, dpopProof)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -318,14 +355,79 @@ func (c *ApiController) RefreshToken() {
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *ApiController) ResponseTokenError(errorMsg string) {
|
||||
func (c *ApiController) ResponseTokenError(errorMsg string, errorDescription string) {
|
||||
c.Data["json"] = &object.TokenError{
|
||||
Error: errorMsg,
|
||||
Error: errorMsg,
|
||||
ErrorDescription: errorDescription,
|
||||
}
|
||||
c.SetTokenErrorHttpStatus()
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *ApiController) ValidateOAuth(ignoreValidSecret bool) (ok bool, application *object.Application, clientId, clientSecret string, err error) {
|
||||
reqClientId := c.Ctx.Input.Query("client_id")
|
||||
reqClientSecret := c.Ctx.Input.Query("client_secret")
|
||||
clientAssertion := c.Ctx.Input.Query("client_assertion")
|
||||
clientAssertionType := c.Ctx.Input.Query("client_assertion_type")
|
||||
|
||||
if reqClientId == "" && clientAssertionType == "" {
|
||||
var tokenRequest TokenRequest
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest); err == nil {
|
||||
reqClientId = tokenRequest.ClientId
|
||||
reqClientSecret = tokenRequest.ClientSecret
|
||||
clientAssertion = tokenRequest.ClientAssertion
|
||||
clientAssertionType = tokenRequest.ClientAssertionType
|
||||
}
|
||||
}
|
||||
|
||||
if clientAssertionType == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" {
|
||||
ok, application, err = object.ValidateClientAssertion(clientAssertion, c.Ctx.Request.Host)
|
||||
if err != nil {
|
||||
c.ResponseTokenError(object.InvalidClient, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !ok || application == nil {
|
||||
c.ResponseTokenError(object.InvalidClient, "client_assertion is invalid")
|
||||
return
|
||||
}
|
||||
|
||||
clientSecret = application.ClientSecret
|
||||
clientId = application.ClientId
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
if reqClientId == "" && reqClientSecret == "" {
|
||||
clientId, clientSecret, ok = c.Ctx.Request.BasicAuth()
|
||||
if !ok {
|
||||
clientId = c.Ctx.Input.Query("client_id")
|
||||
clientSecret = c.Ctx.Input.Query("client_secret")
|
||||
if clientId == "" || clientSecret == "" {
|
||||
c.ResponseTokenError(object.InvalidRequest, "")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clientId = reqClientId
|
||||
clientSecret = reqClientSecret
|
||||
}
|
||||
|
||||
application, err = object.GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
c.ResponseTokenError(object.InvalidClient, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if application == nil || (application.ClientSecret != clientSecret && !ignoreValidSecret) {
|
||||
c.ResponseTokenError(object.InvalidClient, c.T("token:Invalid application or wrong clientSecret"))
|
||||
return
|
||||
}
|
||||
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
// IntrospectToken
|
||||
// @Title IntrospectToken
|
||||
// @Tag Login API
|
||||
@@ -333,7 +435,7 @@ func (c *ApiController) ResponseTokenError(errorMsg string) {
|
||||
// parameter representing an OAuth 2.0 token and returns a JSON document
|
||||
// representing the meta information surrounding the
|
||||
// token, including whether this token is currently active.
|
||||
// This endpoint only support Basic Authorization.
|
||||
// This endpoint support Basic Authorization and authorization defined in RFC 7523.
|
||||
//
|
||||
// @Param token formData string true "access_token's value or refresh_token's value"
|
||||
// @Param token_type_hint formData string true "the token type access_token or refresh_token"
|
||||
@@ -342,25 +444,10 @@ func (c *ApiController) ResponseTokenError(errorMsg string) {
|
||||
// @Success 401 {object} object.TokenError The Response object
|
||||
// @router /login/oauth/introspect [post]
|
||||
func (c *ApiController) IntrospectToken() {
|
||||
tokenValue := c.Input().Get("token")
|
||||
clientId, clientSecret, ok := c.Ctx.Request.BasicAuth()
|
||||
if !ok {
|
||||
clientId = c.Input().Get("client_id")
|
||||
clientSecret = c.Input().Get("client_secret")
|
||||
if clientId == "" || clientSecret == "" {
|
||||
c.ResponseTokenError(object.InvalidRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
tokenValue := c.Ctx.Input.Query("token")
|
||||
|
||||
application, err := object.GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
c.ResponseTokenError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if application == nil || application.ClientSecret != clientSecret {
|
||||
c.ResponseTokenError(c.T("token:Invalid application or wrong clientSecret"))
|
||||
ok, application, clientId, _, err := c.ValidateOAuth(false)
|
||||
if err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -369,12 +456,12 @@ func (c *ApiController) IntrospectToken() {
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
tokenTypeHint := c.Input().Get("token_type_hint")
|
||||
tokenTypeHint := c.Ctx.Input.Query("token_type_hint")
|
||||
var token *object.Token
|
||||
if tokenTypeHint != "" {
|
||||
token, err = object.GetTokenByTokenValue(tokenValue, tokenTypeHint)
|
||||
if err != nil {
|
||||
c.ResponseTokenError(err.Error())
|
||||
c.ResponseTokenError(object.InvalidRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if token == nil || token.ExpiresIn <= 0 {
|
||||
@@ -451,7 +538,7 @@ func (c *ApiController) IntrospectToken() {
|
||||
if tokenTypeHint == "" {
|
||||
token, err = object.GetTokenByTokenValue(tokenValue, introspectionResponse.TokenType)
|
||||
if err != nil {
|
||||
c.ResponseTokenError(err.Error())
|
||||
c.ResponseTokenError(object.InvalidRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if token == nil || token.ExpiresIn <= 0 {
|
||||
@@ -463,7 +550,7 @@ func (c *ApiController) IntrospectToken() {
|
||||
if token != nil {
|
||||
application, err = object.GetApplication(fmt.Sprintf("%s/%s", token.Owner, token.Application))
|
||||
if err != nil {
|
||||
c.ResponseTokenError(err.Error())
|
||||
c.ResponseTokenError(object.InvalidClient, err.Error())
|
||||
return
|
||||
}
|
||||
if application == nil {
|
||||
@@ -473,6 +560,11 @@ func (c *ApiController) IntrospectToken() {
|
||||
|
||||
introspectionResponse.TokenType = token.TokenType
|
||||
introspectionResponse.ClientId = application.ClientId
|
||||
|
||||
// Expose DPoP key binding in the introspection response (RFC 9449 §8).
|
||||
if token.DPoPJkt != "" {
|
||||
introspectionResponse.Cnf = &object.DPoPConfirmation{JKT: token.DPoPJkt}
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = introspectionResponse
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -30,13 +30,13 @@ import (
|
||||
// @Success 200 {array} object.Transaction The Response object
|
||||
// @router /get-transactions [get]
|
||||
func (c *ApiController) GetTransactions() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
var transactions []*object.Transaction
|
||||
@@ -86,7 +86,7 @@ func (c *ApiController) GetTransactions() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
transactions, err := object.GetPaginationTransactions(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -105,7 +105,7 @@ func (c *ApiController) GetTransactions() {
|
||||
// @Success 200 {object} object.Transaction The Response object
|
||||
// @router /get-transaction [get]
|
||||
func (c *ApiController) GetTransaction() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
transaction, err := object.GetTransaction(id)
|
||||
if err != nil {
|
||||
@@ -146,7 +146,7 @@ func (c *ApiController) GetTransaction() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-transaction [post]
|
||||
func (c *ApiController) UpdateTransaction() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var transaction object.Transaction
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &transaction)
|
||||
@@ -175,7 +175,7 @@ func (c *ApiController) AddTransaction() {
|
||||
return
|
||||
}
|
||||
|
||||
dryRunParam := c.Input().Get("dryRun")
|
||||
dryRunParam := c.Ctx.Input.Query("dryRun")
|
||||
dryRun := dryRunParam != ""
|
||||
|
||||
affected, transactionId, err := object.AddTransaction(&transaction, c.GetAcceptLanguage(), dryRun)
|
||||
|
||||
@@ -15,16 +15,23 @@
|
||||
package controllers
|
||||
|
||||
type TokenRequest struct {
|
||||
ClientId string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
GrantType string `json:"grant_type"`
|
||||
Code string `json:"code"`
|
||||
Verifier string `json:"code_verifier"`
|
||||
Scope string `json:"scope"`
|
||||
Nonce string `json:"nonce"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Tag string `json:"tag"`
|
||||
Avatar string `json:"avatar"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Assertion string `json:"assertion"`
|
||||
ClientId string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
ClientAssertion string `json:"client_assertion"`
|
||||
ClientAssertionType string `json:"client_assertion_type"`
|
||||
GrantType string `json:"grant_type"`
|
||||
Code string `json:"code"`
|
||||
Verifier string `json:"code_verifier"`
|
||||
Scope string `json:"scope"`
|
||||
Nonce string `json:"nonce"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Tag string `json:"tag"`
|
||||
Avatar string `json:"avatar"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
SubjectToken string `json:"subject_token"`
|
||||
SubjectTokenType string `json:"subject_token_type"`
|
||||
Audience string `json:"audience"`
|
||||
Resource string `json:"resource"` // RFC 8707 Resource Indicator
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@@ -32,12 +32,12 @@ import (
|
||||
// @Success 200 {array} object.User The Response object
|
||||
// @router /get-global-users [get]
|
||||
func (c *ApiController) GetGlobalUsers() {
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
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 == "" {
|
||||
users, err := object.GetMaskedUsers(object.GetGlobalUsers())
|
||||
@@ -55,7 +55,7 @@ func (c *ApiController) GetGlobalUsers() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
users, err := object.GetPaginationGlobalUsers(paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -80,14 +80,14 @@ func (c *ApiController) GetGlobalUsers() {
|
||||
// @Success 200 {array} object.User The Response object
|
||||
// @router /get-users [get]
|
||||
func (c *ApiController) GetUsers() {
|
||||
owner := c.Input().Get("owner")
|
||||
groupName := c.Input().Get("groupName")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
groupName := c.Ctx.Input.Query("groupName")
|
||||
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 == "" {
|
||||
if groupName != "" {
|
||||
@@ -115,7 +115,7 @@ func (c *ApiController) GetUsers() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
users, err := object.GetPaginationUsers(owner, paginator.Offset(), limit, field, value, sortField, sortOrder, groupName)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -144,11 +144,11 @@ func (c *ApiController) GetUsers() {
|
||||
// @Success 200 {object} object.User The Response object
|
||||
// @router /get-user [get]
|
||||
func (c *ApiController) GetUser() {
|
||||
id := c.Input().Get("id")
|
||||
email := c.Input().Get("email")
|
||||
phone := c.Input().Get("phone")
|
||||
userId := c.Input().Get("userId")
|
||||
owner := c.Input().Get("owner")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
email := c.Ctx.Input.Query("email")
|
||||
phone := c.Ctx.Input.Query("phone")
|
||||
userId := c.Ctx.Input.Query("userId")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
var err error
|
||||
var userFromUserId *object.User
|
||||
if userId != "" && owner != "" {
|
||||
@@ -259,10 +259,10 @@ func (c *ApiController) GetUser() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-user [post]
|
||||
func (c *ApiController) UpdateUser() {
|
||||
id := c.Input().Get("id")
|
||||
userId := c.Input().Get("userId")
|
||||
owner := c.Input().Get("owner")
|
||||
columnsStr := c.Input().Get("columns")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
userId := c.Ctx.Input.Query("userId")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
columnsStr := c.Ctx.Input.Query("columns")
|
||||
|
||||
var user object.User
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
|
||||
@@ -336,7 +336,7 @@ func (c *ApiController) UpdateUser() {
|
||||
}
|
||||
|
||||
isAdmin := c.IsAdmin()
|
||||
allowDisplayNameEmpty := c.Input().Get("allowEmpty") != ""
|
||||
allowDisplayNameEmpty := c.Ctx.Input.Query("allowEmpty") != ""
|
||||
if pass, err := object.CheckPermissionForUpdateUser(oldUser, &user, isAdmin, allowDisplayNameEmpty, c.GetAcceptLanguage()); !pass {
|
||||
c.ResponseError(err)
|
||||
return
|
||||
@@ -690,9 +690,9 @@ func (c *ApiController) CheckUserPassword() {
|
||||
// @Success 200 {array} object.User The Response object
|
||||
// @router /get-sorted-users [get]
|
||||
func (c *ApiController) GetSortedUsers() {
|
||||
owner := c.Input().Get("owner")
|
||||
sorter := c.Input().Get("sorter")
|
||||
limit := util.ParseInt(c.Input().Get("limit"))
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
sorter := c.Ctx.Input.Query("sorter")
|
||||
limit := util.ParseInt(c.Ctx.Input.Query("limit"))
|
||||
|
||||
users, err := object.GetMaskedUsers(object.GetSortedUsers(owner, sorter, limit))
|
||||
if err != nil {
|
||||
@@ -712,8 +712,8 @@ func (c *ApiController) GetSortedUsers() {
|
||||
// @Success 200 {int} int The count of filtered users for an organization
|
||||
// @router /get-user-count [get]
|
||||
func (c *ApiController) GetUserCount() {
|
||||
owner := c.Input().Get("owner")
|
||||
isOnline := c.Input().Get("isOnline")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
isOnline := c.Ctx.Input.Query("isOnline")
|
||||
|
||||
var count int64
|
||||
var err error
|
||||
@@ -730,29 +730,6 @@ func (c *ApiController) GetUserCount() {
|
||||
c.ResponseOk(count)
|
||||
}
|
||||
|
||||
// AddUserKeys
|
||||
// @Title AddUserKeys
|
||||
// @router /add-user-keys [post]
|
||||
// @Tag User API
|
||||
// @Success 200 {object} object.Userinfo The Response object
|
||||
func (c *ApiController) AddUserKeys() {
|
||||
var user object.User
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
isAdmin := c.IsAdmin()
|
||||
affected, err := object.AddUserKeys(&user, isAdmin)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(affected)
|
||||
}
|
||||
|
||||
func (c *ApiController) RemoveUserFromGroup() {
|
||||
owner := c.Ctx.Request.Form.Get("owner")
|
||||
name := c.Ctx.Request.Form.Get("name")
|
||||
@@ -777,3 +754,205 @@ func (c *ApiController) RemoveUserFromGroup() {
|
||||
|
||||
c.ResponseOk(affected)
|
||||
}
|
||||
|
||||
// ImpersonateUser
|
||||
// @Title ImpersonateUser
|
||||
// @Tag User API
|
||||
// @Description set impersonation user for current admin session
|
||||
// @Param username formData string true "The username to impersonate (owner/name)"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /impersonation-user [post]
|
||||
func (c *ApiController) ImpersonateUser() {
|
||||
org, ok := c.RequireAdmin()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
username := c.Ctx.Request.Form.Get("username")
|
||||
if username == "" {
|
||||
c.ResponseError(c.T("general:Missing parameter"))
|
||||
return
|
||||
}
|
||||
|
||||
owner, _, err := util.GetOwnerAndNameFromIdWithError(username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !(owner == org || org == "") {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
targetUser, err := object.GetUser(username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if targetUser == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), username))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.SetSession("impersonateUser", username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.Ctx.SetCookie("impersonateUser", username, 0, "/")
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
||||
// ExitImpersonateUser
|
||||
// @Title ExitImpersonateUser
|
||||
// @Tag User API
|
||||
// @Description clear impersonation info for current session
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /exit-impersonation-user [post]
|
||||
func (c *ApiController) ExitImpersonateUser() {
|
||||
_, ok := c.Ctx.Input.GetData("impersonating").(bool)
|
||||
if !ok {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
err := c.SetSession("impersonateUser", "")
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.Ctx.SetCookie("impersonateUser", "", -1, "/")
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
||||
// VerifyIdentification
|
||||
// @Title VerifyIdentification
|
||||
// @Tag User API
|
||||
// @Description verify user's real identity using ID Verification provider
|
||||
// @Param owner query string false "The owner of the user (optional, defaults to logged-in user)"
|
||||
// @Param name query string false "The name of the user (optional, defaults to logged-in user)"
|
||||
// @Param provider query string false "The name of the ID Verification provider (optional, auto-selected if not provided)"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /verify-identification [post]
|
||||
func (c *ApiController) VerifyIdentification() {
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
name := c.Ctx.Input.Query("name")
|
||||
providerName := c.Ctx.Input.Query("provider")
|
||||
|
||||
// If user not specified, use logged-in user
|
||||
if owner == "" || name == "" {
|
||||
loggedInUser := c.GetSessionUsername()
|
||||
if loggedInUser == "" {
|
||||
c.ResponseError(c.T("general:Please login first"))
|
||||
return
|
||||
}
|
||||
var err error
|
||||
owner, name, err = util.GetOwnerAndNameFromIdWithError(loggedInUser)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// If user is specified, check if current user has permission to verify other users
|
||||
// Only admins can verify other users
|
||||
loggedInUser := c.GetSessionUsername()
|
||||
if loggedInUser != util.GetId(owner, name) && !c.IsAdmin() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
user, err := object.GetUser(util.GetId(owner, name))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(owner, name)))
|
||||
return
|
||||
}
|
||||
|
||||
if user.IdCard == "" || user.IdCardType == "" || user.RealName == "" {
|
||||
c.ResponseError(c.T("user:ID card information and real name are required"))
|
||||
return
|
||||
}
|
||||
|
||||
if user.IsVerified {
|
||||
c.ResponseError(c.T("user:User is already verified"))
|
||||
return
|
||||
}
|
||||
|
||||
var provider *object.Provider
|
||||
// If provider not specified, find suitable IDV provider from user's application
|
||||
if providerName == "" {
|
||||
application, err := object.GetApplicationByUser(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
c.ResponseError(c.T("user:No application found for user"))
|
||||
return
|
||||
}
|
||||
|
||||
// Find IDV provider from application
|
||||
idvProvider, err := object.GetIdvProviderByApplication(util.GetId(application.Owner, application.Name), "false", c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if idvProvider == nil {
|
||||
c.ResponseError(c.T("provider:No ID Verification provider configured"))
|
||||
return
|
||||
}
|
||||
provider = idvProvider
|
||||
} else {
|
||||
provider, err = object.GetProvider(providerName)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if provider == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s does not exist"), providerName))
|
||||
return
|
||||
}
|
||||
|
||||
if provider.Category != "ID Verification" {
|
||||
c.ResponseError(c.T("provider:Provider is not an ID Verification provider"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
idvProvider := object.GetIdvProviderFromProvider(provider)
|
||||
if idvProvider == nil {
|
||||
c.ResponseError(c.T("provider:Failed to initialize ID Verification provider"))
|
||||
return
|
||||
}
|
||||
|
||||
verified, err := idvProvider.VerifyIdentity(user.IdCardType, user.IdCard, user.RealName)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !verified {
|
||||
c.ResponseError(c.T("user:Identity verification failed"))
|
||||
return
|
||||
}
|
||||
|
||||
// Set IsVerified to true upon successful verification
|
||||
user.IsVerified = true
|
||||
_, err = object.UpdateUser(user.GetId(), user, []string{"is_verified"}, false)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(user.RealName)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -106,7 +107,7 @@ func (c *ApiController) RequireSignedInUser() (*object.User, bool) {
|
||||
}
|
||||
|
||||
if object.IsAppUser(userId) {
|
||||
tmpUserId := c.Input().Get("userId")
|
||||
tmpUserId := c.Ctx.Input.Query("userId")
|
||||
if tmpUserId != "" {
|
||||
userId = tmpUserId
|
||||
}
|
||||
@@ -172,7 +173,7 @@ func (c *ApiController) IsOrgAdmin() (bool, bool) {
|
||||
// IsMaskedEnabled ...
|
||||
func (c *ApiController) IsMaskedEnabled() (bool, bool) {
|
||||
isMaskEnabled := true
|
||||
withSecret := c.Input().Get("withSecret")
|
||||
withSecret := c.Ctx.Input.Query("withSecret")
|
||||
if withSecret == "1" {
|
||||
isMaskEnabled = false
|
||||
|
||||
@@ -202,14 +203,14 @@ func refineFullFilePath(fullFilePath string) (string, string) {
|
||||
}
|
||||
|
||||
func (c *ApiController) GetProviderFromContext(category string) (*object.Provider, error) {
|
||||
providerName := c.Input().Get("provider")
|
||||
providerName := c.Ctx.Input.Query("provider")
|
||||
if providerName == "" {
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
if field == "provider" && value != "" {
|
||||
providerName = value
|
||||
} else {
|
||||
fullFilePath := c.Input().Get("fullFilePath")
|
||||
fullFilePath := c.Ctx.Input.Query("fullFilePath")
|
||||
providerName, _ = refineFullFilePath(fullFilePath)
|
||||
}
|
||||
}
|
||||
@@ -230,7 +231,7 @@ func (c *ApiController) GetProviderFromContext(category string) (*object.Provide
|
||||
|
||||
userId, ok := c.RequireSignedIn()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(c.T("general:Please login first"))
|
||||
return nil, errors.New(c.T("general:Please login first"))
|
||||
}
|
||||
|
||||
application, err := object.GetApplicationByUserId(userId)
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/captcha"
|
||||
"github.com/casdoor/casdoor/form"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@@ -49,14 +49,14 @@ func (c *ApiController) GetVerifications() {
|
||||
return
|
||||
}
|
||||
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
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")
|
||||
|
||||
owner := c.Input().Get("owner")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
// For global admin with organizationName parameter, use it to filter
|
||||
// For org admin, use their organization
|
||||
if c.IsGlobalAdmin() && owner != "" {
|
||||
@@ -79,7 +79,7 @@ func (c *ApiController) GetVerifications() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
payments, err := object.GetPaginationVerifications(organization, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -100,8 +100,8 @@ func (c *ApiController) GetVerifications() {
|
||||
// @Success 200 {array} object.Verification The Response object
|
||||
// @router /get-user-payments [get]
|
||||
func (c *ApiController) GetUserVerifications() {
|
||||
owner := c.Input().Get("owner")
|
||||
user := c.Input().Get("user")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
user := c.Ctx.Input.Query("user")
|
||||
|
||||
payments, err := object.GetUserVerifications(owner, user)
|
||||
if err != nil {
|
||||
@@ -120,7 +120,7 @@ func (c *ApiController) GetUserVerifications() {
|
||||
// @Success 200 {object} object.Verification The Response object
|
||||
// @router /get-payment [get]
|
||||
func (c *ApiController) GetVerification() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
payment, err := object.GetVerification(id)
|
||||
if err != nil {
|
||||
@@ -151,42 +151,33 @@ func (c *ApiController) SendVerificationCode() {
|
||||
return
|
||||
}
|
||||
|
||||
provider, err := object.GetCaptchaProviderByApplication(vform.ApplicationId, "false", c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if provider != nil {
|
||||
if vform.CaptchaType != provider.Type {
|
||||
c.ResponseError(c.T("verification:Turing test failed."))
|
||||
return
|
||||
}
|
||||
|
||||
if provider.Type != "Default" {
|
||||
vform.ClientSecret = provider.ClientSecret
|
||||
}
|
||||
|
||||
if vform.CaptchaType != "none" {
|
||||
if captchaProvider := captcha.GetCaptchaProvider(vform.CaptchaType); captchaProvider == nil {
|
||||
c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType)
|
||||
return
|
||||
} else if isHuman, err := captchaProvider.VerifyCaptcha(vform.CaptchaToken, provider.ClientId, vform.ClientSecret, provider.ClientId2); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
} else if !isHuman {
|
||||
c.ResponseError(c.T("verification:Turing test failed."))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
application, err := object.GetApplication(vform.ApplicationId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), vform.ApplicationId))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if "Forgot password?" signin item is visible when using forget verification
|
||||
if vform.Method == ForgetVerification {
|
||||
isForgotPasswordEnabled := false
|
||||
for _, item := range application.SigninItems {
|
||||
if item.Name == "Forgot password?" {
|
||||
isForgotPasswordEnabled = item.Visible
|
||||
break
|
||||
}
|
||||
}
|
||||
// Block access if the signin item is not found or is explicitly hidden
|
||||
if !isForgotPasswordEnabled {
|
||||
c.ResponseError(c.T("verification:The forgot password feature is disabled"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
organization, err := object.GetOrganization(util.GetId(application.Owner, application.Organization))
|
||||
if err != nil {
|
||||
c.ResponseError(c.T(err.Error()))
|
||||
@@ -198,6 +189,7 @@ func (c *ApiController) SendVerificationCode() {
|
||||
}
|
||||
|
||||
var user *object.User
|
||||
// Try to resolve user for CAPTCHA rule checking
|
||||
// checkUser != "", means method is ForgetVerification
|
||||
if vform.CheckUser != "" {
|
||||
owner := application.Organization
|
||||
@@ -215,18 +207,90 @@ func (c *ApiController) SendVerificationCode() {
|
||||
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// mfaUserSession != "", means method is MfaAuthVerification
|
||||
if mfaUserSession := c.getMfaUserSession(); mfaUserSession != "" {
|
||||
} else if mfaUserSession := c.getMfaUserSession(); mfaUserSession != "" {
|
||||
// mfaUserSession != "", means method is MfaAuthVerification
|
||||
user, err = object.GetUser(mfaUserSession)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
} else if vform.Method == ResetVerification {
|
||||
// For reset verification, get the current logged-in user
|
||||
user = c.getCurrentUser()
|
||||
} else if vform.Method == LoginVerification {
|
||||
// For login verification, try to find user by email/phone for CAPTCHA check
|
||||
// This is a preliminary lookup; the actual validation happens later in the switch statement
|
||||
if vform.Type == object.VerifyTypeEmail && util.IsEmailValid(vform.Dest) {
|
||||
user, err = object.GetUserByEmail(organization.Name, vform.Dest)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
} else if vform.Type == object.VerifyTypePhone {
|
||||
// Prefer resolving the user directly by phone, consistent with the later login switch,
|
||||
// so that Dynamic CAPTCHA is not skipped due to missing/invalid country code.
|
||||
user, err = object.GetUserByPhone(organization.Name, vform.Dest)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine username for CAPTCHA check
|
||||
username := ""
|
||||
if user != nil {
|
||||
username = user.Name
|
||||
} else if vform.CheckUser != "" {
|
||||
username = vform.CheckUser
|
||||
}
|
||||
|
||||
// Check if CAPTCHA should be enabled based on the rule (Dynamic/Always/Internet-Only)
|
||||
enableCaptcha, err := object.CheckToEnableCaptcha(application, organization.Name, username, clientIp)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if vform.CaptchaToken != "" {
|
||||
enableCaptcha = true
|
||||
}
|
||||
|
||||
// Only verify CAPTCHA if it should be enabled
|
||||
if enableCaptcha {
|
||||
captchaProvider, err := object.GetCaptchaProviderByApplication(vform.ApplicationId, "false", c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if captchaProvider != nil {
|
||||
if vform.CaptchaType != captchaProvider.Type {
|
||||
c.ResponseError(c.T("verification:Turing test failed."))
|
||||
return
|
||||
}
|
||||
|
||||
if captchaProvider.Type != "Default" {
|
||||
vform.ClientSecret = captchaProvider.ClientSecret
|
||||
}
|
||||
|
||||
if vform.CaptchaType != "none" {
|
||||
if captchaService := captcha.GetCaptchaProvider(vform.CaptchaType); captchaService == nil {
|
||||
c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType)
|
||||
return
|
||||
} else if isHuman, err := captchaService.VerifyCaptcha(vform.CaptchaToken, captchaProvider.ClientId, vform.ClientSecret, captchaProvider.ClientId2); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
} else if !isHuman {
|
||||
c.ResponseError(c.T("verification:Turing test failed."))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendResp := errors.New("invalid dest type")
|
||||
var provider *object.Provider
|
||||
|
||||
switch vform.Type {
|
||||
case object.VerifyTypeEmail:
|
||||
@@ -380,6 +444,8 @@ 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")
|
||||
@@ -434,13 +500,9 @@ func (c *ApiController) ResetEmailOrPhone() {
|
||||
}
|
||||
}
|
||||
|
||||
result, err := object.CheckVerificationCode(checkDest, code, c.GetAcceptLanguage())
|
||||
err = object.CheckVerifyCodeWithLimitAndIp(user, clientIp, checkDest, code, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(c.T(err.Error()))
|
||||
return
|
||||
}
|
||||
if result.Code != object.VerificationSuccess {
|
||||
c.ResponseError(result.Msg)
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -538,15 +600,12 @@ func (c *ApiController) VerifyCode() {
|
||||
}
|
||||
|
||||
if !passed {
|
||||
result, err := object.CheckVerificationCode(checkDest, authForm.Code, c.GetAcceptLanguage())
|
||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||
err = object.CheckVerifyCodeWithLimitAndIp(user, clientIp, checkDest, authForm.Code, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if result.Code != object.VerificationSuccess {
|
||||
c.ResponseError(result.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
err = object.DisableVerificationCode(checkDest)
|
||||
if err != nil {
|
||||
|
||||
@@ -126,8 +126,8 @@ func (c *ApiController) WebAuthnSigninBegin() {
|
||||
return
|
||||
}
|
||||
|
||||
userOwner := c.Input().Get("owner")
|
||||
userName := c.Input().Get("name")
|
||||
userOwner := c.Ctx.Input.Query("owner")
|
||||
userName := c.Ctx.Input.Query("name")
|
||||
|
||||
var options *protocol.CredentialAssertion
|
||||
var sessionData *webauthn.SessionData
|
||||
@@ -171,8 +171,8 @@ func (c *ApiController) WebAuthnSigninBegin() {
|
||||
// @Success 200 {object} controllers.Response "The Response object"
|
||||
// @router /webauthn/signin/finish [post]
|
||||
func (c *ApiController) WebAuthnSigninFinish() {
|
||||
responseType := c.Input().Get("responseType")
|
||||
clientId := c.Input().Get("clientId")
|
||||
responseType := c.Ctx.Input.Query("responseType")
|
||||
clientId := c.Ctx.Input.Query("clientId")
|
||||
webauthnObj, err := object.GetWebAuthnObject(c.Ctx.Request.Host)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
|
||||
@@ -17,7 +17,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/beego/beego/v2/core/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -31,14 +31,14 @@ import (
|
||||
// @router /get-webhooks [get]
|
||||
// @Security test_apiKey
|
||||
func (c *ApiController) GetWebhooks() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
organization := c.Input().Get("organization")
|
||||
owner := c.Ctx.Input.Query("owner")
|
||||
limit := c.Ctx.Input.Query("pageSize")
|
||||
page := c.Ctx.Input.Query("p")
|
||||
field := c.Ctx.Input.Query("field")
|
||||
value := c.Ctx.Input.Query("value")
|
||||
sortField := c.Ctx.Input.Query("sortField")
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
organization := c.Ctx.Input.Query("organization")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
webhooks, err := object.GetWebhooks(owner, organization)
|
||||
@@ -56,7 +56,7 @@ func (c *ApiController) GetWebhooks() {
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
|
||||
|
||||
webhooks, err := object.GetPaginationWebhooks(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
@@ -76,7 +76,7 @@ func (c *ApiController) GetWebhooks() {
|
||||
// @Success 200 {object} object.Webhook The Response object
|
||||
// @router /get-webhook [get]
|
||||
func (c *ApiController) GetWebhook() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
webhook, err := object.GetWebhook(id)
|
||||
if err != nil {
|
||||
@@ -96,7 +96,7 @@ func (c *ApiController) GetWebhook() {
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-webhook [post]
|
||||
func (c *ApiController) UpdateWebhook() {
|
||||
id := c.Input().Get("id")
|
||||
id := c.Ctx.Input.Query("id")
|
||||
|
||||
var webhook object.Webhook
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &webhook)
|
||||
|
||||
202
controllers/webhook_event.go
Normal file
202
controllers/webhook_event.go
Normal file
@@ -0,0 +1,202 @@
|
||||
// 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()
|
||||
}
|
||||
45
controllers/wellknown_oauth_prm.go
Normal file
45
controllers/wellknown_oauth_prm.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// 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 (
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
// GetOauthProtectedResourceMetadata
|
||||
// @Title GetOauthProtectedResourceMetadata
|
||||
// @Tag OAuth 2.0 API
|
||||
// @Description Get OAuth 2.0 Protected Resource Metadata (RFC 9728)
|
||||
// @Success 200 {object} object.OauthProtectedResourceMetadata
|
||||
// @router /.well-known/oauth-protected-resource [get]
|
||||
func (c *RootController) GetOauthProtectedResourceMetadata() {
|
||||
host := c.Ctx.Request.Host
|
||||
c.Data["json"] = object.GetOauthProtectedResourceMetadata(host)
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetOauthProtectedResourceMetadataByApplication
|
||||
// @Title GetOauthProtectedResourceMetadataByApplication
|
||||
// @Tag OAuth 2.0 API
|
||||
// @Description Get OAuth 2.0 Protected Resource Metadata for specific application (RFC 9728)
|
||||
// @Param application path string true "application name"
|
||||
// @Success 200 {object} object.OauthProtectedResourceMetadata
|
||||
// @router /.well-known/:application/oauth-protected-resource [get]
|
||||
func (c *RootController) GetOauthProtectedResourceMetadataByApplication() {
|
||||
application := c.Ctx.Input.Param(":application")
|
||||
host := c.Ctx.Request.Host
|
||||
c.Data["json"] = object.GetOauthProtectedResourceMetadataByApplication(host, application)
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -85,11 +85,12 @@ func (c *RootController) GetJwksByApplication() {
|
||||
// @Success 200 {object} object.WebFinger
|
||||
// @router /.well-known/webfinger [get]
|
||||
func (c *RootController) GetWebFinger() {
|
||||
resource := c.Input().Get("resource")
|
||||
resource := c.Ctx.Input.Query("resource")
|
||||
rels := []string{}
|
||||
host := c.Ctx.Request.Host
|
||||
|
||||
for key, value := range c.Input() {
|
||||
inputs, _ := c.Input()
|
||||
for key, value := range inputs {
|
||||
if strings.HasPrefix(key, "rel") {
|
||||
rels = append(rels, value...)
|
||||
}
|
||||
@@ -115,11 +116,12 @@ func (c *RootController) GetWebFinger() {
|
||||
// @router /.well-known/:application/webfinger [get]
|
||||
func (c *RootController) GetWebFingerByApplication() {
|
||||
application := c.Ctx.Input.Param(":application")
|
||||
resource := c.Input().Get("resource")
|
||||
resource := c.Ctx.Input.Query("resource")
|
||||
rels := []string{}
|
||||
host := c.Ctx.Request.Host
|
||||
|
||||
for key, value := range c.Input() {
|
||||
inputs, _ := c.Input()
|
||||
for key, value := range inputs {
|
||||
if strings.HasPrefix(key, "rel") {
|
||||
rels = append(rels, value...)
|
||||
}
|
||||
@@ -135,3 +137,29 @@ func (c *RootController) GetWebFingerByApplication() {
|
||||
c.Ctx.Output.ContentType("application/jrd+json")
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetOAuthServerMetadata
|
||||
// @Title GetOAuthServerMetadata
|
||||
// @Tag OAuth API
|
||||
// @Description Get OAuth 2.0 Authorization Server Metadata (RFC 8414)
|
||||
// @Success 200 {object} object.OidcDiscovery
|
||||
// @router /.well-known/oauth-authorization-server [get]
|
||||
func (c *RootController) GetOAuthServerMetadata() {
|
||||
host := c.Ctx.Request.Host
|
||||
c.Data["json"] = object.GetOidcDiscovery(host, "")
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetOAuthServerMetadataByApplication
|
||||
// @Title GetOAuthServerMetadataByApplication
|
||||
// @Tag OAuth API
|
||||
// @Description Get OAuth 2.0 Authorization Server Metadata for specific application (RFC 8414)
|
||||
// @Param application path string true "application name"
|
||||
// @Success 200 {object} object.OidcDiscovery
|
||||
// @router /.well-known/:application/oauth-authorization-server [get]
|
||||
func (c *RootController) GetOAuthServerMetadataByApplication() {
|
||||
application := c.Ctx.Input.Param(":application")
|
||||
host := c.Ctx.Request.Host
|
||||
c.Data["json"] = object.GetOidcDiscovery(host, application)
|
||||
c.ServeJSON()
|
||||
}
|
||||
20
docker-compose.dev.yml
Normal file
20
docker-compose.dev.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5434:5432"
|
||||
environment:
|
||||
POSTGRES_USER: casdoor
|
||||
POSTGRES_PASSWORD: casdoor_dev
|
||||
POSTGRES_DB: casdoor
|
||||
volumes:
|
||||
- casdoor_pg_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U casdoor"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
casdoor_pg_data:
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/bin/bash
|
||||
if [ "${MYSQL_ROOT_PASSWORD}" = "" ] ;then MYSQL_ROOT_PASSWORD=123456 ;fi
|
||||
|
||||
service mariadb start
|
||||
if [ -z "${driverName:-}" ]; then
|
||||
export driverName=sqlite
|
||||
fi
|
||||
if [ -z "${dataSourceName:-}" ]; then
|
||||
export dataSourceName="file:casdoor.db?cache=shared"
|
||||
fi
|
||||
|
||||
mysqladmin -u root password ${MYSQL_ROOT_PASSWORD}
|
||||
|
||||
exec /server --createDatabase=true
|
||||
exec /server
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
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", uuid.New().String())
|
||||
req.Header.Set("repeatability-request-id", util.GenerateUUID())
|
||||
req.Header.Set("repeatability-first-sent", time.Now().UTC().Format(http.TimeFormat))
|
||||
|
||||
client := &http.Client{}
|
||||
|
||||
@@ -18,14 +18,17 @@ type EmailProvider interface {
|
||||
Send(fromAddress string, fromName string, toAddress []string, subject string, content string) error
|
||||
}
|
||||
|
||||
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool, endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string, enableProxy bool) EmailProvider {
|
||||
if typ == "Azure ACS" {
|
||||
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, sslMode string, endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string, enableProxy bool) EmailProvider {
|
||||
switch typ {
|
||||
case "Azure ACS":
|
||||
return NewAzureACSEmailProvider(clientSecret, host)
|
||||
} else if typ == "Custom HTTP Email" {
|
||||
case "Custom HTTP Email":
|
||||
return NewHttpEmailProvider(endpoint, method, httpHeaders, bodyMapping, contentType)
|
||||
} else if typ == "SendGrid" {
|
||||
case "SendGrid":
|
||||
return NewSendgridEmailProvider(clientSecret, host, endpoint)
|
||||
} else {
|
||||
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl, enableProxy)
|
||||
case "Resend":
|
||||
return NewResendEmailProvider(clientSecret)
|
||||
default:
|
||||
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, sslMode, enableProxy)
|
||||
}
|
||||
}
|
||||
|
||||
48
email/resend.go
Normal file
48
email/resend.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// 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 email
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/resend/resend-go/v3"
|
||||
)
|
||||
|
||||
type ResendEmailProvider struct {
|
||||
Client *resend.Client
|
||||
}
|
||||
|
||||
func NewResendEmailProvider(apiKey string) *ResendEmailProvider {
|
||||
client := resend.NewClient(apiKey)
|
||||
client.UserAgent += " Casdoor"
|
||||
return &ResendEmailProvider{Client: client}
|
||||
}
|
||||
|
||||
func (s *ResendEmailProvider) Send(fromAddress string, fromName string, toAddresses []string, subject string, content string) error {
|
||||
from := fromAddress
|
||||
if fromName != "" {
|
||||
from = fmt.Sprintf("%s <%s>", fromName, fromAddress)
|
||||
}
|
||||
params := &resend.SendEmailRequest{
|
||||
From: from,
|
||||
To: toAddresses,
|
||||
Subject: subject,
|
||||
Html: content,
|
||||
}
|
||||
if _, err := s.Client.Emails.Send(params); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -25,13 +25,20 @@ type SmtpEmailProvider struct {
|
||||
Dialer *gomail.Dialer
|
||||
}
|
||||
|
||||
func NewSmtpEmailProvider(userName string, password string, host string, port int, typ string, disableSsl bool, enableProxy bool) *SmtpEmailProvider {
|
||||
func NewSmtpEmailProvider(userName string, password string, host string, port int, typ string, sslMode string, enableProxy bool) *SmtpEmailProvider {
|
||||
dialer := gomail.NewDialer(host, port, userName, password)
|
||||
if typ == "SUBMAIL" {
|
||||
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
dialer.SSL = !disableSsl
|
||||
// Handle SSL mode: "Auto" (or empty) means don't override gomail's default behavior
|
||||
// "Enable" means force SSL on, "Disable" means force SSL off
|
||||
if sslMode == "Enable" {
|
||||
dialer.SSL = true
|
||||
} else if sslMode == "Disable" {
|
||||
dialer.SSL = false
|
||||
}
|
||||
// If sslMode is "Auto" or empty, don't set dialer.SSL - let gomail decide based on port
|
||||
|
||||
if enableProxy {
|
||||
socks5Proxy := conf.GetConfigString("socks5Proxy")
|
||||
|
||||
@@ -46,6 +46,7 @@ type AuthForm struct {
|
||||
State string `json:"state"`
|
||||
RedirectUri string `json:"redirectUri"`
|
||||
Method string `json:"method"`
|
||||
CodeVerifier string `json:"codeVerifier"`
|
||||
|
||||
EmailCode string `json:"emailCode"`
|
||||
PhoneCode string `json:"phoneCode"`
|
||||
|
||||
241
go.mod
241
go.mod
@@ -1,60 +1,77 @@
|
||||
module github.com/casdoor/casdoor
|
||||
|
||||
go 1.23.0
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.25.8
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.3
|
||||
github.com/NdoleStudio/lemonsqueezy-go v1.2.4
|
||||
github.com/PaddleHQ/paddle-go-sdk v1.0.0
|
||||
github.com/adyen/adyen-go-api-library/v11 v11.0.0
|
||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||
github.com/alibabacloud-go/cloudauth-20190307/v3 v3.9.2
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.4
|
||||
github.com/alibabacloud-go/facebody-20191230/v5 v5.1.2
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0
|
||||
github.com/alibabacloud-go/tea v1.3.2
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible
|
||||
github.com/aliyun/credentials-go v1.3.10
|
||||
github.com/aws/aws-sdk-go v1.45.5
|
||||
github.com/beego/beego v1.12.12
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0
|
||||
github.com/beego/beego/v2 v2.3.8
|
||||
github.com/beevik/etree v1.1.0
|
||||
github.com/casbin/casbin/v2 v2.77.2
|
||||
github.com/casbin/lego/v4 v4.5.4
|
||||
github.com/casdoor/casdoor-go-sdk v0.50.0
|
||||
github.com/casdoor/go-sms-sender v0.25.0
|
||||
github.com/casdoor/gomail/v2 v2.2.0
|
||||
github.com/casdoor/ldapserver v1.2.0
|
||||
github.com/casdoor/notify v1.0.1
|
||||
github.com/casdoor/notify2 v1.6.0
|
||||
github.com/casdoor/oss v1.8.0
|
||||
github.com/casdoor/xorm-adapter/v3 v3.1.0
|
||||
github.com/casvisor/casvisor-go-sdk v1.4.0
|
||||
github.com/corazawaf/coraza/v3 v3.3.3
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
||||
github.com/denisenkom/go-mssqldb v0.9.0
|
||||
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3
|
||||
github.com/fogleman/gg v1.3.0
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5
|
||||
github.com/go-git/go-git/v5 v5.13.0
|
||||
github.com/go-git/go-git/v5 v5.16.3
|
||||
github.com/go-jose/go-jose/v4 v4.1.3
|
||||
github.com/go-ldap/ldap/v3 v3.4.6
|
||||
github.com/go-mysql-org/go-mysql v1.7.0
|
||||
github.com/go-pay/gopay v1.5.115
|
||||
github.com/go-pay/util v0.0.4
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
||||
github.com/go-webauthn/webauthn v0.10.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hsluoyz/modsecurity-go v0.0.7
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/lestrrat-go/jwx v1.2.29
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/likexian/whois v1.15.1
|
||||
github.com/likexian/whois-parser v1.24.9
|
||||
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3
|
||||
github.com/markbates/goth v1.79.0
|
||||
github.com/markbates/goth v1.82.0
|
||||
github.com/microsoft/go-mssqldb v1.9.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.0
|
||||
github.com/nyaruka/phonenumbers v1.2.2
|
||||
github.com/polarsource/polar-go v0.12.0
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/prometheus/client_golang v1.11.1
|
||||
github.com/prometheus/client_model v0.4.0
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/prometheus/client_model v0.6.2
|
||||
github.com/qiangmzsx/string-adapter/v2 v2.1.0
|
||||
github.com/resend/resend-go/v3 v3.1.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/russellhaering/gosaml2 v0.9.0
|
||||
github.com/russellhaering/goxmldsig v1.2.0
|
||||
github.com/sendgrid/sendgrid-go v3.14.0+incompatible
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/sendgrid/sendgrid-go v3.16.0+incompatible
|
||||
github.com/shirou/gopsutil/v4 v4.25.9
|
||||
github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/stripe/stripe-go/v74 v74.29.0
|
||||
@@ -63,33 +80,41 @@ 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
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/net v0.40.0
|
||||
golang.org/x/oauth2 v0.17.0
|
||||
golang.org/x/text v0.26.0
|
||||
google.golang.org/api v0.150.0
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
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
|
||||
golang.org/x/text v0.33.0
|
||||
golang.org/x/time v0.8.0
|
||||
google.golang.org/api v0.215.0
|
||||
layeh.com/radius v0.0.0-20231213012653-1006025d24f8
|
||||
maunium.net/go/mautrix v0.16.0
|
||||
maunium.net/go/mautrix v0.22.1
|
||||
modernc.org/sqlite v1.18.2
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.110.8 // indirect
|
||||
cloud.google.com/go/compute v1.23.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v1.1.3 // indirect
|
||||
cloud.google.com/go/storage v1.35.1 // indirect
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go v0.116.0 // indirect
|
||||
cloud.google.com/go/auth v0.13.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/iam v1.2.2 // indirect
|
||||
cloud.google.com/go/monitoring v1.21.2 // indirect
|
||||
cloud.google.com/go/storage v1.47.0 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 // indirect
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.3 // indirect
|
||||
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20221121042443-a3fd332d56d9 // indirect
|
||||
github.com/SherClockHolmes/webpush-go v1.2.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20240116134246-a8cbe886bab0 // indirect
|
||||
github.com/SherClockHolmes/webpush-go v1.4.0 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/darabonba-number v1.0.4 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
@@ -100,39 +125,45 @@ require (
|
||||
github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/tea-utils v1.3.6 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.545 // indirect
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible // indirect
|
||||
github.com/aliyun/credentials-go v1.3.10 // indirect
|
||||
github.com/apistd/uni-go-sdk v0.0.2 // indirect
|
||||
github.com/atc0005/go-teams-notify/v2 v2.13.0 // indirect
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.156 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blinkbean/dingtalk v0.0.0-20210905093040-7d935c0f7e19 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/bwmarrin/discordgo v0.27.1 // indirect
|
||||
github.com/casdoor/casdoor-go-sdk v0.50.0 // indirect
|
||||
github.com/casdoor/go-reddit/v2 v2.1.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/blinkbean/dingtalk v1.1.3 // indirect
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
github.com/bwmarrin/discordgo v0.28.1 // indirect
|
||||
github.com/caarlos0/go-reddit/v3 v3.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
|
||||
github.com/corazawaf/libinjection-go v0.2.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/dghubble/oauth1 v0.7.2 // indirect
|
||||
github.com/dghubble/sling v1.4.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/dghubble/oauth1 v0.7.3 // indirect
|
||||
github.com/dghubble/sling v1.4.2 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/di-wu/parser v0.2.2 // indirect
|
||||
github.com/di-wu/xsd-datetime v1.0.0 // indirect
|
||||
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 // indirect
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.9.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
|
||||
github.com/ggicci/httpin v0.19.0 // indirect
|
||||
github.com/ggicci/owl v0.8.2 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.0 // indirect
|
||||
github.com/go-lark/lark v1.9.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-lark/lark v1.15.1 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-pay/crypto v0.0.1 // indirect
|
||||
github.com/go-pay/errgroup v0.0.3 // indirect
|
||||
@@ -140,26 +171,32 @@ require (
|
||||
github.com/go-pay/xlog v0.0.3 // indirect
|
||||
github.com/go-pay/xtime v0.0.2 // indirect
|
||||
github.com/go-webauthn/x v0.1.9 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/go-tpm v0.9.0 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/gregdel/pushover v1.2.1 // indirect
|
||||
github.com/google/jsonschema-go v0.4.2 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
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
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
|
||||
github.com/jcmturner/gofork v1.7.6 // indirect
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
@@ -167,95 +204,117 @@ require (
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/likexian/gokit v0.25.13 // indirect
|
||||
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magefile/mage v1.15.1-0.20241126214340-bdc92f694516 // indirect
|
||||
github.com/markbates/going v1.0.0 // indirect
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-ieproxy v0.0.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/miekg/dns v1.1.57 // indirect
|
||||
github.com/mileusna/viber v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 // indirect
|
||||
github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect
|
||||
github.com/pingcap/log v0.0.0-20210625125904-98ed8e2eb1c7 // indirect
|
||||
github.com/pingcap/tidb/parser v0.0.0-20221126021158-6b02a5d8ba7d // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/common v0.30.0 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/qiniu/go-sdk/v7 v7.12.1 // indirect
|
||||
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 // indirect
|
||||
github.com/redis/go-redis/v9 v9.5.5 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/rs/zerolog v1.30.0 // indirect
|
||||
github.com/rs/zerolog v1.33.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||
github.com/scim2/filter-parser/v2 v2.2.0 // indirect
|
||||
github.com/segmentio/asm v1.1.3 // indirect
|
||||
github.com/segmentio/encoding v0.5.3 // indirect
|
||||
github.com/sendgrid/rest v2.6.9+incompatible // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.0 // indirect
|
||||
github.com/slack-go/slack v0.12.3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/slack-go/slack v0.15.0 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
|
||||
github.com/spyzhov/ajson v0.8.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.744 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.744 // indirect
|
||||
github.com/tidwall/gjson v1.16.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||
github.com/tklauser/numcpus v0.4.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/twilio/twilio-go v1.13.0 // indirect
|
||||
github.com/ucloud/ucloud-sdk-go v0.22.5 // indirect
|
||||
github.com/urfave/cli v1.22.5 // indirect
|
||||
github.com/utahta/go-linenotify v0.5.0 // indirect
|
||||
github.com/valllabh/ocsf-schema-golang v1.0.3 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.117 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
go.mau.fi/util v0.0.0-20230805171708-199bf3eec776 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.mau.fi/util v0.8.3 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.40.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.7.0 // indirect
|
||||
go.uber.org/zap v1.19.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect
|
||||
golang.org/x/image v0.18.0 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/uint128 v1.1.1 // indirect
|
||||
maunium.net/go/maulogger/v2 v2.4.1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.37.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.9 // indirect
|
||||
modernc.org/libc v1.18.0 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.3.0 // indirect
|
||||
modernc.org/opt v0.1.1 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
rsc.io/binaryregexp v0.2.0 // indirect
|
||||
)
|
||||
|
||||
140
i18n/deduplicate_test.go
Normal file
140
i18n/deduplicate_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// 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 i18n
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// DuplicateInfo represents information about a duplicate key
|
||||
type DuplicateInfo struct {
|
||||
Key string
|
||||
OldPrefix string
|
||||
NewPrefix string
|
||||
OldPrefixKey string // e.g., "general:Submitter"
|
||||
NewPrefixKey string // e.g., "permission:Submitter"
|
||||
}
|
||||
|
||||
// findDuplicateKeysInJSON finds duplicate keys across the entire JSON file
|
||||
// Returns a list of duplicate information showing old and new prefix:key pairs
|
||||
// The order is determined by the order keys appear in the JSON file (git history)
|
||||
func findDuplicateKeysInJSON(filePath string) ([]DuplicateInfo, error) {
|
||||
// Read the JSON file
|
||||
fileContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
// Track the first occurrence of each key (prefix where it was first seen)
|
||||
keyFirstPrefix := make(map[string]string)
|
||||
var duplicates []DuplicateInfo
|
||||
|
||||
// To preserve order, we need to parse the JSON with order preservation
|
||||
// We'll use a decoder to read through the top-level object
|
||||
decoder := json.NewDecoder(bytes.NewReader(fileContent))
|
||||
|
||||
// Read the opening brace of the top-level object
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read token: %w", err)
|
||||
}
|
||||
if delim, ok := token.(json.Delim); !ok || delim != '{' {
|
||||
return nil, fmt.Errorf("expected object start, got %v", token)
|
||||
}
|
||||
|
||||
// Read all namespaces in order
|
||||
for decoder.More() {
|
||||
// Read the namespace (prefix) name
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read namespace: %w", err)
|
||||
}
|
||||
|
||||
prefix, ok := token.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected string namespace, got %v", token)
|
||||
}
|
||||
|
||||
// Read the namespace object as raw message
|
||||
var namespaceData map[string]string
|
||||
if err := decoder.Decode(&namespaceData); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode namespace %s: %w", prefix, err)
|
||||
}
|
||||
|
||||
// Now check each key in this namespace
|
||||
for key := range namespaceData {
|
||||
// Check if this key was already seen in a different prefix
|
||||
if firstPrefix, exists := keyFirstPrefix[key]; exists {
|
||||
// This is a duplicate - the key exists in another prefix
|
||||
duplicates = append(duplicates, DuplicateInfo{
|
||||
Key: key,
|
||||
OldPrefix: firstPrefix,
|
||||
NewPrefix: prefix,
|
||||
OldPrefixKey: fmt.Sprintf("%s:%s", firstPrefix, key),
|
||||
NewPrefixKey: fmt.Sprintf("%s:%s", prefix, key),
|
||||
})
|
||||
} else {
|
||||
// First time seeing this key, record the prefix
|
||||
keyFirstPrefix[key] = prefix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return duplicates, nil
|
||||
}
|
||||
|
||||
// TestDeduplicateFrontendI18n checks for duplicate i18n keys in the frontend en.json file
|
||||
func TestDeduplicateFrontendI18n(t *testing.T) {
|
||||
filePath := "../web/src/locales/en/data.json"
|
||||
|
||||
// Find duplicate keys
|
||||
duplicates, err := findDuplicateKeysInJSON(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check for duplicates in frontend i18n file: %v", err)
|
||||
}
|
||||
|
||||
// Print all duplicates and fail the test if any are found
|
||||
if len(duplicates) > 0 {
|
||||
t.Errorf("Found duplicate i18n keys in frontend file (%s):", filePath)
|
||||
for _, dup := range duplicates {
|
||||
t.Errorf(" i18next.t(\"%s\") duplicates with i18next.t(\"%s\")", dup.NewPrefixKey, dup.OldPrefixKey)
|
||||
}
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeduplicateBackendI18n checks for duplicate i18n keys in the backend en.json file
|
||||
func TestDeduplicateBackendI18n(t *testing.T) {
|
||||
filePath := "../i18n/locales/en/data.json"
|
||||
|
||||
// Find duplicate keys
|
||||
duplicates, err := findDuplicateKeysInJSON(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check for duplicates in backend i18n file: %v", err)
|
||||
}
|
||||
|
||||
// Print all duplicates and fail the test if any are found
|
||||
if len(duplicates) > 0 {
|
||||
t.Errorf("Found duplicate i18n keys in backend file (%s):", filePath)
|
||||
for _, dup := range duplicates {
|
||||
t.Errorf(" i18n.Translate(\"%s\") duplicates with i18n.Translate(\"%s\")", dup.NewPrefixKey, dup.OldPrefixKey)
|
||||
}
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@@ -47,7 +48,11 @@ func getAllI18nStringsFrontend(fileContent string) []string {
|
||||
}
|
||||
|
||||
for _, match := range matches {
|
||||
res = append(res, match[1])
|
||||
target, err := strconv.Unquote("\"" + match[1] + "\"")
|
||||
if err != nil {
|
||||
target = match[1]
|
||||
}
|
||||
res = append(res, target)
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -61,7 +66,12 @@ func getAllI18nStringsBackend(fileContent string, isObjectPackage bool) []string
|
||||
}
|
||||
for _, match := range matches {
|
||||
match := strings.SplitN(match[1], ",", 2)
|
||||
res = append(res, match[1][2:])
|
||||
target, err := strconv.Unquote("\"" + match[1][2:] + "\"")
|
||||
if err != nil {
|
||||
target = match[1][2:]
|
||||
}
|
||||
|
||||
res = append(res, target)
|
||||
}
|
||||
} else {
|
||||
matches := reI18nBackendController.FindAllStringSubmatch(fileContent, -1)
|
||||
@@ -69,7 +79,11 @@ func getAllI18nStringsBackend(fileContent string, isObjectPackage bool) []string
|
||||
return res
|
||||
}
|
||||
for _, match := range matches {
|
||||
res = append(res, match[1][1:])
|
||||
target, err := strconv.Unquote("\"" + match[1][1:] + "\"")
|
||||
if err != nil {
|
||||
target = match[1][1:]
|
||||
}
|
||||
res = append(res, target)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,10 +155,26 @@ func parseAllWords(category string) *I18nData {
|
||||
return &data
|
||||
}
|
||||
|
||||
// copyI18nData creates a deep copy of an I18nData structure to prevent shared reference issues
|
||||
// between language translations. This ensures each language starts with fresh English defaults
|
||||
// rather than inheriting values from previously processed languages.
|
||||
func copyI18nData(src *I18nData) *I18nData {
|
||||
dst := I18nData{}
|
||||
for namespace, pairs := range *src {
|
||||
dst[namespace] = make(map[string]string)
|
||||
for key, value := range pairs {
|
||||
dst[namespace][key] = value
|
||||
}
|
||||
}
|
||||
return &dst
|
||||
}
|
||||
|
||||
func applyToOtherLanguage(category string, language string, newData *I18nData) {
|
||||
oldData := readI18nFile(category, language)
|
||||
println(oldData)
|
||||
|
||||
applyData(newData, oldData)
|
||||
writeI18nFile(category, language, newData)
|
||||
// Create a copy of newData to avoid modifying the shared data across languages
|
||||
dataCopy := copyI18nData(newData)
|
||||
applyData(dataCopy, oldData)
|
||||
writeI18nFile(category, language, dataCopy)
|
||||
}
|
||||
|
||||
@@ -22,60 +22,30 @@ func TestGenerateI18nFrontend(t *testing.T) {
|
||||
data := parseAllWords("frontend")
|
||||
|
||||
applyToOtherLanguage("frontend", "en", data)
|
||||
applyToOtherLanguage("frontend", "zh", data)
|
||||
applyToOtherLanguage("frontend", "es", data)
|
||||
applyToOtherLanguage("frontend", "fr", data)
|
||||
applyToOtherLanguage("frontend", "de", data)
|
||||
applyToOtherLanguage("frontend", "id", data)
|
||||
applyToOtherLanguage("frontend", "ja", data)
|
||||
applyToOtherLanguage("frontend", "ko", data)
|
||||
applyToOtherLanguage("frontend", "ru", data)
|
||||
applyToOtherLanguage("frontend", "zh", data)
|
||||
applyToOtherLanguage("frontend", "vi", data)
|
||||
applyToOtherLanguage("frontend", "pt", data)
|
||||
applyToOtherLanguage("frontend", "it", data)
|
||||
applyToOtherLanguage("frontend", "ms", data)
|
||||
applyToOtherLanguage("frontend", "tr", data)
|
||||
applyToOtherLanguage("frontend", "ar", data)
|
||||
applyToOtherLanguage("frontend", "he", data)
|
||||
applyToOtherLanguage("frontend", "nl", data)
|
||||
applyToOtherLanguage("frontend", "pl", data)
|
||||
applyToOtherLanguage("frontend", "fi", data)
|
||||
applyToOtherLanguage("frontend", "sv", data)
|
||||
applyToOtherLanguage("frontend", "uk", data)
|
||||
applyToOtherLanguage("frontend", "kk", data)
|
||||
applyToOtherLanguage("frontend", "fa", data)
|
||||
applyToOtherLanguage("frontend", "cs", data)
|
||||
applyToOtherLanguage("frontend", "sk", data)
|
||||
applyToOtherLanguage("frontend", "az", data)
|
||||
}
|
||||
|
||||
func TestGenerateI18nBackend(t *testing.T) {
|
||||
data := parseAllWords("backend")
|
||||
|
||||
applyToOtherLanguage("backend", "en", data)
|
||||
applyToOtherLanguage("backend", "zh", data)
|
||||
applyToOtherLanguage("backend", "es", data)
|
||||
applyToOtherLanguage("backend", "fr", data)
|
||||
applyToOtherLanguage("backend", "de", data)
|
||||
applyToOtherLanguage("backend", "id", data)
|
||||
applyToOtherLanguage("backend", "ja", data)
|
||||
applyToOtherLanguage("backend", "ko", data)
|
||||
applyToOtherLanguage("backend", "ru", data)
|
||||
applyToOtherLanguage("backend", "zh", data)
|
||||
applyToOtherLanguage("backend", "vi", data)
|
||||
applyToOtherLanguage("backend", "pt", data)
|
||||
applyToOtherLanguage("backend", "it", data)
|
||||
applyToOtherLanguage("backend", "ms", data)
|
||||
applyToOtherLanguage("backend", "tr", data)
|
||||
applyToOtherLanguage("backend", "ar", data)
|
||||
applyToOtherLanguage("backend", "he", data)
|
||||
applyToOtherLanguage("backend", "nl", data)
|
||||
applyToOtherLanguage("backend", "pl", data)
|
||||
applyToOtherLanguage("backend", "fi", data)
|
||||
applyToOtherLanguage("backend", "sv", data)
|
||||
applyToOtherLanguage("backend", "uk", data)
|
||||
applyToOtherLanguage("backend", "kk", data)
|
||||
applyToOtherLanguage("backend", "fa", data)
|
||||
applyToOtherLanguage("backend", "cs", data)
|
||||
applyToOtherLanguage("backend", "sk", data)
|
||||
applyToOtherLanguage("backend", "az", data)
|
||||
}
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
{
|
||||
"account": {
|
||||
"Failed to add user": "فشل إضافة المستخدم",
|
||||
"Get init score failed, error: %w": "فشل الحصول على النتيجة الأولية، الخطأ: %w",
|
||||
"Please sign out first": "يرجى تسجيل الخروج أولاً",
|
||||
"The application does not allow to sign up new account": "التطبيق لا يسمح بالتسجيل بحساب جديد"
|
||||
},
|
||||
"auth": {
|
||||
"Challenge method should be S256": "يجب أن تكون طريقة التحدي S256",
|
||||
"DeviceCode Invalid": "رمز الجهاز غير صالح",
|
||||
"Failed to create user, user information is invalid: %s": "فشل إنشاء المستخدم، معلومات المستخدم غير صالحة: %s",
|
||||
"Failed to login in: %s": "فشل تسجيل الدخول: %s",
|
||||
"Invalid token": "الرمز غير صالح",
|
||||
"State expected: %s, but got: %s": "كان من المتوقع الحالة: %s، لكن حصلنا على: %s",
|
||||
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "الحساب الخاص بالمزود: %s واسم المستخدم: %s (%s) غير موجود ولا يُسمح بالتسجيل كحساب جديد عبر %s، يرجى استخدام طريقة أخرى للتسجيل",
|
||||
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "الحساب الخاص بالمزود: %s واسم المستخدم: %s (%s) غير موجود ولا يُسمح بالتسجيل كحساب جديد، يرجى الاتصال بدعم تكنولوجيا المعلومات",
|
||||
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "الحساب الخاص بالمزود: %s واسم المستخدم: %s (%s) مرتبط بالفعل بحساب آخر: %s (%s)",
|
||||
"The application: %s does not exist": "التطبيق: %s غير موجود",
|
||||
"The application: %s has disabled users to signin": "التطبيق: %s قد عطّل تسجيل دخول المستخدمين",
|
||||
"The group: %s does not exist": "المجموعة: %s غير موجودة",
|
||||
"The login method: login with LDAP is not enabled for the application": "طريقة تسجيل الدخول: تسجيل الدخول باستخدام LDAP غير مفعّلة لهذا التطبيق",
|
||||
"The login method: login with SMS is not enabled for the application": "طريقة تسجيل الدخول: تسجيل الدخول باستخدام الرسائل النصية غير مفعّلة لهذا التطبيق",
|
||||
"The login method: login with email is not enabled for the application": "طريقة تسجيل الدخول: تسجيل الدخول باستخدام البريد الإلكتروني غير مفعّلة لهذا التطبيق",
|
||||
"The login method: login with face is not enabled for the application": "طريقة تسجيل الدخول: تسجيل الدخول باستخدام الوجه غير مفعّلة لهذا التطبيق",
|
||||
"The login method: login with password is not enabled for the application": "طريقة تسجيل الدخول: تسجيل الدخول باستخدام كلمة المرور غير مفعّلة لهذا التطبيق",
|
||||
"The organization: %s does not exist": "المنظمة: %s غير موجودة",
|
||||
"The organization: %s has disabled users to signin": "المنظمة: %s قد عطّلت تسجيل دخول المستخدمين",
|
||||
"The plan: %s does not exist": "الخطة: %s غير موجودة",
|
||||
"The pricing: %s does not exist": "التسعير: %s غير موجود",
|
||||
"The pricing: %s does not have plan: %s": "التسعير: %s لا يحتوي على الخطة: %s",
|
||||
"The provider: %s does not exist": "المزود: %s غير موجود",
|
||||
"The provider: %s is not enabled for the application": "المزود: %s غير مفعّل لهذا التطبيق",
|
||||
"Unauthorized operation": "عملية غير مصرح بها",
|
||||
"Unknown authentication type (not password or provider), form = %s": "نوع مصادقة غير معروف (ليس كلمة مرور أو مزود)، النموذج = %s",
|
||||
"User's tag: %s is not listed in the application's tags": "وسم المستخدم: %s غير مدرج في وسوم التطبيق",
|
||||
"UserCode Expired": "رمز المستخدم منتهي الصلاحية",
|
||||
"UserCode Invalid": "رمز المستخدم غير صالح",
|
||||
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "المستخدم المدفوع %s ليس لديه اشتراك نشط أو معلق والتطبيق: %s ليس لديه تسعير افتراضي",
|
||||
"the application for user %s is not found": "لم يتم العثور على التطبيق الخاص بالمستخدم %s",
|
||||
"the organization: %s is not found": "لم يتم العثور على المنظمة: %s"
|
||||
},
|
||||
"cas": {
|
||||
"Service %s and %s do not match": "الخدمة %s و %s غير متطابقتين"
|
||||
},
|
||||
"check": {
|
||||
"%s does not meet the CIDR format requirements: %s": "%s لا تلبي متطلبات تنسيق CIDR: %s",
|
||||
"Affiliation cannot be blank": "الانتماء لا يمكن أن يكون فارغاً",
|
||||
"CIDR for IP: %s should not be empty": "CIDR لعنوان IP: %s لا يجب أن يكون فارغاً",
|
||||
"Default code does not match the code's matching rules": "الرمز الافتراضي لا يتطابق مع قواعد المطابقة",
|
||||
"DisplayName cannot be blank": "اسم العرض لا يمكن أن يكون فارغاً",
|
||||
"DisplayName is not valid real name": "اسم العرض ليس اسمًا حقيقيًا صالحًا",
|
||||
"Email already exists": "البريد الإلكتروني موجود بالفعل",
|
||||
"Email cannot be empty": "البريد الإلكتروني لا يمكن أن يكون فارغاً",
|
||||
"Email is invalid": "البريد الإلكتروني غير صالح",
|
||||
"Empty username.": "اسم المستخدم فارغ.",
|
||||
"Face data does not exist, cannot log in": "بيانات الوجه غير موجودة، لا يمكن تسجيل الدخول",
|
||||
"Face data mismatch": "عدم تطابق بيانات الوجه",
|
||||
"Failed to parse client IP: %s": "فشل تحليل IP العميل: %s",
|
||||
"FirstName cannot be blank": "الاسم الأول لا يمكن أن يكون فارغاً",
|
||||
"Invitation code cannot be blank": "رمز الدعوة لا يمكن أن يكون فارغاً",
|
||||
"Invitation code exhausted": "رمز الدعوة استُنفِد",
|
||||
"Invitation code is invalid": "رمز الدعوة غير صالح",
|
||||
"Invitation code suspended": "رمز الدعوة موقوف",
|
||||
"LDAP user name or password incorrect": "اسم مستخدم LDAP أو كلمة المرور غير صحيحة",
|
||||
"LastName cannot be blank": "الاسم الأخير لا يمكن أن يكون فارغاً",
|
||||
"Multiple accounts with same uid, please check your ldap server": "حسابات متعددة بنفس uid، يرجى التحقق من خادم ldap الخاص بك",
|
||||
"Organization does not exist": "المنظمة غير موجودة",
|
||||
"Password cannot be empty": "كلمة المرور لا يمكن أن تكون فارغة",
|
||||
"Phone already exists": "الهاتف موجود بالفعل",
|
||||
"Phone cannot be empty": "الهاتف لا يمكن أن يكون فارغاً",
|
||||
"Phone number is invalid": "رقم الهاتف غير صالح",
|
||||
"Please register using the email corresponding to the invitation code": "يرجى التسجيل باستخدام البريد الإلكتروني المطابق لرمز الدعوة",
|
||||
"Please register using the phone corresponding to the invitation code": "يرجى التسجيل باستخدام الهاتف المطابق لرمز الدعوة",
|
||||
"Please register using the username corresponding to the invitation code": "يرجى التسجيل باستخدام اسم المستخدم المطابق لرمز الدعوة",
|
||||
"Session outdated, please login again": "الجلسة منتهية الصلاحية، يرجى تسجيل الدخول مرة أخرى",
|
||||
"The invitation code has already been used": "رمز الدعوة تم استخدامه بالفعل",
|
||||
"The password must contain at least one special character": "يجب أن تحتوي كلمة المرور على حرف خاص واحد على الأقل",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "يجب أن تحتوي كلمة المرور على حرف كبير واحد على الأقل وحرف صغير ورقم",
|
||||
"The password must have at least 6 characters": "يجب أن تحتوي كلمة المرور على 6 أحرف على الأقل",
|
||||
"The password must have at least 8 characters": "يجب أن تحتوي كلمة المرور على 8 أحرف على الأقل",
|
||||
"The password must not contain any repeated characters": "يجب ألا تحتوي كلمة المرور على أي أحرف متكررة",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "تم حذف المستخدم ولا يمكن استخدامه لتسجيل الدخول، يرجى الاتصال بالمسؤول",
|
||||
"The user is forbidden to sign in, please contact the administrator": "المستخدم ممنوع من تسجيل الدخول، يرجى الاتصال بالمسؤول",
|
||||
"The user: %s doesn't exist in LDAP server": "المستخدم: %s غير موجود في خادم LDAP",
|
||||
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "اسم المستخدم يمكن أن يحتوي فقط على أحرف وأرقام، شرطات سفلية أو علوية، لا يمكن أن تحتوي على شرطات متتالية، ولا يمكن أن يبدأ أو ينتهي بشرطة.",
|
||||
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "Hesap alanı \\\"%s\\\" için \\\"%s\\\" değeri, hesap öğesi regex'iyle eşleşmiyor",
|
||||
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "Kayıt alanı \\\"%s\\\" için \\\"%s\\\" değeri, \\\"%s\\\" uygulamasının kayıt öğesi regex'iyle eşleşmiyor",
|
||||
"Username already exists": "اسم المستخدم موجود بالفعل",
|
||||
"Username cannot be an email address": "اسم المستخدم لا يمكن أن يكون عنوان بريد إلكتروني",
|
||||
"Username cannot contain white spaces": "اسم المستخدم لا يمكن أن يحتوي على مسافات",
|
||||
"Username cannot start with a digit": "اسم المستخدم لا يمكن أن يبدأ برقم",
|
||||
"Username is too long (maximum is 255 characters).": "اسم المستخدم طويل جداً (الحد الأقصى 255 حرفاً).",
|
||||
"Username must have at least 2 characters": "اسم المستخدم يجب أن يحتوي على حرفين على الأقل",
|
||||
"Username supports email format. Also The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline. Also pay attention to the email format.": "اسم المستخدم يدعم تنسيق البريد الإلكتروني. كما أن اسم المستخدم يمكن أن يحتوي فقط على أحرف وأرقام، شرطات سفلية أو علوية، لا يمكن أن تحتوي على شرطات متتالية، ولا يمكن أن يبدأ أو ينتهي بشرطة. انتبه أيضًا لتنسيق البريد الإلكتروني.",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "لقد قمت بإدخال كلمة المرور أو الرمز الخطأ عدة مرات، يرجى الانتظار %d دقائق ثم المحاولة مرة أخرى",
|
||||
"Your IP address: %s has been banned according to the configuration of: ": "عنوان IP الخاص بك: %s تم حظره وفقًا لتكوين: ",
|
||||
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Şifrenizin süresi doldu. Lütfen \\\"Şifremi unuttum\\\"a tıklayarak şifrenizi sıfırlayın",
|
||||
"Your region is not allow to signup by phone": "منطقتك لا تسمح بالتسجيل عبر الهاتف",
|
||||
"password or code is incorrect": "كلمة المرور أو الرمز غير صحيح",
|
||||
"password or code is incorrect, you have %s remaining chances": "كلمة المرور أو الرمز غير صحيح، لديك %s فرصة متبقية",
|
||||
"unsupported password type: %s": "نوع كلمة المرور غير مدعوم: %s"
|
||||
},
|
||||
"enforcer": {
|
||||
"the adapter: %s is not found": "المحول: %s غير موجود"
|
||||
},
|
||||
"general": {
|
||||
"Failed to import groups": "فشل استيراد المجموعات",
|
||||
"Failed to import users": "فشل استيراد المستخدمين",
|
||||
"Missing parameter": "المعلمة مفقودة",
|
||||
"Only admin user can specify user": "فقط المسؤول يمكنه تحديد المستخدم",
|
||||
"Please login first": "يرجى تسجيل الدخول أولاً",
|
||||
"The organization: %s should have one application at least": "المنظمة: %s يجب أن تحتوي على تطبيق واحد على الأقل",
|
||||
"The user: %s doesn't exist": "المستخدم: %s غير موجود",
|
||||
"Wrong userId": "معرف المستخدم غير صحيح",
|
||||
"don't support captchaProvider: ": "لا يدعم captchaProvider: ",
|
||||
"this operation is not allowed in demo mode": "هذه العملية غير مسموح بها في وضع العرض التوضيحي",
|
||||
"this operation requires administrator to perform": "هذه العملية تتطلب مسؤولاً لتنفيذها"
|
||||
},
|
||||
"invitation": {
|
||||
"Invitation %s does not exist": "الدعوة %s غير موجودة"
|
||||
},
|
||||
"ldap": {
|
||||
"Ldap server exist": "خادم LDAP موجود"
|
||||
},
|
||||
"link": {
|
||||
"Please link first": "يرجى الربط أولاً",
|
||||
"This application has no providers": "هذا التطبيق لا يحتوي على مزودين",
|
||||
"This application has no providers of type": "هذا التطبيق لا يحتوي على مزودين من النوع",
|
||||
"This provider can't be unlinked": "لا يمكن فصل هذا المزود",
|
||||
"You are not the global admin, you can't unlink other users": "أنت لست المسؤول العام، لا يمكنك فصل مستخدمين آخرين",
|
||||
"You can't unlink yourself, you are not a member of any application": "لا يمكنك فصل نفسك، أنت لست عضواً في أي تطبيق"
|
||||
},
|
||||
"organization": {
|
||||
"Only admin can modify the %s.": "فقط المسؤول يمكنه تعديل %s.",
|
||||
"The %s is immutable.": "%s غير قابل للتعديل.",
|
||||
"Unknown modify rule %s.": "قاعدة تعديل غير معروفة %s.",
|
||||
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "إضافة مستخدم جديد إلى المنظمة \"المدمجة\" غير متوفر حاليًا. يرجى ملاحظة: جميع المستخدمين في المنظمة \"المدمجة\" هم مسؤولون عالميون في Casdoor. يرجى الرجوع إلى الوثائق: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. إذا كنت لا تزال ترغب في إنشاء مستخدم للمنظمة \"المدمجة\"، اไป إلى صفحة إعدادات المنظمة وقم بتمكين خيار \"لديه موافقة صلاحية\"."
|
||||
},
|
||||
"permission": {
|
||||
"The permission: \\\"%s\\\" doesn't exist": "İzin: \\\"%s\\\" mevcut değil"
|
||||
},
|
||||
"provider": {
|
||||
"Invalid application id": "معرف التطبيق غير صالح",
|
||||
"the provider: %s does not exist": "المزود: %s غير موجود"
|
||||
},
|
||||
"resource": {
|
||||
"User is nil for tag: avatar": "المستخدم nil للوسم: avatar",
|
||||
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "اسم المستخدم أو fullFilePath فارغ: username = %s، fullFilePath = %s"
|
||||
},
|
||||
"saml": {
|
||||
"Application %s not found": "التطبيق %s غير موجود"
|
||||
},
|
||||
"saml_sp": {
|
||||
"provider %s's category is not SAML": "فئة المزود %s ليست SAML"
|
||||
},
|
||||
"service": {
|
||||
"Empty parameters for emailForm: %v": "معلمات فارغة لـ emailForm: %v",
|
||||
"Invalid Email receivers: %s": "مستقبلو البريد الإلكتروني غير صالحين: %s",
|
||||
"Invalid phone receivers: %s": "مستقلو الهاتف غير صالحين: %s"
|
||||
},
|
||||
"storage": {
|
||||
"The objectKey: %s is not allowed": "مفتاح الكائن: %s غير مسموح به",
|
||||
"The provider type: %s is not supported": "نوع المزود: %s غير مدعوم"
|
||||
},
|
||||
"subscription": {
|
||||
"Error": "خطأ"
|
||||
},
|
||||
"token": {
|
||||
"Grant_type: %s is not supported in this application": "Grant_type: %s غير مدعوم في هذا التطبيق",
|
||||
"Invalid application or wrong clientSecret": "تطبيق غير صالح أو clientSecret خاطئ",
|
||||
"Invalid client_id": "client_id غير صالح",
|
||||
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "Redirect URI: %s غير موجود في قائمة Redirect URI المسموح بها",
|
||||
"Token not found, invalid accessToken": "الرمز غير موجود، accessToken غير صالح"
|
||||
},
|
||||
"user": {
|
||||
"Display name cannot be empty": "اسم العرض لا يمكن أن يكون فارغاً",
|
||||
"MFA email is enabled but email is empty": "تم تمكين MFA للبريد الإلكتروني لكن البريد الإلكتروني فارغ",
|
||||
"MFA phone is enabled but phone number is empty": "تم تمكين MFA للهاتف لكن رقم الهاتف فارغ",
|
||||
"New password cannot contain blank space.": "كلمة المرور الجديدة لا يمكن أن تحتوي على مسافات.",
|
||||
"The new password must be different from your current password": "يجب أن تكون كلمة المرور الجديدة مختلفة عن كلمة المرور الحالية",
|
||||
"the user's owner and name should not be empty": "مالك المستخدم واسمه لا يجب أن يكونا فارغين"
|
||||
},
|
||||
"util": {
|
||||
"No application is found for userId: %s": "لم يتم العثور على تطبيق لـ userId: %s",
|
||||
"No provider for category: %s is found for application: %s": "لم يتم العثور على مزود للفئة: %s للتطبيق: %s",
|
||||
"The provider: %s is not found": "المزود: %s غير موجود"
|
||||
},
|
||||
"verification": {
|
||||
"Invalid captcha provider.": "مزود captcha غير صالح.",
|
||||
"Phone number is invalid in your region %s": "رقم الهاتف غير صالح في منطقتك %s",
|
||||
"The verification code has already been used!": "رمز التحقق تم استخدامه بالفعل!",
|
||||
"The verification code has not been sent yet!": "رمز التحقق لم يُرسل بعد!",
|
||||
"Turing test failed.": "فشل اختبار تورينغ.",
|
||||
"Unable to get the email modify rule.": "غير قادر على الحصول على قاعدة تعديل البريد الإلكتروني.",
|
||||
"Unable to get the phone modify rule.": "غير قادر على الحصول على قاعدة تعديل الهاتف.",
|
||||
"Unknown type": "نوع غير معروف",
|
||||
"Wrong verification code!": "رمز التحقق خاطئ!",
|
||||
"You should verify your code in %d min!": "يجب عليك التحقق من الرمز خلال %d دقيقة!",
|
||||
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "lütfen uygulama için \\\"Sağlayıcılar\\\" listesine bir SMS sağlayıcı ekleyin: %s",
|
||||
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "lütfen uygulama için \\\"Sağlayıcılar\\\" listesine bir E-posta sağlayıcı ekleyin: %s",
|
||||
"the user does not exist, please sign up first": "المستخدم غير موجود، يرجى التسجيل أولاً"
|
||||
},
|
||||
"webauthn": {
|
||||
"Found no credentials for this user": "لم يتم العثور على بيانات اعتماد لهذا المستخدم",
|
||||
"Please call WebAuthnSigninBegin first": "يرجى استدعاء WebAuthnSigninBegin أولاً"
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
{
|
||||
"account": {
|
||||
"Failed to add user": "İstifadəçi əlavə etmə uğursuz oldu",
|
||||
"Get init score failed, error: %w": "Başlanğıc xal alınması uğursuz oldu, xəta: %w",
|
||||
"Please sign out first": "Xahiş edirik əvvəlcə çıxış edin",
|
||||
"The application does not allow to sign up new account": "Tətbiq yeni hesab qeydiyyatına icazə vermir"
|
||||
},
|
||||
"auth": {
|
||||
"Challenge method should be S256": "Çağırış metodu S256 olmalıdır",
|
||||
"DeviceCode Invalid": "Cihaz Kodu Etibarsızdır",
|
||||
"Failed to create user, user information is invalid: %s": "İstifadəçi yaratma uğursuz oldu, istifadəçi məlumatları etibarsızdır: %s",
|
||||
"Failed to login in: %s": "Giriş uğursuz oldu: %s",
|
||||
"Invalid token": "Etibarsız token",
|
||||
"State expected: %s, but got: %s": "Gözlənilən vəziyyət: %s, lakin alınan: %s",
|
||||
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Provayder üçün hesab: %s və istifadəçi adı: %s (%s) mövcud deyil və %s vasitəsilə yeni hesab olaraq qeydiyyatdan keçməyə icazə verilmir, xahiş edirik qeydiyyat üçün başqa üsul istifadə edin",
|
||||
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Provayder üçün hesab: %s və istifadəçi adı: %s (%s) mövcud deyil və yeni hesab olaraq qeydiyyatdan keçməyə icazə verilmir, xahiş edirik IT dəstəyinizlə əlaqə saxlayın",
|
||||
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Provayder üçün hesab: %s və istifadəçi adı: %s (%s) artıq başqa hesabla əlaqələndirilmişdir: %s (%s)",
|
||||
"The application: %s does not exist": "Tətbiq: %s mövcud deyil",
|
||||
"The application: %s has disabled users to signin": "Tətbiq: %s istifadəçilərin girişini söndürmüşdür",
|
||||
"The group: %s does not exist": "Qrup: %s mövcud deyil",
|
||||
"The login method: login with LDAP is not enabled for the application": "Giriş metodu: LDAP ilə giriş bu tətbiq üçün aktiv deyil",
|
||||
"The login method: login with SMS is not enabled for the application": "Giriş metodu: SMS ilə giriş bu tətbiq üçün aktiv deyil",
|
||||
"The login method: login with email is not enabled for the application": "Giriş metodu: email ilə giriş bu tətbiq üçün aktiv deyil",
|
||||
"The login method: login with face is not enabled for the application": "Giriş metodu: üz ilə giriş bu tətbiq üçün aktiv deyil",
|
||||
"The login method: login with password is not enabled for the application": "Giriş metodu: şifrə ilə giriş bu tətbiq üçün aktiv deyil",
|
||||
"The organization: %s does not exist": "Təşkilat: %s mövcud deyil",
|
||||
"The organization: %s has disabled users to signin": "Təşkilat: %s istifadəçilərin girişini söndürmüşdür",
|
||||
"The plan: %s does not exist": "Plan: %s mövcud deyil",
|
||||
"The pricing: %s does not exist": "Qiymətləndirmə: %s mövcud deyil",
|
||||
"The pricing: %s does not have plan: %s": "Qiymətləndirmə: %s planı yoxdur: %s",
|
||||
"The provider: %s does not exist": "Provayder: %s mövcud deyil",
|
||||
"The provider: %s is not enabled for the application": "Provayder: %s bu tətbiq üçün aktiv deyil",
|
||||
"Unauthorized operation": "İcazəsiz əməliyyat",
|
||||
"Unknown authentication type (not password or provider), form = %s": "Naməlum təsdiq növü (şifrə və ya provayder deyil), forma = %s",
|
||||
"User's tag: %s is not listed in the application's tags": "İstifadəçinin teqi: %s tətbiqin teqləri siyahısında yoxdur",
|
||||
"UserCode Expired": "İstifadəçi Kodunun Vaxtı Keçib",
|
||||
"UserCode Invalid": "İstifadəçi Kodu Etibarsızdır",
|
||||
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "ödənişli istifadəçi %s aktiv və ya gözləyən abunəyə malik deyil və tətbiq: %s defolt qiymətləndirməyə malik deyil",
|
||||
"the application for user %s is not found": "istifadəçi %s üçün tətbiq tapılmadı",
|
||||
"the organization: %s is not found": "təşkilat: %s tapılmadı"
|
||||
},
|
||||
"cas": {
|
||||
"Service %s and %s do not match": "Xidmət %s və %s uyğun gəlmir"
|
||||
},
|
||||
"check": {
|
||||
"%s does not meet the CIDR format requirements: %s": "%s CIDR format tələblərinə cavab vermir: %s",
|
||||
"Affiliation cannot be blank": "Mənsub sahəsi boş ola bilməz",
|
||||
"CIDR for IP: %s should not be empty": "IP üçün CIDR: %s boş olmamalıdır",
|
||||
"Default code does not match the code's matching rules": "Defolt kod kodun uyğunluq qaydalara uyğun gəlmir",
|
||||
"DisplayName cannot be blank": "Göstərilən Ad boş ola bilməz",
|
||||
"DisplayName is not valid real name": "Göstərilən Ad etibarlı həqiqi ad deyil",
|
||||
"Email already exists": "Email artıq mövcuddur",
|
||||
"Email cannot be empty": "Email boş ola bilməz",
|
||||
"Email is invalid": "Email etibarsızdır",
|
||||
"Empty username.": "Boş istifadəçi adı.",
|
||||
"Face data does not exist, cannot log in": "Üz məlumatları mövcud deyil, giriş edilə bilməz",
|
||||
"Face data mismatch": "Üz məlumatları uyğun gəlmir",
|
||||
"Failed to parse client IP: %s": "Müştəri IP-ni təhlil etmək uğursuz oldu: %s",
|
||||
"FirstName cannot be blank": "Ad boş ola bilməz",
|
||||
"Invitation code cannot be blank": "Dəvət kodu boş ola bilməz",
|
||||
"Invitation code exhausted": "Dəvət kodu tükənib",
|
||||
"Invitation code is invalid": "Dəvət kodu etibarsızdır",
|
||||
"Invitation code suspended": "Dəvət kodu dayandırılıb",
|
||||
"LDAP user name or password incorrect": "LDAP istifadəçi adı və ya şifrə yanlışdır",
|
||||
"LastName cannot be blank": "Soyad boş ola bilməz",
|
||||
"Multiple accounts with same uid, please check your ldap server": "Eyni uid ilə çoxlu hesablar, xahiş edirik ldap serverinizi yoxlayın",
|
||||
"Organization does not exist": "Təşkilat mövcud deyil",
|
||||
"Password cannot be empty": "Şifrə boş ola bilməz",
|
||||
"Phone already exists": "Telefon artıq mövcuddur",
|
||||
"Phone cannot be empty": "Telefon boş ola bilməz",
|
||||
"Phone number is invalid": "Telefon nömrəsi etibarsızdır",
|
||||
"Please register using the email corresponding to the invitation code": "Xahiş edirik dəvət koduna uyğun email istifadə edərək qeydiyyatdan keçin",
|
||||
"Please register using the phone corresponding to the invitation code": "Xahiş edirik dəvət koduna uyğun telefon istifadə edərək qeydiyyatdan keçin",
|
||||
"Please register using the username corresponding to the invitation code": "Xahiş edirik dəvət koduna uyğun istifadəçi adı istifadə edərək qeydiyyatdan keçin",
|
||||
"Session outdated, please login again": "Sessiyanın vaxtı keçib, xahiş edirik yenidən daxil olun",
|
||||
"The invitation code has already been used": "Dəvət kodu artıq istifadə edilib",
|
||||
"The password must contain at least one special character": "Parol ən azı bir xüsusi simvol ehtiva etməlidir",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Parol ən azı bir böyük hərf, bir kiçik hərf və bir rəqəm ehtiva etməlidir",
|
||||
"The password must have at least 6 characters": "Parol ən azı 6 simvoldan ibarət olmalıdır",
|
||||
"The password must have at least 8 characters": "Parol ən azı 8 simvoldan ibarət olmalıdır",
|
||||
"The password must not contain any repeated characters": "Parol təkrarlanan simvollar ehtiva etməməlidir",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "İstifadəçi silinib və daxil olmaq üçün istifadə edilə bilməz, zəhmət olmasa administratorla əlaqə saxlayın",
|
||||
"The user is forbidden to sign in, please contact the administrator": "İstifadəçinin girişi qadağandır, xahiş edirik administratorla əlaqə saxlayın",
|
||||
"The user: %s doesn't exist in LDAP server": "İstifadəçi: %s LDAP serverində mövcud deyil",
|
||||
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "İstifadəçi adı yalnız hərf-rəqəm simvolları, alt xətt və ya defis ehtiva edə bilər, ardıcıl defis və ya alt xətt ola bilməz və defis və ya alt xəttlə başlaya və ya bitə bilməz.",
|
||||
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "Hesab sahəsi \\\"%s\\\" üçün dəyər \\\"%s\\\" hesab elementi regex-inə uyğun gəlmir",
|
||||
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "Qeydiyyat sahəsi \\\"%s\\\" üçün dəyər \\\"%s\\\" tətbiq \\\"%s\\\"in qeydiyyat elementi regex-inə uyğun gəlmir",
|
||||
"Username already exists": "İstifadəçi adı artıq mövcuddur",
|
||||
"Username cannot be an email address": "İstifadəçi adı email ünvanı ola bilməz",
|
||||
"Username cannot contain white spaces": "İstifadəçi adı boşluqlar ehtiva edə bilməz",
|
||||
"Username cannot start with a digit": "İstifadəçi adı rəqəmlə başlaya bilməz",
|
||||
"Username is too long (maximum is 255 characters).": "İstifadəçi adı çox uzundur (maksimum 255 simvoldur).",
|
||||
"Username must have at least 2 characters": "İstifadəçi adı ən azı 2 simvola malik olmalıdır",
|
||||
"Username supports email format. Also The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline. Also pay attention to the email format.": "İstifadəçi adı email formatını dəstəkləyir. Həmçinin istifadəçi adı yalnız hərf-rəqəm simvolları, alt xətt və ya defis ehtiva edə bilər, ardıcıl defis və ya alt xətt ola bilməz və defis və ya alt xəttlə başlaya və ya bitə bilməz. Həmçinin email formatına diqqət edin.",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Yanlış şifrə və ya kod dəfələrlə daxil etmisiniz, xahiş edirik %d dəqiqə gözləyin və yenidən cəhd edin",
|
||||
"Your IP address: %s has been banned according to the configuration of: ": "IP ünvanınız: %s konfiqurasiyaya görə qadağan edilib: ",
|
||||
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Şifrənizin vaxtı keçib. Xahiş edirik \\\"Şifrəni unutdum\\\" düyməsinə basaraq şifrənizi sıfırlayın",
|
||||
"Your region is not allow to signup by phone": "Regionunuzda telefonla qeydiyyata icazə verilmir",
|
||||
"password or code is incorrect": "şifrə və ya kod yanlışdır",
|
||||
"password or code is incorrect, you have %s remaining chances": "şifrə və ya kod yanlışdır, %s şansınız qalıb",
|
||||
"unsupported password type: %s": "dəstəklənməyən şifrə növü: %s"
|
||||
},
|
||||
"enforcer": {
|
||||
"the adapter: %s is not found": "adapter: %s tapılmadı"
|
||||
},
|
||||
"general": {
|
||||
"Failed to import groups": "Qrupları idxal etmək uğursuz oldu",
|
||||
"Failed to import users": "İstifadəçiləri idxal etmək uğursuz oldu",
|
||||
"Missing parameter": "Parametr çatışmır",
|
||||
"Only admin user can specify user": "Yalnız admin istifadəçi başqa istifadəçini təyin edə bilər",
|
||||
"Please login first": "Xahiş edirik əvvəlcə daxil olun",
|
||||
"The organization: %s should have one application at least": "Təşkilat: %s ən azı bir tətbiqə malik olmalıdır",
|
||||
"The user: %s doesn't exist": "İstifadəçi: %s mövcud deyil",
|
||||
"Wrong userId": "Yanlış istifadəçi ID-si",
|
||||
"don't support captchaProvider: ": "captcha provayderini dəstəkləmir: ",
|
||||
"this operation is not allowed in demo mode": "bu əməliyyat demo rejimində icazə verilmir",
|
||||
"this operation requires administrator to perform": "bu əməliyyat administrator tərəfindən yerinə yetirilməsini tələb edir"
|
||||
},
|
||||
"invitation": {
|
||||
"Invitation %s does not exist": "Dəvət %s mövcud deyil"
|
||||
},
|
||||
"ldap": {
|
||||
"Ldap server exist": "LDAP serveri mövcuddur"
|
||||
},
|
||||
"link": {
|
||||
"Please link first": "Xahiş edirik əvvəlcə əlaqələndirin",
|
||||
"This application has no providers": "Bu tətbiqin provayderləri yoxdur",
|
||||
"This application has no providers of type": "Bu tətbiqin bu növdə provayderi yoxdur",
|
||||
"This provider can't be unlinked": "Bu provayderin əlaqəsi kəsilə bilməz",
|
||||
"You are not the global admin, you can't unlink other users": "Siz qlobal admin deyilsiniz, digər istifadəçilərin əlaqəsini kəsə bilməzsiniz",
|
||||
"You can't unlink yourself, you are not a member of any application": "Öz əlaqənizi kəsə bilməzsiniz, heç bir tətbiqin üzvü deyilsiniz"
|
||||
},
|
||||
"organization": {
|
||||
"Only admin can modify the %s.": "Yalnız admin %s-ni dəyişdirə bilər.",
|
||||
"The %s is immutable.": "%s dəyişilməzdir.",
|
||||
"Unknown modify rule %s.": "Naməlum dəyişdirmə qaydası %s.",
|
||||
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "'built-in' təşkilatına yeni istifadəçi əlavə etmək hazırda söndürülüb. Qeyd edin: 'built-in' təşkilatındakı bütün istifadəçilər Casdoor-da qlobal administratorlardır. Sənədlərə baxın: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. Əgər hələ də 'built-in' təşkilat üçün istifadəçi yaratmaq istəyirsinizsə, təşkilatın tənzimləmələr səhifəsinə gedib 'İmtiyaz razılığına malikdir' seçimini aktiv edin."
|
||||
},
|
||||
"permission": {
|
||||
"The permission: \\\"%s\\\" doesn't exist": "İcazə: \\\"%s\\\" mövcud deyil"
|
||||
},
|
||||
"provider": {
|
||||
"Invalid application id": "Etibarsız tətbiq id-si",
|
||||
"the provider: %s does not exist": "provayder: %s mövcud deyil"
|
||||
},
|
||||
"resource": {
|
||||
"User is nil for tag: avatar": "Avatar teqi üçün istifadəçi nil-dir",
|
||||
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "İstifadəçi adı və ya tam fayl yolu boşdur: istifadəçi adı = %s, tam fayl yolu = %s"
|
||||
},
|
||||
"saml": {
|
||||
"Application %s not found": "Tətbiq %s tapılmadı"
|
||||
},
|
||||
"saml_sp": {
|
||||
"provider %s's category is not SAML": "provayder %s-in kateqoriyası SAML deyil"
|
||||
},
|
||||
"service": {
|
||||
"Empty parameters for emailForm: %v": "emailForm üçün boş parametrlər: %v",
|
||||
"Invalid Email receivers: %s": "Etibarsız Email qəbuledicilər: %s",
|
||||
"Invalid phone receivers: %s": "Etibarsız telefon qəbuledicilər: %s"
|
||||
},
|
||||
"storage": {
|
||||
"The objectKey: %s is not allowed": "obyekt açarı: %s icazə verilmir",
|
||||
"The provider type: %s is not supported": "provayder növü: %s dəstəklənmir"
|
||||
},
|
||||
"subscription": {
|
||||
"Error": "Xəta"
|
||||
},
|
||||
"token": {
|
||||
"Grant_type: %s is not supported in this application": "Grant_type: %s bu tətbiqdə dəstəklənmir",
|
||||
"Invalid application or wrong clientSecret": "Etibarsız tətbiq və ya yanlış müştəri sirri",
|
||||
"Invalid client_id": "Etibarsız client_id",
|
||||
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "Yönləndirmə URI: %s icazə verilən Yönləndirmə URI siyahısında mövcud deyil",
|
||||
"Token not found, invalid accessToken": "Token tapılmadı, etibarsız accessToken"
|
||||
},
|
||||
"user": {
|
||||
"Display name cannot be empty": "Göstərilən ad boş ola bilməz",
|
||||
"MFA email is enabled but email is empty": "MFA email aktiv edilib, lakin email boşdur",
|
||||
"MFA phone is enabled but phone number is empty": "MFA telefon aktiv edilib, lakin telefon nömrəsi boşdur",
|
||||
"New password cannot contain blank space.": "Yeni şifrə boş yer ehtiva edə bilməz.",
|
||||
"The new password must be different from your current password": "Yeni şifrə cari şifrənizdən fərqli olmalıdır",
|
||||
"the user's owner and name should not be empty": "istifadəçinin sahibi və adı boş olmamalıdır"
|
||||
},
|
||||
"util": {
|
||||
"No application is found for userId: %s": "İstifadəçi ID-si üçün heç bir tətbiq tapılmadı: %s",
|
||||
"No provider for category: %s is found for application: %s": "Tətbiq üçün kateqoriya üçün heç bir provayder tapılmadı: %s: %s",
|
||||
"The provider: %s is not found": "Provayder: %s tapılmadı"
|
||||
},
|
||||
"verification": {
|
||||
"Invalid captcha provider.": "Etibarsız captcha provaydeři.",
|
||||
"Phone number is invalid in your region %s": "Telefon nömrəsi sizin regionunuzda etibarsızdır %s",
|
||||
"The verification code has already been used!": "Doğrulama kodu artıq istifadə edilib!",
|
||||
"The verification code has not been sent yet!": "Doğrulama kodu hələ göndərilməyib!",
|
||||
"Turing test failed.": "Türinq testi uğursuz oldu.",
|
||||
"Unable to get the email modify rule.": "Email dəyişdirmə qaydasını əldə etmək mümkün olmadı.",
|
||||
"Unable to get the phone modify rule.": "Telefon dəyişdirmə qaydasını əldə etmək mümkün olmadı.",
|
||||
"Unknown type": "Naməlum növ",
|
||||
"Wrong verification code!": "Yanlış doğrulama kodu!",
|
||||
"You should verify your code in %d min!": "Kodunuzu %d dəqiqə içində doğrulamalısınız!",
|
||||
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "xahiş edirik tətbiq üçün \\\"Provaydeerlər\\\" siyahısına SMS provaydeři əlavə edin: %s",
|
||||
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "xahiş edirik tətbiq üçün \\\"Provaydeerlər\\\" siyahısına Email provaydeři əlavə edin: %s",
|
||||
"the user does not exist, please sign up first": "istifadəçi mövcud deyil, xahiş edirik əvvəlcə qeydiyyatdan keçin"
|
||||
},
|
||||
"webauthn": {
|
||||
"Found no credentials for this user": "Bu istifadəçi üçün heç bir etimadnamə tapılmadı",
|
||||
"Please call WebAuthnSigninBegin first": "Xahiş edirik əvvəlcə WebAuthnSigninBegin çağırın"
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
{
|
||||
"account": {
|
||||
"Failed to add user": "Nepodařilo se přidat uživatele",
|
||||
"Get init score failed, error: %w": "Nepodařilo se získat počáteční skóre, chyba: %w",
|
||||
"Please sign out first": "Nejprve se prosím odhlaste",
|
||||
"The application does not allow to sign up new account": "Aplikace neumožňuje registraci nového účtu"
|
||||
},
|
||||
"auth": {
|
||||
"Challenge method should be S256": "Metoda výzvy by měla být S256",
|
||||
"DeviceCode Invalid": "DeviceCode je neplatný",
|
||||
"Failed to create user, user information is invalid: %s": "Nepodařilo se vytvořit uživatele, informace o uživateli jsou neplatné: %s",
|
||||
"Failed to login in: %s": "Nepodařilo se přihlásit: %s",
|
||||
"Invalid token": "Neplatný token",
|
||||
"State expected: %s, but got: %s": "Očekávaný stav: %s, ale získán: %s",
|
||||
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Účet pro poskytovatele: %s a uživatelské jméno: %s (%s) neexistuje a není povoleno se registrovat jako nový účet přes %s, prosím použijte jiný způsob registrace",
|
||||
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Účet pro poskytovatele: %s a uživatelské jméno: %s (%s) neexistuje a není povoleno se registrovat jako nový účet, prosím kontaktujte svou IT podporu",
|
||||
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Účet pro poskytovatele: %s a uživatelské jméno: %s (%s) je již propojen s jiným účtem: %s (%s)",
|
||||
"The application: %s does not exist": "Aplikace: %s neexistuje",
|
||||
"The application: %s has disabled users to signin": "Aplikace: %s zakázala přihlašování uživatelů",
|
||||
"The group: %s does not exist": "Skupina: %s neexistuje",
|
||||
"The login method: login with LDAP is not enabled for the application": "Přihlašovací metoda: přihlášení pomocí LDAP není pro aplikaci povolena",
|
||||
"The login method: login with SMS is not enabled for the application": "Přihlašovací metoda: přihlášení pomocí SMS není pro aplikaci povolena",
|
||||
"The login method: login with email is not enabled for the application": "Přihlašovací metoda: přihlášení pomocí e-mailu není pro aplikaci povolena",
|
||||
"The login method: login with face is not enabled for the application": "Přihlašovací metoda: přihlášení pomocí obličeje není pro aplikaci povolena",
|
||||
"The login method: login with password is not enabled for the application": "Metoda přihlášení: přihlášení pomocí hesla není pro aplikaci povolena",
|
||||
"The organization: %s does not exist": "Organizace: %s neexistuje",
|
||||
"The organization: %s has disabled users to signin": "Organizace: %s zakázala přihlašování uživatelů",
|
||||
"The plan: %s does not exist": "Plán: %s neexistuje",
|
||||
"The pricing: %s does not exist": "Ceník: %s neexistuje",
|
||||
"The pricing: %s does not have plan: %s": "Ceník: %s nemá plán: %s",
|
||||
"The provider: %s does not exist": "Poskytovatel: %s neexistuje",
|
||||
"The provider: %s is not enabled for the application": "Poskytovatel: %s není pro aplikaci povolen",
|
||||
"Unauthorized operation": "Neoprávněná operace",
|
||||
"Unknown authentication type (not password or provider), form = %s": "Neznámý typ autentizace (není heslo nebo poskytovatel), formulář = %s",
|
||||
"User's tag: %s is not listed in the application's tags": "Uživatelův tag: %s není uveden v tagech aplikace",
|
||||
"UserCode Expired": "UserCode vypršel",
|
||||
"UserCode Invalid": "UserCode je neplatný",
|
||||
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "Placený uživatel %s nemá aktivní ani čekající předplatné a aplikace: %s nemá výchozí ceny",
|
||||
"the application for user %s is not found": "Aplikace pro uživatele %s nebyla nalezena",
|
||||
"the organization: %s is not found": "Organizace: %s nebyla nalezena"
|
||||
},
|
||||
"cas": {
|
||||
"Service %s and %s do not match": "Služba %s a %s se neshodují"
|
||||
},
|
||||
"check": {
|
||||
"%s does not meet the CIDR format requirements: %s": "%s nesplňuje požadavky formátu CIDR: %s",
|
||||
"Affiliation cannot be blank": "Příslušnost nemůže být prázdná",
|
||||
"CIDR for IP: %s should not be empty": "CIDR pro IP: %s by neměl být prázdný",
|
||||
"Default code does not match the code's matching rules": "Výchozí kód neodpovídá pravidlům shody kódu",
|
||||
"DisplayName cannot be blank": "Zobrazované jméno nemůže být prázdné",
|
||||
"DisplayName is not valid real name": "Zobrazované jméno není platné skutečné jméno",
|
||||
"Email already exists": "Email již existuje",
|
||||
"Email cannot be empty": "Email nemůže být prázdný",
|
||||
"Email is invalid": "Email je neplatný",
|
||||
"Empty username.": "Prázdné uživatelské jméno.",
|
||||
"Face data does not exist, cannot log in": "Data obličeje neexistují, nelze se přihlásit",
|
||||
"Face data mismatch": "Neshoda dat obličeje",
|
||||
"Failed to parse client IP: %s": "Nepodařilo se parsovat IP klienta: %s",
|
||||
"FirstName cannot be blank": "Křestní jméno nemůže být prázdné",
|
||||
"Invitation code cannot be blank": "Pozvánkový kód nemůže být prázdný",
|
||||
"Invitation code exhausted": "Pozvánkový kód je vyčerpán",
|
||||
"Invitation code is invalid": "Pozvánkový kód je neplatný",
|
||||
"Invitation code suspended": "Pozvánkový kód je pozastaven",
|
||||
"LDAP user name or password incorrect": "Uživatelské jméno nebo heslo LDAP je nesprávné",
|
||||
"LastName cannot be blank": "Příjmení nemůže být prázdné",
|
||||
"Multiple accounts with same uid, please check your ldap server": "Více účtů se stejným uid, prosím zkontrolujte svůj ldap server",
|
||||
"Organization does not exist": "Organizace neexistuje",
|
||||
"Password cannot be empty": "Heslo nemůže být prázdné",
|
||||
"Phone already exists": "Telefon již existuje",
|
||||
"Phone cannot be empty": "Telefon nemůže být prázdný",
|
||||
"Phone number is invalid": "Telefonní číslo je neplatné",
|
||||
"Please register using the email corresponding to the invitation code": "Prosím registrujte se pomocí e-mailu odpovídajícího pozvánkovému kódu",
|
||||
"Please register using the phone corresponding to the invitation code": "Prosím registrujte se pomocí telefonu odpovídajícího pozvánkovému kódu",
|
||||
"Please register using the username corresponding to the invitation code": "Prosím registrujte se pomocí uživatelského jména odpovídajícího pozvánkovému kódu",
|
||||
"Session outdated, please login again": "Relace je zastaralá, prosím přihlaste se znovu",
|
||||
"The invitation code has already been used": "Pozvánkový kód již byl použit",
|
||||
"The password must contain at least one special character": "Heslo musí obsahovat alespoň jeden speciální znak",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Heslo musí obsahovat alespoň jedno velké písmeno, jedno malé písmeno a jednu číslici",
|
||||
"The password must have at least 6 characters": "Heslo musí mít alespoň 6 znaků",
|
||||
"The password must have at least 8 characters": "Heslo musí mít alespoň 8 znaků",
|
||||
"The password must not contain any repeated characters": "Heslo nesmí obsahovat opakující se znaky",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "Uživatel byl odstraněn a nelze jej použít k přihlášení, kontaktujte prosím správce",
|
||||
"The user is forbidden to sign in, please contact the administrator": "Uživatel má zakázáno se přihlásit, prosím kontaktujte administrátora",
|
||||
"The user: %s doesn't exist in LDAP server": "Uživatel: %s neexistuje na LDAP serveru",
|
||||
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Uživatelské jméno může obsahovat pouze alfanumerické znaky, podtržítka nebo pomlčky, nemůže mít po sobě jdoucí pomlčky nebo podtržítka a nemůže začínat nebo končit pomlčkou nebo podtržítkem.",
|
||||
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "Hodnota \\\"%s\\\" pro pole účtu \\\"%s\\\" neodpovídá regexu položky účtu",
|
||||
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "Hodnota \\\"%s\\\" pro pole registrace \\\"%s\\\" neodpovídá regexu položky registrace aplikace \\\"%s\\\"",
|
||||
"Username already exists": "Uživatelské jméno již existuje",
|
||||
"Username cannot be an email address": "Uživatelské jméno nemůže být emailová adresa",
|
||||
"Username cannot contain white spaces": "Uživatelské jméno nemůže obsahovat mezery",
|
||||
"Username cannot start with a digit": "Uživatelské jméno nemůže začínat číslicí",
|
||||
"Username is too long (maximum is 255 characters).": "Uživatelské jméno je příliš dlouhé (maximálně 255 znaků).",
|
||||
"Username must have at least 2 characters": "Uživatelské jméno musí mít alespoň 2 znaky",
|
||||
"Username supports email format. Also The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline. Also pay attention to the email format.": "Uživatelské jméno podporuje formát e-mailu. Také uživatelské jméno může obsahovat pouze alfanumerické znaky, podtržítka nebo pomlčky, nemůže mít souvislé pomlčky nebo podtržítka a nemůže začínat nebo končit pomlčkou nebo podtržítkem. Také dbejte na formát e-mailu.",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Zadali jste špatné heslo nebo kód příliš mnohokrát, prosím počkejte %d minut a zkuste to znovu",
|
||||
"Your IP address: %s has been banned according to the configuration of: ": "Vaše IP adresa: %s byla zablokována podle konfigurace: ",
|
||||
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Vaše heslo vypršelo. Prosím resetujte si heslo kliknutím na \\\"Zapomněl jsem heslo\\\"",
|
||||
"Your region is not allow to signup by phone": "Vaše oblast neumožňuje registraci pomocí telefonu",
|
||||
"password or code is incorrect": "heslo nebo kód je nesprávný",
|
||||
"password or code is incorrect, you have %s remaining chances": "heslo nebo kód je nesprávné, máte %s zbývajících pokusů",
|
||||
"unsupported password type: %s": "nepodporovaný typ hesla: %s"
|
||||
},
|
||||
"enforcer": {
|
||||
"the adapter: %s is not found": "adaptér: %s nebyl nalezen"
|
||||
},
|
||||
"general": {
|
||||
"Failed to import groups": "Nepodařilo se importovat skupiny",
|
||||
"Failed to import users": "Nepodařilo se importovat uživatele",
|
||||
"Missing parameter": "Chybějící parametr",
|
||||
"Only admin user can specify user": "Pouze administrátor může určit uživatele",
|
||||
"Please login first": "Prosím, přihlaste se nejprve",
|
||||
"The organization: %s should have one application at least": "Organizace: %s by měla mít alespoň jednu aplikaci",
|
||||
"The user: %s doesn't exist": "Uživatel: %s neexistuje",
|
||||
"Wrong userId": "Nesprávné uživatelské ID",
|
||||
"don't support captchaProvider: ": "nepodporuje captchaProvider: ",
|
||||
"this operation is not allowed in demo mode": "tato operace není povolena v demo režimu",
|
||||
"this operation requires administrator to perform": "tato operace vyžaduje administrátora k provedení"
|
||||
},
|
||||
"invitation": {
|
||||
"Invitation %s does not exist": "Pozvánka %s neexistuje"
|
||||
},
|
||||
"ldap": {
|
||||
"Ldap server exist": "Ldap server existuje"
|
||||
},
|
||||
"link": {
|
||||
"Please link first": "Prosím, nejprve propojte",
|
||||
"This application has no providers": "Tato aplikace nemá žádné poskytovatele",
|
||||
"This application has no providers of type": "Tato aplikace nemá žádné poskytovatele typu",
|
||||
"This provider can't be unlinked": "Tento poskytovatel nemůže být odpojen",
|
||||
"You are not the global admin, you can't unlink other users": "Nejste globální administrátor, nemůžete odpojovat jiné uživatele",
|
||||
"You can't unlink yourself, you are not a member of any application": "Nemůžete odpojit sami sebe, nejste členem žádné aplikace"
|
||||
},
|
||||
"organization": {
|
||||
"Only admin can modify the %s.": "Pouze administrátor může upravit %s.",
|
||||
"The %s is immutable.": "%s je neměnný.",
|
||||
"Unknown modify rule %s.": "Neznámé pravidlo úpravy %s.",
|
||||
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "Přidání nového uživatele do 'vestavěné' organizace je momentálně zakázáno. Poznámka: všichni uživatelé v 'vestavěné' organizaci jsou globálními správci v Casdooru. Viz docs: https://casdoor.org/docs/basic/core-concepts#how -dělá-casdoor-spravovat-sám. Pokud stále chcete vytvořit uživatele pro 'vestavěnou' organizaci, přejděte na stránku nastavení organizace a aktivujte možnost 'Má souhlas s oprávněními'."
|
||||
},
|
||||
"permission": {
|
||||
"The permission: \\\"%s\\\" doesn't exist": "Oprávnění: \\\"%s\\\" neexistuje"
|
||||
},
|
||||
"provider": {
|
||||
"Invalid application id": "Neplatné ID aplikace",
|
||||
"the provider: %s does not exist": "poskytovatel: %s neexistuje"
|
||||
},
|
||||
"resource": {
|
||||
"User is nil for tag: avatar": "Uživatel je nil pro tag: avatar",
|
||||
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "Uživatelské jméno nebo úplná cesta k souboru je prázdná: uživatelské jméno = %s, úplná cesta k souboru = %s"
|
||||
},
|
||||
"saml": {
|
||||
"Application %s not found": "Aplikace %s nebyla nalezena"
|
||||
},
|
||||
"saml_sp": {
|
||||
"provider %s's category is not SAML": "poskytovatel %s není kategorie SAML"
|
||||
},
|
||||
"service": {
|
||||
"Empty parameters for emailForm: %v": "Prázdné parametry pro emailForm: %v",
|
||||
"Invalid Email receivers: %s": "Neplatní příjemci emailu: %s",
|
||||
"Invalid phone receivers: %s": "Neplatní příjemci telefonu: %s"
|
||||
},
|
||||
"storage": {
|
||||
"The objectKey: %s is not allowed": "objectKey: %s není povolen",
|
||||
"The provider type: %s is not supported": "typ poskytovatele: %s není podporován"
|
||||
},
|
||||
"subscription": {
|
||||
"Error": "Chyba"
|
||||
},
|
||||
"token": {
|
||||
"Grant_type: %s is not supported in this application": "Grant_type: %s není v této aplikaci podporován",
|
||||
"Invalid application or wrong clientSecret": "Neplatná aplikace nebo špatný clientSecret",
|
||||
"Invalid client_id": "Neplatné client_id",
|
||||
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "Přesměrovací URI: %s neexistuje v seznamu povolených přesměrovacích URI",
|
||||
"Token not found, invalid accessToken": "Token nenalezen, neplatný accessToken"
|
||||
},
|
||||
"user": {
|
||||
"Display name cannot be empty": "Zobrazované jméno nemůže být prázdné",
|
||||
"MFA email is enabled but email is empty": "MFA e-mail je povolen, ale e-mail je prázdný",
|
||||
"MFA phone is enabled but phone number is empty": "MFA telefon je povolen, ale telefonní číslo je prázdné",
|
||||
"New password cannot contain blank space.": "Nové heslo nemůže obsahovat prázdné místo.",
|
||||
"The new password must be different from your current password": "Nové heslo musí být odlišné od vašeho současného hesla",
|
||||
"the user's owner and name should not be empty": "vlastník a jméno uživatele by neměly být prázdné"
|
||||
},
|
||||
"util": {
|
||||
"No application is found for userId: %s": "Pro userId: %s nebyla nalezena žádná aplikace",
|
||||
"No provider for category: %s is found for application: %s": "Pro kategorii: %s nebyl nalezen žádný poskytovatel pro aplikaci: %s",
|
||||
"The provider: %s is not found": "Poskytovatel: %s nebyl nalezen"
|
||||
},
|
||||
"verification": {
|
||||
"Invalid captcha provider.": "Neplatný poskytovatel captcha.",
|
||||
"Phone number is invalid in your region %s": "Telefonní číslo je ve vaší oblasti %s neplatné",
|
||||
"The verification code has already been used!": "Ověřovací kód již byl použit!",
|
||||
"The verification code has not been sent yet!": "Ověřovací kód ještě nebyl odeslán!",
|
||||
"Turing test failed.": "Turingův test selhal.",
|
||||
"Unable to get the email modify rule.": "Nelze získat pravidlo pro úpravu emailu.",
|
||||
"Unable to get the phone modify rule.": "Nelze získat pravidlo pro úpravu telefonu.",
|
||||
"Unknown type": "Neznámý typ",
|
||||
"Wrong verification code!": "Špatný ověřovací kód!",
|
||||
"You should verify your code in %d min!": "Měli byste ověřit svůj kód do %d minut!",
|
||||
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "prosím přidejte SMS poskytovatele do seznamu \\\"Providers\\\" pro aplikaci: %s",
|
||||
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "prosím přidejte e-mailového poskytovatele do seznamu \\\"Providers\\\" pro aplikaci: %s",
|
||||
"the user does not exist, please sign up first": "uživatel neexistuje, prosím nejprve se zaregistrujte"
|
||||
},
|
||||
"webauthn": {
|
||||
"Found no credentials for this user": "Pro tohoto uživatele nebyly nalezeny žádné přihlašovací údaje",
|
||||
"Please call WebAuthnSigninBegin first": "Prosím, nejprve zavolejte WebAuthnSigninBegin"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user