Compare commits

...

260 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
406 changed files with 39848 additions and 6953 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,13 +36,13 @@ jobs:
frontend:
name: Front-end
runs-on: ubuntu-latest
needs: [ go-tests ]
needs: [go-tests]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
cache: 'yarn'
cache: "yarn"
cache-dependency-path: ./web/yarn.lock
- run: yarn install && CI=false yarn run build
working-directory: ./web
@@ -54,12 +56,12 @@ jobs:
backend:
name: Back-end
runs-on: ubuntu-latest
needs: [ go-tests ]
needs: [go-tests]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.23'
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: ./go.mod
- run: go version
- name: Build
@@ -70,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
@@ -104,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 &
@@ -129,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
@@ -137,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
@@ -159,7 +162,7 @@ jobs:
contents: write
issues: write
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push'
needs: [ frontend, backend, linter, e2e ]
needs: [frontend, backend, linter, e2e]
outputs:
new-release-published: ${{ steps.semantic.outputs.new_release_published }}
new-release-version: ${{ steps.semantic.outputs.new_release_version }}
@@ -180,13 +183,18 @@ jobs:
contents: write
issues: write
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && needs.tag-release.outputs.new-release-published == 'true'
needs: [ tag-release ]
needs: [tag-release]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: ./go.mod
- name: Free disk space
uses: jlumbroso/free-disk-space@v1.3.1
with:
@@ -213,7 +221,7 @@ jobs:
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: '~> v2'
version: "~> v2"
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -225,7 +233,7 @@ jobs:
contents: write
issues: write
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && needs.tag-release.outputs.new-release-published == 'true'
needs: [ tag-release ]
needs: [tag-release]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -295,7 +303,7 @@ jobs:
if: steps.should_push.outputs.push=='true'
with:
repository: casdoor/casdoor-helm
ref: 'master'
ref: "master"
token: ${{ secrets.GH_BOT_TOKEN }}
- name: Update Helm Chart

1
.gitignore vendored
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$

View File

@@ -1,4 +1,4 @@
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
@@ -9,7 +9,7 @@ RUN yarn install --frozen-lockfile --network-timeout 1000000
COPY ./web .
RUN NODE_OPTIONS="--max-old-space-size=4096" yarn run build
FROM --platform=$BUILDPLATFORM golang:1.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
@@ -19,7 +19,6 @@ RUN go mod download
# Copy source files
COPY . .
RUN go test -v -run TestGetVersionInfo ./util/system_test.go ./util/system.go ./util/variable.go
RUN ./build.sh
FROM alpine:latest AS STANDARD
@@ -51,22 +50,14 @@ 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

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

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,6 +68,8 @@ 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, *, *
@@ -83,12 +86,19 @@ 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, *, *
@@ -116,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)
@@ -168,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,6 +15,7 @@
package conf
import (
_ "embed"
"fmt"
"os"
"runtime"
@@ -24,6 +25,9 @@ import (
"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"}

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

@@ -21,6 +21,7 @@ import (
"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"
@@ -312,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)
}
@@ -339,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(context.Background()))
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
@@ -382,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
}
@@ -392,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(context.Background()))
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()
@@ -449,6 +478,10 @@ func (c *ApiController) SsoLogout() {
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)
@@ -528,6 +561,9 @@ func (c *ApiController) SsoLogout() {
}
}
// Propagate logout to external Custom OAuth2 providers
object.InvokeCustomProviderLogout(ssoApplication, ssoSessionToken)
c.ResponseOk()
}
@@ -539,6 +575,11 @@ 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
@@ -665,6 +706,51 @@ func (c *ApiController) GetCaptcha() {
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())
@@ -698,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
}

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

@@ -37,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"
)
@@ -161,12 +160,26 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
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
@@ -184,10 +197,15 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
} else {
scope := c.Ctx.Input.Query("scope")
nonce := c.Ctx.Input.Query("nonce")
token, _ := object.GetTokenByUser(application, user, scope, nonce, c.Ctx.Request.Host)
resp = tokenToResponse(token)
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)
@@ -437,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
@@ -738,7 +805,11 @@ 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)
@@ -784,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"))
}
}
}
@@ -824,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 {
@@ -867,10 +912,16 @@ func (c *ApiController) Login() {
return
}
if application.IsSignupItemRequired("Invitation code") {
c.ResponseError(c.T("check:Invitation code cannot be blank"))
// Check and validate invitation code
invitation, msg := object.CheckInvitationCode(application, organization, &authForm, c.GetAcceptLanguage())
if msg != "" {
c.ResponseError(msg)
return
}
invitationName := ""
if invitation != nil {
invitationName = invitation.Name
}
// Handle UseEmailAsUsername for OAuth and Web3
if organization.UseEmailAsUsername && userInfo.Email != "" {
@@ -886,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])
}
@@ -937,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
@@ -956,6 +1007,16 @@ func (c *ApiController) Login() {
c.ResponseError(fmt.Sprintf(c.T("auth:Failed to create user, user information is invalid: %s"), util.StructToJson(user)))
return
}
// Increment invitation usage count
if invitation != nil {
invitation.UsedCount += 1
_, err = object.UpdateInvitation(invitation.GetId(), invitation, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
}
}
// sync info from 3rd-party if possible
@@ -1345,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)
}

View File

@@ -21,6 +21,7 @@ import (
"github.com/beego/beego/v2/core/logs"
"github.com/beego/beego/v2/server/web"
"github.com/casdoor/casdoor/mcpself"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
@@ -291,3 +292,14 @@ func (c *ApiController) Finish() {
}
c.Controller.Finish()
}
func (c *ApiController) McpResponseError(id interface{}, code int, message string, data interface{}) {
resp := mcpself.BuildMcpResponse(id, nil, &mcpself.McpError{
Code: code,
Message: message,
Data: data,
})
c.Ctx.Output.Header("Content-Type", "application/json")
c.Data["json"] = resp
c.ServeJSON()
}

View File

