Compare commits

...

422 Commits

Author SHA1 Message Date
7c38a99973 fix(ci): исправить путь container registry
All checks were successful
Build & Push Docker Image / build (push) Successful in 2m15s
- исправлен путь образа на gromov/casdoor
2026-04-11 19:42:25 +03:00
7c26dbb7d0 fix(ci): убрать тест версии из Dockerfile
Some checks failed
Build & Push Docker Image / build (push) Failing after 6m15s
- удалён TestGetVersionInfo, падает без .git в контейнере
2026-04-11 19:32:22 +03:00
61bc75b12e chore(ci): добавить ручной запуск workflow
Some checks failed
Build & Push Docker Image / build (push) Failing after 2m5s
- добавлен workflow_dispatch для ручного запуска сборки
2026-04-11 19:26:23 +03:00
18a8694d28 chore(ci): добавить Gitea Actions для сборки Docker-образа
- добавлен workflow сборки и пуша в container registry
- сборка при пуше в ветку custom
- target STANDARD (Alpine)
2026-04-11 19:19:01 +03:00
8478543c6b feat(i18n): добавить русский язык и конфигурацию разработки
- добавлен русский перевод интерфейса (web/src/locales/ru)
- восстановлен русский перевод бэкенда из Crowdin (i18n/locales/ru)
- добавлен ru в список языков организации
- добавлен Русский в селект языков
- добавлена конфигурация для локальной разработки (PostgreSQL, порт 5434)
- добавлен docker-compose.dev.yml
2026-04-11 19:11:15 +03:00
Yang Luo
25d8595e66 fix: improve top-left logo position 2026-04-11 22:33:20 +08:00
Yang Luo
3aafa91937 fix: improve hosting badge position and UI 2026-04-11 22:23:46 +08:00
Yang Luo
0077839549 fix: hide global scrollbar 2026-04-11 22:07:54 +08:00
Yang Luo
e1ee2ddee8 fix: add margin to 3 store pages 2026-04-11 22:02:05 +08:00
Yang Luo
b93be2d3e2 fix: add top breadcrumb bar 2026-04-11 21:53:57 +08:00
Yang Luo
77b56a2e40 fix: increase left sidebar width 2026-04-11 21:48:13 +08:00
Yang Luo
c0591f316e fix: increase org-select's width 2026-04-11 21:43:56 +08:00
Yang Luo
6749d46561 fix: improve top-left logo position 2026-04-11 21:42:21 +08:00
Yang Luo
a4a50f182b fix: hide left sidebar's scrollbar 2026-04-11 21:21:11 +08:00
Yang Luo
221d10a172 fix: fix Can't resolve 'rc-util/es/isEqual' bug 2026-04-11 20:46:34 +08:00
Yang Luo
5c051ba03d feat: improve table column width
BREAKING CHANGE: major release
2026-04-11 19:08:54 +08:00
Yang Luo
c16f4d2fb5 fix: improve xxx list page table's column row height 2026-04-11 19:01:28 +08:00
Yang Luo
fe185f880c fix: improve i18n keys 2026-04-11 19:00:45 +08:00
Yang Luo
b3bed1992b fix: improve "Loading" position 2026-04-11 18:54:05 +08:00
Yang Luo
be38d178fd fix: increase org-select's width 2026-04-11 18:50:19 +08:00
Yang Luo
3eb164e149 fix: add left margin to top-right user avatar 2026-04-11 18:46:47 +08:00
Yang Luo
6c3cd8a74b fix: set Sidebar menu: selected item - darker background 2026-04-11 18:43:10 +08:00
Yang Luo
c5ab4eec59 fix: fix top-left logo missing bug 2026-04-11 18:41:20 +08:00
Yang Luo
e8170884d7 fix: improve record and session list page UI 2026-04-11 18:40:22 +08:00
Yang Luo
729b21e8ae fix: use Apple Inter font 2026-04-11 18:32:36 +08:00
Yang Luo
bed67a1ff2 fix: improve top-left menu text 2026-04-11 18:21:29 +08:00
Yang Luo
df5f5def31 fix: improve list page's table title bar height 2026-04-11 18:10:29 +08:00
Yang Luo
76c56e9b2d fix: fix top-left menu highlight 2026-04-11 18:09:27 +08:00
Yang Luo
f46e229d5b fix: improve top-left logo 2026-04-11 17:50:25 +08:00
Yang Luo
112be9714b fix: reduce content area margin 2026-04-11 17:47:46 +08:00
Yang Luo
9d85362a24 fix: reduce top bar height 2026-04-11 17:43:03 +08:00
Yang Luo
37e2f13d99 feat: change to left sidebar 2026-04-11 17:32:13 +08:00
Yang Luo
f35398ea5c fix: use outlined icons in top navbar 2026-04-11 17:19:09 +08:00
Yang Luo
5a5470d5a3 fix: use shadcn theme by default 2026-04-11 17:15:35 +08:00
Yang Luo
948fc017e1 fix: improve i18n data 2026-04-11 17:01:26 +08:00
Yang Luo
c63184fc67 feat: upgrade to Antd 6.3.5 2026-04-11 16:53:23 +08:00
Yang Luo
f5f4032b3b fix: fix code format 2026-04-11 11:02:59 +08:00
Yang Luo
7006041fa9 fix: remove OpenClaw session heartbeat logs 2026-04-11 10:54:59 +08:00
Yang Luo
d7bc2bf052 feat: add support for OAuth 2.0 DPoP (Demonstrating Proof of Possession) 2026-04-11 10:45:33 +08:00
Yang Luo
29eeb03f85 fix: refactor out token_oauth_util.go 2026-04-11 10:19:04 +08:00
Yang Luo
14b4b557f9 feat: support user's accessKey in auto signin filter 2026-04-11 01:07:36 +08:00
Yang Luo
49d35ac161 feat: add Provider.CustomLogoutUrl field 2026-04-11 01:01:58 +08:00
Yang Luo
5ed9158368 fix: improve GetSortedUsers code 2026-04-11 00:58:40 +08:00
Yang Luo
2bb728ad7d feat: fix wrong verb sent in oauth state (signup instead of signin) 2026-04-11 00:42:57 +08:00
nkanf-dev
f4665df477 fix: fix checking shared-app login permission in user organization (#5381) 2026-04-11 00:09:59 +08:00
Paperlz
12bbecb69d feat: build OpenClaw session graphs from DB entries (#5382) 2026-04-11 00:02:04 +08:00
Yang Luo
a5079cd0c5 feat: fix UpdatePolicy/UpdatePolicies APIs for empty field update 2026-04-09 01:00:02 +08:00
Yang Luo
e361044f86 fix: add RequestBodyFilter to fix POST body JSON error without Content-Type issue 2026-04-09 00:35:05 +08:00
Yang Luo
91cdf56636 feat: Enable ABAC support in /api/enforce and /api/batch-enforce 2026-04-09 00:32:48 +08:00
Yang Luo
10daed237e feat: improve objOwner check 2026-04-08 23:17:54 +08:00
cooronx
315a6bb040 feat: deduplicate permission RBAC by building grouping policies in run time (#5374) 2026-04-08 23:01:00 +08:00
Yang Luo
cef6b85389 feat: failed HTTP Basic Authentication won't trigger error now 2026-04-08 22:59:52 +08:00
Yang Luo
14a802f2c5 fix: fix add-new logic for provider 2026-04-08 22:18:44 +08:00
Yang Luo
40d1f63cd6 fix: don't collect OpenClaw metrics entries for now 2026-04-08 19:23:14 +08:00
Yang Luo
85c91c50d3 fix: add provider.State to log providers 2026-04-08 09:52:45 +08:00
Yang Luo
0e5f810f2f feat: fix "sqlite3" driverName to "sqlite" 2026-04-08 09:27:16 +08:00
asuka
e9c2ec0d6c fix: fix authz permission for well-known OAuth endpoints (#5372) 2026-04-07 22:38:12 +08:00
Paperlz
2a8ac578da feat: add local OpenClaw transcript sync for session logs (#5370) 2026-04-07 22:34:55 +08:00
DacongDA
31ce1512df feat: apply loginPage captcha rule check to SendCodeInput.js (#5369) 2026-04-06 17:54:27 +08:00
Yang Luo
bac824cb4f feat: improve filter check 2026-04-06 12:26:29 +08:00
DacongDA
1637ca1dfb feat: support GET request in MCP SSE (#5363) 2026-04-06 10:35:49 +08:00
Paperlz
c7ad2052c9 feat: add provider-based entry viewers for SELinux logs (#5364) 2026-04-06 00:59:32 +08:00
Yang Luo
117bf608ea fix: include xs namespace in C14N10 exclusive canonicalization prefix list 2026-04-05 21:09:54 +08:00
DacongDA
13e0af4b0a feat: switch server source to "https://mcp.casdoor.org/registry.json" (#5366) 2026-04-05 20:57:03 +08:00
Yang Luo
e8a0b268dc feat: add redirectUriMatchesPattern() 2026-04-05 20:38:12 +08:00
Yang Luo
2762390c32 fix: add Clear button to MCP server edit page 2026-04-05 17:54:27 +08:00
cooronx
a69c4454ca feat: add back buy-product API for compatibility (#5362) 2026-04-05 17:41:15 +08:00
Paperlz
c76d0d17ed fix: use SELinux collector for SELinux log providers (#5361) 2026-04-05 16:55:55 +08:00
Yang Luo
e10706cb6d fix: fix linter in saml_sp.go 2026-04-05 15:43:03 +08:00
Yang Luo
d92b856868 feat: add parseAndValidateSubjectToken() 2026-04-05 15:32:38 +08:00
Yang Luo
d14674e60e fix: improve buildSpCertificateStore logic 2026-04-05 15:22:09 +08:00
DacongDA
284dde292a feat: add Sync button to sync tools in MCP server edit page (#5360) 2026-04-05 15:09:39 +08:00
Yang Luo
ea56cfec2b fix: improve IsRedirectUriValid logic 2026-04-05 15:00:00 +08:00
Yang Luo
82d7f241bb fix: refactor out application_util.go 2026-04-05 14:56:24 +08:00
Yang Luo
56ac5cd221 feat: add Application.EnableGuestSignin field 2026-04-05 14:28:53 +08:00
Yang Luo
203a61cfef feat: improve GetOAuthToken logic 2026-04-05 14:22:09 +08:00
Yang Luo
b9500a27d9 fix: improve buildAuthFilterString logic 2026-04-05 10:19:19 +08:00
Yang Luo
c979a05c25 fix: fix bug in responseOtlpError() 2026-04-05 09:44:45 +08:00
Yang Luo
1e7a2d8dad fix: improve random handling 2026-04-05 09:42:52 +08:00
Yang Luo
f6a3fb9455 feat: improve error handling in OLTP APIs 2026-04-05 02:50:12 +08:00
Yang Luo
9030a06792 fix: add client IP and UA to entry 2026-04-05 02:08:45 +08:00
Yang Luo
fffb26deb9 fix: remove entry's useless fields 2026-04-05 01:59:32 +08:00
Yang Luo
fab57364db fix: refactor out log/agent_openclaw.go 2026-04-05 01:52:25 +08:00
Yang Luo
e73cfe8b40 fix: fix go linter issue 2026-04-05 00:18:31 +08:00
Yang Luo
facc1ec203 feat: support "/api/v1/metrics" and "/api/v1/logs" APIs from OpenClaw 2026-04-05 00:13:37 +08:00
Yang Luo
6cb9978475 feat: add SELinuxLogProvider 2026-04-04 23:48:56 +08:00
Yang Luo
f75cee76ae fix: set empty providerUrl in new provider 2026-04-04 23:43:43 +08:00
Yang Luo
c92e553e9b fix: fix provider_log_system_log's wrong provider field 2026-04-04 23:36:06 +08:00
Yang Luo
a824fc0f3c fix: improve getAutoProviderName with subtype 2026-04-04 23:36:06 +08:00
Yang Luo
98dea3a15a fix: add provider_log_agent_openclaw and IP check 2026-04-04 23:36:06 +08:00
DacongDA
c0d3fdf812 feat: can scan intranet MCP servers (#5359) 2026-04-04 23:25:13 +08:00
Yang Luo
1c60a4ddfa fix: fix parseAndPersistEvents() code in Windows system log provider 2026-04-04 23:06:51 +08:00
Yang Luo
ac43fb9cac fix: fix system log provider code 2026-04-04 22:46:19 +08:00
Yang Luo
2f7e6c1cc2 fix: refactor system log provider 2026-04-04 22:11:47 +08:00
Yang Luo
28b76cce76 fix: improve entry list page UI again 2026-04-04 22:03:54 +08:00
Yang Luo
319896267e fix: improve entry list page UI 2026-04-04 21:51:38 +08:00
Yang Luo
a3698024bc fix: fix writePermissionLog() hook position 2026-04-04 21:33:48 +08:00
Yang Luo
8ffca95c59 fix: add "Casdoor Permission Log" option to frontend 2026-04-04 21:03:03 +08:00
Yang Luo
4f68432349 feat: auto set name and display name for new provider 2026-04-04 21:00:52 +08:00
Yang Luo
17a52da2b8 fix: add Entry.Provider field 2026-04-04 21:00:52 +08:00
Yang Luo
5140053083 fix: add "Casdoor Permission Log" provider 2026-04-04 20:59:21 +08:00
Yang Luo
9b86530763 fix: rename to log package 2026-04-04 20:59:21 +08:00
Yang Luo
84f289ddc4 fix: ignore claude config 2026-04-04 20:59:20 +08:00
Modo
23cdb279e6 feat: add EntryMessageViewer (#5357) 2026-04-04 16:48:00 +08:00
Yang Luo
ea2408a7d2 feat: improve permission command API 2026-04-04 01:30:14 +08:00
Yang Luo
4ccb28571b fix: improve value in GetSession() 2026-04-04 01:15:29 +08:00
Yang Luo
1439031780 feat: add LogProvider interface 2026-04-03 01:33:15 +08:00
Modo
2ebe3f1d5d feat: add "/api/v1/traces" API to receive OpenClaw's OpenTelemetry metric (#5349) 2026-04-01 12:13:44 +08:00
Paperlz
0ff862dbc5 feat: enhance webhook worker reliability and event pagination (#5344) 2026-03-31 09:51:24 +08:00
DacongDA
bb11511029 feat: add MCP store page (#5343) 2026-03-31 09:49:49 +08:00
Yang Luo
18979caea4 feat: add Entry list and edit pages 2026-03-31 00:25:44 +08:00
Yang Luo
a61575f9d1 fix: improve menu for LLM AI 2026-03-30 23:26:37 +08:00
Paperlz
863d86d55f feat: add webhook delivery persistence, retry mechanism and replay UI (#5337) 2026-03-30 22:53:56 +08:00
Modo
b690ee4ea3 feat: add Agent list and edit pages (#5338) 2026-03-30 09:10:18 +08:00
ANormalDD
5b58d8bf16 feat: move configs of defaultApplication and maxItemsForFlatMenu to backend app.conf (#5325) 2026-03-28 12:27:53 +08:00
Yijun Gao
e5d2feb73d feat: add disk and network usage to system info page with improved layout (#5304) 2026-03-27 21:39:23 +08:00
Yang Luo
96359f78c5 feat: add CheckVerifyCodeWithLimitAndIp() 2026-03-26 23:02:11 +08:00
Stephane P. Pericat
6f18f67138 feat: upgrade Go toolchain to 1.25.8 (#5328) 2026-03-26 00:51:27 +08:00
Yang Luo
9038d8ab5b fix: improve LDAP handling for "(uid={})" filter 2026-03-25 22:40:27 +08:00
Yang Luo
b396a69ed7 fix: fix go.sum 2026-03-24 23:26:52 +08:00
Chlx
189277f9a2 feat: add Antd Select component for language selection (#5319) 2026-03-24 23:23:12 +08:00
Yang Luo
655777f0f1 feat: upgrade grpc dependency (#5318) 2026-03-24 22:12:48 +08:00
Yang Luo
fb0b93873c feat: fix 100% CPU/memory usage displaying as green success state on System Info page (#5306) 2026-03-23 21:28:13 +08:00
DacongDA
f5af87683d feat: can sync MCP tools and set "is allowed" for MCP tool (#5301) 2026-03-23 11:47:06 +08:00
DacongDA
df47f5785c fix: rename mcp package to mcpself (#5302) 2026-03-23 10:59:10 +08:00
ANormalDD
4879926977 fix: remove old use keys (#5299) 2026-03-22 14:40:14 +08:00
ANormalDD
7148c9db85 fix: check owner in UpdateKey() API (#5297) 2026-03-22 12:50:03 +08:00
Modo
29dccbe32f feat: preserve RFC 8707 resource across browser login flow (#5298) 2026-03-22 09:30:59 +08:00
Br1an
65755d3b28 feat: redirect to account page for forced password change (#5181) 2026-03-22 00:55:58 +08:00
Yang Luo
239e8bd694 feat: add key list/edit pages (#5285) 2026-03-21 20:06:06 +08:00
Modo
d23e8b205b feat: add permanent avatar switch to organization settings (#5295) 2026-03-21 09:21:11 +08:00
ANormalDD
1260db8c27 feat: remove Casvisor dependency and use local Record struct (#5287) 2026-03-19 22:48:06 +08:00
DacongDA
1506a5c895 feat: add MCP server list/edit pages (#5278) 2026-03-18 22:43:45 +08:00
ANormalDD
7b5f4aefab feat: use backend config app.conf instead of frontend config (#5279) 2026-03-18 21:37:46 +08:00
Modo
75bc8e6b0d feat: wrap xorm-adapter RemovePolicy to prevent mass deletion on empty fields (#5282) 2026-03-18 17:32:31 +08:00
Yang Luo
5965e75610 fix: add missing swagger annotations to rule and site APIs (#5281) 2026-03-18 17:31:05 +08:00
Modo
899c2546cf feat: fix last element not visible in Edit Application form tabs (#5275) 2026-03-17 20:41:34 +08:00
Yang Luo
95defad3b1 feat: fix OAuth state parameter re-encoding in redirect URL to prevent OIDC state mismatch (#5262) 2026-03-17 20:41:03 +08:00
Yang Luo
6a263cb5cb feat: fix LDAP sync crash on large user sets due to PostgreSQL parameter limit (#5268) 2026-03-14 23:07:22 +08:00
ANormalDD
54d6a59cb6 feat: add rate limiting to /api/verify-code OTP endpoint (#5270) 2026-03-14 23:01:52 +08:00
DacongDA
2693c07b3c feat: only init site map when proxy server is started (#5265) 2026-03-13 00:27:16 +08:00
Yang Luo
2895c72d32 fix: improve Actions field UI in permission list page 2026-03-11 21:43:32 +08:00
ANormalDD
f6129b09c8 feat: implement minimal HTML+JS OAuth callback and provider_hint flows (#5238) 2026-03-10 19:04:55 +08:00
Yang Luo
0bbbb48af1 feat: upgrade to golang:1.24.13 and node:20.20.1 (LTS), update Dockerfile base images (#5246) 2026-03-10 18:12:12 +08:00
gaël Prudhomme
34a8b252d5 feat: fix bug in site's owner/organization (#5239) 2026-03-09 23:55:41 +08:00
DacongDA
c756e56f74 feat: fix proxy server panic issue if port has been used (#5240) 2026-03-09 22:16:58 +08:00
DacongDA
dbc2a676ba feat: allow user to set binding rule in provider items (#5224) 2026-03-07 22:20:48 +08:00
Yang Luo
74e6b73e7b feat: fix empty "Binding providers" prompt step appearing after signup (#5221) 2026-03-07 17:41:24 +08:00
Yang Luo
07de8a40d6 feat: fix invitation code lost during signup when form field is not visible (#5231) 2026-03-07 15:35:47 +08:00
ANormalDD
c6a6ec8869 feat: fix bug that invitation links fail with external OAuth providers (#5229) 2026-03-07 14:45:22 +08:00
Yang Luo
394b3e1372 feat: add Kerberos/SPNEGO authentication (#5225) 2026-03-07 09:46:45 +08:00
Yang Luo
fa93d4eb8b feat: add LDAP server attribute filtering per organization (#5222) 2026-03-07 00:53:20 +08:00
Yang Luo
47a5fc8b09 feat: support regex/wildcard scopes in OAuth authorization requests (#5223) 2026-03-07 00:52:27 +08:00
Yang Luo
c1acb7a432 fix: fix go.sum 2026-03-07 00:02:33 +08:00
Yang Luo
c10b2c162f feat: fix Twilio SMS sending verification code bug (#5205) 2026-03-06 22:32:09 +08:00
ANormalDD
41ec8ba44f feat(web): add AutoComplete for SAML attributes value (#5215) 2026-03-06 20:27:40 +08:00
Yang Luo
7df722a103 fix: set organization.balanceCredit's max to 0 2026-03-06 14:10:07 +08:00
Yang Luo
04b1ca1157 fix: revert "feat: fix BalanceCredit to act as overdraft limit, not minimum balance floor" (#5214) 2026-03-06 13:36:21 +08:00
DacongDA
b0fecefeb7 feat: add Site and Rule to Casdoor (#5194) 2026-03-06 01:02:16 +08:00
Yang Luo
167d24fb1f fix: fix getOAuthGetParameters bug in Moodle 2026-03-05 23:05:20 +08:00
Yang Luo
dc58ac0503 feat: fix BalanceCredit to act as overdraft limit, not minimum balance floor (#5210) 2026-03-05 22:56:46 +08:00
Br1an
038d021797 fix: skip password columns in syncer when remote has no password data (#5183) 2026-03-05 22:35:27 +08:00
Yang Luo
7ba660fd7f feat: fix normal users blocked from /product-store (#5195) 2026-03-05 22:24:36 +08:00
Tomáš Karela Procházka
b1c31a4a9d feat: add Resend email provider (#5200) 2026-03-05 20:55:23 +08:00
Yang Luo
90d7add503 fix: remove useless returnUrl field from ProductEditPage (#5190) 2026-03-04 21:48:47 +08:00
Yang Luo
c961e75ad3 feat: fall back to English when unsupported Accept-Language locale is requested (#5177) 2026-03-04 21:41:10 +08:00
Br1an
547189a034 feat: add missing "min" param for Cloud PNVS SMS provider (#5180) 2026-03-03 09:08:31 +08:00
DacongDA
be725eda74 feat: merge CasWAF's cert related code into Casdoor's cert code (#5171) 2026-02-27 01:36:07 +08:00
Ke Wang
0765b352c9 fix: respect application's ID signup rule in WeChat Mini Program login (#5168) 2026-02-24 21:21:18 +08:00
Yang Luo
a2a8b582d9 feat: make DingTalk syncer respect TableColumns field mapping configuration (#5073) 2026-02-24 12:55:40 +08:00
Sriram-B-Srivatsa
0973652be4 fix: reduce code duplication in Logout logic (#5163) 2026-02-24 12:53:31 +08:00
Yang Luo
fef75715bf fix(web): prevent dashboard graph overlap when y-axis values increase 2026-02-23 15:24:05 +08:00
hikarukimi
4f78d56e31 feat: add OAuth consent page 2026-02-23 15:16:04 +08:00
hikarukimi
712bc756bc fix: improve code format 2026-02-23 15:09:57 +08:00
DacongDA
1c9952e3d9 feat: support JWT Profile for OAuth 2.0 Client Grants (RFC 7523) (#5124) 2026-02-23 14:44:34 +08:00
Yang Luo
bbaa28133f feat: apply application.DefaultGroup for OAuth signups (#5157) 2026-02-22 01:06:18 +08:00
Yang Luo
baef7680ea feat: validate OAuth scopes against Application config; return invalid_scope per RFC 6749 (#5153) 2026-02-21 17:44:26 +08:00
Yang Luo
d15b66177c feat: add missing Telegram field to User struct (#5151) 2026-02-21 17:21:31 +08:00
Yang Luo
5ce6bac529 fix: improve provider table links 2026-02-21 01:36:00 +08:00
Yang Luo
0621f35665 fix: improve tabs height UI in app edit page 2026-02-21 01:16:36 +08:00
Yang Luo
1ac2490419 fix: add OIDC and SAML tabs in application edit page 2026-02-21 01:13:54 +08:00
DacongDA
8c50ada494 feat: refactor provider edit page into different JS files (#5141) 2026-02-21 00:57:38 +08:00
Yang Luo
22da90576e feat: can free input in "Tag" in Addresses table 2026-02-20 16:49:50 +08:00
Yang Luo
b00404cb3a fix: fix RegionSelect cannot save value bug in Addresses table 2026-02-20 16:45:43 +08:00
Yang Luo
2ed27f4f0a fix: improve tables UI in my account page 2026-02-20 16:35:29 +08:00
Yang Luo
bf538d5260 fix: update UpdateUser() columns for missing User fields 2026-02-20 11:02:52 +08:00
Yang Luo
13ee5fd150 feat: sync newOrganization() accountItems with getBuiltInAccountItems() (#5146) 2026-02-20 10:47:02 +08:00
Yang Luo
04cdd5a012 feat: add missing user fields to GetTranslatedUserItems, getBuiltInAccountItems, init_data template, and UserFields (#5144) 2026-02-20 10:37:51 +08:00
Yang Luo
7b4873734b feat: fix "--config" flag to actually load specified configuration file (#5139) 2026-02-19 02:13:29 +08:00
Yang Luo
8d2290944a fix: add back Payment.ProductName and ProductDisplayName fields for backward compatibility 2026-02-18 19:28:14 +08:00
Yang Luo
6a2bba1627 feat: fix field visibility logic for provider types in ProviderEditPage (#5134) 2026-02-18 15:22:28 +08:00
Yang Luo
07554bbbe5 feat: fix Alipay OAuth provider by loading private key from cert object (#5119) 2026-02-17 14:42:21 +08:00
karatekaneen
a050403ee5 feat: fix bug that PKCE fails when multiple custom OAuth providers are configured (#5117) 2026-02-16 23:32:07 +08:00
IsAurora6
118eb0af80 feat: Optimize the display of payment products. (#5115) 2026-02-16 16:32:02 +08:00
Yang Luo
c16aebe642 fix: update README slogan 2026-02-16 02:33:45 +08:00
Yang Luo
3b8e7c9da2 fix: extend application with reverse proxy fields (#5113) 2026-02-16 02:23:47 +08:00
Yang Luo
4d5de767b0 fix: sync frontend i18n strings 2026-02-16 02:01:48 +08:00
Yang Luo
54bf8eae5c fix: improve category column UI in app list page 2026-02-16 01:46:06 +08:00
IsAurora6
1731b74fa0 fix: fix issue that dummy payments failed when there were too many items in the order (#5108) 2026-02-15 22:35:59 +08:00
Yang Luo
6e1e5dd569 feat: add scope-to-tool permission checking for Casdoor MCP server (#5104) 2026-02-15 22:31:35 +08:00
Yang Luo
b183359daf fix: rename order state PaymentFailed to Failed and improve UI (#5107) 2026-02-15 21:52:24 +08:00
Yang Luo
3cb9df3723 feat: [mcp-5] add Application.Category and Application.Type fields for agent applications (MCP, A2A) (#5102) 2026-02-15 21:28:00 +08:00
Yang Luo
9d1e5c10d0 feat: [mcp-4] implement RFC 8707 Resource Indicators for OAuth 2.0 (#5098) 2026-02-15 18:03:22 +08:00
Yang Luo
ef84c4b0b4 feat: [mcp-3] implement OAuth 2.0 Dynamic Client Registration (RFC 7591) (#5097) 2026-02-15 17:25:44 +08:00
Yang Luo
5a108bd921 fix: [mcp-2] add OAuth 2.0 Authorization Server Metadata endpoints (RFC 8414) (#5094) 2026-02-15 17:00:40 +08:00
Yang Luo
ac671ec1ee fix: rename to wellknown_oidc_discovery.go 2026-02-15 16:42:00 +08:00
Yang Luo
7814caf2ab feat: implement RFC 9728 OAuth 2.0 Protected Resource Metadata for MCP server discovery (#5092) 2026-02-15 16:40:48 +08:00
Yang Luo
f966f4a0f9 feat: fix Dummy payment provider returning zero price in NotifyResult (#5090) 2026-02-15 02:31:56 +08:00
Yang Luo
a4b1a068a8 feat: fix Azure SQL DB panic by migrating to the official go-mssqldb fork (#5082) 2026-02-15 01:52:06 +08:00
Yang Luo
362797678d feat: fix nil pointer panic in update-user API for non-existent account items (#5084) 2026-02-15 01:44:26 +08:00
Yang Luo
7879e1bf09 fix: fix Dummy payment provider to simulate external callback flow (#5080) 2026-02-15 00:18:09 +08:00
IsAurora6
c246f102c9 feat: fix issue that User.Cart cannot be updated without org account items (#5076) 2026-02-14 22:32:58 +08:00
IsAurora6
37d1c4910c feat: Fixed an error when clicking the “delete” button on the cart list page. (#5075) 2026-02-13 20:49:08 +08:00
Yang Luo
3bcde7cb7c feat: add Cart and payment fields to organization account items (#5070) 2026-02-13 10:40:37 +08:00
Yang Luo
6a90d21941 fix: add CreatedTime field to cart items and sort by timestamp (#5066) 2026-02-13 10:36:20 +08:00
Yang Luo
80b4c0b1a7 feat: remove special handling for Dummy payment provider (#5068) 2026-02-13 10:06:14 +08:00
Yang Luo
eb5a422026 feat: replace DisableSsl boolean with SslMode enum for Email providers (#5063) 2026-02-13 02:15:20 +08:00
DacongDA
f7bd70e0a3 feat: improve tab height UI in application edit page (#5055) 2026-02-12 21:57:57 +08:00
Copilot
5e7dbe4b56 feat: fix CAPTCHA rule enforcement in verification code flow (#5009) 2026-02-12 21:22:47 +08:00
Yang Luo
bd1fca2f32 feat: Add LDAP group/OU hierarchy syncing with automatic user membership (#5052) 2026-02-12 17:11:20 +08:00
IsAurora6
3d4cc42f1f feat: mark cart items as invalid when product is removed, renamed, or currency is changed. (#5050) 2026-02-12 00:46:54 +08:00
Yang Luo
1836cab44d feat: fix icons for 5 payment providers 2026-02-11 01:42:37 +08:00
Yang Luo
75b18635f7 feat: fix issue that Webhook records for set-password API were missing user context (#5008) 2026-02-11 01:32:11 +08:00
Yang Luo
47cd44c7ce feat: support "snsapi_privateinfo" scope in WeCom OAuth provider to support fetching Emails (#5034) 2026-02-11 01:21:29 +08:00
Yang Luo
090ca97dcd feat: bind provider IDs in WeCom/DingTalk/Lark syncers (#5033) 2026-02-11 01:04:26 +08:00
Yang Luo
bed01b31f1 feat: add AWS IAM syncer (#5043) 2026-02-11 01:00:41 +08:00
Yang Luo
c8f8f88d85 feat: add "Existing Field" category for token attributes table in application edit page (#5041) 2026-02-11 00:58:50 +08:00
IsAurora6
7acb303995 feat: Fixed cart anomalies when updating product information. (#5039) 2026-02-10 20:58:18 +08:00
IsAurora6
2607f8d3e5 feat: fix DingTalk syncer to fetch nested departments recursively (#5036) 2026-02-10 18:11:03 +08:00
IsAurora6
481db33e58 feat: Optimize the display of rechargeable product content on the ProductStorePage.js. (#5028) 2026-02-09 20:28:18 +08:00
DacongDA
f556c7e11f feat: add PaginateSelect widget to fix non-pagination fetch API issue (#5023) 2026-02-09 20:07:41 +08:00
IsAurora6
f590992f28 feat: update i18n translations (#5021) 2026-02-09 00:05:08 +08:00
Yang Luo
80f9db0fa2 feat: move captcha provider validation from frontend filter to backend check (#5019) 2026-02-08 02:16:47 +08:00
Yang Luo
0748661d2a feat: store OAuth tokens per provider instead of single originalToken field (#5016) 2026-02-08 01:22:24 +08:00
Yang Luo
83552ed143 feat: fix renderRightDropdown() scrollbar UI bug 2026-02-08 00:45:46 +08:00
Yang Luo
8cb8541f96 feat: add Plan.IsExclusive field for single subscription enforcement (#5004) 2026-02-07 01:23:22 +08:00
Yang Luo
5b646a726c fix: fix format issue in DuplicateInfo 2026-02-07 00:51:11 +08:00
Yang Luo
19b9586670 fix: fix broken links for role/plan/user/payment columns (#4999) 2026-02-07 00:46:36 +08:00
Yang Luo
73f8d19c5f fix: de-duplicate i18n translation keys in frontend and backend (#4997) 2026-02-07 00:35:46 +08:00
Yang Luo
04da531df3 fix: sync all i18n strings 2026-02-07 00:18:07 +08:00
Yang Luo
d97558051d fix: add duplicate key detection tests for i18n JSON files (#4994) 2026-02-07 00:17:53 +08:00
Yang Luo
ac55355290 fix: deduplicate the i18n strings 2026-02-06 21:42:10 +08:00
Yang Luo
a2da380be4 feat: add organization sync to DingTalk syncer (#4989) 2026-02-06 20:39:31 +08:00
IsAurora6
ecf8039c5d feat: Add cart icon to ProductStore/ProductBuy and quantity controls to ProductStore/ProductBuy/CartList. (#4984) 2026-02-05 23:07:22 +08:00
Yang Luo
0a6948034c feat: add OAuth 2.0 Token Exchange (RFC 8693) support (#4981) 2026-02-05 19:12:39 +08:00
Yang Luo
442f8fb19e feat: fix DeleteSession to handle missing sessions gracefully (#4979) 2026-02-05 15:41:15 +08:00
Yang Luo
b771add9e3 feat: auto-redirect OAuth signup to callback URL (#4941) 2026-02-05 15:18:33 +08:00
Yang Luo
df8e9fceea feat: disable /forget API when "Forgot Password?" signin item is hidden (#4977) 2026-02-04 23:04:00 +08:00
Yang Luo
d674f0c33d feat: update Swagger docs 2026-02-03 21:34:38 +08:00
buzaslan129
1e1b5273d9 feat: expose get-all-* Casbin endpoints in Swagger (#4952) 2026-02-03 21:32:50 +08:00
IsAurora6
cf5e88915c feat: The order.products display is divided into two parts. Remove the "startTime" and "endTime", and add an updateTime field. (#4968) 2026-02-03 21:12:27 +08:00
Yang Luo
c8973e6c9e feat: add Cloud PNVS SMS provider (#4964) 2026-02-03 02:00:08 +08:00
Yang Luo
87ea451561 feat: support group sync in Google Workspace syncer (#4962) 2026-02-03 01:58:28 +08:00
Yang Luo
8f32779b42 feat: fix invitation code group assignment for OAuth provider signup (#4961) 2026-02-03 01:23:36 +08:00
Yang Luo
aba471b4e8 feat: install lsof in ALLINONE Docker image (#4958) 2026-02-02 23:51:49 +08:00
DacongDA
72b70c3b03 feat: use sqlite DB instead of mariadb for all-in-one Docker image (#4949) 2026-02-02 00:13:14 +08:00
DacongDA
a1c56894c7 feat: add tabs to user edit page (#4945) 2026-02-01 14:01:28 +08:00
Yang Luo
a9ae9394c7 feat: add Linux machine login via LDAP with POSIX attributes (#4944) 2026-01-31 22:37:29 +08:00
Yang Luo
5f0fa5f23e feat: fix properties field xlsx import issue in user list page (#4943) 2026-01-31 01:49:36 +08:00
Yang Luo
f99aa047a9 feat: add Org.AccountItems.Tab field to have tabs in user edit page (#4892) 2026-01-30 21:56:35 +08:00
Yang Luo
1d22b7ebd0 feat: prevent duplicate webhook events from redundant payment notifications (#4936) 2026-01-30 21:56:09 +08:00
IsAurora6
d147053329 feat: Optimize the display of the products column on the order and payment, adjust the color of the “Add to Cart” button. (#4933) 2026-01-30 14:03:15 +08:00
IsAurora6
0f8cd92be4 feat: resolve returnUrl redirection failure of UserEditPage (#4931) 2026-01-29 09:37:47 +08:00
DacongDA
7ea6f1296d feat: fix i18n/generate.go bug in handling "\" (#4930) 2026-01-28 23:35:23 +08:00
Yang Luo
db8c649f5e feat: include payment status in notify-payment webhook payload (#4929) 2026-01-28 19:59:10 +08:00
DacongDA
a06d003589 feat: make codeChallenge dynamic for custom OAuth provider (#4924) 2026-01-28 17:56:28 +08:00
Jacob
33298e44d4 feat(ldap-sync): support syncing phone country code and formatting mobile number (#4919) 2026-01-28 14:09:52 +08:00
IsAurora6
f4d86f8d92 feat: fix incorrect clearing of the returnUrl path parameter in redirects (#4920) 2026-01-28 10:51:44 +08:00
Yang Luo
af4337a1ae feat: add multi-address support to user edit page (#4916) 2026-01-27 21:46:41 +08:00
IsAurora6
81e650df65 feat: Optimize the display of the order price column and improve parameter passing in the OrderPay view mode. (#4912) 2026-01-27 12:17:15 +08:00
Yang Luo
fcea1e4c07 feat: add SCIM 2.0 syncer (#4909) 2026-01-27 01:47:50 +08:00
Yang Luo
639a8a47b1 feat: add Okta syncer (#4908) 2026-01-27 01:19:39 +08:00
Yang Luo
43f61d4426 feat: add Lark syncer (#4897) 2026-01-27 01:00:19 +08:00
IsAurora6
e90cdb8a74 feat: add default payment providers on startup, improve checkProduct() logic (#4895) 2026-01-27 00:23:09 +08:00
DacongDA
bfe8955250 feat: remove bottom save button and extra scrollbar in application edit page (#4890) 2026-01-25 11:03:52 +08:00
DacongDA
36b9c4602a feat: add tab menu for application edit page (#4889) 2026-01-24 18:05:17 +08:00
IsAurora6
18117833e1 feat: Optimize button logic of product buy/store page,non-Created orders display "Detail" Button, and add "clear cart" Button (#4887) 2026-01-24 12:17:44 +08:00
Yang Luo
78dde97b64 feat: add PKCE support for Custom OAuth providers (#4880) 2026-01-23 21:29:57 +08:00
Yang Luo
3a06c66057 feat: fix Azure AD syncer OAuth2 token request - send parameters in body (#4878) 2026-01-22 23:01:38 +08:00
Yang Luo
aa59901400 feat: change Application.TermsOfUse length to 200 chars 2026-01-21 17:24:06 +08:00
IsAurora6
8e03b2d97c feat: Enable subscription-based products to be added to the cart and purchased, and optimize the cart page. (#4868) 2026-01-21 17:18:11 +08:00
MarshallHuang
d1da9499e8 fix: update OIDC discovery to use consistent authorization endpoint (#4872) 2026-01-21 16:54:51 +08:00
Yang Luo
2e7673c015 feat: use unionid in DingTalk syncer for consistency with OAuth provider (#4870) 2026-01-21 00:07:31 +08:00
DacongDA
2d1ace427e feat: support GetVersionInfo() API in released binary (#4860) 2026-01-20 18:05:11 +08:00
IsAurora6
039c12afa3 feat: add the shopping cart page (#4855) 2026-01-19 12:12:15 +08:00
slavb18
4236160fa7 feat: add User.OriginalRefreshToken field (#4721) 2026-01-19 12:08:18 +08:00
Yang Luo
071b5ddec0 feat: fix error for "/.well-known/:application/openid-configuration" API (#4866) 2026-01-19 03:06:33 +08:00
Gucheng Wang
f46b92d225 feat: reduce i18n languages (#4862) 2026-01-18 18:28:13 +08:00
Yang Luo
cc7eb4664c feat: support comma-separated user tags in application tag validation (#4856) 2026-01-17 01:26:12 +08:00
Yang Luo
1567723e2b feat: fix null issue for GrantTypes and RedirectUris in application 2026-01-17 01:18:57 +08:00
IsAurora6
074253f45e feat: Optimize PlaceOrder-related methods and pages to support the creation of multi-item orders. (#4847) 2026-01-16 16:52:02 +08:00
Yang Luo
23c86e9018 feat: add application.EnableSamlAssertionSignature to allow disabling SAML assertion signatures (#4850) 2026-01-16 14:30:48 +08:00
DacongDA
f088827a50 feat: redirect user to last login org's login page while cookie expired (#4844) 2026-01-15 18:17:12 +08:00
IsAurora6
663815fefe feat: The frontend supports payment logic for multi-item orders. (#4843) 2026-01-15 18:16:28 +08:00
DacongDA
0d003d347e fix: improve error handling in the syncer (#4845) 2026-01-15 15:02:24 +08:00
IsAurora6
7d495ca5f2 feat: The backend supports payment logic for multi-item orders. (#4842) 2026-01-14 21:57:09 +08:00
Jiachen Ren
f89495b35c fix: use unionid instead of job_number as user name in the OAuth provider (#4841) 2026-01-14 20:02:35 +08:00
IsAurora6
4a3aefc5f5 feat: improve filter logic in order, payment, subscription get APIs (#4839) 2026-01-14 12:08:29 +08:00
Yang Luo
15646b23ff feat: support ES/ECDSA signing method in ParseStandardJwtToken() (#4837) 2026-01-14 00:47:31 +08:00
gufeiyan1215
4b663a437f feat: add RRSA (RAM roles) support for the OSS storage provider (#4831) 2026-01-13 23:01:04 +08:00
DacongDA
9fb90fbb95 feat: support user impersonation (#4817) 2026-01-13 20:47:35 +08:00
Yang Luo
65eeaef8a7 feat: fix payment currency display to use product currency instead of user balance currency (#4822) 2026-01-13 20:47:31 +08:00
IsAurora6
ecf8e2eb32 feat: add supported currency validation for payment providers (#4818) 2026-01-13 20:47:28 +08:00
soliujing
e49e678d16 feat: improve build performance, separate build dependency to allow docker cache (#4815) 2026-01-13 20:47:24 +08:00
DacongDA
623ee23285 feat: in some case, saml replay state will include special character (#4814) 2026-01-13 20:47:09 +08:00
soliujing
0901a1d5a0 feat: handle default organization in get-orders API (#4790) 2026-01-13 20:46:50 +08:00
Yang Luo
58ff2fe69c feat: include access tokens in session-level (logoutAll=false) sso-logout notifications for Single Logout (SLO) (#4804) 2026-01-13 20:46:27 +08:00
IsAurora6
737f44a059 feat: optimize authentication handling in MCP (#4801) 2026-01-09 21:27:21 +08:00
soliujing
32cef8e828 feat: add permissions for get-order and get-orders APIs (#4788) 2026-01-09 17:33:29 +08:00
Yang Luo
9e854abc77 feat: don't auto-login for single SAML provider (#4795) 2026-01-09 17:03:16 +08:00
Yang Luo
9b3343d3db feat: fix multiple webhooks don't work bug (#4798) 2026-01-08 23:41:40 +08:00
Yang Luo
5b71725c94 feat: add OIDC-compliant email_verified claim to all JWT token formats (#4797) 2026-01-08 21:12:34 +08:00
IsAurora6
59b6854ccc feat: Optimize the notifications/initialized request and authentication failure handling in MCP. (#4781) 2026-01-08 17:42:36 +08:00
Yang Luo
0daf67c52c feat: fix UTF-8 encoding error in Active Directory syncer (#4783) 2026-01-08 01:50:47 +08:00
Yang Luo
4b612269ea feat: check whether refresh token is expired after SSO logout (#4771) 2026-01-07 19:42:35 +08:00
0xkrypton
f438d39720 feat: fix Telegram OAuth login error: "failed to verify Telegram auth data: data verification failed." (#4776) 2026-01-07 19:41:43 +08:00
Eng Zer Jun
f8df200dbf feat: update github.com/shirou/gopsutil to v4 (#4773) 2026-01-07 00:51:37 +08:00
IsAurora6
cb1b3b767e feat: improve "/api/mcp" check with demo mode (#4772) 2026-01-06 14:48:24 +08:00
IsAurora6
3bec49f16c feat: enhance MCP Permissions and Response Workflow, fix bugs (#4767) 2026-01-05 22:54:12 +08:00
Yang Luo
e28344f0e7 feat: add DingTalk syncer (#4766) 2026-01-05 21:43:57 +08:00
Yang Luo
93fefed6e8 feat: add Casdoor MCP server at "/api/mcp" for application management (#4752) 2026-01-05 21:38:34 +08:00
Yang Luo
ea9abb2f29 feat: fix bugs in ticket pages 2026-01-02 23:17:30 +08:00
Yang Luo
337a8c357b feat: fix error in order APIs 2026-01-02 22:04:51 +08:00
IsAurora6
d8cebfbf04 feat: Fixed the logic for updating order and transaction statuses in payment notifications. (#4749) 2026-01-02 19:30:23 +08:00
Yang Luo
91d5039155 feat: add all API endpoints to webhook Events dropdown (#4748) 2026-01-01 22:39:18 +08:00
DacongDA
5996ee8695 feat: add ID verification to init data template and organization UI (#4744) 2026-01-01 15:16:51 +08:00
Yang Luo
8c9331932b feat: initialize default values for fields like signupItems when adding applications via SDK (#4733) 2025-12-29 20:29:02 +08:00
DacongDA
db594e2096 feat: use org name as TOTP issuer (#4731) 2025-12-29 13:49:01 +08:00
Yang Luo
b46b79ee44 feat: improve error handling of hasGravatar() 2025-12-28 22:36:47 +08:00
Yang Luo
b9dbbca716 chore: improve README 2025-12-28 19:37:51 +08:00
Yang Luo
313cf6d480 fix: add missing ID Verification category to OtherProviderInfo (#4727) 2025-12-27 18:48:11 +08:00
DacongDA
0548597d04 feat: update dependencies (aws-sdk-go, go-git, goth and go-jose) to latest (#4729) 2025-12-27 18:17:18 +08:00
DacongDA
eb8e26748f feat: replace notify with notify2 for notification provider (#4728) 2025-12-27 10:47:36 +08:00
Yang Luo
516a23ab1b feat: fix CAPTCHA modal appearing when provider Rule is set to None (#4725) 2025-12-27 09:46:33 +08:00
DacongDA
9887d80e55 feat: upgrade beego to v2 (#4720) 2025-12-26 12:46:13 +08:00
slavb18
13dd4337a6 feat: Add phone number to CustomUserInfo (#4718) 2025-12-25 09:29:58 +08:00
Yang Luo
36c69a6da1 feat: add Telegram to OAuth provider options in web UI (#4719) 2025-12-25 09:29:36 +08:00
Yang Luo
3f4a60096a feat: add 28 missing User fields to syncer UI dropdown (#4713) 2025-12-24 20:56:11 +08:00
Yang Luo
b6240fa356 feat: improve GetFilteredUsers() 2025-12-24 20:31:09 +08:00
Yang Luo
d61f06b053 feat: add WebauthnCredentials and 27 other User fields to syncer (#4705) 2025-12-24 01:52:52 +08:00
IsAurora6
6fe785b6a4 feat: fix null address causing TypeError in management UI (#4706) 2025-12-24 01:31:47 +08:00
DacongDA
cccddea67e feat: fix unauthorized error when using app API to login (#4702) 2025-12-23 20:29:46 +08:00
IsAurora6
83b8c5477a feat: fix Transaction State field type from pp.PaymentState to string (#4699) 2025-12-21 01:31:54 +08:00
IsAurora6
ac0e069f71 feat: add Adyen payment provider (#4667) 2025-12-21 01:25:17 +08:00
DacongDA
4b25e56048 feat: Make session and cookie timeout configurable per application (#4698) 2025-12-21 01:04:38 +08:00
DacongDA
39740e3d6c feat: add support to delete single session and report err while deleting current session (#4694) 2025-12-18 21:15:57 +08:00
IsAurora6
87c5bf3855 fix: fixed balance and dummy payment errors (#4692) 2025-12-14 22:52:13 +08:00
IsAurora6
c4a28acbd8 feat: fix bug in i18n applyToOtherLanguage() (#4691) 2025-12-14 19:24:01 +08:00
IsAurora6
ee26b896f6 fix: show recharge options UI in product store page (#4682) 2025-12-13 15:46:26 +08:00
Yang Luo
4a8cb9535e feat: enforce failed signin limit for LDAP login (#4686) 2025-12-13 00:30:05 +08:00
Yang Luo
387a22d5f8 feat: add ticket list/edit pages (#4651) 2025-12-12 23:16:47 +08:00
Yang Luo
36cadded1c feat: add missing grant types to OIDC discovery endpoint (#4677) 2025-12-12 23:12:13 +08:00
DacongDA
7d130392d9 feat: add session-level single sign-out with authentication and configurable scope (#4678) 2025-12-12 23:08:01 +08:00
IsAurora6
f82c90b901 feat: Optimise the order confirmation page prompts and fix the issue where the transaction.application field was incorrectly populated as organisation. (#4681) 2025-12-12 21:31:22 +08:00
Yang Luo
1a08d6514e fix: improve IsRedirectUriValid() (#4672) 2025-12-11 22:18:56 +08:00
Yang Luo
4d5bf09b36 feat: fix signup application bug in /sso-logout API 2025-12-11 22:10:24 +08:00
Yang Luo
f050deada7 feat: add GoReleaser workflow for multi-platform binary releases (#4665) 2025-12-10 12:10:23 +08:00
Yang Luo
dee94666e0 fix: disable isValidRealName() check in backend 2025-12-10 12:00:23 +08:00
Yang Luo
b84b7d787b fix: fix isSelf() identity check for users without ID field in account items of user edit page (#4669) 2025-12-10 11:40:05 +08:00
Yang Luo
d425183137 feat: update Swagger docs 2025-12-10 01:55:08 +08:00
Yang Luo
ff7fcd277c feat: fix SAML authentication failure when username attribute is unmapped (#4663) 2025-12-10 01:50:03 +08:00
Yang Luo
ed5c0b2713 feat: remove "Please sign out first" check from signup and login APIs (#4659) 2025-12-09 21:16:54 +08:00
Yang Luo
eb60e43192 feat: use bcrypt password type by default for all organizations (#4654) 2025-12-08 22:11:19 +08:00
Yang Luo
d0170532e6 fix: improve Swagger annotations for session and token APIs (#4652) 2025-12-08 22:04:53 +08:00
Yang Luo
7ddb87cdf8 fix: Fix JWT-Custom token format: always include nonce/scope, add signinMethod and provider to dropdown (#4649) 2025-12-08 17:55:31 +08:00
Yang Luo
fac45f5ac7 feat: add Alibaba Cloud ID verification provider (#4645) 2025-12-08 17:48:52 +08:00
Yang Luo
266d361244 feat: fix "only the last session is displayed" bug by respecting application.EnableExclusiveSignin when adding sessions (#4643) 2025-12-08 17:14:11 +08:00
DacongDA
b454ab1931 feat: fix generated link has no org info bug while using shared application (#4647) 2025-12-08 16:35:17 +08:00
Yang Luo
ff39b6f186 feat: add Jumio ID Verification provider (#4641) 2025-12-08 00:39:34 +08:00
DacongDA
0597dbbe20 feat: always return array if item contains roles, groups or permissions in JWT (#4640) 2025-12-08 00:11:39 +08:00
Yang Luo
49c417c70e fix: add excel import support for groups, permissions, and roles (#4585) 2025-12-07 22:24:12 +08:00
IsAurora6
8b30e12915 feat: improve inventory logic: check stock before order and update stock/sales after payment. (#4633) 2025-12-07 19:38:41 +08:00
Jacob
2e18c65429 feat: add Application.DisableSamlAttributes field and fix C14N namespace issue (#4634) 2025-12-06 21:45:02 +08:00
IsAurora6
27c98bb056 feat: improve payment flow with order navigation and remove returnUrl field (#4632) 2025-12-06 17:57:59 +08:00
DacongDA
4400b66862 feat: fix silentSignin not working bug (#4629) 2025-12-06 11:10:10 +08:00
IsAurora6
e7e7d18ee7 fix: add permission control and view mode for product/order/payment/plan/pricing/subscription pages. (#4628) 2025-12-04 23:08:41 +08:00
IsAurora6
66d1e28300 feat: Add payment column to order list and refine product store card layout. (#4625) 2025-12-04 18:18:10 +08:00
IsAurora6
53782a6706 feat: support recharge products with preset amounts and disable custom amount option. (#4619) 2025-12-03 13:50:33 +08:00
Yang Luo
30bb0ce92f feat: fix signupItem.regex validation not working in signup page frontend (#4614) 2025-12-03 08:56:45 +08:00
Yang Luo
29f7dda858 feat: fix 403 error on /api/acs endpoint for SAML IdP responses (#4620) 2025-12-02 21:19:00 +08:00
Yang Luo
68b82ed524 fix: accept all file types in resources list page's upload button 2025-11-30 20:42:54 +08:00
Yang Luo
c4ce88198f feat: improve password popover positioning on signup page 2025-11-30 18:10:19 +08:00
Yang Luo
a11fa23add fix: fix i18n for "Please input your {field}!" validation message in signup page (#4610) 2025-11-30 17:47:25 +08:00
Yang Luo
add6ba32db fix: improve application edit page's Providers dropdown with search, icons, and display names (#4608) 2025-11-30 17:13:06 +08:00
Yang Luo
37379dee13 fix: fix get-groups API call in ApplicationEditPage to use correct owner parameter (#4606) 2025-11-30 16:23:28 +08:00
Yang Luo
2066670b76 feat: add Lemon Squeezy payment provider (#4604) 2025-11-30 13:40:48 +08:00
Yang Luo
e751148be2 feat: add FastSpring payment provider (#4601) 2025-11-30 12:02:18 +08:00
Yang Luo
c541d0bcdd feat: add Paddle payment provider (#4598) 2025-11-30 11:31:16 +08:00
Yang Luo
f0db95d006 feat: add Polar payment provider (#4595) 2025-11-30 10:45:11 +08:00
IsAurora6
e4db367eaa feat: Remove BuyProduct endpoint and legacy purchase logic. (#4591) 2025-11-28 23:51:22 +08:00
IsAurora6
9df81e3ffc feat: feat: add OrderPayPage.js, fix subscription redirect & refine list time format. (#4586) 2025-11-27 20:49:49 +08:00
IsAurora6
048d6acc83 feat: Implement the complete process of product purchase, order placement, and payment. (#4588) 2025-11-27 20:49:34 +08:00
Yang Luo
e440199977 feat: regenerate the Swagger docs 2025-11-25 22:24:32 +08:00
IsAurora6
cb4e559d51 feat: Added PlaceOrder, CancelOrder, and PayOrder methods, and added corresponding buttons to the frontend. (#4583) 2025-11-25 22:22:46 +08:00
zjumathcode
4d1d0b95d6 feat: drop legacy // +build comment (#4582) 2025-11-25 20:21:09 +08:00
Yang Luo
9cc1133a96 feat: upgrade gomail to v2.2.0 2025-11-25 01:03:45 +08:00
Yang Luo
897c28e8ad fix: fix SQL query in Keycloak syncer (#4578) 2025-11-24 23:40:30 +08:00
Yang Luo
9d37a7e38e fix: fix memory leaks in database syncer from unclosed connections (#4574) 2025-11-24 23:38:50 +08:00
Yang Luo
ea597296b4 fix: allow normal users to view their own transactions (#4572) 2025-11-24 01:47:10 +08:00
Yang Luo
427ddd215e feat: add Telegram OAuth provider (#4570) 2025-11-24 01:04:36 +08:00
Yang Luo
24de79b100 Improve getTransactionTableColumns UI 2025-11-23 22:07:33 +08:00
DacongDA
9ab9c7c8e0 fix: show error better for user upload (#4568) 2025-11-23 21:52:44 +08:00
Yang Luo
0728a9716b feat: deduplicate code between TransactionTable and TransactionListPage (#4567) 2025-11-23 21:47:58 +08:00
Yang Luo
471570f24a Improve AddTransaction API return value 2025-11-23 21:02:06 +08:00
Yang Luo
2fa520844b fix: fix product store page to pass owner parameter to API (#4565) 2025-11-23 20:48:15 +08:00
Yang Luo
2306acb416 fix: improve balanceCredit for org and user 2025-11-23 19:51:39 +08:00
Yang Luo
d3f3f76290 fix: add dry run mode to add-transaction API (#4563) 2025-11-23 17:36:51 +08:00
DacongDA
fe93128495 feat: improve user upload UX (#4542) 2025-11-23 16:05:46 +08:00
seth-shi
7fd890ff14 fix: ticket error handling in HandleOfficialAccountEvent() (#4557) 2025-11-23 14:58:23 +08:00
Yang Luo
83b56d7ceb feat: add product store page (#4544) 2025-11-23 14:54:35 +08:00
Yang Luo
503e5a75d2 feat: add User.OriginalToken field to expose OAuth provider access tokens (#4559) 2025-11-23 14:54:02 +08:00
seth-shi
5a607b4991 fix: close file handle in GetUploadXlsxPath to prevent resource leak (#4558) 2025-11-23 14:37:06 +08:00
Yang Luo
ca2dc2825d feat: add SSO logout notifications to user's signup application (#4547) 2025-11-23 00:47:29 +08:00
Yang Luo
446d0b9047 Improve TransactionTable UI 2025-11-23 00:45:47 +08:00
Yang Luo
ee708dbf48 feat: add Organization.OrgBalanceCredit and User.BalanceCredit fields for credit limit enforcement (#4552) 2025-11-23 00:37:44 +08:00
Yang Luo
221ca28488 fix: flatten top navbar to single level when ≤7 items (#4550) 2025-11-23 00:34:17 +08:00
Yang Luo
e93d3f6c13 Improve transaction list page UI 2025-11-22 23:35:04 +08:00
Yang Luo
e285396d4e fix: fix recharge transaction default values (#4546) 2025-11-22 23:27:29 +08:00
Yang Luo
10320bb49f Improve TransactionTable UI 2025-11-22 21:39:56 +08:00
seth-shi
4d27ebd82a feat: Use email as username when organization setting is enabled during login (#4539) 2025-11-22 20:58:27 +08:00
Yang Luo
6d5e6dab0a Fix account table missing item 2025-11-22 20:56:45 +08:00
Yang Luo
e600ea7efd feat: add i18n support for table column widgets (#4541) 2025-11-22 16:39:44 +08:00
559 changed files with 56733 additions and 33833 deletions

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

View File

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

@@ -20,6 +20,7 @@ bin/
.idea/
*.iml
.vscode/settings.json
.claude
tmp/
tmpFiles/

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,23 +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, *, *
@@ -101,6 +116,7 @@ p, *, *, GET, /api/get-pricing, *, *
p, *, *, GET, /api/get-plan, *, *
p, *, *, GET, /api/get-subscription, *, *
p, *, *, GET, /api/get-transactions, *, *
p, *, *, GET, /api/get-transaction, *, *
p, *, *, GET, /api/get-provider, *, *
p, *, *, GET, /api/get-organization-names, *, *
p, *, *, GET, /api/get-all-objects, *, *
@@ -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
View 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
}

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,80 @@ 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
}
util.LogInfo(c.Ctx, "API: [%s] logged out from all applications", user)
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())
return
}
if userObj != nil {
err = object.SendSsoLogoutNotifications(userObj, sessionIds, tokens)
if err != nil {
c.ResponseError(err.Error())
return
}
}
// Propagate logout to external Custom OAuth2 providers
object.InvokeCustomProviderLogout(ssoApplication, ssoSessionToken)
c.ResponseOk()
}
@@ -497,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 {
@@ -620,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())
@@ -656,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
}

View File

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

View File

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

View File

@@ -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 {
@@ -724,6 +795,7 @@ func (c *ApiController) Login() {
return
}
userInfo := &idp.UserInfo{}
var token *oauth2.Token
if provider.Category == "SAML" {
// SAML
userInfo, err = object.ParseSamlResponse(authForm.SamlResponse, provider, c.Ctx.Request.Host)
@@ -733,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 {
@@ -754,7 +831,6 @@ func (c *ApiController) Login() {
}
// https://github.com/golang/oauth2/issues/123#issuecomment-103715338
var token *oauth2.Token
token, err = idProvider.GetToken(authForm.Code)
if err != nil {
c.ResponseError(err.Error())
@@ -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"))
}
}
}
@@ -804,7 +880,7 @@ func (c *ApiController) Login() {
if user != nil && !user.IsDeleted {
// Sign in via OAuth (want to sign up but already have account)
// sync info from 3rd-party if possible
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, provider.UserMapping)
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, token, provider.UserMapping)
if err != nil {
c.ResponseError(err.Error())
return
@@ -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,21 @@ 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 != "" {
userInfo.Username = userInfo.Email
}
// Handle username conflicts
var tmpUser *object.User
@@ -876,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])
}
@@ -927,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
@@ -946,10 +1007,20 @@ 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
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, provider.UserMapping)
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, token, provider.UserMapping)
if err != nil {
c.ResponseError(err.Error())
return
@@ -997,7 +1068,7 @@ func (c *ApiController) Login() {
}
// sync info from 3rd-party if possible
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, provider.UserMapping)
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, token, provider.UserMapping)
if err != nil {
c.ResponseError(err.Error())
return
@@ -1149,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())
@@ -1160,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())
@@ -1193,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"`
@@ -1213,7 +1284,7 @@ func (c *ApiController) HandleOfficialAccountEvent() {
return
}
if data.Ticket == "" {
c.ResponseError(err.Error())
c.ResponseError("empty ticket")
return
}
@@ -1228,10 +1299,6 @@ func (c *ApiController) HandleOfficialAccountEvent() {
return
}
if data.Ticket == "" {
c.ResponseError("empty ticket")
return
}
if !idp.VerifyWechatSignature(provider.Content, nonce, timestamp, signature) {
c.ResponseError("invalid signature")
return
@@ -1257,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]
@@ -1277,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())
@@ -1305,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 {
@@ -1339,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)
}
@@ -1350,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{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,78 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
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
}

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -0,0 +1,222 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/beego/beego/v2/core/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetKeys
// @Title GetKeys
// @Tag Key API
// @Description get keys
// @Param owner query string true "The owner of keys"
// @Success 200 {array} object.Key The Response object
// @router /get-keys [get]
func (c *ApiController) GetKeys() {
owner := c.Ctx.Input.Query("owner")
limit := c.Ctx.Input.Query("pageSize")
page := c.Ctx.Input.Query("p")
field := c.Ctx.Input.Query("field")
value := c.Ctx.Input.Query("value")
sortField := c.Ctx.Input.Query("sortField")
sortOrder := c.Ctx.Input.Query("sortOrder")
if limit == "" || page == "" {
keys, err := object.GetKeys(owner)
if err != nil {
c.ResponseError(err.Error())
return
}
maskedKeys, err := object.GetMaskedKeys(keys, true, nil)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedKeys)
} else {
limit := util.ParseInt(limit)
count, err := object.GetKeyCount(owner, field, value)
if err != nil {
c.ResponseError(err.Error())
return
}
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
keys, err := object.GetPaginationKeys(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil {
c.ResponseError(err.Error())
return
}
maskedKeys, err := object.GetMaskedKeys(keys, true, nil)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedKeys, paginator.Nums())
}
}
// GetGlobalKeys
// @Title GetGlobalKeys
// @Tag Key API
// @Description get global keys
// @Success 200 {array} object.Key The Response object
// @router /get-global-keys [get]
func (c *ApiController) GetGlobalKeys() {
limit := c.Ctx.Input.Query("pageSize")
page := c.Ctx.Input.Query("p")
field := c.Ctx.Input.Query("field")
value := c.Ctx.Input.Query("value")
sortField := c.Ctx.Input.Query("sortField")
sortOrder := c.Ctx.Input.Query("sortOrder")
if limit == "" || page == "" {
keys, err := object.GetGlobalKeys()
if err != nil {
c.ResponseError(err.Error())
return
}
maskedKeys, err := object.GetMaskedKeys(keys, true, nil)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedKeys)
} else {
limit := util.ParseInt(limit)
count, err := object.GetGlobalKeyCount(field, value)
if err != nil {
c.ResponseError(err.Error())
return
}
paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
keys, err := object.GetPaginationGlobalKeys(paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil {
c.ResponseError(err.Error())
return
}
maskedKeys, err := object.GetMaskedKeys(keys, true, nil)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedKeys, paginator.Nums())
}
}
// GetKey
// @Title GetKey
// @Tag Key API
// @Description get key
// @Param id query string true "The id ( owner/name ) of the key"
// @Success 200 {object} object.Key The Response object
// @router /get-key [get]
func (c *ApiController) GetKey() {
id := c.Ctx.Input.Query("id")
key, err := object.GetKey(id)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(key)
}
// UpdateKey
// @Title UpdateKey
// @Tag Key API
// @Description update key
// @Param id query string true "The id ( owner/name ) of the key"
// @Param body body object.Key true "The details of the key"
// @Success 200 {object} controllers.Response The Response object
// @router /update-key [post]
func (c *ApiController) UpdateKey() {
id := c.Ctx.Input.Query("id")
oldKey, err := object.GetKey(id)
if err != nil {
c.ResponseError(err.Error())
return
}
if oldKey == nil {
c.Data["json"] = wrapActionResponse(false)
c.ServeJSON()
return
}
var key object.Key
err = json.Unmarshal(c.Ctx.Input.RequestBody, &key)
if err != nil {
c.ResponseError(err.Error())
return
}
if !c.IsGlobalAdmin() && oldKey.Owner != key.Owner {
c.ResponseError(c.T("auth:Unauthorized operation"))
return
}
c.Data["json"] = wrapActionResponse(object.UpdateKey(id, &key))
c.ServeJSON()
}
// AddKey
// @Title AddKey
// @Tag Key API
// @Description add key
// @Param body body object.Key true "The details of the key"
// @Success 200 {object} controllers.Response The Response object
// @router /add-key [post]
func (c *ApiController) AddKey() {
var key object.Key
err := json.Unmarshal(c.Ctx.Input.RequestBody, &key)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddKey(&key))
c.ServeJSON()
}
// DeleteKey
// @Title DeleteKey
// @Tag Key API
// @Description delete key
// @Param body body object.Key true "The details of the key"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-key [post]
func (c *ApiController) DeleteKey() {
var key object.Key
err := json.Unmarshal(c.Ctx.Input.RequestBody, &key)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteKey(&key))
c.ServeJSON()
}

View File

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

@@ -0,0 +1,112 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"net/http"
"net/http/httputil"
"net/url"
"github.com/casdoor/casdoor/mcpself"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// ProxyServer
// @Title ProxyServer
// @Tag Server API
// @Description proxy request to the upstream MCP server by Server URL
// @Param owner path string true "The owner name of the server"
// @Param name path string true "The name of the server"
// @Success 200 {object} mcp.McpResponse The Response object
// @router /server/:owner/:name [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, &params)
if err != nil {
c.McpResponseError(mcpReq.ID, -32600, "Invalid request", err.Error())
return
}
for _, tool := range server.Tools {
if tool.Name == params.Name && !tool.IsAllowed {
c.McpResponseError(mcpReq.ID, -32600, "tool is forbidden", nil)
return
} else if tool.Name == params.Name {
break
}
}
}
proxy := httputil.NewSingleHostReverseProxy(targetUrl)
proxy.ErrorHandler = func(writer http.ResponseWriter, request *http.Request, proxyErr error) {
c.Ctx.Output.SetStatus(http.StatusBadGateway)
c.McpResponseError(mcpReq.ID, -32603, "failed to proxy server request: %s", proxyErr.Error())
}
proxy.Director = func(request *http.Request) {
request.URL.Scheme = targetUrl.Scheme
request.URL.Host = targetUrl.Host
request.Host = targetUrl.Host
request.URL.Path = targetUrl.Path
request.URL.RawPath = ""
request.URL.RawQuery = targetUrl.RawQuery
if server.Token != "" {
request.Header.Set("Authorization", "Bearer "+server.Token)
}
}
proxy.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)
}

View File

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

View File

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

View File

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

160
controllers/order_pay.go Normal file
View File

@@ -0,0 +1,160 @@
// Copyright 2025 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"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// PlaceOrder
// @Title PlaceOrder
// @Tag Order API
// @Description place an order for a product
// @Param productId query string true "The id ( owner/name ) of the product"
// @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 customPrice query number false "Custom price for recharge products"
// @Param userName query string false "The username to place order for (admin only)"
// @Success 200 {object} object.Order The Response object
// @router /place-order [post]
func (c *ApiController) PlaceOrder() {
owner := c.Ctx.Input.Query("owner")
paidUserName := c.Ctx.Input.Query("userName")
var req struct {
ProductInfos []object.ProductInfo `json:"productInfos"`
}
err := json.Unmarshal(c.Ctx.Input.RequestBody, &req)
if err != nil {
c.ResponseError(err.Error())
return
}
productInfos := req.ProductInfos
if len(productInfos) == 0 {
c.ResponseError(c.T("product:Product list cannot be empty"))
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
}
order, err := object.PlaceOrder(owner, productInfos, user)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(order)
}
// PayOrder
// @Title PayOrder
// @Tag Order API
// @Description pay an existing order
// @Param id query string true "The id ( owner/name ) of the order"
// @Param providerName query string true "The name of the provider"
// @Success 200 {object} controllers.Response The Response object
// @router /pay-order [post]
func (c *ApiController) PayOrder() {
id := c.Ctx.Input.Query("id")
host := c.Ctx.Request.Host
providerName := c.Ctx.Input.Query("providerName")
paymentEnv := c.Ctx.Input.Query("paymentEnv")
order, err := object.GetOrder(id)
if err != nil {
c.ResponseError(err.Error())
return
}
if order == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The order: %s does not exist"), id))
return
}
userId := c.GetSessionUsername()
orderUserId := util.GetId(order.Owner, order.User)
if userId != orderUserId && !c.IsAdmin() {
c.ResponseError(c.T("auth:Unauthorized operation"))
return
}
payment, attachInfo, err := object.PayOrder(providerName, host, paymentEnv, order, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(payment, attachInfo)
}
// CancelOrder
// @Title CancelOrder
// @Tag Order API
// @Description cancel an order
// @Param id query string true "The id ( owner/name ) of the order"
// @Success 200 {object} controllers.Response The Response object
// @router /cancel-order [post]
func (c *ApiController) CancelOrder() {
id := c.Ctx.Input.Query("id")
order, err := object.GetOrder(id)
if err != nil {
c.ResponseError(err.Error())
return
}
if order == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The order: %s does not exist"), id))
return
}
userId := c.GetSessionUsername()
orderUserId := util.GetId(order.Owner, order.User)
if userId != orderUserId && !c.IsAdmin() {
c.ResponseError(c.T("auth:Unauthorized operation"))
return
}
c.Data["json"] = wrapActionResponse(object.CancelOrder(order))
c.ServeJSON()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ import (
"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"
)
@@ -32,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)
@@ -56,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())
@@ -75,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 {
@@ -101,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)
@@ -153,19 +153,24 @@ func (c *ApiController) DeleteProduct() {
}
// BuyProduct
// @Title BuyProduct
// @Title BuyProduct (Deprecated)
// @Tag Product API
// @Description buy product
// @Param id query string true "The id ( owner/name ) of the product"
// @Param providerName query string true "The name of the provider"
// @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.Input().Get("id")
id := c.Ctx.Input.Query("id")
host := c.Ctx.Request.Host
providerName := c.Input().Get("providerName")
paymentEnv := c.Input().Get("paymentEnv")
customPriceStr := c.Input().Get("customPrice")
providerName := c.Ctx.Input.Query("providerName")
paymentEnv := c.Ctx.Input.Query("paymentEnv")
customPriceStr := c.Ctx.Input.Query("customPrice")
if customPriceStr == "" {
customPriceStr = "0"
}
@@ -176,15 +181,16 @@ func (c *ApiController) BuyProduct() {
return
}
// buy `pricingName/planName` for `paidUserName`
pricingName := c.Input().Get("pricingName")
planName := c.Input().Get("planName")
paidUserName := c.Input().Get("userName")
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)
@@ -212,7 +218,7 @@ func (c *ApiController) BuyProduct() {
return
}
payment, attachInfo, err := object.BuyProduct(id, user, providerName, pricingName, planName, host, paymentEnv, customPrice)
payment, attachInfo, err := object.BuyProduct(id, user, providerName, pricingName, planName, host, paymentEnv, customPrice, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {
@@ -113,6 +113,27 @@ func (c *ApiController) GetTransaction() {
return
}
if transaction == nil {
c.ResponseOk(nil)
return
}
// Check if non-admin user is trying to access someone else's transaction
if !c.IsAdmin() {
user := c.GetSessionUsername()
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
if userErr != nil {
c.ResponseError(userErr.Error())
return
}
// Only allow users to view their own transactions
if transaction.User != userName {
c.ResponseError(c.T("auth:Unauthorized operation"))
return
}
}
c.ResponseOk(transaction)
}
@@ -125,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)
@@ -143,6 +164,7 @@ func (c *ApiController) UpdateTransaction() {
// @Tag Transaction API
// @Description add transaction
// @Param body body object.Transaction true "The details of the transaction"
// @Param dryRun query string false "Dry run mode: set to 'true' or '1' to validate without committing"
// @Success 200 {object} controllers.Response The Response object
// @router /add-transaction [post]
func (c *ApiController) AddTransaction() {
@@ -153,7 +175,10 @@ func (c *ApiController) AddTransaction() {
return
}
affected, transactionId, err := object.AddTransaction(&transaction, c.GetAcceptLanguage())
dryRunParam := c.Ctx.Input.Query("dryRun")
dryRun := dryRunParam != ""
affected, transactionId, err := object.AddTransaction(&transaction, c.GetAcceptLanguage(), dryRun)
if err != nil {
c.ResponseError(err.Error())
return

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package deployment

20
docker-compose.dev.yml Normal file
View 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:

View File

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

View File

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

View File

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

View File

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

View File

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

256
go.mod
View File

@@ -1,92 +1,120 @@
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.1.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.10.0
github.com/stretchr/testify v1.11.1
github.com/stripe/stripe-go/v74 v74.29.0
github.com/tealeg/xlsx v1.0.5
github.com/thanhpk/randstr v1.0.4
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
@@ -97,37 +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
@@ -135,25 +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
@@ -161,94 +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.12 // 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.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mattn/go-isatty v0.0.20 // 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
)

1628
go.sum

File diff suppressed because it is too large Load Diff

140
i18n/deduplicate_test.go Normal file
View 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()
}
}

View File

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

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package i18n
@@ -23,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)
}

View File

@@ -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 أولاً"
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More