@@ -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 {
@@ -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,6 +306,13 @@ 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.Ctx.Input.Query("userId")
if userId == "" {
@@ -322,6 +332,13 @@ 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.Ctx.Input.Query("userId")
if userId == "" {
@@ -341,6 +358,13 @@ 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.Ctx.Input.Query("userId")
if userId == "" {

View File

@@ -26,6 +26,8 @@ import (
"strings"
"sync"
"time"
"github.com/casdoor/casdoor/conf"
)
type CLIVersionInfo struct {
@@ -164,6 +166,11 @@ 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

View File

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

@@ -16,6 +16,7 @@ import (
"time"
"github.com/beego/beego/v2/server/web"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/util"
)
@@ -446,8 +447,8 @@ func downloadCLI() error {
// @Success 200 {object} controllers.Response The Response object
// @router /refresh-engines [post]
func (c *ApiController) RefreshEngines() {
if !web.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
}

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

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

@@ -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
}
@@ -230,7 +230,7 @@ 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
}

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

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
@@ -77,7 +76,7 @@ func (c *ApiController) MfaSetupInitiate() {
return
}
recoveryCode := uuid.NewString()
recoveryCode := util.GenerateUUID()
mfaProps.RecoveryCodes = []string{recoveryCode}
mfaProps.MfaRememberInHours = organization.MfaRememberInHours

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

@@ -16,6 +16,8 @@ package controllers
import (
"encoding/json"
"fmt"
"strconv"
"github.com/beego/beego/v2/core/utils/pagination"
"github.com/casdoor/casdoor/object"
@@ -149,3 +151,78 @@ func (c *ApiController) DeleteProduct() {
c.Data["json"] = wrapActionResponse(object.DeleteProduct(&product))
c.ServeJSON()
}
// BuyProduct
// @Title BuyProduct (Deprecated)
// @Tag Product API
// @Description buy product using the deprecated compatibility endpoint, prefer place-order plus pay-order for new integrations
// @Param id query string true "The id ( owner/name ) of the product"
// @Param providerName query string true "The name of the provider"
// @Param pricingName query string false "The name of the pricing (for subscription)"
// @Param planName query string false "The name of the plan (for subscription)"
// @Param userName query string false "The username to buy product for (admin only)"
// @Param paymentEnv query string false "The payment environment"
// @Param customPrice query number false "Custom price for recharge products"
// @Success 200 {object} controllers.Response The Response object
// @router /buy-product [post]
func (c *ApiController) BuyProduct() {
id := c.Ctx.Input.Query("id")
host := c.Ctx.Request.Host
providerName := c.Ctx.Input.Query("providerName")
paymentEnv := c.Ctx.Input.Query("paymentEnv")
customPriceStr := c.Ctx.Input.Query("customPrice")
if customPriceStr == "" {
customPriceStr = "0"
}
customPrice, err := strconv.ParseFloat(customPriceStr, 64)
if err != nil {
c.ResponseError(err.Error())
return
}
pricingName := c.Ctx.Input.Query("pricingName")
planName := c.Ctx.Input.Query("planName")
paidUserName := c.Ctx.Input.Query("userName")
owner, _, err := util.GetOwnerAndNameFromIdWithError(id)
if err != nil {
c.ResponseError(err.Error())
return
}
var userId string
if paidUserName != "" {
userId = util.GetId(owner, paidUserName)
if userId != c.GetSessionUsername() && !c.IsAdmin() && userId != c.GetPaidUsername() {
c.ResponseError(c.T("general:Only admin user can specify user"))
return
}
c.SetSession("paidUsername", "")
} else {
userId = c.GetSessionUsername()
}
if userId == "" {
c.ResponseError(c.T("general:Please login first"))
return
}
user, err := object.GetUser(userId)
if err != nil {
c.ResponseError(err.Error())
return
}
if user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
return
}
payment, attachInfo, err := object.BuyProduct(id, user, providerName, pricingName, planName, host, paymentEnv, customPrice, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(payment, attachInfo)
}

View File

@@ -17,8 +17,6 @@ package controllers
import (
"encoding/json"
"github.com/casvisor/casvisor-go-sdk/casvisorsdk"
"github.com/beego/beego/v2/core/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
@@ -59,7 +57,7 @@ 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())
@@ -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())

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
}

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

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,6 +16,7 @@ package controllers
import (
"encoding/json"
"fmt"
"github.com/beego/beego/v2/core/utils/pagination"
"github.com/casdoor/casdoor/object"
@@ -150,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

@@ -162,6 +162,9 @@ func (c *ApiController) DeleteToken() {
func (c *ApiController) GetOAuthToken() {
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")
@@ -173,6 +176,10 @@ func (c *ApiController) GetOAuthToken() {
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
@@ -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"
@@ -343,24 +445,9 @@ func (c *ApiController) ResponseTokenError(errorMsg string) {
// @router /login/oauth/introspect [post]
func (c *ApiController) IntrospectToken() {
tokenValue := c.Ctx.Input.Query("token")
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
}
}
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
}
@@ -374,7 +461,7 @@ func (c *ApiController) IntrospectToken() {
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

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

@@ -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")
@@ -942,7 +919,7 @@ func (c *ApiController) VerifyIdentification() {
}
if provider == nil {
c.ResponseError(fmt.Sprintf(c.T("provider:The provider: %s does not exist"), providerName))
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s does not exist"), providerName))
return
}

View File

@@ -15,6 +15,7 @@
package controllers
import (
"errors"
"fmt"
"strings"
@@ -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

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

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

@@ -137,3 +137,29 @@ func (c *RootController) GetWebFingerByApplication() {
c.Ctx.Output.ContentType("application/jrd+json")
c.ServeJSON()
}
// GetOAuthServerMetadata
// @Title GetOAuthServerMetadata
// @Tag OAuth API
// @Description Get OAuth 2.0 Authorization Server Metadata (RFC 8414)
// @Success 200 {object} object.OidcDiscovery
// @router /.well-known/oauth-authorization-server [get]
func (c *RootController) GetOAuthServerMetadata() {
host := c.Ctx.Request.Host
c.Data["json"] = object.GetOidcDiscovery(host, "")
c.ServeJSON()
}
// GetOAuthServerMetadataByApplication
// @Title GetOAuthServerMetadataByApplication
// @Tag OAuth API
// @Description Get OAuth 2.0 Authorization Server Metadata for specific application (RFC 8414)
// @Param application path string true "application name"
// @Success 200 {object} object.OidcDiscovery
// @router /.well-known/:application/oauth-authorization-server [get]
func (c *RootController) GetOAuthServerMetadataByApplication() {
application := c.Ctx.Input.Param(":application")
host := c.Ctx.Request.Host
c.Data["json"] = object.GetOidcDiscovery(host, application)
c.ServeJSON()
}

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

121
go.mod
View File

@@ -1,6 +1,8 @@
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
@@ -14,26 +16,29 @@ require (
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/aws/aws-sdk-go-v2/service/s3 v1.95.0
github.com/beego/beego/v2 v2.3.8
github.com/beevik/etree v1.1.0
github.com/casbin/casbin/v2 v2.77.2
github.com/casbin/lego/v4 v4.5.4
github.com/casdoor/casdoor-go-sdk v0.50.0
github.com/casdoor/go-sms-sender v0.25.0
github.com/casdoor/gomail/v2 v2.2.0
github.com/casdoor/ldapserver v1.2.0
github.com/casdoor/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.16.3
github.com/go-jose/go-jose/v4 v4.1.2
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
@@ -41,20 +46,27 @@ require (
github.com/go-sql-driver/mysql v1.8.1
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/go-webauthn/webauthn v0.10.2
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/hsluoyz/modsecurity-go v0.0.7
github.com/jcmturner/gokrb5/v8 v8.4.4
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.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.19.0
github.com/prometheus/client_model v0.6.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
@@ -68,10 +80,12 @@ require (
github.com/xorm-io/builder v0.3.13
github.com/xorm-io/core v0.7.4
github.com/xorm-io/xorm v1.1.6
golang.org/x/crypto v0.40.0
golang.org/x/net v0.41.0
golang.org/x/oauth2 v0.27.0
golang.org/x/text v0.27.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.22.1
@@ -79,11 +93,11 @@ require (
)
require (
cel.dev/expr v0.18.0 // 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.6.0 // 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
@@ -93,7 +107,7 @@ require (
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.25.0 // 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
@@ -111,10 +125,8 @@ 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/apistd/uni-go-sdk v0.0.2 // indirect
github.com/atc0005/go-teams-notify/v2 v2.13.0 // indirect
github.com/aws/aws-sdk-go v1.45.5 // 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
@@ -122,16 +134,16 @@ require (
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/casdoor/casdoor-go-sdk v0.50.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // 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.4.1 // indirect
github.com/davecgh/go-spew v1.1.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
@@ -141,8 +153,8 @@ require (
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 // indirect
github.com/ebitengine/purego v0.9.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/go-control-plane v0.13.1 // indirect
github.com/envoyproxy/protoc-gen-validate v1.1.0 // 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
@@ -150,7 +162,7 @@ require (
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // 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.2 // 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
@@ -161,21 +173,30 @@ require (
github.com/go-webauthn/x v0.1.9 // 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-20241129210726-2c02b8208cf8 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-tpm v0.9.0 // indirect
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
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
@@ -187,41 +208,51 @@ require (
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/likexian/gokit v0.25.13 // indirect
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magefile/mage v1.15.1-0.20241126214340-bdc92f694516 // indirect
github.com/markbates/going v1.0.0 // indirect
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-ieproxy v0.0.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/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.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.0 // 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.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.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
@@ -237,40 +268,43 @@ require (
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/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/contrib/detectors/gcp v1.32.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/otel/metric v1.32.0 // indirect
go.opentelemetry.io/otel/sdk v1.32.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.32.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-20241215155358-4a5509556b9e // indirect
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.34.0 // 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-20241209162323-e6fa225c2576 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
google.golang.org/grpc v1.68.0 // indirect
google.golang.org/grpc/stats/opentelemetry v0.0.0-20241028142157-ada6787961b3 // indirect
google.golang.org/protobuf v1.36.1 // 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.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
@@ -282,4 +316,5 @@ require (
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
)

255
go.sum
View File

@@ -1,5 +1,5 @@
cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo=
cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -186,8 +186,8 @@ cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZ
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY=
cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck=
cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w=
@@ -627,6 +627,16 @@ gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGq
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk=
github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58=
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
@@ -642,12 +652,14 @@ github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUM
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 h1:o90wcURuxekmXrtxmYWTyNla0+ZEHhud6DI1ZTxd1vI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0/go.mod h1:6fTWu4m3jocfUZLYF5KsZC1TUfRvEjs7lM4crme/irw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.49.0 h1:jJKWl98inONJAr/IZrdFQUWcwUO95DLY1XMD1ZIut+g=
@@ -766,8 +778,9 @@ github.com/alibabacloud-go/tea-xml v1.1.1/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCE
github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/aliyun/alibaba-cloud-sdk-go v1.62.545 h1:0LfzeUr4quwrrrTHn1kfLA0FBdsChCMs8eK2EzOwXVQ=
github.com/aliyun/alibaba-cloud-sdk-go v1.62.545/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1183/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible h1:9gWa46nstkJ9miBReJcN8Gq34cBFbzSpQZVVT9N09TM=
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
@@ -838,6 +851,8 @@ github.com/casbin/casbin/v2 v2.28.3/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRt
github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/casbin/casbin/v2 v2.77.2 h1:yQinn/w9x8AswiwqwtrXz93VU48R1aYTXdHEx4RI3jM=
github.com/casbin/casbin/v2 v2.77.2/go.mod h1:mzGx0hYW9/ksOSpw3wNjk3NRAroq5VMFYUQ6G43iGPk=
github.com/casbin/lego/v4 v4.5.4 h1:WdVEj1A5KmKZheNuFNLF/5+UUkpXLt9mEOrLX3E81Vo=
github.com/casbin/lego/v4 v4.5.4/go.mod h1:JjTyJgN5pyrDPcg3+aAM1NtFQIXl8zDgsoSS1TnVpJ8=
github.com/casdoor/casdoor-go-sdk v0.50.0 h1:bUYbz/MzJuWfLKJbJM0+U0YpYewAur+THp5TKnufWZM=
github.com/casdoor/casdoor-go-sdk v0.50.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
github.com/casdoor/go-sms-sender v0.25.0 h1:eF4cOCSbjVg7+0uLlJQnna/FQ0BWW+Fp/x4cXhzQu1Y=
@@ -852,15 +867,12 @@ github.com/casdoor/oss v1.8.0 h1:uuyKhDIp7ydOtV4lpqhAY23Ban2Ln8La8+QT36CwylM=
github.com/casdoor/oss v1.8.0/go.mod h1:uaqO7KBI2lnZcnB8rF7O6C2bN7llIbfC5Ql8ex1yR1U=
github.com/casdoor/xorm-adapter/v3 v3.1.0 h1:NodWayRtSLVSeCvL9H3Hc61k0G17KhV9IymTCNfh3kk=
github.com/casdoor/xorm-adapter/v3 v3.1.0/go.mod h1:4WTcUw+bTgBylGHeGHzTtBvuTXRS23dtwzFLl9tsgFM=
github.com/casvisor/casvisor-go-sdk v1.4.0 h1:hbZEGGJ1cwdHFAxeXrMoNw6yha6Oyg2F0qQhBNCN/dg=
github.com/casvisor/casvisor-go-sdk v1.4.0/go.mod h1:frnNtH5GA0wxzAQLyZxxfL0RSsSub9GQPi2Ybe86ocE=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -893,12 +905,20 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc h1:OlJhrgI3I+FLUCTI3JJW8MoqyM78WbqJjecqMnqG+wc=
github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc/go.mod h1:7rsocqNDkTCira5T0M7buoKR2ehh7YZiPkzxRuAgvVU=
github.com/corazawaf/coraza/v3 v3.3.3 h1:kqjStHAgWqwP5dh7n0vhTOF0a3t+VikNS/EaMiG0Fhk=
github.com/corazawaf/coraza/v3 v3.3.3/go.mod h1:xSaXWOhFMSbrV8qOOfBKAyw3aOqfwaSaOy5BgSF8XlA=
github.com/corazawaf/libinjection-go v0.2.2 h1:Chzodvb6+NXh6wew5/yhD0Ggioif9ACrQGR4qjTCs1g=
github.com/corazawaf/libinjection-go v0.2.2/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb h1:7X9nrm+LNWdxzQOiCjy0G51rNUxbH35IDHCjAMvogyM=
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb/go.mod h1:RfQ9wji3fjcSEsQ+uFCtIh3+BXgcZum8Kt3JxvzYzlk=
@@ -908,8 +928,9 @@ github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs
github.com/cznic/sortutil v0.0.0-20181122101858-f5f958428db8/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ=
github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M=
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY=
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
@@ -917,7 +938,6 @@ github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPc
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/denisenkom/go-mssqldb v0.9.0 h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D0yGjAk=
github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dghubble/oauth1 v0.7.3 h1:EkEM/zMDMp3zOsX2DC/ZQ2vnEX3ELK0/l9kb+vs4ptE=
github.com/dghubble/oauth1 v0.7.3/go.mod h1:oxTe+az9NSMIucDPDCCtzJGsPhciJV33xocHfcR2sVY=
@@ -960,14 +980,18 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34=
github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI=
github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE=
github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
@@ -979,6 +1003,8 @@ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzP
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
@@ -1012,8 +1038,8 @@ github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lo
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs=
@@ -1030,8 +1056,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-mysql-org/go-mysql v1.7.0 h1:qE5FTRb3ZeTQmlk3pjE+/m2ravGxxRDrVDTyDe9tvqI=
@@ -1088,10 +1114,13 @@ github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -1163,6 +1192,8 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -1236,7 +1267,9 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1 h1:LqbZZ9sNMWVjeXS4NN5oVvhMjDyLhmA1LG86oSo+IqY=
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
@@ -1247,6 +1280,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -1268,6 +1303,8 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
@@ -1277,6 +1314,8 @@ github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hsluoyz/modsecurity-go v0.0.7 h1:W5ChaDrm4kM/UhHxoD2zyxQ+6s5kSj6cVftDFgdFzBM=
github.com/hsluoyz/modsecurity-go v0.0.7/go.mod h1:hi81ySzwvlQFd5pip9c3uwXHDAW9ayxwLbt8ufxRkdY=
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -1286,11 +1325,21 @@ github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da h1:FjHUJJ7oBW4G/9
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jcchavezs/mergefs v0.1.0 h1:7oteO7Ocl/fnfFMkoVLJxTveCjrsd//UB0j89xmnpec=
github.com/jcchavezs/mergefs v0.1.0/go.mod h1:eRLTrsA+vFwQZ48hj8p8gki/5v9C2bFtHH5Mnn4bcGk=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko=
github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=
@@ -1310,6 +1359,7 @@ github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -1352,6 +1402,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
@@ -1376,6 +1428,12 @@ github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/likexian/gokit v0.25.13 h1:p2Uw3+6fGG53CwdU2Dz0T6bOycdb2+bAFAa3ymwWVkM=
github.com/likexian/gokit v0.25.13/go.mod h1:qQhEWFBEfqLCO3/vOEo2EDKd+EycekVtUK4tex+l2H4=
github.com/likexian/whois v1.15.1 h1:6vTMI8n9s1eJdmcO4R9h1x99aQWIZZX1CD3am68gApU=
github.com/likexian/whois v1.15.1/go.mod h1:/nxmQ6YXvLz+qTxC/QFtEJNAt0zLuRxJrKiWpBJX8X0=
github.com/likexian/whois-parser v1.24.9 h1:BT6fzO3lj3F07yzVv0YXoaj+K4Ush0/cF+Yp6tvJJgk=
github.com/likexian/whois-parser v1.24.9/go.mod h1:b6STMHHDaSKbd4PzGrP50wWE5NzeBUETa/hT9gI0G9I=
github.com/line/line-bot-sdk-go v7.8.0+incompatible h1:Uf9/OxV0zCVfqyvwZPH8CrdiHXXmMRa/L91G3btQblQ=
github.com/line/line-bot-sdk-go v7.8.0+incompatible/go.mod h1:0RjLjJEAU/3GIcHkC3av6O4jInAbt25nnZVmOFUgDBg=
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q=
@@ -1387,6 +1445,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2
github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
github.com/magefile/mage v1.15.1-0.20241126214340-bdc92f694516 h1:aAO0L0ulox6m/CLRYvJff+jWXYYCKGpEm3os7dM/Z+M=
github.com/magefile/mage v1.15.1-0.20241126214340-bdc92f694516/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/markbates/going v1.0.0 h1:DQw0ZP7NbNlFGcKbcE/IVSOAFzScxRtLpd0rLMzLhq0=
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
github.com/markbates/goth v1.82.0 h1:8j/c34AjBSTNzO7zTsOyP5IYCQCMBTRBHAbBt/PI0bQ=
@@ -1416,9 +1476,13 @@ github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4
github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microsoft/go-mssqldb v1.9.0 h1:5Vq+u2f4LDujJNeZn62Z4kBDEC9MjLv0ukRzOuEuvdA=
github.com/microsoft/go-mssqldb v1.9.0/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/mileusna/viber v1.0.1 h1:gWB6/lKoWYVxkH0Jb8jRnGIRZ/9DEM7RBZRJHRfdYWs=
github.com/mileusna/viber v1.0.1/go.mod h1:Pxu/iPMnYjnHgu+bEp3SiKWHWmlf/kDp/yOX8XUdYrQ=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
@@ -1436,6 +1500,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modelcontextprotocol/go-sdk v1.4.0 h1:u0kr8lbJc1oBcawK7Df+/ajNMpIDFE41OEPxdeTLOn8=
github.com/modelcontextprotocol/go-sdk v1.4.0/go.mod h1:Nxc2n+n/GdCebUaqCOhTetptS17SXXNu9IfNTaLDi1E=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -1481,6 +1547,8 @@ github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM=
github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 h1:1Kw2vDBXmjop+LclnzCb/fFy+sgb3gYARwfmoUcQe6o=
github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
@@ -1498,6 +1566,8 @@ github.com/pingcap/tidb/parser v0.0.0-20221126021158-6b02a5d8ba7d h1:1DyyRrgYeNj
github.com/pingcap/tidb/parser v0.0.0-20221126021158-6b02a5d8ba7d/go.mod h1:ElJiub4lRy6UZDb+0JHDkGEdr6aOli+ykhyej7VCLoI=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1-0.20161029093637-248dadf4e906/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -1509,8 +1579,9 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/polarsource/polar-go v0.12.0 h1:um+6ftOPUMg2TQq9Kv/6fKGBOAl7dOc2YiDdx4Bb0y8=
github.com/polarsource/polar-go v0.12.0/go.mod h1:FB11Q4m2n3wIk6l/POOkz0MVOUx1o0Yt4Y97MnQfe0c=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@@ -1531,8 +1602,8 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
@@ -1554,12 +1625,16 @@ github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdk
github.com/qiniu/go-sdk/v7 v7.12.1 h1:FZG5dhs2MZBV/mHVhmHnsgsQ+j1gSE0RqIoA2WwEDwY=
github.com/qiniu/go-sdk/v7 v7.12.1/go.mod h1:btsaOc8CA3hdVloULfFdDgDc+g4f3TDZEFsDY0BLE+w=
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 h1:dq90+d51/hQRaHEqRAsQ1rE/pC1GUS4sc2rCbbFsAIY=
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.5.5 h1:51VEyMF8eOO+NUHFm8fpg+IOc1xFuFOhxs3R+kPu1FM=
github.com/redis/go-redis/v9 v9.5.5/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/resend/resend-go/v3 v3.1.0 h1:bJpU5gYCDcczLdhCo37oy9mOmdtSVlOzM6IfWX9zhMw=
github.com/resend/resend-go/v3 v3.1.0/go.mod h1:iI7VA0NoGjWvsNii5iNC5Dy0llsI3HncXPejhniYzwE=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
@@ -1577,6 +1652,7 @@ github.com/russellhaering/gosaml2 v0.9.0 h1:CNMnH42z/GirrKjdmNrSS6bAAs47F9bPdl4P
github.com/russellhaering/gosaml2 v0.9.0/go.mod h1:byViER/1YPUa0Puj9ROZblpoq2jsE7h/CJmitzX0geU=
github.com/russellhaering/goxmldsig v1.2.0 h1:Y6GTTc9Un5hCxSzVz4UIWQ/zuVwDvzJk80guqzwx6Vg=
github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
@@ -1586,6 +1662,10 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/scim2/filter-parser/v2 v2.2.0 h1:QGadEcsmypxg8gYChRSM2j1edLyE/2j72j+hdmI4BJM=
github.com/scim2/filter-parser/v2 v2.2.0/go.mod h1:jWnkDToqX/Y0ugz0P5VvpVEUKcWcyHHj+X+je9ce5JA=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBUpXyTcTXy/Rl9N9aZ/Qw=
@@ -1598,6 +1678,7 @@ github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaK
github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 h1:xT+JlYxNGqyT+XcU8iUrN18JYed2TvG9yN5ULG2jATM=
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
@@ -1618,7 +1699,9 @@ github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQ
github.com/slack-go/slack v0.15.0 h1:LE2lj2y9vqqiOf+qIIy0GvEoxgF1N5yLGZffmEZykt0=
github.com/slack-go/slack v0.15.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sony/sonyflake v1.0.0 h1:MpU6Ro7tfXwgn2l5eluf9xQvQJDROTBImNCfRXn/YeM=
@@ -1627,6 +1710,8 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/spyzhov/ajson v0.8.0 h1:sFXyMbi4Y/BKjrsfkUZHSjA2JM1184enheSjjoT/zCc=
github.com/spyzhov/ajson v0.8.0/go.mod h1:63V+CGM6f1Bu/p4nLIN8885ojBdt88TbLoSFzyqMuVA=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
@@ -1635,6 +1720,7 @@ github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5J
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
@@ -1678,6 +1764,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/timtadh/data-structures v0.5.3/go.mod h1:9R4XODhJ8JdWFEI8P/HJKqxuJctfBQw6fDibMQny2oU=
github.com/timtadh/lexmachine v0.2.2/go.mod h1:GBJvD5OAfRn/gnp92zb9KTgHLB7akKyxmVivoYCcjQI=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
@@ -1694,9 +1782,13 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ucloud/ucloud-sdk-go v0.22.5 h1:GIltVwMDUqQj4iPL/emsZAMhEYWjLTwZqpOxdkdDrM8=
github.com/ucloud/ucloud-sdk-go v0.22.5/go.mod h1:dyLmFHmUfgb4RZKYQP9IArlvQ2pxzFthfhwxRzOEPIw=
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/utahta/go-linenotify v0.5.0 h1:E1tJaB/XhqRY/iz203FD0MaHm10DjQPOq5/Mem2A3Gs=
github.com/utahta/go-linenotify v0.5.0/go.mod h1:KsvBXil2wx+ByaCR0e+IZKTbp4pDesc7yjzRigLf6pE=
github.com/valllabh/ocsf-schema-golang v1.0.3 h1:eR8k/3jP/OOqB8LRCtdJ4U+vlgd/gk5y3KMXoodrsrw=
github.com/valllabh/ocsf-schema-golang v1.0.3/go.mod h1:sZ3as9xqm1SSK5feFWIR2CuGeGRhsM7TR1MbpBctzPk=
github.com/volcengine/volc-sdk-golang v1.0.117 h1:ykFVSwsVq9qvIoWP9jeP+VKNAUjrblAdsZl46yVWiH8=
github.com/volcengine/volc-sdk-golang v1.0.117/go.mod h1:ojXSFvj404o2UKnZR9k9LUUWIUU+9XtlRlzk2+UFc/M=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
@@ -1712,6 +1804,8 @@ github.com/xorm-io/core v0.7.4 h1:qIznlqqmYNEb03ewzRXCrNkbbxpkgc/44nVF8yoFV7Y=
github.com/xorm-io/core v0.7.4/go.mod h1:GueyhafDnkB0KK0fXX/dEhr/P1EAGW0GLmoNDUEE1Mo=
github.com/xorm-io/xorm v1.1.6 h1:s4fDpUXJx8Zr/PBovXNaadn+v1P3h/U3iV4OxAkWS8s=
github.com/xorm-io/xorm v1.1.6/go.mod h1:7nsSUdmgLIcqHSSaKOzbVQiZtzIzbpGf1GGSYp6DD70=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -1740,27 +1834,31 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/detectors/gcp v1.32.0 h1:P78qWqkLSShicHmAzfECaTgvslqHxblNE9j62Ws1NK8=
go.opentelemetry.io/contrib/detectors/gcp v1.32.0/go.mod h1:TVqo0Sda4Cv8gCIixd7LuLwW4EylumVWfhjZJjDD4DU=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0 h1:qtFISDHKolvIxzSs0gIaiPUPR0Cucb0F2coHC7ZLdps=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0/go.mod h1:Y+Pop1Q6hCOnETWTW4NROK/q1hv50hM7yDaUTjG8lp8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.40.0 h1:Awaf8gmW99tZTOWqkLCOl6aw1/rxAWVlHsHIZ3fT2sA=
go.opentelemetry.io/contrib/detectors/gcp v1.40.0/go.mod h1:99OY9ZCqyLkzJLTh5XhECpLRSxcZl+ZDKBEO+jMBFR4=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
@@ -1809,6 +1907,7 @@ golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
@@ -1823,8 +1922,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20181106170214-d68db9428509/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1855,8 +1954,9 @@ golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+o
golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4=
golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1889,8 +1989,8 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20171115151908-9dfe39835686/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1978,8 +2078,8 @@ golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -2011,8 +2111,8 @@ golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20171101214715-fd80eb99c8f6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -2035,8 +2135,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -2162,8 +2262,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -2187,8 +2287,8 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -2213,8 +2313,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -2297,8 +2397,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -2311,6 +2411,8 @@ gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJ
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
@@ -2527,15 +2629,15 @@ google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -2577,11 +2679,9 @@ google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/grpc/stats/opentelemetry v0.0.0-20241028142157-ada6787961b3 h1:hUfOButuEtpc0UvYiaYRbNwxVYr0mQQOWq6X8beJ9Gc=
google.golang.org/grpc/stats/opentelemetry v0.0.0-20241028142157-ada6787961b3/go.mod h1:jzYlkSMbKypzuu6xoAEijsNVo9ZeDF1u/zCfFgsx7jg=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -2600,8 +2700,8 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
@@ -2614,14 +2714,17 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
@@ -2637,6 +2740,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -2727,6 +2831,7 @@ modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfp
modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM=
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=

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

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "Konnte den Benutzer nicht hinzufügen",
"Get init score failed, error: %w": "Init-Score konnte nicht abgerufen werden, Fehler: %w",
"Please sign out first": "Bitte melden Sie sich zuerst ab",
"The application does not allow to sign up new account": "Die Anwendung erlaubt es nicht, sich für ein neues Konto anzumelden"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "Die Anmeldemethode: Anmeldung per E-Mail ist für die Anwendung nicht aktiviert",
"The login method: login with face is not enabled for the application": "Die Anmeldemethode: Anmeldung per Gesicht ist für die Anwendung nicht aktiviert",
"The login method: login with password is not enabled for the application": "Die Anmeldeart \"Anmeldung mit Passwort\" ist für die Anwendung nicht aktiviert",
"The order: %s does not exist": "Die Bestellung: %s existiert nicht",
"The organization: %s does not exist": "Die Organisation: %s existiert nicht",
"The organization: %s has disabled users to signin": "Die Organisation: %s hat die Anmeldung von Benutzern deaktiviert",
"The plan: %s does not exist": "Der Plan: %s existiert nicht",
@@ -48,7 +48,7 @@
"CIDR for IP: %s should not be empty": "CIDR für IP: %s darf nicht leer sein",
"Default code does not match the code's matching rules": "Standardcode entspricht nicht den Übereinstimmungsregeln des Codes",
"DisplayName cannot be blank": "Anzeigename kann nicht leer sein",
"DisplayName is not valid real name": "DisplayName ist kein gültiger Vorname",
"DisplayName is not valid real name": "Der Anzeigename ist kein gültiger echter Name",
"Email already exists": "E-Mail existiert bereits",
"Email cannot be empty": "E-Mail darf nicht leer sein",
"Email is invalid": "E-Mail ist ungültig",
@@ -57,11 +57,11 @@
"Face data mismatch": "Gesichtsdaten stimmen nicht überein",
"Failed to parse client IP: %s": "Fehler beim Parsen der Client-IP: %s",
"FirstName cannot be blank": "Vorname darf nicht leer sein",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Gastbenutzer müssen ihr Konto aktualisieren, indem sie einen Benutzernamen und ein Passwort festlegen, bevor sie sich direkt anmelden können",
"Invitation code cannot be blank": "Einladungscode darf nicht leer sein",
"Invitation code exhausted": "Einladungscode aufgebraucht",
"Invitation code is invalid": "Einladungscode ist ungültig",
"Invitation code suspended": "Einladungscode ausgesetzt",
"LDAP user name or password incorrect": "Ldap Benutzername oder Passwort falsch",
"LastName cannot be blank": "Nachname darf nicht leer sein",
"Multiple accounts with same uid, please check your ldap server": "Mehrere Konten mit derselben uid, bitte überprüfen Sie Ihren LDAP-Server",
"Organization does not exist": "Organisation existiert nicht",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Gruppen importieren fehlgeschlagen",
"Failed to import users": "Fehler beim Importieren von Benutzern",
"Insufficient balance: new balance %v would be below credit limit %v": "Unzureichendes Guthaben: neues Guthaben %v wäre unter dem Kreditlimit %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Unzureichendes Guthaben: neues Organisationsguthaben %v wäre unter dem Kreditlimit %v",
"Missing parameter": "Fehlender Parameter",
"Only admin user can specify user": "Nur Administrator kann Benutzer angeben",
"Please login first": "Bitte zuerst einloggen",
"The LDAP: %s does not exist": "Das LDAP: %s existiert nicht",
"The organization: %s should have one application at least": "Die Organisation: %s sollte mindestens eine Anwendung haben",
"The syncer: %s does not exist": "Der Synchronizer: %s existiert nicht",
"The user: %s doesn't exist": "Der Benutzer %s existiert nicht",
"The user: %s is not found": "Der Benutzer: %s wurde nicht gefunden",
"User is required for User category transaction": "Benutzer ist für Benutzer-Kategorie-Transaktionen erforderlich",
"Wrong userId": "Falsche Benutzer-ID",
"don't support captchaProvider: ": "Unterstütze captchaProvider nicht:",
"this operation is not allowed in demo mode": "Dieser Vorgang ist im Demo-Modus nicht erlaubt",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "Die Berechtigung: \"%s\" existiert nicht"
},
"product": {
"Product list cannot be empty": "Produktliste darf nicht leer sein"
},
"provider": {
"Failed to initialize ID Verification provider": "ID-Verifizierungsanbieter konnte nicht initialisiert werden",
"Invalid application id": "Ungültige Anwendungs-ID",
"No ID Verification provider configured": "Kein ID-Verifizierungsanbieter konfiguriert",
"Provider is not an ID Verification provider": "Anbieter ist kein ID-Verifizierungsanbieter",
"the provider: %s does not exist": "Der Anbieter %s existiert nicht"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Ungültige E-Mail-Empfänger: %s",
"Invalid phone receivers: %s": "Ungültige Telefonempfänger: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "Sitzungs-ID %s ist die aktuelle Sitzung und kann nicht gelöscht werden"
},
"storage": {
"The objectKey: %s is not allowed": "Der Objektschlüssel %s ist nicht erlaubt",
"The provider type: %s is not supported": "Der Anbieter-Typ %s wird nicht unterstützt"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Fehler"
},
"ticket": {
"Ticket not found": "Ticket nicht gefunden"
},
"token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s wird von dieser Anwendung nicht unterstützt",
"Invalid application or wrong clientSecret": "Ungültige Anwendung oder falsches clientSecret",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "Anzeigename darf nicht leer sein",
"ID card information and real name are required": "Personalausweisinformationen und vollständiger Name sind erforderlich",
"Identity verification failed": "Identitätsprüfung fehlgeschlagen",
"MFA email is enabled but email is empty": "MFA-E-Mail ist aktiviert, aber E-Mail ist leer",
"MFA phone is enabled but phone number is empty": "MFA-Telefon ist aktiviert, aber Telefonnummer ist leer",
"New password cannot contain blank space.": "Das neue Passwort darf keine Leerzeichen enthalten.",
"No application found for user": "Keine Anwendung für Benutzer gefunden",
"The new password must be different from your current password": "Das neue Passwort muss sich von Ihrem aktuellen Passwort unterscheiden",
"User is already verified": "Benutzer ist bereits verifiziert",
"the user's owner and name should not be empty": "Eigentümer und Name des Benutzers dürfen nicht leer sein"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "Ungültiger Captcha-Anbieter.",
"Phone number is invalid in your region %s": "Die Telefonnummer ist in Ihrer Region %s ungültig",
"The forgot password feature is disabled": "Die Funktion \"Passwort vergessen\" ist deaktiviert",
"The verification code has already been used!": "Der Verifizierungscode wurde bereits verwendet!",
"The verification code has not been sent yet!": "Der Verifizierungscode wurde noch nicht gesendet!",
"Turing test failed.": "Turing-Test fehlgeschlagen.",

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "Failed to add user",
"Get init score failed, error: %w": "Get init score failed, error: %w",
"Please sign out first": "Please sign out first",
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email 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 face is not enabled for the application",
"The login method: login with password is not enabled for the application": "The login method: login with password is not enabled for the application",
"The order: %s does not exist": "The order: %s does not exist",
"The organization: %s does not exist": "The organization: %s does not exist",
"The organization: %s has disabled users to signin": "The organization: %s has disabled users to signin",
"The plan: %s does not exist": "The plan: %s does not exist",
@@ -57,11 +57,11 @@
"Face data mismatch": "Face data mismatch",
"Failed to parse client IP: %s": "Failed to parse client IP: %s",
"FirstName cannot be blank": "FirstName cannot be blank",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Guest users must upgrade their account by setting a username and password before they can sign in directly",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code exhausted": "Invitation code exhausted",
"Invitation code is invalid": "Invitation code is invalid",
"Invitation code suspended": "Invitation code suspended",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Insufficient balance: new balance %v would be below credit limit %v": "Insufficient balance: new balance %v would be below credit limit %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Insufficient balance: new organization balance %v would be below credit limit %v",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
"The LDAP: %s does not exist": "The LDAP: %s does not exist",
"The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The syncer: %s does not exist": "The syncer: %s does not exist",
"The user: %s doesn't exist": "The user: %s doesn't exist",
"The user: %s is not found": "The user: %s is not found",
"User is required for User category transaction": "User is required for User category transaction",
"Wrong userId": "Wrong userId",
"don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "The permission: \"%s\" doesn't exist"
},
"product": {
"Product list cannot be empty": "Product list cannot be empty"
},
"provider": {
"Failed to initialize ID Verification provider": "Failed to initialize ID Verification provider",
"Invalid application id": "Invalid application id",
"No ID Verification provider configured": "No ID Verification provider configured",
"Provider is not an ID Verification provider": "Provider is not an ID Verification provider",
"the provider: %s does not exist": "the provider: %s does not exist"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Invalid Email receivers: %s",
"Invalid phone receivers: %s": "Invalid phone receivers: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
},
"storage": {
"The objectKey: %s is not allowed": "The objectKey: %s is not allowed",
"The provider type: %s is not supported": "The provider type: %s is not supported"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Error"
},
"ticket": {
"Ticket not found": "Ticket not found"
},
"token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s is not supported in this application",
"Invalid application or wrong clientSecret": "Invalid application or wrong clientSecret",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "Display name cannot be empty",
"ID card information and real name are required": "ID card information and real name are required",
"Identity verification failed": "Identity verification failed",
"MFA email is enabled but email is empty": "MFA email is enabled but email is empty",
"MFA phone is enabled but phone number is empty": "MFA phone is enabled but phone number is empty",
"New password cannot contain blank space.": "New password cannot contain blank space.",
"No application found for user": "No application found for user",
"The new password must be different from your current password": "The new password must be different from your current password",
"User is already verified": "User is already verified",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"The forgot password feature is disabled": "The forgot password feature is disabled",
"The verification code has already been used!": "The verification code has already been used!",
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
"Turing test failed.": "Turing test failed.",

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "No se pudo agregar el usuario",
"Get init score failed, error: %w": "Error al obtener el puntaje de inicio, error: %w",
"Please sign out first": "Por favor, cierra sesión primero",
"The application does not allow to sign up new account": "La aplicación no permite registrarse con una cuenta nueva"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "El método de inicio de sesión: inicio de sesión con correo electrónico no está habilitado para la aplicación",
"The login method: login with face is not enabled for the application": "El método de inicio de sesión: inicio de sesión con reconocimiento facial no está habilitado para la aplicación",
"The login method: login with password is not enabled for the application": "El método de inicio de sesión: inicio de sesión con contraseña no está habilitado para la aplicación",
"The order: %s does not exist": "El pedido: %s no existe",
"The organization: %s does not exist": "La organización: %s no existe",
"The organization: %s has disabled users to signin": "La organización: %s ha desactivado el inicio de sesión de usuarios",
"The plan: %s does not exist": "El plan: %s no existe",
@@ -35,7 +35,7 @@
"User's tag: %s is not listed in the application's tags": "La etiqueta del usuario: %s no está incluida en las etiquetas de la aplicación",
"UserCode Expired": "Código de usuario expirado",
"UserCode Invalid": "Código de usuario inválido",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "El usuario de pago %s no tiene una suscripción activa o pendiente y la aplicación: %s no tiene precio predeterminado",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "El usuario de pago %s no tiene una suscripción activa o pendiente y la aplicación %s no tiene precios predeterminados",
"the application for user %s is not found": "no se encontró la aplicación para el usuario %s",
"the organization: %s is not found": "no se encontró la organización: %s"
},
@@ -44,9 +44,9 @@
},
"check": {
"%s does not meet the CIDR format requirements: %s": "%s no cumple con los requisitos del formato CIDR: %s",
"Affiliation cannot be blank": "Afiliación no puede estar en blanco",
"Affiliation cannot be blank": "La afiliación no puede estar vacía",
"CIDR for IP: %s should not be empty": "El CIDR para la IP: %s no debe estar vacío",
"Default code does not match the code's matching rules": "El código predeterminado no coincide con las reglas de coincidencia de códigos",
"Default code does not match the code's matching rules": "El código predeterminado no cumple con las reglas de validación del código",
"DisplayName cannot be blank": "El nombre de visualización no puede estar en blanco",
"DisplayName is not valid real name": "El nombre de pantalla no es un nombre real válido",
"Email already exists": "El correo electrónico ya existe",
@@ -57,11 +57,11 @@
"Face data mismatch": "Los datos faciales no coinciden",
"Failed to parse client IP: %s": "Error al analizar la IP del cliente: %s",
"FirstName cannot be blank": "El nombre no puede estar en blanco",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Los usuarios invitados deben actualizar su cuenta configurando un nombre de usuario y una contraseña antes de poder iniciar sesión directamente",
"Invitation code cannot be blank": "El código de invitación no puede estar vacío",
"Invitation code exhausted": "Código de invitación agotado",
"Invitation code is invalid": "Código de invitación inválido",
"Invitation code suspended": "Código de invitación suspendido",
"LDAP user name or password incorrect": "Nombre de usuario o contraseña de Ldap incorrectos",
"LastName cannot be blank": "El apellido no puede estar en blanco",
"Multiple accounts with same uid, please check your ldap server": "Cuentas múltiples con el mismo uid, por favor revise su servidor ldap",
"Organization does not exist": "La organización no existe",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Error al importar grupos",
"Failed to import users": "Error al importar usuarios",
"Insufficient balance: new balance %v would be below credit limit %v": "Saldo insuficiente: el nuevo saldo %v estaría por debajo del límite de crédito %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Saldo insuficiente: el nuevo saldo de la organización %v estaría por debajo del límite de crédito %v",
"Missing parameter": "Parámetro faltante",
"Only admin user can specify user": "Solo el usuario administrador puede especificar usuario",
"Please login first": "Por favor, inicia sesión primero",
"The LDAP: %s does not exist": "El LDAP: %s no existe",
"The organization: %s should have one application at least": "La organización: %s debe tener al menos una aplicación",
"The syncer: %s does not exist": "El sincronizador: %s no existe",
"The user: %s doesn't exist": "El usuario: %s no existe",
"The user: %s is not found": "El usuario: %s no encontrado",
"User is required for User category transaction": "El usuario es obligatorio para la transacción de la categoría Usuario",
"Wrong userId": "ID de usuario incorrecto",
"don't support captchaProvider: ": "No apoyo a captchaProvider",
"this operation is not allowed in demo mode": "esta operación no está permitida en modo de demostración",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "El permiso: \"%s\" no existe"
},
"product": {
"Product list cannot be empty": "La lista de productos no puede estar vacía"
},
"provider": {
"Failed to initialize ID Verification provider": "Error al inicializar el proveedor de verificación de ID",
"Invalid application id": "Identificación de aplicación no válida",
"No ID Verification provider configured": "No hay proveedor de verificación de ID configurado",
"Provider is not an ID Verification provider": "El proveedor no es un proveedor de verificación de ID",
"the provider: %s does not exist": "El proveedor: %s no existe"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Receptores de correo electrónico no válidos: %s",
"Invalid phone receivers: %s": "Receptores de teléfono no válidos: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
},
"storage": {
"The objectKey: %s is not allowed": "El objectKey: %s no está permitido",
"The provider type: %s is not supported": "El tipo de proveedor: %s no es compatible"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Error"
},
"ticket": {
"Ticket not found": "Ticket no encontrado"
},
"token": {
"Grant_type: %s is not supported in this application": "El tipo de subvención: %s no es compatible con esta aplicación",
"Invalid application or wrong clientSecret": "Solicitud inválida o clientSecret incorrecto",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "El nombre de pantalla no puede estar vacío",
"ID card information and real name are required": "Se requiere información de la tarjeta de identificación y el nombre real",
"Identity verification failed": "Falló la verificación de identidad",
"MFA email is enabled but email is empty": "El correo electrónico MFA está habilitado pero el correo está vacío",
"MFA phone is enabled but phone number is empty": "El teléfono MFA está habilitado pero el número de teléfono está vacío",
"New password cannot contain blank space.": "La nueva contraseña no puede contener espacios en blanco.",
"No application found for user": "No se encontró aplicación para el usuario",
"The new password must be different from your current password": "La nueva contraseña debe ser diferente de su contraseña actual",
"User is already verified": "El usuario ya está verificado",
"the user's owner and name should not be empty": "el propietario y el nombre del usuario no deben estar vacíos"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "Proveedor de captcha no válido.",
"Phone number is invalid in your region %s": "El número de teléfono es inválido en tu región %s",
"The forgot password feature is disabled": "La función de contraseña olvidada está deshabilitada",
"The verification code has already been used!": "¡El código de verificación ya ha sido utilizado!",
"The verification code has not been sent yet!": "¡El código de verificación aún no ha sido enviado!",
"Turing test failed.": "El test de Turing falló.",

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "Échec d'ajout d'utilisateur",
"Get init score failed, error: %w": "Obtention du score initiale échouée, erreur : %w",
"Please sign out first": "Veuillez vous déconnecter en premier",
"The application does not allow to sign up new account": "L'application ne permet pas de créer un nouveau compte"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "La méthode de connexion : connexion par e-mail n'est pas activée pour l'application",
"The login method: login with face is not enabled for the application": "La méthode de connexion : connexion par visage n'est pas activée pour l'application",
"The login method: login with password is not enabled for the application": "La méthode de connexion : connexion avec mot de passe n'est pas activée pour l'application",
"The order: %s does not exist": "La commande : %s n'existe pas",
"The organization: %s does not exist": "L'organisation : %s n'existe pas",
"The organization: %s has disabled users to signin": "L'organisation: %s a désactivé la connexion des utilisateurs",
"The plan: %s does not exist": "Le plan : %s n'existe pas",
@@ -35,7 +35,7 @@
"User's tag: %s is not listed in the application's tags": "Le tag de l'utilisateur : %s n'est pas répertorié dans les tags de l'application",
"UserCode Expired": "Code utilisateur expiré",
"UserCode Invalid": "Code utilisateur invalide",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "L'utilisateur payant %s n'a pas d'abonnement actif ou en attente et l'application : %s n'a pas de tarification par défaut",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "L'utilisateur payant %s n'a pas d'abonnement actif ou en attente et l'application %s n'a pas de tarification par défaut",
"the application for user %s is not found": "L'application pour l'utilisateur %s est introuvable",
"the organization: %s is not found": "L'organisation : %s est introuvable"
},
@@ -44,9 +44,9 @@
},
"check": {
"%s does not meet the CIDR format requirements: %s": "%s ne respecte pas les exigences du format CIDR : %s",
"Affiliation cannot be blank": "Affiliation ne peut pas être vide",
"Affiliation cannot be blank": "L'affiliation ne peut pas être vide",
"CIDR for IP: %s should not be empty": "Le CIDR pour l'IP : %s ne doit pas être vide",
"Default code does not match the code's matching rules": "Le code par défaut ne correspond pas aux règles de correspondance du code",
"Default code does not match the code's matching rules": "Le code par défaut ne respecte pas les règles de validation du code",
"DisplayName cannot be blank": "Le nom d'affichage ne peut pas être vide",
"DisplayName is not valid real name": "DisplayName n'est pas un nom réel valide",
"Email already exists": "E-mail déjà existant",
@@ -57,11 +57,11 @@
"Face data mismatch": "Données faciales incorrectes",
"Failed to parse client IP: %s": "Échec de l'analyse de l'IP client : %s",
"FirstName cannot be blank": "Le prénom ne peut pas être laissé vide",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Les utilisateurs invités doivent mettre à niveau leur compte en définissant un nom d'utilisateur et un mot de passe avant de pouvoir se connecter directement",
"Invitation code cannot be blank": "Le code d'invitation ne peut pas être vide",
"Invitation code exhausted": "Code d'invitation épuisé",
"Invitation code is invalid": "Code d'invitation invalide",
"Invitation code suspended": "Code d'invitation suspendu",
"LDAP user name or password incorrect": "Nom d'utilisateur ou mot de passe LDAP incorrect",
"LastName cannot be blank": "Le nom de famille ne peut pas être vide",
"Multiple accounts with same uid, please check your ldap server": "Plusieurs comptes avec le même identifiant d'utilisateur, veuillez vérifier votre serveur LDAP",
"Organization does not exist": "L'organisation n'existe pas",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Échec de l'importation des groupes",
"Failed to import users": "Échec de l'importation des utilisateurs",
"Insufficient balance: new balance %v would be below credit limit %v": "Solde insuffisant : le nouveau solde %v serait inférieur à la limite de crédit %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Solde insuffisant : le nouveau solde de l'organisation %v serait inférieur à la limite de crédit %v",
"Missing parameter": "Paramètre manquant",
"Only admin user can specify user": "Seul un administrateur peut désigner un utilisateur",
"Please login first": "Veuillez d'abord vous connecter",
"The LDAP: %s does not exist": "Le LDAP : %s n'existe pas",
"The organization: %s should have one application at least": "L'organisation : %s doit avoir au moins une application",
"The syncer: %s does not exist": "Le synchroniseur : %s n'existe pas",
"The user: %s doesn't exist": "L'utilisateur : %s n'existe pas",
"The user: %s is not found": "L'utilisateur : %s est introuvable",
"User is required for User category transaction": "L'utilisateur est requis pour la transaction de catégorie Utilisateur",
"Wrong userId": "ID utilisateur incorrect",
"don't support captchaProvider: ": "ne prend pas en charge captchaProvider: ",
"this operation is not allowed in demo mode": "cette opération n'est pas autorisée en mode démo",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "La permission : \"%s\" n'existe pas"
},
"product": {
"Product list cannot be empty": "La liste des produits ne peut pas être vide"
},
"provider": {
"Failed to initialize ID Verification provider": "Échec de l'initialisation du fournisseur de vérification d'identité",
"Invalid application id": "Identifiant d'application invalide",
"No ID Verification provider configured": "Aucun fournisseur de vérification d'identité configuré",
"Provider is not an ID Verification provider": "Le fournisseur n'est pas un fournisseur de vérification d'identité",
"the provider: %s does not exist": "Le fournisseur : %s n'existe pas"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Destinataires d'e-mail invalides : %s",
"Invalid phone receivers: %s": "Destinataires de téléphone invalide : %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
},
"storage": {
"The objectKey: %s is not allowed": "La clé d'objet : %s n'est pas autorisée",
"The provider type: %s is not supported": "Le type de fournisseur : %s n'est pas pris en charge"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Erreur"
},
"ticket": {
"Ticket not found": "Ticket introuvable"
},
"token": {
"Grant_type: %s is not supported in this application": "Type_de_subvention : %s n'est pas pris en charge dans cette application",
"Invalid application or wrong clientSecret": "Application invalide ou clientSecret incorrect",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "Le nom d'affichage ne peut pas être vide",
"ID card information and real name are required": "Les informations de la carte d'identité et le nom réel sont requis",
"Identity verification failed": "Échec de la vérification d'identité",
"MFA email is enabled but email is empty": "L'authentification MFA par e-mail est activée mais l'e-mail est vide",
"MFA phone is enabled but phone number is empty": "L'authentification MFA par téléphone est activée mais le numéro de téléphone est vide",
"New password cannot contain blank space.": "Le nouveau mot de passe ne peut pas contenir d'espace.",
"No application found for user": "Aucune application trouvée pour l'utilisateur",
"The new password must be different from your current password": "Le nouveau mot de passe doit être différent de votre mot de passe actuel",
"User is already verified": "L'utilisateur est déjà vérifié",
"the user's owner and name should not be empty": "le propriétaire et le nom de l'utilisateur ne doivent pas être vides"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "Fournisseur de captcha invalide.",
"Phone number is invalid in your region %s": "Le numéro de téléphone n'est pas valide dans votre région %s",
"The forgot password feature is disabled": "La fonction de mot de passe oublié est désactivée",
"The verification code has already been used!": "Le code de vérification a déjà été utilisé !",
"The verification code has not been sent yet!": "Le code de vérification n'a pas encore été envoyé !",
"Turing test failed.": "Le test de Turing a échoué.",

View File

@@ -2,7 +2,6 @@
"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": {
@@ -23,6 +22,7 @@
"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 order: %s does not exist": "注文:%s は存在しません",
"The organization: %s does not exist": "組織「%s」は存在しません",
"The organization: %s has disabled users to signin": "組織: %s はユーザーのサインインを無効にしました",
"The plan: %s does not exist": "プラン: %sは存在しません",
@@ -57,11 +57,11 @@
"Face data mismatch": "顔認証データが一致しません",
"Failed to parse client IP: %s": "クライアント IP「%s」の解析に失敗しました",
"FirstName cannot be blank": "ファーストネームは空白にできません",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "ゲストユーザーは直接サインインする前に、ユーザー名とパスワードを設定してアカウントをアップグレードする必要があります",
"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": "組織は存在しません",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "グループのインポートに失敗しました",
"Failed to import users": "ユーザーのインポートに失敗しました",
"Insufficient balance: new balance %v would be below credit limit %v": "残高不足:新しい残高 %v がクレジット制限 %v を下回ります",
"Insufficient balance: new organization balance %v would be below credit limit %v": "残高不足:新しい組織残高 %v がクレジット制限 %v を下回ります",
"Missing parameter": "不足しているパラメーター",
"Only admin user can specify user": "管理者ユーザーのみがユーザーを指定できます",
"Please login first": "最初にログインしてください",
"The LDAP: %s does not exist": "LDAP%s は存在しません",
"The organization: %s should have one application at least": "組織「%s」は少なくとも1つのアプリケーションを持っている必要があります",
"The syncer: %s does not exist": "同期装置:%s は存在しません",
"The user: %s doesn't exist": "そのユーザー:%sは存在しません",
"The user: %s is not found": "ユーザー:%s が見つかりません",
"User is required for User category transaction": "ユーザーカテゴリトランザクションにはユーザーが必要です",
"Wrong userId": "無効なユーザーIDです",
"don't support captchaProvider: ": "captchaProviderをサポートしないでください",
"this operation is not allowed in demo mode": "この操作はデモモードでは許可されていません",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "権限「%s」は存在しません"
},
"product": {
"Product list cannot be empty": "商品リストは空にできません"
},
"provider": {
"Failed to initialize ID Verification provider": "ID認証プロバイダーの初期化に失敗しました",
"Invalid application id": "アプリケーションIDが無効です",
"No ID Verification provider configured": "ID認証プロバイダーが設定されていません",
"Provider is not an ID Verification provider": "プロバイダーはID認証プロバイダーではありません",
"the provider: %s does not exist": "プロバイダー%sは存在しません"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "無効な電子メール受信者:%s",
"Invalid phone receivers: %s": "電話受信者が無効です:%s"
},
"session": {
"session id %s is the current session and cannot be deleted": "セッションID %s は現在のセッションであり、削除できません"
},
"storage": {
"The objectKey: %s is not allowed": "オブジェクトキー %s は許可されていません",
"The provider type: %s is not supported": "プロバイダータイプ:%sはサポートされていません"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "エラー"
},
"ticket": {
"Ticket not found": "チケットが見つかりません"
},
"token": {
"Grant_type: %s is not supported in this application": "grant_type%sはこのアプリケーションでサポートされていません",
"Invalid application or wrong clientSecret": "無効なアプリケーションまたは誤ったクライアントシークレットです",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "表示名は空にできません",
"ID card information and real name are required": "身分証明書の情報と実名が必要です",
"Identity verification failed": "身元確認に失敗しました",
"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.": "新しいパスワードにはスペースを含めることはできません。",
"No application found for user": "ユーザーのアプリケーションが見つかりません",
"The new password must be different from your current password": "新しいパスワードは現在のパスワードと異なる必要があります",
"User is already verified": "ユーザーは既に認証済みです",
"the user's owner and name should not be empty": "ユーザーのオーナーと名前は空にできません"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "無効なCAPTCHAプロバイダー。",
"Phone number is invalid in your region %s": "電話番号はあなたの地域で無効です %s",
"The forgot password feature is disabled": "パスワードを忘れた機能は無効になっています",
"The verification code has already been used!": "この検証コードは既に使用されています!",
"The verification code has not been sent yet!": "検証コードはまだ送信されていません!",
"Turing test failed.": "チューリングテストは失敗しました。",

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "Nie udało się dodać użytkownika",
"Get init score failed, error: %w": "Pobranie początkowego wyniku nie powiodło się, błąd: %w",
"Please sign out first": "Najpierw się wyloguj",
"The application does not allow to sign up new account": "Aplikacja nie pozwala na rejestrację nowego konta"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "Metoda logowania: logowanie przez email nie jest włączona dla aplikacji",
"The login method: login with face is not enabled for the application": "Metoda logowania: logowanie przez twarz nie jest włączona dla aplikacji",
"The login method: login with password is not enabled for the application": "Metoda logowania: logowanie przez hasło nie jest włączone dla aplikacji",
"The order: %s does not exist": "Zamówienie: %s nie istnieje",
"The organization: %s does not exist": "Organizacja: %s nie istnieje",
"The organization: %s has disabled users to signin": "Organizacja: %s wyłączyła logowanie użytkowników",
"The plan: %s does not exist": "Plan: %s nie istnieje",
@@ -57,11 +57,11 @@
"Face data mismatch": "Niezgodność danych twarzy",
"Failed to parse client IP: %s": "Nie udało się przeanalizować IP klienta: %s",
"FirstName cannot be blank": "Imię nie może być puste",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Użytkownicy-goście muszą uaktualnić swoje konto, ustawiając nazwę użytkownika i hasło, zanim będą mogli się zalogować bezpośrednio",
"Invitation code cannot be blank": "Kod zaproszenia nie może być pusty",
"Invitation code exhausted": "Kod zaproszenia został wykorzystany",
"Invitation code is invalid": "Kod zaproszenia jest nieprawidłowy",
"Invitation code suspended": "Kod zaproszenia został zawieszony",
"LDAP user name or password incorrect": "Nazwa użytkownika LDAP lub hasło jest nieprawidłowe",
"LastName cannot be blank": "Nazwisko nie może być puste",
"Multiple accounts with same uid, please check your ldap server": "Wiele kont z tym samym uid, sprawdź swój serwer ldap",
"Organization does not exist": "Organizacja nie istnieje",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Nie udało się zaimportować grup",
"Failed to import users": "Nie udało się zaimportować użytkowników",
"Insufficient balance: new balance %v would be below credit limit %v": "Niewystarczające saldo: nowe saldo %v byłoby poniżej limitu kredytowego %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Niewystarczające saldo: nowe saldo organizacji %v byłoby poniżej limitu kredytowego %v",
"Missing parameter": "Brakujący parametr",
"Only admin user can specify user": "Tylko administrator może wskazać użytkownika",
"Please login first": "Najpierw się zaloguj",
"The LDAP: %s does not exist": "LDAP: %s nie istnieje",
"The organization: %s should have one application at least": "Organizacja: %s powinna mieć co najmniej jedną aplikację",
"The syncer: %s does not exist": "Synchronizer: %s nie istnieje",
"The user: %s doesn't exist": "Użytkownik: %s nie istnieje",
"The user: %s is not found": "Użytkownik: %s nie został znaleziony",
"User is required for User category transaction": "Użytkownik jest wymagany do transakcji kategorii użytkownika",
"Wrong userId": "Nieprawidłowy userId",
"don't support captchaProvider: ": "nie obsługuje captchaProvider: ",
"this operation is not allowed in demo mode": "ta operacja nie jest dozwolona w trybie demo",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "Uprawnienie: \"%s\" nie istnieje"
},
"product": {
"Product list cannot be empty": "Lista produktów nie może być pusta"
},
"provider": {
"Failed to initialize ID Verification provider": "Nie udało się zainicjować dostawcy weryfikacji ID",
"Invalid application id": "Nieprawidłowe id aplikacji",
"No ID Verification provider configured": "Brak skonfigurowanego dostawcy weryfikacji ID",
"Provider is not an ID Verification provider": "Dostawca nie jest dostawcą weryfikacji ID",
"the provider: %s does not exist": "dostawca: %s nie istnieje"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Nieprawidłowi odbiorcy email: %s",
"Invalid phone receivers: %s": "Nieprawidłowi odbiorcy telefonu: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "identyfikator sesji %s jest bieżącą sesją i nie może być usunięty"
},
"storage": {
"The objectKey: %s is not allowed": "Klucz obiektu: %s jest niedozwolony",
"The provider type: %s is not supported": "Typ dostawcy: %s nie jest obsługiwany"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Błąd"
},
"ticket": {
"Ticket not found": "Nie znaleziono biletu"
},
"token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s nie jest obsługiwany w tej aplikacji",
"Invalid application or wrong clientSecret": "Nieprawidłowa aplikacja lub błędny clientSecret",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "Nazwa wyświetlana nie może być pusta",
"ID card information and real name are required": "Wymagane są informacje z dowodu osobistego i prawdziwe nazwisko",
"Identity verification failed": "Weryfikacja tożsamości nie powiodła się",
"MFA email is enabled but email is empty": "MFA email jest włączone, ale email jest pusty",
"MFA phone is enabled but phone number is empty": "MFA telefon jest włączony, ale numer telefonu jest pusty",
"New password cannot contain blank space.": "Nowe hasło nie może zawierać spacji.",
"No application found for user": "Nie znaleziono aplikacji dla użytkownika",
"The new password must be different from your current password": "Nowe hasło musi różnić się od obecnego hasła",
"User is already verified": "Użytkownik jest już zweryfikowany",
"the user's owner and name should not be empty": "właściciel i nazwa użytkownika nie powinny być puste"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "Nieprawidłowy dostawca captcha.",
"Phone number is invalid in your region %s": "Numer telefonu jest nieprawidłowy w twoim regionie %s",
"The forgot password feature is disabled": "Funkcja \"Zapomniałem hasła\" jest wyłączona",
"The verification code has already been used!": "Kod weryfikacyjny został już wykorzystany!",
"The verification code has not been sent yet!": "Kod weryfikacyjny nie został jeszcze wysłany!",
"Turing test failed.": "Test Turinga nie powiódł się.",

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "Falha ao adicionar usuário",
"Get init score failed, error: %w": "Falha ao obter pontuação inicial, erro: %w",
"Please sign out first": "Por favor, saia primeiro",
"The application does not allow to sign up new account": "O aplicativo não permite a criação de novas contas"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "O método de login com e-mail não está habilitado para o aplicativo",
"The login method: login with face is not enabled for the application": "O método de login com reconhecimento facial não está habilitado para o aplicativo",
"The login method: login with password is not enabled for the application": "O método de login com senha não está habilitado para o aplicativo",
"The order: %s does not exist": "O pedido: %s não existe",
"The organization: %s does not exist": "A organização: %s não existe",
"The organization: %s has disabled users to signin": "A organização: %s desativou o login de usuários",
"The plan: %s does not exist": "O plano: %s não existe",
@@ -57,11 +57,11 @@
"Face data mismatch": "Dados faciais não correspondem",
"Failed to parse client IP: %s": "Falha ao analisar o IP do cliente: %s",
"FirstName cannot be blank": "O primeiro nome não pode estar em branco",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Usuários convidados devem atualizar suas contas definindo um nome de usuário e senha antes de poderem entrar diretamente",
"Invitation code cannot be blank": "O código de convite não pode estar em branco",
"Invitation code exhausted": "O código de convite foi esgotado",
"Invitation code is invalid": "Código de convite inválido",
"Invitation code suspended": "Código de convite suspenso",
"LDAP user name or password incorrect": "Nome de usuário ou senha LDAP incorretos",
"LastName cannot be blank": "O sobrenome não pode estar em branco",
"Multiple accounts with same uid, please check your ldap server": "Múltiplas contas com o mesmo uid, verifique seu servidor LDAP",
"Organization does not exist": "A organização não existe",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Falha ao importar grupos",
"Failed to import users": "Falha ao importar usuários",
"Insufficient balance: new balance %v would be below credit limit %v": "Saldo insuficiente: o novo saldo %v estaria abaixo do limite de crédito %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Saldo insuficiente: o novo saldo da organização %v estaria abaixo do limite de crédito %v",
"Missing parameter": "Parâmetro ausente",
"Only admin user can specify user": "Apenas um administrador pode especificar um usuário",
"Please login first": "Por favor, faça login primeiro",
"The LDAP: %s does not exist": "O LDAP: %s não existe",
"The organization: %s should have one application at least": "A organização: %s deve ter pelo menos um aplicativo",
"The syncer: %s does not exist": "O sincronizador: %s não existe",
"The user: %s doesn't exist": "O usuário: %s não existe",
"The user: %s is not found": "O usuário: %s não foi encontrado",
"User is required for User category transaction": "Usuário é obrigatório para transação de categoria de usuário",
"Wrong userId": "ID de usuário incorreto",
"don't support captchaProvider: ": "captchaProvider não suportado: ",
"this operation is not allowed in demo mode": "esta operação não é permitida no modo de demonstração",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "A permissão: \"%s\" não existe"
},
"product": {
"Product list cannot be empty": "A lista de produtos não pode estar vazia"
},
"provider": {
"Failed to initialize ID Verification provider": "Falha ao inicializar provedor de verificação de ID",
"Invalid application id": "ID de aplicativo inválido",
"No ID Verification provider configured": "Nenhum provedor de verificação de ID configurado",
"Provider is not an ID Verification provider": "Provedor não é um provedor de verificação de ID",
"the provider: %s does not exist": "O provedor: %s não existe"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Destinatários de e-mail inválidos: %s",
"Invalid phone receivers: %s": "Destinatários de telefone inválidos: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "ID da sessão %s é a sessão atual e não pode ser excluída"
},
"storage": {
"The objectKey: %s is not allowed": "A chave de objeto: %s não é permitida",
"The provider type: %s is not supported": "O tipo de provedor: %s não é suportado"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Erro"
},
"ticket": {
"Ticket not found": "Ticket não encontrado"
},
"token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s não é suportado neste aplicativo",
"Invalid application or wrong clientSecret": "Aplicativo inválido ou clientSecret incorreto",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "O nome de exibição não pode estar vazio",
"ID card information and real name are required": "Informações do documento de identidade e nome verdadeiro são obrigatórios",
"Identity verification failed": "Falha na verificação de identidade",
"MFA email is enabled but email is empty": "MFA por e-mail está habilitado, mas o e-mail está vazio",
"MFA phone is enabled but phone number is empty": "MFA por telefone está habilitado, mas o número de telefone está vazio",
"New password cannot contain blank space.": "A nova senha não pode conter espaços em branco.",
"No application found for user": "Nenhum aplicativo encontrado para o usuário",
"The new password must be different from your current password": "A nova senha deve ser diferente da senha atual",
"User is already verified": "Usuário já está verificado",
"the user's owner and name should not be empty": "O proprietário e o nome do usuário não devem estar vazios"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "Provedor de captcha inválido.",
"Phone number is invalid in your region %s": "Número de telefone inválido na sua região %s",
"The forgot password feature is disabled": "A funcionalidade de esqueci a senha está desabilitada",
"The verification code has already been used!": "O código de verificação já foi utilizado!",
"The verification code has not been sent yet!": "O código de verificação ainda não foi enviado!",
"Turing test failed.": "O teste de Turing falhou.",

230
i18n/locales/ru/data.json Normal file
View File

@@ -0,0 +1,230 @@
{
"account": {
"Failed to add user": "Не удалось добавить пользователя",
"Get init score failed, error: %w": "Не удалось получить исходный балл, ошибка: %w",
"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) не существует и не может быть зарегистрирован как новый аккаунт. Пожалуйста, обратитесь в службу поддержки IT",
"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": "Метод входа через SMS отключен для этого приложения",
"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 order: %s does not exist": "The order: %s does not exist",
"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": "DisplayName не является действительным именем",
"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": "Имя не может быть пустым",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Guest users must upgrade their account by setting a username and password before they can sign in directly",
"Invitation code cannot be blank": "Код приглашения не может быть пустым",
"Invitation code exhausted": "Код приглашения исчерпан",
"Invitation code is invalid": "Код приглашения недействителен",
"Invitation code suspended": "Код приглашения приостановлен",
"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": "The value \"%s\" for account field \"%s\" doesn't match the account item regex",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"",
"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": "Имя пользователя должно содержать не менее 2 символов",
"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\"": "Срок действия вашего пароля истек. Пожалуйста, сбросьте пароль, нажав \"Забыли пароль\"",
"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": "Не удалось импортировать пользователей",
"Insufficient balance: new balance %v would be below credit limit %v": "Insufficient balance: new balance %v would be below credit limit %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Insufficient balance: new organization balance %v would be below credit limit %v",
"Missing parameter": "Отсутствующий параметр",
"Only admin user can specify user": "Только администратор может указать пользователя",
"Please login first": "Пожалуйста, сначала войдите в систему",
"The LDAP: %s does not exist": "Группа LDAP: %s не существует",
"The organization: %s should have one application at least": "Организация: %s должна иметь хотя бы одно приложение",
"The syncer: %s does not exist": "The syncer: %s does not exist",
"The user: %s doesn't exist": "Пользователь %s не существует",
"The user: %s is not found": "The user: %s is not found",
"User is required for User category transaction": "User is required for User category transaction",
"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.": "Добавление нового пользователя в организацию «built-in» (встроенная) в настоящее время отключено. Обратите внимание: все пользователи в организации «built-in» являются глобальными администраторами в Casdoor. См. документацию: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. Если вы все еще хотите создать пользователя для организации «built-in», перейдите на страницу настроек организации и включите опцию «Имеет согласие на привилегии»."
},
"permission": {
"The permission: \"%s\" doesn't exist": "The permission: \"%s\" doesn't exist"
},
"product": {
"Product list cannot be empty": "Product list cannot be empty"
},
"provider": {
"Failed to initialize ID Verification provider": "Failed to initialize ID Verification provider",
"Invalid application id": "Неверный идентификатор приложения",
"No ID Verification provider configured": "No ID Verification provider configured",
"Provider is not an ID Verification provider": "Provider is not an ID Verification provider",
"the provider: %s does not exist": "Провайдер: %s не существует"
},
"resource": {
"User is nil for tag: avatar": "Пользователь равен нулю для тега: аватар",
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "Имя пользователя или полный путь к файлу пусты: имя_пользователя = %s, полный_путь_к_файлу = %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"
},
"session": {
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
},
"storage": {
"The objectKey: %s is not allowed": "Объект «objectKey: %s» не разрешен",
"The provider type: %s is not supported": "Тип провайдера: %s не поддерживается"
},
"subscription": {
"Error": "Ошибка"
},
"ticket": {
"Ticket not found": "Ticket not found"
},
"token": {
"Grant_type: %s is not supported in this application": "Тип предоставления: %s не поддерживается в данном приложении",
"Invalid application or wrong clientSecret": "Недействительное приложение или неправильный clientSecret",
"Invalid client_id": "Недействительный идентификатор клиента",
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "URI перенаправления: %s не существует в списке разрешенных URI перенаправления",
"Token not found, invalid accessToken": "Токен не найден, недействительный accessToken"
},
"user": {
"Display name cannot be empty": "Отображаемое имя не может быть пустым",
"ID card information and real name are required": "ID card information and real name are required",
"Identity verification failed": "Identity verification failed",
"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.": "Новый пароль не может содержать пробелы.",
"No application found for user": "No application found for user",
"The new password must be different from your current password": "Новый пароль должен отличаться от текущего пароля",
"User is already verified": "Пользователь уже подтвержден",
"the user's owner and name should not be empty": "владелец и имя пользователя не должны быть пустыми"
},
"util": {
"No application is found for userId: %s": "Не найдено заявки для пользователя с идентификатором: %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 forgot password feature is disabled": "The forgot password feature is disabled",
"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": "Пожалуйста, добавьте SMS-провайдера в список \"Провайдеры\" для приложения: %s",
"please add an Email provider to the \"Providers\" list for the application: %s": "пожалуйста, добавьте Email-провайдера в список \\\"Провайдеры\\\" для приложения: %s",
"the user does not exist, please sign up first": "Пользователь не существует, пожалуйста, сначала зарегистрируйтесь"
},
"webauthn": {
"Found no credentials for this user": "Учетные данные для этого пользователя не найдены",
"Please call WebAuthnSigninBegin first": "Пожалуйста, сначала вызовите WebAuthnSigninBegin"
}
}

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "Kullanıcı eklenemedi",
"Get init score failed, error: %w": "Başlangıç puanı alınamadı, hata: %w",
"Please sign out first": "Lütfen önce çıkış yapın",
"The application does not allow to sign up new account": "Uygulama yeni hesap kaydına izin vermiyor"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "Uygulama için e-posta ile giriş yöntemi etkin değil",
"The login method: login with face is not enabled for the application": "Uygulama için yüz ile giriş yöntemi etkin değil",
"The login method: login with password is not enabled for the application": "Şifre ile giriş yöntemi bu uygulama için etkin değil",
"The order: %s does not exist": "Sipariş: %s mevcut değil",
"The organization: %s does not exist": "Organizasyon: %s mevcut değil",
"The organization: %s has disabled users to signin": "Organizasyon: %s kullanıcıların oturum açmasını devre dışı bıraktı",
"The plan: %s does not exist": "Plan: %s mevcut değil",
@@ -57,11 +57,11 @@
"Face data mismatch": "Yüz verisi uyuşmazlığı",
"Failed to parse client IP: %s": "İstemci IP'si ayrıştırılamadı: %s",
"FirstName cannot be blank": "Ad boş olamaz",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Misafir kullanıcılar doğrudan giriş yapabilmek için kullanıcı adı ve şifre belirleyerek hesaplarını yükseltmelidir",
"Invitation code cannot be blank": "Davet kodu boş olamaz",
"Invitation code exhausted": "Davet kodu kullanım dışı",
"Invitation code is invalid": "Davet kodu geçersiz",
"Invitation code suspended": "Davet kodu askıya alındı",
"LDAP user name or password incorrect": "LDAP kullanıcı adı veya şifre yanlış",
"LastName cannot be blank": "Soyad boş olamaz",
"Multiple accounts with same uid, please check your ldap server": "Aynı uid'ye sahip birden fazla hesap, lütfen ldap sunucunuzu kontrol edin",
"Organization does not exist": "Organizasyon bulunamadı",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Gruplar içe aktarılamadı",
"Failed to import users": "Kullanıcılar içe aktarılamadı",
"Insufficient balance: new balance %v would be below credit limit %v": "Yetersiz bakiye: yeni bakiye %v kredi limitinin altında olacak %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Yetersiz bakiye: yeni organizasyon bakiyesi %v kredi limitinin altında olacak %v",
"Missing parameter": "Eksik parametre",
"Only admin user can specify user": "Yalnızca yönetici kullanıcı kullanıcı belirleyebilir",
"Please login first": "Lütfen önce giriş yapın",
"The LDAP: %s does not exist": "LDAP: %s mevcut değil",
"The organization: %s should have one application at least": "Organizasyon: %s en az bir uygulamaya sahip olmalı",
"The syncer: %s does not exist": "Senkronizasyon: %s mevcut değil",
"The user: %s doesn't exist": "Kullanıcı: %s bulunamadı",
"The user: %s is not found": "Kullanıcı: %s bulunamadı",
"User is required for User category transaction": "Kullanıcı kategorisi işlemi için kullanıcı gerekli",
"Wrong userId": "Yanlış kullanıcı kimliği",
"don't support captchaProvider: ": "captchaProvider desteklenmiyor: ",
"this operation is not allowed in demo mode": "bu işlem demo modunda izin verilmiyor",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "İzin: \"%s\" mevcut değil"
},
"product": {
"Product list cannot be empty": "Ürün listesi boş olamaz"
},
"provider": {
"Failed to initialize ID Verification provider": "Kimlik Doğrulama sağlayıcısı başlatılamadı",
"Invalid application id": "Geçersiz uygulama id",
"No ID Verification provider configured": "Kimlik Doğrulama sağlayıcısı yapılandırılmamış",
"Provider is not an ID Verification provider": "Sağlayıcı bir Kimlik Doğrulama sağlayıcısı değil",
"the provider: %s does not exist": "provider: %s bulunamadı"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Geçersiz e-posta alıcıları: %s",
"Invalid phone receivers: %s": "Geçersiz telefon alıcıları: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "oturum kimliği %s geçerli oturumdur ve silinemez"
},
"storage": {
"The objectKey: %s is not allowed": "objectKey: %s izin verilmiyor",
"The provider type: %s is not supported": "provider türü: %s desteklenmiyor"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Hata"
},
"ticket": {
"Ticket not found": "Bilet bulunamadı"
},
"token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s bu uygulamada desteklenmiyor",
"Invalid application or wrong clientSecret": "Geçersiz uygulama veya yanlış clientSecret",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "Görünen ad boş olamaz",
"ID card information and real name are required": "Kimlik kartı bilgileri ve gerçek adı gereklidir",
"Identity verification failed": "Kimlik doğrulama başarısız",
"MFA email is enabled but email is empty": "MFA e-postası etkin ancak e-posta boş",
"MFA phone is enabled but phone number is empty": "MFA telefonu etkin ancak telefon numarası boş",
"New password cannot contain blank space.": "Yeni şifre boşluk içeremez.",
"No application found for user": "Kullanıcı için uygulama bulunamadı",
"The new password must be different from your current password": "Yeni şifre mevcut şifrenizden farklı olmalıdır",
"User is already verified": "Kullanıcı zaten doğrulanmış",
"the user's owner and name should not be empty": "kullanıcının sahibi ve adı boş olmamalıdır"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "Geçersiz captcha sağlayıcı.",
"Phone number is invalid in your region %s": "Telefon numaranız bölgenizde geçersiz %s",
"The forgot password feature is disabled": "Şifremi unuttum özelliği devre dışı",
"The verification code has already been used!": "Doğrulama kodu zaten kullanılmış!",
"The verification code has not been sent yet!": "Doğrulama kodu henüz gönderilmedi!",
"Turing test failed.": "Turing testi başarısız.",

View File

@@ -2,7 +2,6 @@
"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": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "Метод входу через email не увімкнено для цього додатка",
"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 order: %s does not exist": "Замовлення: %s не існує",
"The organization: %s does not exist": "Організація: %s не існує",
"The organization: %s has disabled users to signin": "Організація: %s вимкнула вхід користувачів",
"The plan: %s does not exist": "План: %s не існує",
@@ -57,11 +57,11 @@
"Face data mismatch": "Невідповідність даних обличчя",
"Failed to parse client IP: %s": "Не вдалося розібрати IP клієнта: %s",
"FirstName cannot be blank": "Ім’я не може бути порожнім",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Гостьові користувачі повинні оновити свій обліковий запис, встановивши ім'я користувача та пароль, перш ніж вони зможуть увійти безпосередньо",
"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": "Організація не існує",
@@ -83,8 +83,8 @@
"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": "Värdet \"%s\" för kontofältet \"%s\" matchar inte kontots regex",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "Värdet \"%s\" för registreringsfältet \"%s\" matchar inte registreringsfältets regex för applikationen \"%s\"",
"The value \"%s\" for account field \"%s\" doesn't match the account item regex": "Значення \"%s\" для поля облікового запису \"%s\" не відповідає регулярному виразу облікового запису",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "Значення \"%s\" для поля реєстрації \"%s\" не відповідає регулярному виразу поля реєстрації додатка \"%s\"",
"Username already exists": "Ім’я користувача вже існує",
"Username cannot be an email address": "Ім’я користувача не може бути email-адресою",
"Username cannot contain white spaces": "Ім’я користувача не може містити пробіли",
@@ -94,7 +94,7 @@
"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.": "Ім’я користувача підтримує формат email. Також може містити лише буквено-цифрові символи, підкреслення або дефіси, не може мати послідовні дефіси або підкреслення та не може починатися або закінчуватися дефісом або підкресленням. Зверніть увагу на формат email.",
"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\"": "Ditt lösenord har gått ut. Återställ det genom att klicka på \"Glömt lösenord\"",
"Your password has expired. Please reset your password by clicking \"Forgot password\"": "Ваш пароль застарів. Будь ласка, скиньте пароль, натиснувши \"Забув пароль\"",
"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 спроб",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Не вдалося імпортувати групи",
"Failed to import users": "Не вдалося імпортувати користувачів",
"Insufficient balance: new balance %v would be below credit limit %v": "Недостатній баланс: новий баланс %v буде нижче кредитного ліміту %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Недостатній баланс: новий баланс організації %v буде нижче кредитного ліміту %v",
"Missing parameter": "Відсутній параметр",
"Only admin user can specify user": "Лише адміністратор може вказати користувача",
"Please login first": "Спочатку увійдіть",
"The LDAP: %s does not exist": "LDAP: %s не існує",
"The organization: %s should have one application at least": "Організація: %s має мати щонайменше один додаток",
"The syncer: %s does not exist": "Синхронізатор: %s не існує",
"The user: %s doesn't exist": "Користувач: %s не існує",
"The user: %s is not found": "Користувач: %s не знайдено",
"User is required for User category transaction": "Користувач обов'язковий для транзакції категорії користувача",
"Wrong userId": "Неправильний userId",
"don't support captchaProvider: ": "не підтримується captchaProvider: ",
"this operation is not allowed in demo mode": "ця операція недоступна в демо-режимі",
@@ -137,10 +143,16 @@
"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» (вбудованої) на даний момент вимкнено. Зауважте: усі користувачі в організації «built-in» є глобальними адміністраторами в Casdoor. Дивіться документацію: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. Якщо ви все ще хочете створити користувача для організації «built-in», перейдіть на сторінку налаштувань організації та увімкніть опцію «Має згоду на привілеї»."
},
"permission": {
"The permission: \"%s\" doesn't exist": "Behörigheten: \"%s\" finns inte"
"The permission: \"%s\" doesn't exist": "Дозвіл: \"%s\" не існує"
},
"product": {
"Product list cannot be empty": "Список товарів не може бути порожнім"
},
"provider": {
"Failed to initialize ID Verification provider": "Не вдалося ініціалізувати провайдера верифікації ID",
"Invalid application id": "Недійсний id додатка",
"No ID Verification provider configured": "Провайдер верифікації ID не налаштований",
"Provider is not an ID Verification provider": "Провайдер не є провайдером верифікації ID",
"the provider: %s does not exist": "провайдер: %s не існує"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Недійсні отримувачі Email: %s",
"Invalid phone receivers: %s": "Недійсні отримувачі телефону: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "ідентифікатор сесії %s є поточною сесією і не може бути видалений"
},
"storage": {
"The objectKey: %s is not allowed": "objectKey: %s не дозволено",
"The provider type: %s is not supported": "Тип провайдера: %s не підтримується"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Помилка"
},
"ticket": {
"Ticket not found": "Квиток не знайдено"
},
"token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s не підтримується в цьому додатку",
"Invalid application or wrong clientSecret": "Недійсний додаток або неправильний clientSecret",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "Відображуване ім’я не може бути порожнім",
"ID card information and real name are required": "Інформація про посвідчення особи та справжнє ім'я обов'язкові",
"Identity verification failed": "Верифікація особи не вдалася",
"MFA email is enabled but email is empty": "MFA email увімкнено, але email порожній",
"MFA phone is enabled but phone number is empty": "MFA телефон увімкнено, але номер телефону порожній",
"New password cannot contain blank space.": "Новий пароль не може містити пробіли.",
"No application found for user": "Не знайдено додаток для користувача",
"The new password must be different from your current password": "Новий пароль повинен відрізнятися від поточного пароля",
"User is already verified": "Користувач уже верифікований",
"the user's owner and name should not be empty": "власник ім’я користувача не повинні бути порожніми"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "Недійсний провайдер captcha.",
"Phone number is invalid in your region %s": "Номер телефону недійсний у вашому регіоні %s",
"The forgot password feature is disabled": "Функція відновлення пароля вимкнена",
"The verification code has already been used!": "Код підтвердження вже використано!",
"The verification code has not been sent yet!": "Код підтвердження ще не надіслано!",
"Turing test failed.": "Тест Тюрінга не пройдено.",
@@ -196,8 +219,8 @@
"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ägg till en SMS-leverantör i listan \"Leverantörer\" för applikationen: %s",
"please add an Email provider to the \"Providers\" list for the application: %s": "lägg till en e-postleverantör i listan \"Leverantörer\" för applikationen: %s",
"please add a SMS provider to the \"Providers\" list for the application: %s": "будь ласка, додайте SMS-провайдера до списку \"Провайдери\" для додатка: %s",
"please add an Email provider to the \"Providers\" list for the application: %s": "будь ласка, додайте Email-провайдера до списку \"Провайдери\" для додатка: %s",
"the user does not exist, please sign up first": "користувача не існує, спочатку зареєструйтесь"
},
"webauthn": {

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "Không thể thêm người dùng",
"Get init score failed, error: %w": "Lấy điểm khởi đầu thất bại, lỗi: %w",
"Please sign out first": "Vui lòng đăng xuất trước",
"The application does not allow to sign up new account": "Ứng dụng không cho phép đăng ký tài khoản mới"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "Phương thức đăng nhập bằng email chưa được bật cho ứng dụng",
"The login method: login with face is not enabled for the application": "Phương thức đăng nhập bằng khuôn mặt chưa được bật cho ứng dụng",
"The login method: login with password is not enabled for the application": "Phương thức đăng nhập: đăng nhập bằng mật khẩu không được kích hoạt cho ứng dụng",
"The order: %s does not exist": "Đơn hàng: %s không tồn tại",
"The organization: %s does not exist": "Tổ chức: %s không tồn tại",
"The organization: %s has disabled users to signin": "Tổ chức: %s đã vô hiệu hóa đăng nhập của người dùng",
"The plan: %s does not exist": "Kế hoạch: %s không tồn tại",
@@ -57,11 +57,11 @@
"Face data mismatch": "Dữ liệu khuôn mặt không khớp",
"Failed to parse client IP: %s": "Không thể phân tích IP khách: %s",
"FirstName cannot be blank": "Tên không được để trống",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Người dùng khách phải nâng cấp tài khoản bằng cách đặt tên người dùng và mật khẩu trước khi có thể đăng nhập trực tiếp",
"Invitation code cannot be blank": "Mã mời không được để trống",
"Invitation code exhausted": "Mã mời đã hết",
"Invitation code is invalid": "Mã mời không hợp lệ",
"Invitation code suspended": "Mã mời đã bị tạm ngưng",
"LDAP user name or password incorrect": "Tên người dùng hoặc mật khẩu Ldap không chính xác",
"LastName cannot be blank": "Họ không thể để trống",
"Multiple accounts with same uid, please check your ldap server": "Nhiều tài khoản với cùng một uid, vui lòng kiểm tra máy chủ ldap của bạn",
"Organization does not exist": "Tổ chức không tồn tại",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Không thể nhập nhóm",
"Failed to import users": "Không thể nhập người dùng",
"Insufficient balance: new balance %v would be below credit limit %v": "Số dư không đủ: số dư mới %v sẽ thấp hơn giới hạn tín dụng %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Số dư không đủ: số dư tổ chức mới %v sẽ thấp hơn giới hạn tín dụng %v",
"Missing parameter": "Thiếu tham số",
"Only admin user can specify user": "Chỉ người dùng quản trị mới có thể chỉ định người dùng",
"Please login first": "Vui lòng đăng nhập trước",
"The LDAP: %s does not exist": "LDAP: %s không tồn tại",
"The organization: %s should have one application at least": "Tổ chức: %s cần có ít nhất một ứng dụng",
"The syncer: %s does not exist": "Bộ đồng bộ: %s không tồn tại",
"The user: %s doesn't exist": "Người dùng: %s không tồn tại",
"The user: %s is not found": "Người dùng: %s không được tìm thấy",
"User is required for User category transaction": "Người dùng được yêu cầu cho giao dịch danh mục Người dùng",
"Wrong userId": "ID người dùng sai",
"don't support captchaProvider: ": "không hỗ trợ captchaProvider: ",
"this operation is not allowed in demo mode": "thao tác này không được phép trong chế độ demo",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "Quyền: \"%s\" không tồn tại"
},
"product": {
"Product list cannot be empty": "Danh sách sản phẩm không thể trống"
},
"provider": {
"Failed to initialize ID Verification provider": "Không thể khởi tạo nhà cung cấp Xác minh ID",
"Invalid application id": "Sai ID ứng dụng",
"No ID Verification provider configured": "Không có nhà cung cấp Xác minh ID được cấu hình",
"Provider is not an ID Verification provider": "Nhà cung cấp không phải là nhà cung cấp Xác minh ID",
"the provider: %s does not exist": "Nhà cung cấp: %s không tồn tại"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Người nhận Email không hợp lệ: %s",
"Invalid phone receivers: %s": "Người nhận điện thoại không hợp lệ: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "id phiên %s là phiên hiện tại và không thể bị xóa"
},
"storage": {
"The objectKey: %s is not allowed": "Khóa đối tượng: %s không được phép",
"The provider type: %s is not supported": "Loại nhà cung cấp: %s không được hỗ trợ"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Lỗi"
},
"ticket": {
"Ticket not found": "Không tìm thấy vé"
},
"token": {
"Grant_type: %s is not supported in this application": "Loại cấp phép: %s không được hỗ trợ trong ứng dụng này",
"Invalid application or wrong clientSecret": "Đơn đăng ký không hợp lệ hoặc sai clientSecret",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "Tên hiển thị không thể trống",
"ID card information and real name are required": "Thông tin chứng minh nhân dân và tên thật là bắt buộc",
"Identity verification failed": "Xác minh danh tính thất bại",
"MFA email is enabled but email is empty": "MFA email đã bật nhưng email trống",
"MFA phone is enabled but phone number is empty": "MFA điện thoại đã bật nhưng số điện thoại trống",
"New password cannot contain blank space.": "Mật khẩu mới không thể chứa dấu trắng.",
"No application found for user": "Không tìm thấy ứng dụng cho người dùng",
"The new password must be different from your current password": "Mật khẩu mới phải khác với mật khẩu hiện tại của bạn",
"User is already verified": "Người dùng đã được xác minh",
"the user's owner and name should not be empty": "chủ sở hữu và tên người dùng không được để trống"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "Nhà cung cấp captcha không hợp lệ.",
"Phone number is invalid in your region %s": "Số điện thoại không hợp lệ trong vùng của bạn %s",
"The forgot password feature is disabled": "Tính năng quên mật khẩu đã bị tắt",
"The verification code has already been used!": "Mã xác thực đã được sử dụng!",
"The verification code has not been sent yet!": "Mã xác thực chưa được gửi!",
"Turing test failed.": "Kiểm định Turing thất bại.",

View File

@@ -2,7 +2,6 @@
"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": {
@@ -23,6 +22,7 @@
"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 order: %s does not exist": "订单: %s 不存在",
"The organization: %s does not exist": "组织: %s 不存在",
"The organization: %s has disabled users to signin": "组织: %s 禁止用户登录",
"The plan: %s does not exist": "计划: %s不存在",
@@ -35,7 +35,7 @@
"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": "paid-user %s 没有激活或正在等待订阅且应用: %s 没有默认",
"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 不存在"
},
@@ -57,11 +57,11 @@
"Face data mismatch": "人脸不匹配",
"Failed to parse client IP: %s": "无法解析客户端 IP 地址: %s",
"FirstName cannot be blank": "名不可以为空",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "访客用户必须通过设置用户名和密码来升级账户,然后才能直接登录",
"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": "组织不存在",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "导入群组失败",
"Failed to import users": "导入用户失败",
"Insufficient balance: new balance %v would be below credit limit %v": "余额不足:新余额 %v 将低于信用限额 %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "余额不足:新组织余额 %v 将低于信用限额 %v",
"Missing parameter": "缺少参数",
"Only admin user can specify user": "仅管理员用户可以指定用户",
"Please login first": "请先登录",
"The LDAP: %s does not exist": "LDAP: %s 不存在",
"The organization: %s should have one application at least": "组织: %s 应该拥有至少一个应用",
"The syncer: %s does not exist": "同步器: %s 不存在",
"The user: %s doesn't exist": "用户: %s不存在",
"The user: %s is not found": "用户: %s 未找到",
"User is required for User category transaction": "用户类别交易需要用户",
"Wrong userId": "错误的 userId",
"don't support captchaProvider: ": "不支持验证码提供商: ",
"this operation is not allowed in demo mode": "demo模式下不允许该操作",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "权限: \"%s\" 不存在"
},
"product": {
"Product list cannot be empty": "产品列表不能为空"
},
"provider": {
"Failed to initialize ID Verification provider": "初始化身份验证提供商失败",
"Invalid application id": "无效的应用ID",
"No ID Verification provider configured": "未配置身份验证提供商",
"Provider is not an ID Verification provider": "提供商不是身份验证提供商",
"the provider: %s does not exist": "提供商: %s不存在"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "无效的邮箱收件人: %s",
"Invalid phone receivers: %s": "无效的手机短信收信人: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "会话ID %s 是当前会话,无法删除"
},
"storage": {
"The objectKey: %s is not allowed": "objectKey: %s被禁止",
"The provider type: %s is not supported": "不支持的提供商类型: %s"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "错误"
},
"ticket": {
"Ticket not found": "工单未找到"
},
"token": {
"Grant_type: %s is not supported in this application": "该应用不支持Grant_type: %s",
"Invalid application or wrong clientSecret": "无效应用或错误的clientSecret",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "显示名称不可为空",
"ID card information and real name are required": "需要身份证信息和真实姓名",
"Identity verification failed": "身份验证失败",
"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.": "新密码不可以包含空格",
"No application found for user": "未找到用户的应用程序",
"The new password must be different from your current password": "新密码必须与您当前的密码不同",
"User is already verified": "用户已验证",
"the user's owner and name should not be empty": "用户的组织和名称不能为空"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "非法的验证码提供商",
"Phone number is invalid in your region %s": "您所在地区的电话号码无效 %s",
"The forgot password feature is disabled": "忘记密码功能已被禁用",
"The verification code has already been used!": "验证码已使用过!",
"The verification code has not been sent yet!": "验证码未发送!",
"Turing test failed.": "验证码还未发送",

View File

@@ -98,15 +98,22 @@ func Translate(language string, errorText string) string {
if langMap[language] == nil {
file, err := f.ReadFile(fmt.Sprintf("locales/%s/data.json", language))
if err != nil {
return fmt.Sprintf("Translate error: the language \"%s\" is not supported, err = %s", language, err.Error())
originalLanguage := language
language = "en"
file, err = f.ReadFile(fmt.Sprintf("locales/%s/data.json", language))
if err != nil {
return fmt.Sprintf("Translate error: the language \"%s\" is not supported, err = %s", originalLanguage, err.Error())
}
}
data := I18nData{}
err = util.JsonToStruct(string(file), &data)
if err != nil {
panic(err)
if langMap[language] == nil {
data := I18nData{}
err = util.JsonToStruct(string(file), &data)
if err != nil {
panic(err)
}
langMap[language] = data
}
langMap[language] = data
}
res := langMap[language][tokens[0]][tokens[1]]

View File

@@ -17,6 +17,7 @@ package idp
import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -102,7 +103,7 @@ func (idp *AdfsIdProvider) GetToken(code string) (*oauth2.Token, error) {
return nil, err
}
if pToken.ErrMsg != "" {
return nil, fmt.Errorf(pToken.ErrMsg)
return nil, errors.New(pToken.ErrMsg)
}
token := &oauth2.Token{

View File

@@ -264,27 +264,31 @@ func rsaSignWithRSA256(signContent string, privateKey string) (string, error) {
// privateKey in database is a string, format it to PEM style
func formatPrivateKey(privateKey string) string {
// each line length is 64
preFmtPrivateKey := ""
for i := 0; ; {
if i+64 <= len(privateKey) {
preFmtPrivateKey = preFmtPrivateKey + privateKey[i:i+64] + "\n"
i += 64
} else {
preFmtPrivateKey = preFmtPrivateKey + privateKey[i:]
break
// Check if the key is already in PEM format
if strings.HasPrefix(privateKey, "-----BEGIN PRIVATE KEY-----") ||
strings.HasPrefix(privateKey, "-----BEGIN RSA PRIVATE KEY-----") {
// Key is already in PEM format, return as is
return privateKey
}
// Remove any whitespace from the key
privateKey = strings.ReplaceAll(privateKey, "\n", "")
privateKey = strings.ReplaceAll(privateKey, "\r", "")
privateKey = strings.ReplaceAll(privateKey, " ", "")
// Format the key with line breaks every 64 characters using strings.Builder
var builder strings.Builder
for i := 0; i < len(privateKey); i += 64 {
end := i + 64
if end > len(privateKey) {
end = len(privateKey)
}
builder.WriteString(privateKey[i:end])
if end < len(privateKey) {
builder.WriteString("\n")
}
}
privateKey = strings.Trim(preFmtPrivateKey, "\n")
// add pkcs#8 BEGIN and END
PemBegin := "-----BEGIN PRIVATE KEY-----\n"
PemEnd := "\n-----END PRIVATE KEY-----"
if !strings.HasPrefix(privateKey, PemBegin) {
privateKey = PemBegin + privateKey
}
if !strings.HasSuffix(privateKey, PemEnd) {
privateKey = privateKey + PemEnd
}
return privateKey
return "-----BEGIN PRIVATE KEY-----\n" + builder.String() + "\n-----END PRIVATE KEY-----"
}

View File

@@ -16,6 +16,7 @@ package idp
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
@@ -158,7 +159,7 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
}
if dtUserInfo.OpenId == "" || dtUserInfo.UnionId == "" {
return nil, fmt.Errorf(string(data))
return nil, errors.New(string(data))
}
countryCode, err := util.GetCountryCode(dtUserInfo.StateCode, dtUserInfo.Mobile)
@@ -267,7 +268,7 @@ func (idp *DingTalkIdProvider) getUserId(unionId string, accessToken string) (st
if data.ErrCode == 60121 {
return "", fmt.Errorf("该应用只允许本企业内部用户登录,您不属于该企业,无法登录")
} else if data.ErrCode != 0 {
return "", fmt.Errorf(data.ErrMessage)
return "", errors.New(data.ErrMessage)
}
return data.Result.UserId, nil
}
@@ -294,7 +295,7 @@ func (idp *DingTalkIdProvider) getUserCorpEmail(userId string, accessToken strin
return "", "", "", err
}
if data.ErrMessage != "ok" {
return "", "", "", fmt.Errorf(data.ErrMessage)
return "", "", "", errors.New(data.ErrMessage)
}
return data.Result.Mobile, data.Result.Email, data.Result.UnionId, nil
}

View File

@@ -19,6 +19,7 @@ import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -124,7 +125,7 @@ func (idp *WeChatIdProvider) GetToken(code string) (*oauth2.Token, error) {
// {"errcode":40163,"errmsg":"code been used, rid: 6206378a-793424c0-2e4091cc"}
if strings.Contains(buf.String(), "errcode") {
return nil, fmt.Errorf(buf.String())
return nil, errors.New(buf.String())
}
var wechatAccessToken WechatAccessToken

View File

@@ -17,6 +17,7 @@ package idp
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -93,7 +94,7 @@ func (idp *WeChatMobileIdProvider) GetToken(code string) (*oauth2.Token, error)
// Check for error response
if bytes.Contains(buf.Bytes(), []byte("errcode")) {
return nil, fmt.Errorf(buf.String())
return nil, errors.New(buf.String())
}
var wechatAccessToken WechatAccessToken

View File

@@ -54,6 +54,7 @@
"pt",
"tr",
"pl",
"ru",
"uk"
],
"masterPassword": "",
@@ -67,6 +68,8 @@
{"name": "ID", "visible": true, "viewRule": "Public", "modifyRule": "Immutable"},
{"name": "Name", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
{"name": "Display name", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "First name", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Last name", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Avatar", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "User type", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
{"name": "Password", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
@@ -81,14 +84,28 @@
{"name": "Title", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "ID card type", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "ID card", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "ID card info", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "Real name", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "ID verification", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "Homepage", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Bio", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Tag", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
{"name": "Language", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Gender", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Birthday", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Education", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Balance", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "Balance credit", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "Balance currency", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "Cart", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "Transactions", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "Score", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Karma", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Ranking", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Signup application", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
{"name": "Register type", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
{"name": "Register source", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
{"name": "API key", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "Roles", "visible": true, "viewRule": "Public", "modifyRule": "Immutable"},
{"name": "Permissions", "visible": true, "viewRule": "Public", "modifyRule": "Immutable"},
{"name": "Groups", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
@@ -98,9 +115,14 @@
{"name": "Is forbidden", "visible": true, "viewRule": "Admin", "modifyRule": "Admin"},
{"name": "Is deleted", "visible": true, "viewRule": "Admin", "modifyRule": "Admin"},
{"name": "Multi-factor authentication", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "MFA items", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "WebAuthn credentials", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "Last change password time", "visible": true, "viewRule": "Admin", "modifyRule": "Admin"},
{"name": "Managed accounts", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "MFA accounts", "visible": true, "viewRule": "Self", "modifyRule": "Self"}
{"name": "Face ID", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "MFA accounts", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "Need update password", "visible": true, "viewRule": "Admin", "modifyRule": "Admin"},
{"name": "IP whitelist", "visible": true, "viewRule": "Admin", "modifyRule": "Admin"}
]
}
],

43
ip/ip.go Normal file
View File

@@ -0,0 +1,43 @@
// Copyright 2024 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 ip
import (
"fmt"
"github.com/casdoor/casdoor/util"
)
func InitIpDb() {
err := Init("ip/17monipdb.dat")
if err != nil {
panic(err)
}
}
func IsAbroadIp(ip string) bool {
// If it's an intranet IP, it's not abroad
if util.IsIntranetIp(ip) {
return false
}
info, err := Find(ip)
if err != nil {
fmt.Printf("error: ip = %s, error = %s\n", ip, err.Error())
return false
}
return info.Country != "中国"
}

199
ip/ip17mon.go Normal file
View File

@@ -0,0 +1,199 @@
// Copyright 2022 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 ip
import (
"bytes"
"encoding/binary"
"errors"
"io/ioutil"
"net"
)
const Null = "N/A"
var (
ErrInvalidIp = errors.New("invalid ip format")
std *Locator
)
// Init default locator with dataFile
func Init(dataFile string) (err error) {
if std != nil {
return
}
std, err = NewLocator(dataFile)
return
}
// Init default locator with data
func InitWithData(data []byte) {
if std != nil {
return
}
std = NewLocatorWithData(data)
return
}
// Find locationInfo by ip string
// It will return err when ipstr is not a valid format
func Find(ipstr string) (*LocationInfo, error) {
return std.Find(ipstr)
}
// Find locationInfo by uint32
func FindByUint(ip uint32) *LocationInfo {
return std.FindByUint(ip)
}
//-----------------------------------------------------------------------------
// New locator with dataFile
func NewLocator(dataFile string) (loc *Locator, err error) {
data, err := ioutil.ReadFile(dataFile)
if err != nil {
return
}
loc = NewLocatorWithData(data)
return
}
// New locator with data
func NewLocatorWithData(data []byte) (loc *Locator) {
loc = new(Locator)
loc.init(data)
return
}
type Locator struct {
textData []byte
indexData1 []uint32
indexData2 []int
indexData3 []int
index []int
}
type LocationInfo struct {
Country string
Region string
City string
Isp string
}
// Find locationInfo by ip string
// It will return err when ipstr is not a valid format
func (loc *Locator) Find(ipstr string) (info *LocationInfo, err error) {
ip := net.ParseIP(ipstr).To4()
if ip == nil || ip.To4() == nil {
err = ErrInvalidIp
return
}
info = loc.FindByUint(binary.BigEndian.Uint32([]byte(ip)))
return
}
// Find locationInfo by uint32
func (loc *Locator) FindByUint(ip uint32) (info *LocationInfo) {
end := len(loc.indexData1) - 1
if ip>>24 != 0xff {
end = loc.index[(ip>>24)+1]
}
idx := loc.findIndexOffset(ip, loc.index[ip>>24], end)
off := loc.indexData2[idx]
return newLocationInfo(loc.textData[off : off+loc.indexData3[idx]])
}
// binary search
func (loc *Locator) findIndexOffset(ip uint32, start, end int) int {
for start < end {
mid := (start + end) / 2
if ip > loc.indexData1[mid] {
start = mid + 1
} else {
end = mid
}
}
if loc.indexData1[end] >= ip {
return end
}
return start
}
func (loc *Locator) init(data []byte) {
textoff := int(binary.BigEndian.Uint32(data[:4]))
loc.textData = data[textoff-1024:]
loc.index = make([]int, 256)
for i := 0; i < 256; i++ {
off := 4 + i*4
loc.index[i] = int(binary.LittleEndian.Uint32(data[off : off+4]))
}
nidx := (textoff - 4 - 1024 - 1024) / 8
loc.indexData1 = make([]uint32, nidx)
loc.indexData2 = make([]int, nidx)
loc.indexData3 = make([]int, nidx)
for i := 0; i < nidx; i++ {
off := 4 + 1024 + i*8
loc.indexData1[i] = binary.BigEndian.Uint32(data[off : off+4])
loc.indexData2[i] = int(uint32(data[off+4]) | uint32(data[off+5])<<8 | uint32(data[off+6])<<16)
loc.indexData3[i] = int(data[off+7])
}
return
}
func newLocationInfo(str []byte) *LocationInfo {
var info *LocationInfo
fields := bytes.Split(str, []byte("\t"))
switch len(fields) {
case 4:
// free version
info = &LocationInfo{
Country: string(fields[0]),
Region: string(fields[1]),
City: string(fields[2]),
}
case 5:
// pay version
info = &LocationInfo{
Country: string(fields[0]),
Region: string(fields[1]),
City: string(fields[2]),
Isp: string(fields[4]),
}
default:
panic("unexpected ip info:" + string(str))
}
if len(info.Country) == 0 {
info.Country = Null
}
if len(info.Region) == 0 {
info.Region = Null
}
if len(info.City) == 0 {
info.City = Null
}
if len(info.Isp) == 0 {
info.Isp = Null
}
return info
}

View File

@@ -203,41 +203,103 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
return
}
for _, user := range users {
dn := fmt.Sprintf("uid=%s,cn=%s,%s", user.Id, user.Name, string(r.BaseObject()))
e := ldap.NewSearchResultEntry(dn)
uidNumberStr := fmt.Sprintf("%v", hash(user.Name))
e.AddAttribute("uidNumber", message.AttributeValue(uidNumberStr))
e.AddAttribute("gidNumber", message.AttributeValue(uidNumberStr))
e.AddAttribute("homeDirectory", message.AttributeValue("/home/"+user.Name))
e.AddAttribute("cn", message.AttributeValue(user.Name))
e.AddAttribute("uid", message.AttributeValue(user.Id))
e.AddAttribute("mail", message.AttributeValue(user.Email))
e.AddAttribute("mobile", message.AttributeValue(user.Phone))
e.AddAttribute("sn", message.AttributeValue(user.LastName))
e.AddAttribute("givenName", message.AttributeValue(user.FirstName))
for _, group := range user.Groups {
e.AddAttribute(ldapMemberOfAttr, message.AttributeValue(group))
}
attrs := r.Attributes()
for _, attr := range attrs {
if string(attr) == "*" {
attrs = AdditionalLdapAttributes
break
}
}
for _, attr := range attrs {
e.AddAttribute(message.AttributeDescription(attr), getAttribute(string(attr), user))
if string(attr) == "title" {
e.AddAttribute(message.AttributeDescription(attr), getAttribute("title", user))
}
}
orgCache := make(map[string]*object.Organization)
for _, user := range users {
if _, ok := orgCache[user.Owner]; !ok {
org, err := object.GetOrganizationByUser(user)
if err != nil {
log.Printf("handleSearch: failed to get organization for user %s: %v", user.Name, err)
}
orgCache[user.Owner] = org
}
org := orgCache[user.Owner]
e := buildUserSearchEntry(user, string(r.BaseObject()), resolveRequestAttributes(r.Attributes()), org)
w.Write(e)
}
w.Write(res)
}
// resolveRequestAttributes expands the "*" wildcard to the full list of additional LDAP attributes.
func resolveRequestAttributes(attrs message.AttributeSelection) []string {
result := make([]string, 0, len(attrs))
for _, attr := range attrs {
if string(attr) == "*" {
result = make([]string, 0, len(AdditionalLdapAttributes))
for _, a := range AdditionalLdapAttributes {
result = append(result, string(a))
}
return result
}
result = append(result, string(attr))
}
return result
}
// buildUserSearchEntry constructs an LDAP search result entry for the given user,
// respecting the organization's LdapAttributes filter.
func buildUserSearchEntry(user *object.User, baseDN string, attrs []string, org *object.Organization) message.SearchResultEntry {
dn := fmt.Sprintf("uid=%s,cn=%s,%s", user.Id, user.Name, baseDN)
e := ldap.NewSearchResultEntry(dn)
uidNumberStr := fmt.Sprintf("%v", hash(user.Name))
if IsLdapAttrAllowed(org, "uidNumber") {
e.AddAttribute("uidNumber", message.AttributeValue(uidNumberStr))
}
if IsLdapAttrAllowed(org, "gidNumber") {
e.AddAttribute("gidNumber", message.AttributeValue(uidNumberStr))
}
if IsLdapAttrAllowed(org, "homeDirectory") {
e.AddAttribute("homeDirectory", message.AttributeValue("/home/"+user.Name))
}
if IsLdapAttrAllowed(org, "cn") {
e.AddAttribute("cn", message.AttributeValue(user.Name))
}
if IsLdapAttrAllowed(org, "uid") {
e.AddAttribute("uid", message.AttributeValue(user.Id))
}
if IsLdapAttrAllowed(org, "mail") {
e.AddAttribute("mail", message.AttributeValue(user.Email))
}
if IsLdapAttrAllowed(org, "mobile") {
e.AddAttribute("mobile", message.AttributeValue(user.Phone))
}
if IsLdapAttrAllowed(org, "sn") {
e.AddAttribute("sn", message.AttributeValue(user.LastName))
}
if IsLdapAttrAllowed(org, "givenName") {
e.AddAttribute("givenName", message.AttributeValue(user.FirstName))
}
// Add POSIX attributes for Linux machine login support
if IsLdapAttrAllowed(org, "loginShell") {
e.AddAttribute("loginShell", getAttribute("loginShell", user))
}
if IsLdapAttrAllowed(org, "gecos") {
e.AddAttribute("gecos", getAttribute("gecos", user))
}
// Add SSH public key if available
if IsLdapAttrAllowed(org, "sshPublicKey") {
sshKey := getAttribute("sshPublicKey", user)
if sshKey != "" {
e.AddAttribute("sshPublicKey", sshKey)
}
}
// Add objectClass for posixAccount
e.AddAttribute("objectClass", "posixAccount")
if IsLdapAttrAllowed(org, ldapMemberOfAttr) {
for _, group := range user.Groups {
e.AddAttribute(ldapMemberOfAttr, message.AttributeValue(group))
}
}
for _, attr := range attrs {
if !IsLdapAttrAllowed(org, attr) {
continue
}
e.AddAttribute(message.AttributeDescription(attr), getAttribute(attr, user))
}
return e
}
func handleRootSearch(w ldap.ResponseWriter, r *message.SearchRequest, res *message.SearchResultDone, m *ldap.Message) {
if len(r.Attributes()) == 0 {
w.Write(res)

View File

@@ -83,6 +83,45 @@ var ldapAttributesMapping = map[string]FieldRelation{
return message.AttributeValue(getUserPasswordWithType(user))
},
},
"loginShell": {
userField: "loginShell",
notSearchable: true,
fieldMapper: func(user *object.User) message.AttributeValue {
// Check user properties first, otherwise return default shell
if user.Properties != nil {
if shell, ok := user.Properties["loginShell"]; ok && shell != "" {
return message.AttributeValue(shell)
}
}
return message.AttributeValue("/bin/bash")
},
},
"gecos": {
userField: "gecos",
notSearchable: true,
fieldMapper: func(user *object.User) message.AttributeValue {
// GECOS field typically contains full name and other user info
// Format: Full Name,Room Number,Work Phone,Home Phone,Other
gecos := user.DisplayName
if gecos == "" {
gecos = user.Name
}
return message.AttributeValue(gecos)
},
},
"sshPublicKey": {
userField: "sshPublicKey",
notSearchable: true,
fieldMapper: func(user *object.User) message.AttributeValue {
// Return SSH public key from user properties
if user.Properties != nil {
if sshKey, ok := user.Properties["sshPublicKey"]; ok && sshKey != "" {
return message.AttributeValue(sshKey)
}
}
return message.AttributeValue("")
},
},
}
const ldapMemberOfAttr = "memberOf"
@@ -159,6 +198,20 @@ func stringInSlice(value string, list []string) bool {
return false
}
// IsLdapAttrAllowed checks whether the given LDAP attribute is allowed for the organization.
// An empty filter or a filter containing "All" means all attributes are allowed.
func IsLdapAttrAllowed(org *object.Organization, attr string) bool {
if org == nil || len(org.LdapAttributes) == 0 {
return true
}
for _, f := range org.LdapAttributes {
if strings.EqualFold(f, "All") || strings.EqualFold(f, attr) {
return true
}
}
return false
}
func buildUserFilterCondition(filter interface{}) (builder.Cond, error) {
switch f := filter.(type) {
case message.FilterAnd:

63
log/agent_openclaw.go Normal file
View File

@@ -0,0 +1,63 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package log
import "fmt"
// OtlpAdder persists a single OTLP record into the backing store.
// Parameters: entryType ("trace"/"metrics"/"log"), message (JSON payload),
// clientIp and userAgent from the originating HTTP request.
// The unique entry name is generated by the implementation.
type OtlpAdder func(entryType, message, clientIp, userAgent string) error
// OpenClawProvider receives OpenTelemetry data pushed by an OpenClaw agent over
// HTTP and persists each record as an Entry row via the OtlpAdder supplied at
// construction time. It is passive (push-based via HTTP): Start/Stop are no-ops
// and Write is not applicable.
type OpenClawProvider struct {
providerName string
addOtlpEntry OtlpAdder
}
// NewOpenClawProvider creates an OpenClawProvider backed by addOtlpEntry.
func NewOpenClawProvider(providerName string, addOtlpEntry OtlpAdder) *OpenClawProvider {
return &OpenClawProvider{providerName: providerName, addOtlpEntry: addOtlpEntry}
}
// Write is not applicable for an HTTP-push provider and always returns an error.
func (p *OpenClawProvider) Write(_, _ string) error {
return fmt.Errorf("OpenClawProvider receives data over HTTP and does not accept Write calls")
}
// Start is a no-op; OpenClawProvider is passive and has no background goroutine.
func (p *OpenClawProvider) Start(_ EntryAdder, _ func(error)) error { return nil }
// Stop is a no-op.
func (p *OpenClawProvider) Stop() error { return nil }
// AddTrace persists an OTLP trace payload (already serialised to JSON).
func (p *OpenClawProvider) AddTrace(message []byte, clientIp, userAgent string) error {
return p.addOtlpEntry("trace", string(message), clientIp, userAgent)
}
// AddMetrics persists an OTLP metrics payload (already serialised to JSON).
func (p *OpenClawProvider) AddMetrics(message []byte, clientIp, userAgent string) error {
return p.addOtlpEntry("metrics", string(message), clientIp, userAgent)
}
// AddLogs persists an OTLP logs payload (already serialised to JSON).
func (p *OpenClawProvider) AddLogs(message []byte, clientIp, userAgent string) error {
return p.addOtlpEntry("log", string(message), clientIp, userAgent)
}

47
log/casdoor_permission.go Normal file
View File

@@ -0,0 +1,47 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package log
import (
"fmt"
"time"
)
// PermissionLogProvider records Casbin authorization decisions as Entry rows.
// It is push-based: callers supply log lines via Write, which are immediately
// persisted through the injected EntryAdder. Start and Stop are no-ops.
type PermissionLogProvider struct {
providerName string
addEntry EntryAdder
}
// NewPermissionLogProvider creates a PermissionLogProvider backed by addEntry.
func NewPermissionLogProvider(providerName string, addEntry EntryAdder) *PermissionLogProvider {
return &PermissionLogProvider{providerName: providerName, addEntry: addEntry}
}
// Write stores one permission-log entry.
// severity follows syslog conventions (e.g. info, warning, err).
func (p *PermissionLogProvider) Write(severity string, message string) error {
createdTime := time.Now().UTC().Format(time.RFC3339)
return p.addEntry("built-in", createdTime, p.providerName, fmt.Sprintf("[%s] %s", severity, message))
}
// Start is a no-op for PermissionLogProvider; it received its EntryAdder at
// construction time and does not require background collection.
func (p *PermissionLogProvider) Start(_ EntryAdder, _ func(error)) error { return nil }
// Stop is a no-op for PermissionLogProvider.
func (p *PermissionLogProvider) Stop() error { return nil }

75
log/provider.go Normal file
View File

@@ -0,0 +1,75 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package log
import (
"fmt"
"github.com/thanhpk/randstr"
)
// GenerateEntryName returns a cryptographically random 32-character hex string
// suitable for use as an Entry.Name primary key.
func GenerateEntryName() string {
return randstr.Hex(16)
}
// EntryAdder persists a collected log entry into the backing store.
// Parameters map to the Entry table columns: owner, createdTime (RFC3339),
// provider (the log provider name), and message. The unique entry name is
// generated by the implementation, so callers do not need to supply one.
// Defined here so it is shared by all LogProvider implementations without
// creating import cycles with the object package.
type EntryAdder func(owner, createdTime, provider, message string) error
// LogProvider is the common interface for all log providers.
//
// Push-based providers (e.g. PermissionLogProvider) receive individual log
// lines through Write and persist them immediately. Start and Stop are no-ops
// for these providers.
//
// Pull-based providers (e.g. SystemLogProvider) actively collect logs from an
// external source. Start begins a background collection goroutine that calls
// addEntry for every new record; Stop halts collection. Write returns an error
// for these providers as they are not designed to accept external input.
type LogProvider interface {
// Write records a single log line. Used by push-based providers.
Write(severity string, message string) error
// Start begins background log collection with the given EntryAdder.
// For push-based providers this is a no-op (they received addEntry at
// construction time). onError is called from the background goroutine
// when collection stops with a fatal error; it may be nil.
Start(addEntry EntryAdder, onError func(error)) error
// Stop halts background collection and releases any OS resources.
Stop() error
}
// GetLogProvider returns a concrete log provider for the given type and connection settings.
// The title parameter is used as the OS log tag for System Log.
// Types that are not yet implemented return a non-nil error.
func GetLogProvider(typ string, _ string, _ int, title string) (LogProvider, error) {
switch typ {
case "System Log":
tag := title
if tag == "" {
tag = "casdoor"
}
return NewSystemLogProvider(tag)
case "SELinux Log":
return NewSELinuxLogProvider()
default:
return nil, fmt.Errorf("unsupported log provider type: %s", typ)
}
}

66
log/selinux_log.go Normal file
View File

@@ -0,0 +1,66 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package log
import (
"context"
"fmt"
)
// SELinuxLogProvider collects SELinux audit events (AVC denials and related
// records) from the local system and stores each record as an Entry row via
// the EntryAdder supplied to Start.
//
// It is pull-based: Write is not applicable and returns an error.
// Start launches the background collector; Stop cancels it.
// On platforms where SELinux is not supported, Start returns an error.
type SELinuxLogProvider struct {
cancel context.CancelFunc
}
// NewSELinuxLogProvider creates a SELinuxLogProvider.
// Call Start to begin collection.
func NewSELinuxLogProvider() (*SELinuxLogProvider, error) {
return &SELinuxLogProvider{}, nil
}
// Write is not applicable for a pull-based collector and always returns an error.
func (s *SELinuxLogProvider) Write(severity string, message string) error {
return fmt.Errorf("SELinuxLogProvider is a log collector and does not accept Write calls")
}
// Start launches a background goroutine that reads new SELinux audit records
// and persists each one by calling addEntry. Returns immediately; collection
// runs until Stop is called. If the goroutine encounters a fatal error,
// onError is called with that error (onError may be nil).
func (s *SELinuxLogProvider) Start(addEntry EntryAdder, onError func(error)) error {
ctx, cancel := context.WithCancel(context.Background())
s.cancel = cancel
go func() {
if err := collectSELinuxLogs(ctx, addEntry); err != nil && onError != nil {
onError(err)
}
}()
return nil
}
// Stop cancels background collection. It is safe to call multiple times.
func (s *SELinuxLogProvider) Stop() error {
if s.cancel != nil {
s.cancel()
s.cancel = nil
}
return nil
}

140
log/selinux_log_linux.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.
//go:build linux
package log
import (
"bufio"
"context"
"fmt"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
)
const auditLogPath = "/var/log/audit/audit.log"
// selinuxAuditTypes is the set of audit record types that are SELinux-related.
var selinuxAuditTypes = map[string]bool{
"AVC": true,
"USER_AVC": true,
"SELINUX_ERR": true,
"MAC_POLICY_LOAD": true,
"MAC_STATUS": true,
}
// auditTimestampRe matches the msg=audit(seconds.millis:serial) field.
var auditTimestampRe = regexp.MustCompile(`msg=audit\((\d+)\.\d+:\d+\)`)
// CheckSELinuxAvailable returns nil if SELinux is active and the audit log is
// readable on this system. Returns a descriptive error otherwise.
func CheckSELinuxAvailable() error {
if _, err := os.Stat("/sys/fs/selinux/enforce"); os.IsNotExist(err) {
return fmt.Errorf("SELinux is not available or not mounted on this system")
}
if _, err := os.Stat(auditLogPath); os.IsNotExist(err) {
return fmt.Errorf("SELinux audit log not found at %s (is auditd running?)", auditLogPath)
}
return nil
}
// collectSELinuxLogs tails /var/log/audit/audit.log and persists each
// SELinux-related audit record via addEntry until ctx is cancelled.
func collectSELinuxLogs(ctx context.Context, addEntry EntryAdder) error {
if err := CheckSELinuxAvailable(); err != nil {
return fmt.Errorf("SELinuxLogProvider: %w", err)
}
cmd := exec.CommandContext(ctx, "tail", "-f", "-n", "0", auditLogPath)
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("SELinuxLogProvider: failed to open audit log pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("SELinuxLogProvider: failed to start tail: %w", err)
}
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
select {
case <-ctx.Done():
return nil
default:
}
line := scanner.Text()
if !isSELinuxAuditLine(line) {
continue
}
severity := selinuxSeverity(line)
createdTime := parseAuditTimestamp(line)
if err := addEntry("built-in", createdTime, "",
fmt.Sprintf("[%s] %s", severity, line)); err != nil {
return fmt.Errorf("SELinuxLogProvider: failed to persist audit entry: %w", err)
}
}
if err := scanner.Err(); err != nil {
if ctx.Err() != nil {
return nil
}
return fmt.Errorf("SELinuxLogProvider: audit log read error: %w", err)
}
return nil
}
// isSELinuxAuditLine reports whether the audit log line is an SELinux record.
func isSELinuxAuditLine(line string) bool {
// Audit lines start with "type=<TYPE> "
const prefix = "type="
if !strings.HasPrefix(line, prefix) {
return false
}
end := strings.IndexByte(line[len(prefix):], ' ')
var typ string
if end < 0 {
typ = line[len(prefix):]
} else {
typ = line[len(prefix) : len(prefix)+end]
}
return selinuxAuditTypes[typ]
}
// selinuxSeverity maps SELinux audit record types to a syslog severity name.
func selinuxSeverity(line string) string {
if strings.HasPrefix(line, "type=AVC") || strings.HasPrefix(line, "type=USER_AVC") || strings.HasPrefix(line, "type=SELINUX_ERR") {
return "warning"
}
return "info"
}
// parseAuditTimestamp extracts the Unix timestamp from an audit log line and
// returns it as an RFC3339 string. Falls back to the current time on failure.
func parseAuditTimestamp(line string) string {
m := auditTimestampRe.FindStringSubmatch(line)
if m == nil {
return time.Now().UTC().Format(time.RFC3339)
}
sec, err := strconv.ParseInt(m[1], 10, 64)
if err != nil {
return time.Now().UTC().Format(time.RFC3339)
}
return time.Unix(sec, 0).UTC().Format(time.RFC3339)
}

View File

@@ -0,0 +1,33 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !linux
package log
import (
"context"
"fmt"
"runtime"
)
// CheckSELinuxAvailable always returns an error on non-Linux platforms.
func CheckSELinuxAvailable() error {
return fmt.Errorf("SELinux is not supported on %s", runtime.GOOS)
}
// collectSELinuxLogs is a no-op on non-Linux platforms.
func collectSELinuxLogs(_ context.Context, _ EntryAdder) error {
return CheckSELinuxAvailable()
}

79
log/system_log.go Normal file
View File

@@ -0,0 +1,79 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package log
import (
"context"
"fmt"
)
// platformCollector is an OS-specific log reader.
// Implementations are in system_log_unix.go and system_log_windows.go.
type platformCollector interface {
// collect blocks and streams new OS log records to addEntry until ctx is
// cancelled or a fatal error occurs. It must return promptly when
// ctx.Done() is closed. A non-nil error means collection stopped
// unexpectedly and should be reported to the operator.
collect(ctx context.Context, addEntry EntryAdder) error
}
// SystemLogProvider collects log records from the operating-system's native
// logging facility (journald/syslog on Linux/Unix, Event Log on Windows) and
// stores each record as an Entry row via the EntryAdder supplied to Start.
//
// It is pull-based: Write is not applicable and returns an error.
// Start launches the background collector; Stop cancels it.
type SystemLogProvider struct {
tag string
cancel context.CancelFunc
}
// NewSystemLogProvider creates a SystemLogProvider that will identify itself
// with the given tag when collecting OS log records.
// Call Start to begin collection.
func NewSystemLogProvider(tag string) (*SystemLogProvider, error) {
return &SystemLogProvider{tag: tag}, nil
}
// Write is not applicable for a pull-based collector and always returns an
// error. Callers in the permission-log path should skip System Log providers.
func (s *SystemLogProvider) Write(severity string, message string) error {
return fmt.Errorf("SystemLogProvider is a log collector and does not accept Write calls")
}
// Start launches a background goroutine that reads new OS log records and
// persists each one by calling addEntry. It returns immediately; collection
// runs until Stop is called. If the goroutine encounters a fatal error,
// onError is called with that error (onError may be nil).
func (s *SystemLogProvider) Start(addEntry EntryAdder, onError func(error)) error {
ctx, cancel := context.WithCancel(context.Background())
s.cancel = cancel
collector := newPlatformCollector(s.tag)
go func() {
if err := collector.collect(ctx, addEntry); err != nil && onError != nil {
onError(err)
}
}()
return nil
}
// Stop cancels background collection. It is safe to call multiple times.
func (s *SystemLogProvider) Stop() error {
if s.cancel != nil {
s.cancel()
s.cancel = nil
}
return nil
}

121
log/system_log_unix.go Normal file
View File

@@ -0,0 +1,121 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !windows
package log
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os/exec"
"strconv"
"time"
)
type unixCollector struct {
tag string
}
func newPlatformCollector(tag string) platformCollector {
return &unixCollector{tag: tag}
}
// collect streams new journald records to addEntry until ctx is cancelled or
// a fatal error occurs. It runs `journalctl -n 0 -f --output=json` so only
// records that arrive after Start is called are collected (no backfill).
// Returns nil when ctx is cancelled normally; returns a non-nil error if the
// process could not be started or the output pipe broke unexpectedly.
func (u *unixCollector) collect(ctx context.Context, addEntry EntryAdder) error {
cmd := exec.CommandContext(ctx, "journalctl", "-n", "0", "-f", "--output=json")
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("SystemLogProvider: failed to open journalctl stdout pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("SystemLogProvider: failed to start journalctl: %w", err)
}
scanner := bufio.NewScanner(stdout)
// journald JSON lines can be large; use a 1 MB buffer.
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
for scanner.Scan() {
select {
case <-ctx.Done():
return nil
default:
}
var fields map[string]interface{}
if err := json.Unmarshal(scanner.Bytes(), &fields); err != nil {
continue
}
severity := journalSeverity(fields)
message := journalMessage(fields)
createdTime := journalTimestamp(fields)
if err := addEntry("built-in", createdTime, u.tag,
fmt.Sprintf("[%s] %s", severity, message)); err != nil {
return fmt.Errorf("SystemLogProvider: failed to persist journal entry: %w", err)
}
}
if err := scanner.Err(); err != nil {
// A cancelled context causes the pipe to close; treat that as normal exit.
if ctx.Err() != nil {
return nil
}
return fmt.Errorf("SystemLogProvider: journalctl output error: %w", err)
}
return nil
}
// journalSeverity maps the journald PRIORITY field to a syslog severity name.
// PRIORITY values: 0=emerg 1=alert 2=crit 3=err 4=warning 5=notice 6=info 7=debug
func journalSeverity(fields map[string]interface{}) string {
mapping := map[string]string{
"0": "emerg", "1": "alert", "2": "crit", "3": "err",
"4": "warning", "5": "notice", "6": "info", "7": "debug",
}
if p, ok := fields["PRIORITY"].(string); ok {
if s, ok2 := mapping[p]; ok2 {
return s
}
}
return "info"
}
// journalMessage extracts the human-readable message from journald JSON.
func journalMessage(fields map[string]interface{}) string {
if msg, ok := fields["MESSAGE"].(string); ok {
return msg
}
return ""
}
// journalTimestamp converts the journald __REALTIME_TIMESTAMP (microseconds
// since Unix epoch) to an RFC3339 string.
func journalTimestamp(fields map[string]interface{}) string {
if ts, ok := fields["__REALTIME_TIMESTAMP"].(string); ok {
usec, err := strconv.ParseInt(ts, 10, 64)
if err == nil {
t := time.Unix(usec/1_000_000, (usec%1_000_000)*1_000).UTC()
return t.Format(time.RFC3339)
}
}
return time.Now().UTC().Format(time.RFC3339)
}

180
log/system_log_windows.go Normal file
View File

@@ -0,0 +1,180 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package log
import (
"context"
"encoding/xml"
"fmt"
"io"
"os/exec"
"strings"
"time"
)
// Windows Event Log channels to collect from.
var eventLogChannels = []string{"System", "Application"}
type windowsCollector struct {
tag string
}
func newPlatformCollector(tag string) platformCollector {
return &windowsCollector{tag: tag}
}
// collect polls Windows Event Log channels every 5 seconds via wevtutil.exe
// and persists new records to addEntry. Only events that arrive after Start
// is called are collected; historical events are not backfilled.
// Returns nil when ctx is cancelled normally.
func (w *windowsCollector) collect(ctx context.Context, addEntry EntryAdder) error {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
lastCheck := time.Now().UTC()
for {
select {
case <-ctx.Done():
return nil
case tick := <-ticker.C:
for _, channel := range eventLogChannels {
if err := w.queryChannel(ctx, channel, lastCheck, addEntry); err != nil {
return fmt.Errorf("SystemLogProvider: error querying channel %s: %w", channel, err)
}
}
lastCheck = tick.UTC()
}
}
}
// queryChannel runs wevtutil.exe to fetch events from channel that were
// created after since, then stores each event via addEntry.
// Returns a non-nil error if the wevtutil command fails or XML parsing fails.
func (w *windowsCollector) queryChannel(ctx context.Context, channel string, since time.Time, addEntry EntryAdder) error {
sinceStr := since.Format("2006-01-02T15:04:05.000Z")
query := fmt.Sprintf("*[System[TimeCreated[@SystemTime>='%s']]]", sinceStr)
cmd := exec.CommandContext(ctx, "wevtutil.exe", "qe", channel,
"/f:RenderedXml", "/rd:false",
fmt.Sprintf("/q:%s", query),
)
out, err := cmd.Output()
if err != nil {
// A cancelled context is a normal shutdown, not an error.
if ctx.Err() != nil {
return nil
}
return fmt.Errorf("wevtutil.exe failed for channel %s: %w", channel, err)
}
if len(out) == 0 {
return nil
}
return w.parseAndPersistEvents(out, channel, addEntry)
}
// parseAndPersistEvents decodes wevtutil XML output and persists each Event
// record via addEntry. wevtutil outputs one <Event> element per record;
// the output is wrapped in a synthetic <Events> root so the decoder can
// handle multiple records in one pass. Token()+DecodeElement() is used to
// skip the wrapper element without triggering an XMLName mismatch error.
func (w *windowsCollector) parseAndPersistEvents(out []byte, channel string, addEntry EntryAdder) error {
wrapped := "<Events>" + string(out) + "</Events>"
decoder := xml.NewDecoder(strings.NewReader(wrapped))
for {
token, err := decoder.Token()
if err != nil {
if err == io.EOF {
break
}
return fmt.Errorf("SystemLogProvider: failed to parse event XML (channel=%s): %w", channel, err)
}
se, ok := token.(xml.StartElement)
if !ok || se.Name.Local != "Event" {
continue
}
var event winEvent
if err := decoder.DecodeElement(&event, &se); err != nil {
return fmt.Errorf("SystemLogProvider: failed to decode event XML (channel=%s): %w", channel, err)
}
severity := winEventSeverity(event.System.Level)
message := strings.TrimSpace(event.RenderingInfo.Message)
if message == "" {
message = fmt.Sprintf("EventID=%d Source=%s", event.System.EventID, event.System.Provider.Name)
}
createdTime := winEventTimestamp(event.System.TimeCreated.SystemTime)
if err := addEntry("built-in", createdTime, w.tag,
fmt.Sprintf("[%s] [%s] %s", severity, channel, message)); err != nil {
return fmt.Errorf("SystemLogProvider: failed to persist event (channel=%s EventID=%d): %w",
channel, event.System.EventID, err)
}
}
return nil
}
// winEvent represents the subset of the Windows Event XML schema that we need.
type winEvent struct {
XMLName xml.Name `xml:"Event"`
System struct {
Provider struct {
Name string `xml:"Name,attr"`
} `xml:"Provider"`
EventID int `xml:"EventID"`
Level int `xml:"Level"`
TimeCreated struct {
SystemTime string `xml:"SystemTime,attr"`
} `xml:"TimeCreated"`
} `xml:"System"`
RenderingInfo struct {
Message string `xml:"Message"`
} `xml:"RenderingInfo"`
}
// winEventSeverity maps Windows Event Log Level values to syslog severity names.
// Level: 1=Critical 2=Error 3=Warning 4=Information 5=Verbose
func winEventSeverity(level int) string {
switch level {
case 1:
return "crit"
case 2:
return "err"
case 3:
return "warning"
case 5:
return "debug"
default: // 4=Information and anything else
return "info"
}
}
// winEventTimestamp parses a Windows Event SystemTime attribute string to RFC3339.
func winEventTimestamp(s string) string {
// SystemTime is in the form "2024-01-15T10:30:00.000000000Z"
t, err := time.Parse(time.RFC3339Nano, s)
if err != nil {
// Try without nanoseconds
t, err = time.Parse("2006-01-02T15:04:05.000000000Z", s)
if err != nil {
return time.Now().UTC().Format(time.RFC3339)
}
}
return t.UTC().Format(time.RFC3339)
}

17
main.go
View File

@@ -29,6 +29,7 @@ import (
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/radius"
"github.com/casdoor/casdoor/routers"
"github.com/casdoor/casdoor/service"
"github.com/casdoor/casdoor/util"
)
@@ -65,14 +66,20 @@ func main() {
}
object.InitDefaultStorageProvider()
object.InitLogProviders()
object.InitLdapAutoSynchronizer()
proxy.InitHttpClient()
authz.InitApi()
object.InitUserManager()
object.InitFromFile()
object.InitCasvisorConfig()
object.InitCleanupTokens()
object.InitSiteMap()
if len(object.SiteMap) != 0 {
object.InitRuleMap()
object.StartMonitorSitesLoop()
}
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
util.SafeGoroutine(func() { controllers.InitCLIDownloader() })
@@ -83,6 +90,7 @@ func main() {
web.SetStaticPath("/swagger", "swagger")
web.SetStaticPath("/files", "files")
// https://studygolang.com/articles/2303
web.InsertFilter("*", web.BeforeStatic, routers.RequestBodyFilter)
web.InsertFilter("*", web.BeforeRouter, routers.StaticFilter)
web.InsertFilter("*", web.BeforeRouter, routers.AutoSigninFilter)
web.InsertFilter("*", web.BeforeRouter, routers.CorsFilter)
@@ -126,5 +134,12 @@ func main() {
go radius.StartRadiusServer()
go object.ClearThroughputPerSecond()
// Start webhook delivery worker
object.StartWebhookDeliveryWorker()
if len(object.SiteMap) != 0 {
service.Start()
}
web.Run(fmt.Sprintf(":%v", port))
}

367
mcp/util.go Normal file
View File

@@ -0,0 +1,367 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mcp
import (
"context"
"encoding/binary"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"time"
"github.com/casdoor/casdoor/util"
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
"golang.org/x/oauth2"
)
type InnerMcpServer struct {
Host string `json:"host"`
Port int `json:"port"`
Path string `json:"path"`
Url string `json:"url"`
}
func GetServerTools(owner, name, url, token string) ([]*mcpsdk.Tool, error) {
var session *mcpsdk.ClientSession
var err error
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10)
defer cancel()
client := mcpsdk.NewClient(&mcpsdk.Implementation{Name: util.GetId(owner, name), Version: "1.0.0"}, nil)
if strings.HasSuffix(url, "sse") {
if token != "" {
httpClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}))
session, err = client.Connect(ctx, &mcpsdk.StreamableClientTransport{Endpoint: url, HTTPClient: httpClient}, nil)
} else {
session, err = client.Connect(ctx, &mcpsdk.StreamableClientTransport{Endpoint: url}, nil)
}
} else {
if token != "" {
httpClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}))
session, err = client.Connect(ctx, &mcpsdk.StreamableClientTransport{Endpoint: url, HTTPClient: httpClient}, nil)
} else {
session, err = client.Connect(ctx, &mcpsdk.StreamableClientTransport{Endpoint: url}, nil)
}
}
if err != nil {
return nil, err
}
defer session.Close()
toolResult, err := session.ListTools(ctx, nil)
if err != nil {
return nil, err
}
return toolResult.Tools, nil
}
func SanitizeScheme(scheme string) string {
scheme = strings.ToLower(strings.TrimSpace(scheme))
if scheme == "https" {
return "https"
}
return "http"
}
func SanitizeTimeout(timeoutMs int, defaultTimeoutMs int, maxTimeoutMs int) time.Duration {
if timeoutMs <= 0 {
timeoutMs = defaultTimeoutMs
}
if timeoutMs > maxTimeoutMs {
timeoutMs = maxTimeoutMs
}
return time.Duration(timeoutMs) * time.Millisecond
}
func SanitizeConcurrency(maxConcurrency int, defaultConcurrency int, maxAllowed int) int {
if maxConcurrency <= 0 {
maxConcurrency = defaultConcurrency
}
if maxConcurrency > maxAllowed {
maxConcurrency = maxAllowed
}
return maxConcurrency
}
func SanitizePorts(portInputs []string, defaultPorts []int) []int {
if len(portInputs) == 0 {
return append([]int{}, defaultPorts...)
}
portSet := map[int]struct{}{}
result := make([]int, 0, len(portInputs))
for _, portInput := range portInputs {
portInput = strings.TrimSpace(portInput)
if portInput == "" {
continue
}
if strings.Contains(portInput, "-") {
parts := strings.SplitN(portInput, "-", 2)
if len(parts) != 2 {
continue
}
start, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil {
continue
}
end, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
continue
}
if start > end {
continue
}
if start < 1 {
start = 1
}
if end > 65535 {
end = 65535
}
if start > end {
continue
}
for port := start; port <= end; port++ {
if _, ok := portSet[port]; ok {
continue
}
portSet[port] = struct{}{}
result = append(result, port)
}
continue
}
port, err := strconv.Atoi(portInput)
if err != nil {
continue
}
if port <= 0 || port > 65535 {
continue
}
if _, ok := portSet[port]; ok {
continue
}
portSet[port] = struct{}{}
result = append(result, port)
}
if len(result) == 0 {
return append([]int{}, defaultPorts...)
}
return result
}
func SanitizePaths(paths []string, defaultPaths []string) []string {
if len(paths) == 0 {
return append([]string{}, defaultPaths...)
}
pathSet := map[string]struct{}{}
result := make([]string, 0, len(paths))
for _, path := range paths {
path = strings.TrimSpace(path)
if path == "" {
continue
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
if _, ok := pathSet[path]; ok {
continue
}
pathSet[path] = struct{}{}
result = append(result, path)
}
if len(result) == 0 {
return append([]string{}, defaultPaths...)
}
return result
}
func ParseScanTargets(targets []string, maxHosts int) ([]net.IP, error) {
hostSet := map[uint32]struct{}{}
hosts := make([]net.IP, 0)
addHost := func(ipv4 net.IP) error {
value := binary.BigEndian.Uint32(ipv4)
if _, ok := hostSet[value]; ok {
return nil
}
if len(hosts) >= maxHosts {
return fmt.Errorf("scan targets exceed max %d hosts", maxHosts)
}
hostSet[value] = struct{}{}
host := make(net.IP, net.IPv4len)
copy(host, ipv4)
hosts = append(hosts, host)
return nil
}
for _, target := range targets {
target = strings.TrimSpace(target)
if target == "" {
continue
}
if ip := net.ParseIP(target); ip != nil {
ipv4 := ip.To4()
if ipv4 == nil {
return nil, fmt.Errorf("only IPv4 is supported: %s", target)
}
if !util.IsIntranetIp(ipv4.String()) {
return nil, fmt.Errorf("target must be intranet: %s", target)
}
if err := addHost(ipv4); err != nil {
return nil, err
}
continue
}
cidrHosts, err := ParseCIDRHosts(target, maxHosts)
if err != nil {
return nil, err
}
for _, host := range cidrHosts {
if !util.IsIntranetIp(host.String()) {
return nil, fmt.Errorf("target must be intranet: %s", target)
}
if err = addHost(host.To4()); err != nil {
return nil, err
}
}
}
if len(hosts) == 0 {
return nil, fmt.Errorf("cidr is required")
}
return hosts, nil
}
func ParseCIDRHosts(cidr string, maxHosts int) ([]net.IP, error) {
baseIp, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
return nil, err
}
ipv4 := baseIp.To4()
if ipv4 == nil {
return nil, fmt.Errorf("only IPv4 CIDR is supported")
}
if !util.IsIntranetIp(ipv4.String()) {
return nil, fmt.Errorf("cidr must be intranet: %s", cidr)
}
ones, bits := ipNet.Mask.Size()
hostBits := bits - ones
if hostBits < 0 {
return nil, fmt.Errorf("invalid cidr mask: %s", cidr)
}
if hostBits >= 63 {
return nil, fmt.Errorf("cidr range is too large")
}
total := uint64(1) << hostBits
if total > uint64(maxHosts)+2 {
return nil, fmt.Errorf("cidr range is too large, max %d hosts", maxHosts)
}
totalInt := int(total)
start := binary.BigEndian.Uint32(ipv4.Mask(ipNet.Mask))
end := start + uint32(total) - 1
hosts := make([]net.IP, 0, totalInt)
for value := start; value <= end; value++ {
if total > 2 && (value == start || value == end) {
continue
}
candidate := make(net.IP, net.IPv4len)
binary.BigEndian.PutUint32(candidate, value)
if ipNet.Contains(candidate) {
hosts = append(hosts, candidate)
}
}
if len(hosts) == 0 {
return nil, fmt.Errorf("cidr has no usable hosts: %s", cidr)
}
return hosts, nil
}
func ProbeHost(ctx context.Context, client *http.Client, scheme, host string, ports []int, paths []string, timeout time.Duration) (bool, []*InnerMcpServer) {
if !util.IsIntranetIp(host) {
return false, nil
}
dialer := &net.Dialer{Timeout: timeout}
isOnline := false
var servers []*InnerMcpServer
for _, port := range ports {
address := net.JoinHostPort(host, strconv.Itoa(port))
conn, err := dialer.DialContext(ctx, "tcp", address)
if err != nil {
continue
}
_ = conn.Close()
isOnline = true
for _, path := range paths {
server, ok := probeMcpInitialize(ctx, client, scheme, host, port, path)
if ok {
servers = append(servers, server)
}
}
}
return isOnline, servers
}
func probeMcpInitialize(ctx context.Context, client *http.Client, scheme, host string, port int, path string) (*InnerMcpServer, bool) {
fullUrl := fmt.Sprintf("%s://%s%s", scheme, net.JoinHostPort(host, strconv.Itoa(port)), path)
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, fullUrl, nil)
if err != nil {
return nil, false
}
resp, err := client.Do(httpReq)
if err != nil {
return nil, false
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode == http.StatusNotFound {
return nil, false
}
return &InnerMcpServer{
Host: host,
Port: port,
Path: path,
Url: fullUrl,
}, true
}

View File

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

View File

@@ -12,9 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package mcp
package mcpself
import (
"strings"
"time"
"github.com/casdoor/casdoor/object"
@@ -120,3 +121,58 @@ func (c *McpController) GetAcceptLanguage() string {
}
return language
}
// GetTokenFromRequest extracts the Bearer token from the Authorization header
func (c *McpController) GetTokenFromRequest() string {
authHeader := c.Ctx.Request.Header.Get("Authorization")
if authHeader == "" {
return ""
}
// Extract Bearer token
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
return ""
}
return parts[1]
}
// GetClaimsFromToken parses and validates the JWT token and returns the claims
// Returns nil if no token is present or if token is invalid
func (c *McpController) GetClaimsFromToken() *object.Claims {
tokenString := c.GetTokenFromRequest()
if tokenString == "" {
return nil
}
// Try to find the application for this token
// For MCP, we'll try to parse using the first available application's certificate
// In a production scenario, you might want to use a specific MCP application
token, err := object.GetTokenByAccessToken(tokenString)
if err != nil || token == nil {
return nil
}
application, err := object.GetApplication(token.Application)
if err != nil || application == nil {
return nil
}
claims, err := object.ParseJwtTokenByApplication(tokenString, application)
if err != nil {
return nil
}
return claims
}
// GetScopesFromClaims extracts the scopes from JWT claims and returns them as a slice
func GetScopesFromClaims(claims *object.Claims) []string {
if claims == nil || claims.Scope == "" {
return []string{}
}
// Scopes are space-separated in OAuth 2.0
return strings.Split(claims.Scope, " ")
}

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package mcp
package mcpself
import (
"encoding/json"
@@ -268,7 +268,160 @@ func (c *McpController) handlePing(req McpRequest) {
}
func (c *McpController) handleToolsList(req McpRequest) {
tools := []McpTool{
allTools := c.getAllTools()
// Get JWT claims from the request
claims := c.GetClaimsFromToken()
// If no token is present, check session authentication
if claims == nil {
username := c.GetSessionUsername()
// If user is authenticated via session, return all tools (backward compatibility)
if username != "" {
result := McpListToolsResult{
Tools: allTools,
}
c.McpResponseOk(req.ID, result)
return
}
// Unauthenticated request - return all tools for discovery
// This allows clients to see what tools are available before authenticating
result := McpListToolsResult{
Tools: allTools,
}
c.McpResponseOk(req.ID, result)
return
}
// Token-based authentication - filter tools by scopes
grantedScopes := GetScopesFromClaims(claims)
allowedTools := GetToolsForScopes(grantedScopes, BuiltinScopes)
// Filter tools based on allowed scopes
var filteredTools []McpTool
for _, tool := range allTools {
if allowedTools[tool.Name] {
filteredTools = append(filteredTools, tool)
}
}
result := McpListToolsResult{
Tools: filteredTools,
}
c.McpResponseOk(req.ID, result)
}
func (c *McpController) handleToolsCall(req McpRequest) {
var params McpCallToolParams
err := json.Unmarshal(req.Params, &params)
if err != nil {
c.sendInvalidParamsError(req.ID, err.Error())
return
}
// Check scope-tool permission
if !c.checkToolPermission(req.ID, params.Name) {
return // Error already sent by checkToolPermission
}
// Route to the appropriate tool handler
switch params.Name {
case "get_applications":
var args GetApplicationsArgs
if err := json.Unmarshal(params.Arguments, &args); err != nil {
c.sendInvalidParamsError(req.ID, err.Error())
return
}
c.handleGetApplicationsTool(req.ID, args)
case "get_application":
var args GetApplicationArgs
if err := json.Unmarshal(params.Arguments, &args); err != nil {
c.sendInvalidParamsError(req.ID, err.Error())
return
}
c.handleGetApplicationTool(req.ID, args)
case "add_application":
var args AddApplicationArgs
if err := json.Unmarshal(params.Arguments, &args); err != nil {
c.sendInvalidParamsError(req.ID, err.Error())
return
}
c.handleAddApplicationTool(req.ID, args)
case "update_application":
var args UpdateApplicationArgs
if err := json.Unmarshal(params.Arguments, &args); err != nil {
c.sendInvalidParamsError(req.ID, err.Error())
return
}
c.handleUpdateApplicationTool(req.ID, args)
case "delete_application":
var args DeleteApplicationArgs
if err := json.Unmarshal(params.Arguments, &args); err != nil {
c.sendInvalidParamsError(req.ID, err.Error())
return
}
c.handleDeleteApplicationTool(req.ID, args)
default:
c.McpResponseError(req.ID, -32602, "Invalid tool name", fmt.Sprintf("Tool '%s' not found", params.Name))
}
}
// checkToolPermission validates that the current token has the required scope for the tool
// Returns false and sends an error response if permission is denied
func (c *McpController) checkToolPermission(id interface{}, toolName string) bool {
// Get JWT claims from the request
claims := c.GetClaimsFromToken()
// If no token is present, check if the user is authenticated via session
if claims == nil {
username := c.GetSessionUsername()
// If user is authenticated via session (e.g., session cookie), allow access
// This maintains backward compatibility with existing session-based auth
if username != "" {
return true
}
// No authentication present - deny access
c.sendInsufficientScopeError(id, toolName, []string{})
return false
}
// Extract scopes from claims
grantedScopes := GetScopesFromClaims(claims)
// Get allowed tools for the granted scopes
allowedTools := GetToolsForScopes(grantedScopes, BuiltinScopes)
// Check if the requested tool is allowed
if !allowedTools[toolName] {
c.sendInsufficientScopeError(id, toolName, grantedScopes)
return false
}
return true
}
// sendInsufficientScopeError sends an error response for insufficient scope
func (c *McpController) sendInsufficientScopeError(id interface{}, toolName string, grantedScopes []string) {
// Find required scope for this tool
requiredScope := GetRequiredScopeForTool(toolName, BuiltinScopes)
errorData := map[string]interface{}{
"tool": toolName,
"granted_scopes": grantedScopes,
}
if requiredScope != "" {
errorData["required_scope"] = requiredScope
}
c.McpResponseError(id, -32001, "insufficient_scope", errorData)
}
// getAllTools returns all available MCP tools
func (c *McpController) getAllTools() []McpTool {
return []McpTool{
{
Name: "get_applications",
Description: "Get all applications for a specific owner",
@@ -344,60 +497,4 @@ func (c *McpController) handleToolsList(req McpRequest) {
},
},
}
result := McpListToolsResult{
Tools: tools,
}
c.McpResponseOk(req.ID, result)
}
func (c *McpController) handleToolsCall(req McpRequest) {
var params McpCallToolParams
err := json.Unmarshal(req.Params, &params)
if err != nil {
c.sendInvalidParamsError(req.ID, err.Error())
return
}
// Route to the appropriate tool handler
switch params.Name {
case "get_applications":
var args GetApplicationsArgs
if err := json.Unmarshal(params.Arguments, &args); err != nil {
c.sendInvalidParamsError(req.ID, err.Error())
return
}
c.handleGetApplicationsTool(req.ID, args)
case "get_application":
var args GetApplicationArgs
if err := json.Unmarshal(params.Arguments, &args); err != nil {
c.sendInvalidParamsError(req.ID, err.Error())
return
}
c.handleGetApplicationTool(req.ID, args)
case "add_application":
var args AddApplicationArgs
if err := json.Unmarshal(params.Arguments, &args); err != nil {
c.sendInvalidParamsError(req.ID, err.Error())
return
}
c.handleAddApplicationTool(req.ID, args)
case "update_application":
var args UpdateApplicationArgs
if err := json.Unmarshal(params.Arguments, &args); err != nil {
c.sendInvalidParamsError(req.ID, err.Error())
return
}
c.handleUpdateApplicationTool(req.ID, args)
case "delete_application":
var args DeleteApplicationArgs
if err := json.Unmarshal(params.Arguments, &args); err != nil {
c.sendInvalidParamsError(req.ID, err.Error())
return
}
c.handleDeleteApplicationTool(req.ID, args)
default:
c.McpResponseError(req.ID, -32602, "Invalid tool name", fmt.Sprintf("Tool '%s' not found", params.Name))
}
}

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