Compare commits

..

35 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
19a179a47c Add missing user items to GetTranslatedUserItems, init.go, init_data.json.template, and UserFields
Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com>
2026-02-20 02:15:22 +00:00
copilot-swe-agent[bot]
dff052f1d2 Initial plan 2026-02-20 01:59:53 +00: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
86 changed files with 2608 additions and 609 deletions

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

View File

@@ -323,7 +323,7 @@ func (c *ApiController) Signup() {
// If OAuth parameters are present, generate OAuth code and return it
if clientId != "" && responseType == ResponseTypeCode {
code, err := object.GetOAuthCode(userId, clientId, "", "password", responseType, redirectUri, scope, state, nonce, codeChallenge, c.Ctx.Request.Host, c.GetAcceptLanguage())
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
@@ -688,6 +688,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())

View File

@@ -161,12 +161,13 @@ 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())
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
@@ -738,7 +739,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)

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

@@ -176,6 +176,7 @@ func (c *ApiController) GetOAuthToken() {
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()
@@ -231,6 +232,9 @@ func (c *ApiController) GetOAuthToken() {
if audience == "" {
audience = tokenRequest.Audience
}
if resource == "" {
resource = tokenRequest.Resource
}
}
}
@@ -275,7 +279,7 @@ func (c *ApiController) GetOAuthToken() {
}
host := c.Ctx.Request.Host
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage(), subjectToken, subjectTokenType, audience)
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage(), subjectToken, subjectTokenType, audience, resource)
if err != nil {
c.ResponseError(err.Error())
return

View File

@@ -30,4 +30,5 @@ type TokenRequest struct {
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

@@ -151,39 +151,14 @@ func (c *ApiController) SendVerificationCode() {
return
}
provider, err := object.GetCaptchaProviderByApplication(vform.ApplicationId, "false", c.GetAcceptLanguage())
application, err := object.GetApplication(vform.ApplicationId)
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())
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), vform.ApplicationId))
return
}
@@ -214,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
@@ -231,18 +207,86 @@ 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
}
// 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:

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

View File

@@ -18,7 +18,7 @@ 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 {
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 {
if typ == "Azure ACS" {
return NewAzureACSEmailProvider(clientSecret, host)
} else if typ == "Custom HTTP Email" {
@@ -26,6 +26,6 @@ func GetEmailProvider(typ string, clientId string, clientSecret string, host str
} else if typ == "SendGrid" {
return NewSendgridEmailProvider(clientSecret, host, endpoint)
} else {
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl, enableProxy)
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, sslMode, enableProxy)
}
}

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

5
go.mod
View File

@@ -30,7 +30,6 @@ require (
github.com/casdoor/xorm-adapter/v3 v3.1.0
github.com/casvisor/casvisor-go-sdk v1.4.0
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
@@ -50,6 +49,7 @@ require (
github.com/lib/pq v1.10.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/nyaruka/phonenumbers v1.2.2
github.com/polarsource/polar-go v0.12.0
@@ -161,7 +161,8 @@ 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

24
go.sum
View File

@@ -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,6 +652,8 @@ 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=
@@ -917,7 +929,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=
@@ -1090,8 +1101,11 @@ github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w
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-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=
@@ -1350,6 +1364,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=
@@ -1414,6 +1430,8 @@ 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=
@@ -1496,6 +1514,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=

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

@@ -67,6 +67,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 +83,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 +114,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"}
]
}
],

View File

@@ -15,6 +15,7 @@
package mcp
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

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

158
mcp/permission.go Normal file
View File

@@ -0,0 +1,158 @@
// 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 (
"github.com/casdoor/casdoor/object"
)
// BuiltinScopes defines the default scope-to-tool mappings for Casdoor's MCP server
var BuiltinScopes = []*object.ScopeItem{
{
Name: "application:read",
DisplayName: "Read Applications",
Description: "View application list and details",
Tools: []string{"get_applications", "get_application"},
},
{
Name: "application:write",
DisplayName: "Manage Applications",
Description: "Create, update, and delete applications",
Tools: []string{"add_application", "update_application", "delete_application"},
},
{
Name: "user:read",
DisplayName: "Read Users",
Description: "View user list and details",
Tools: []string{"get_users", "get_user"},
},
{
Name: "user:write",
DisplayName: "Manage Users",
Description: "Create, update, and delete users",
Tools: []string{"add_user", "update_user", "delete_user"},
},
{
Name: "organization:read",
DisplayName: "Read Organizations",
Description: "View organization list and details",
Tools: []string{"get_organizations", "get_organization"},
},
{
Name: "organization:write",
DisplayName: "Manage Organizations",
Description: "Create, update, and delete organizations",
Tools: []string{"add_organization", "update_organization", "delete_organization"},
},
{
Name: "permission:read",
DisplayName: "Read Permissions",
Description: "View permission list and details",
Tools: []string{"get_permissions", "get_permission"},
},
{
Name: "permission:write",
DisplayName: "Manage Permissions",
Description: "Create, update, and delete permissions",
Tools: []string{"add_permission", "update_permission", "delete_permission"},
},
{
Name: "role:read",
DisplayName: "Read Roles",
Description: "View role list and details",
Tools: []string{"get_roles", "get_role"},
},
{
Name: "role:write",
DisplayName: "Manage Roles",
Description: "Create, update, and delete roles",
Tools: []string{"add_role", "update_role", "delete_role"},
},
{
Name: "provider:read",
DisplayName: "Read Providers",
Description: "View provider list and details",
Tools: []string{"get_providers", "get_provider"},
},
{
Name: "provider:write",
DisplayName: "Manage Providers",
Description: "Create, update, and delete providers",
Tools: []string{"add_provider", "update_provider", "delete_provider"},
},
{
Name: "token:read",
DisplayName: "Read Tokens",
Description: "View token list and details",
Tools: []string{"get_tokens", "get_token"},
},
{
Name: "token:write",
DisplayName: "Manage Tokens",
Description: "Delete tokens",
Tools: []string{"delete_token"},
},
}
// ConvenienceScopes defines alias scopes that expand to multiple resource scopes
var ConvenienceScopes = map[string][]string{
"read": {"application:read", "user:read", "organization:read", "permission:read", "role:read", "provider:read", "token:read"},
"write": {"application:write", "user:write", "organization:write", "permission:write", "role:write", "provider:write", "token:write"},
"admin": {"application:read", "application:write", "user:read", "user:write", "organization:read", "organization:write", "permission:read", "permission:write", "role:read", "role:write", "provider:read", "provider:write", "token:read", "token:write"},
}
// GetToolsForScopes returns a map of tools allowed by the given scopes
// The grantedScopes are the scopes present in the token
// The registry contains the scope-to-tool mappings (either BuiltinScopes or Application.Scopes)
func GetToolsForScopes(grantedScopes []string, registry []*object.ScopeItem) map[string]bool {
allowed := make(map[string]bool)
// Expand convenience scopes first
expandedScopes := make([]string, 0)
for _, scopeName := range grantedScopes {
if expansion, isConvenience := ConvenienceScopes[scopeName]; isConvenience {
expandedScopes = append(expandedScopes, expansion...)
} else {
expandedScopes = append(expandedScopes, scopeName)
}
}
// Map scopes to tools
for _, scopeName := range expandedScopes {
for _, item := range registry {
if item.Name == scopeName {
for _, tool := range item.Tools {
allowed[tool] = true
}
break
}
}
}
return allowed
}
// GetRequiredScopeForTool returns the first scope that provides access to the given tool
// Returns an empty string if no scope is found for the tool
func GetRequiredScopeForTool(toolName string, registry []*object.ScopeItem) string {
for _, scopeItem := range registry {
for _, tool := range scopeItem.Tools {
if tool == toolName {
return scopeItem.Name
}
}
}
return ""
}

View File

@@ -67,12 +67,22 @@ type JwtItem struct {
Type string `json:"type"`
}
type ScopeItem struct {
Name string `json:"name"`
DisplayName string `json:"displayName"`
Description string `json:"description"`
Tools []string `json:"tools"` // MCP tools allowed by this scope
}
type Application struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Category string `xorm:"varchar(20)" json:"category"`
Type string `xorm:"varchar(20)" json:"type"`
Scopes []*ScopeItem `xorm:"mediumtext" json:"scopes"`
Logo string `xorm:"varchar(200)" json:"logo"`
Title string `xorm:"varchar(100)" json:"title"`
Favicon string `xorm:"varchar(200)" json:"favicon"`
@@ -144,6 +154,13 @@ type Application struct {
FailedSigninLimit int `json:"failedSigninLimit"`
FailedSigninFrozenTime int `json:"failedSigninFrozenTime"`
CodeResendTimeout int `json:"codeResendTimeout"`
// Reverse proxy fields
Domain string `xorm:"varchar(100)" json:"domain"`
OtherDomains []string `xorm:"varchar(1000)" json:"otherDomains"`
UpstreamHost string `xorm:"varchar(100)" json:"upstreamHost"`
SslMode string `xorm:"varchar(100)" json:"sslMode"`
SslCert string `xorm:"varchar(100)" json:"sslCert"`
}
func GetApplicationCount(owner, field, value string) (int64, error) {

View File

@@ -20,7 +20,8 @@ import "github.com/casdoor/casdoor/email"
// TestSmtpServer Test the SMTP server
func TestSmtpServer(provider *Provider) error {
smtpEmailProvider := email.NewSmtpEmailProvider(provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.Type, provider.DisableSsl, provider.EnableProxy)
sslMode := getSslMode(provider)
smtpEmailProvider := email.NewSmtpEmailProvider(provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.Type, sslMode, provider.EnableProxy)
sender, err := smtpEmailProvider.Dialer.Dial()
if err != nil {
return err
@@ -31,7 +32,8 @@ func TestSmtpServer(provider *Provider) error {
}
func SendEmail(provider *Provider, title string, content string, dest []string, sender string) error {
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl, provider.Endpoint, provider.Method, provider.HttpHeaders, provider.UserMapping, provider.IssuerUrl, provider.EnableProxy)
sslMode := getSslMode(provider)
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, sslMode, provider.Endpoint, provider.Method, provider.HttpHeaders, provider.UserMapping, provider.IssuerUrl, provider.EnableProxy)
fromAddress := provider.ClientId2
if fromAddress == "" {
@@ -45,3 +47,19 @@ func SendEmail(provider *Provider, title string, content string, dest []string,
return emailProvider.Send(fromAddress, fromName, dest, title, content)
}
// getSslMode returns the SSL mode for the provider, with backward compatibility for DisableSsl
func getSslMode(provider *Provider) string {
// If SslMode is set, use it
if provider.SslMode != "" {
return provider.SslMode
}
// Backward compatibility: convert DisableSsl to SslMode
if provider.DisableSsl {
return "Disable"
}
// Default to "Auto" for new configurations or when DisableSsl is false
return "Auto"
}

View File

@@ -15,6 +15,7 @@
package object
import (
"fmt"
"sync"
"time"
@@ -49,7 +50,12 @@ func GetDashboard(owner string) (*map[string][]int64, error) {
dashboard[tableName+"Counts"] = make([]int64, 31)
tableFullName := tableNamePrefix + tableName
go func(ch chan error) {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
ch <- fmt.Errorf("panic in dashboard goroutine: %v", r)
}
wg.Done()
}()
dashboardDateItems := []DashboardDateItem{}
var countResult int64

View File

@@ -53,6 +53,8 @@ func getBuiltInAccountItems() []*AccountItem {
{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"},
@@ -67,14 +69,28 @@ func getBuiltInAccountItems() []*AccountItem {
{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"},
@@ -84,9 +100,14 @@ func getBuiltInAccountItems() []*AccountItem {
{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: "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"},
}
}
@@ -120,6 +141,7 @@ func initBuiltInOrganization() bool {
IsProfilePublic: false,
UseEmailAsUsername: false,
EnableTour: true,
DcrPolicy: "open",
}
_, err = AddOrganization(organization)
if err != nil {
@@ -185,6 +207,9 @@ func initBuiltInApplication() {
Name: "app-built-in",
CreatedTime: util.GetCurrentTime(),
DisplayName: "Casdoor",
Category: "Default",
Type: "All",
Scopes: []*ScopeItem{},
Logo: fmt.Sprintf("%s/img/casdoor-logo_1185x256.png", conf.GetConfigString("staticBaseUrl")),
HomepageUrl: "https://casdoor.org",
Organization: "built-in",

View File

@@ -37,8 +37,9 @@ type Ldap struct {
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
CustomAttributes map[string]string `json:"customAttributes"`
AutoSync int `json:"autoSync"`
LastSync string `xorm:"varchar(100)" json:"lastSync"`
AutoSync int `json:"autoSync"`
LastSync string `xorm:"varchar(100)" json:"lastSync"`
EnableGroups bool `xorm:"bool" json:"enableGroups"`
}
func AddLdap(ldap *Ldap) (bool, error) {
@@ -152,7 +153,7 @@ func UpdateLdap(ldap *Ldap) (bool, error) {
}
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type", "allow_self_signed_cert", "custom_attributes").Update(ldap)
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type", "allow_self_signed_cert", "custom_attributes", "enable_groups").Update(ldap)
if err != nil {
return false, nil
}

View File

@@ -91,13 +91,28 @@ func (l *LdapAutoSynchronizer) syncRoutine(ldap *Ldap, stopChan chan struct{}) e
return err
}
// fetch all users
// fetch all users and groups
conn, err := ldap.GetLdapConn()
if err != nil {
logs.Warning(fmt.Sprintf("autoSync failed for %s, error %s", ldap.Id, err))
continue
}
// Sync groups first if enabled (so they exist before assigning users)
if ldap.EnableGroups {
groups, err := conn.GetLdapGroups(ldap)
if err != nil {
logs.Warning(fmt.Sprintf("autoSync failed to fetch groups for %s, error %s", ldap.Id, err))
} else {
newGroups, updatedGroups, err := SyncLdapGroups(ldap.Owner, groups, ldap.Id)
if err != nil {
logs.Warning(fmt.Sprintf("autoSync failed to sync groups for %s, error %s", ldap.Id, err))
} else {
logs.Info(fmt.Sprintf("ldap group sync success for %s, %d new groups, %d updated groups", ldap.Id, newGroups, updatedGroups))
}
}
}
users, err := conn.GetLdapUsers(ldap)
if err != nil {
conn.Close()

View File

@@ -87,10 +87,19 @@ type LdapUser struct {
GroupId string `json:"groupId"`
Address string `json:"address"`
MemberOf string `json:"memberOf"`
MemberOf []string `json:"memberOf"`
Attributes map[string]string `json:"attributes"`
}
type LdapGroup struct {
Dn string `json:"dn"`
Cn string `json:"cn"`
Name string `json:"name"`
Description string `json:"description"`
Member []string `json:"member"`
ParentDn string `json:"parentDn"`
}
func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) {
var conn *goldap.Conn
tlsConfig := tls.Config{
@@ -179,7 +188,7 @@ func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]LdapUser, error) {
SearchAttributes := []string{
"uidNumber", "cn", "sn", "gidNumber", "entryUUID", "displayName", "mail", "email",
"emailAddress", "telephoneNumber", "mobile", "mobileTelephoneNumber", "registeredAddress", "postalAddress",
"c", "co",
"c", "co", "memberOf",
}
if l.IsAD {
SearchAttributes = append(SearchAttributes, "sAMAccountName")
@@ -247,7 +256,7 @@ func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]LdapUser, error) {
case "co":
user.CountryName = attribute.Values[0]
case "memberOf":
user.MemberOf = attribute.Values[0]
user.MemberOf = attribute.Values
default:
if propName, ok := ldapServer.CustomAttributes[attribute.Name]; ok {
if user.Attributes == nil {
@@ -263,42 +272,135 @@ func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]LdapUser, error) {
return ldapUsers, nil
}
// FIXME: The Base DN does not necessarily contain the Group
//
// func (l *ldapConn) GetLdapGroups(baseDn string) (map[string]ldapGroup, error) {
// SearchFilter := "(objectClass=posixGroup)"
// SearchAttributes := []string{"cn", "gidNumber"}
// groupMap := make(map[string]ldapGroup)
//
// searchReq := goldap.NewSearchRequest(baseDn,
// goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
// SearchFilter, SearchAttributes, nil)
// searchResult, err := l.Conn.Search(searchReq)
// if err != nil {
// return nil, err
// }
//
// if len(searchResult.Entries) == 0 {
// return nil, errors.New("no result")
// }
//
// for _, entry := range searchResult.Entries {
// var ldapGroupItem ldapGroup
// for _, attribute := range entry.Attributes {
// switch attribute.Name {
// case "gidNumber":
// ldapGroupItem.GidNumber = attribute.Values[0]
// break
// case "cn":
// ldapGroupItem.Cn = attribute.Values[0]
// break
// }
// }
// groupMap[ldapGroupItem.GidNumber] = ldapGroupItem
// }
//
// return groupMap, nil
// }
// GetLdapGroups fetches LDAP groups and organizational units
func (l *LdapConn) GetLdapGroups(ldapServer *Ldap) ([]LdapGroup, error) {
var allGroups []LdapGroup
// Search for LDAP groups (groupOfNames, groupOfUniqueNames, posixGroup)
groupFilters := []string{
"(objectClass=groupOfNames)",
"(objectClass=groupOfUniqueNames)",
"(objectClass=posixGroup)",
}
// Add Active Directory group filter
if l.IsAD {
groupFilters = append(groupFilters, "(objectClass=group)")
}
// Build combined filter
var filterBuilder strings.Builder
filterBuilder.WriteString("(|")
for _, filter := range groupFilters {
filterBuilder.WriteString(filter)
}
filterBuilder.WriteString(")")
SearchAttributes := []string{"cn", "name", "description", "member", "uniqueMember", "memberUid"}
searchReq := goldap.NewSearchRequest(ldapServer.BaseDn,
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
filterBuilder.String(), SearchAttributes, nil)
searchResult, err := l.Conn.SearchWithPaging(searchReq, 100)
if err != nil {
// Groups might not exist, which is okay
return allGroups, nil
}
for _, entry := range searchResult.Entries {
group := LdapGroup{
Dn: entry.DN,
}
for _, attribute := range entry.Attributes {
switch attribute.Name {
case "cn":
group.Cn = attribute.Values[0]
case "name":
group.Name = attribute.Values[0]
case "description":
if len(attribute.Values) > 0 {
group.Description = attribute.Values[0]
}
case "member", "uniqueMember", "memberUid":
group.Member = append(group.Member, attribute.Values...)
}
}
// Use cn as name if name is not set
if group.Name == "" {
group.Name = group.Cn
}
// Parse parent DN from the entry DN
group.ParentDn = getParentDn(entry.DN)
allGroups = append(allGroups, group)
}
// Also fetch organizational units as groups
ouFilter := "(objectClass=organizationalUnit)"
ouSearchReq := goldap.NewSearchRequest(ldapServer.BaseDn,
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
ouFilter, []string{"ou", "description"}, nil)
ouSearchResult, err := l.Conn.SearchWithPaging(ouSearchReq, 100)
if err == nil {
for _, entry := range ouSearchResult.Entries {
ou := LdapGroup{
Dn: entry.DN,
}
for _, attribute := range entry.Attributes {
switch attribute.Name {
case "ou":
ou.Name = attribute.Values[0]
ou.Cn = attribute.Values[0]
case "description":
if len(attribute.Values) > 0 {
ou.Description = attribute.Values[0]
}
}
}
// Parse parent DN from the entry DN
ou.ParentDn = getParentDn(entry.DN)
allGroups = append(allGroups, ou)
}
}
return allGroups, nil
}
// getParentDn extracts the parent DN from a full DN
func getParentDn(dn string) string {
// Split DN by comma
parts := strings.Split(dn, ",")
if len(parts) <= 1 {
return ""
}
// Remove the first component (the current node) and rejoin
return strings.Join(parts[1:], ",")
}
// parseDnToGroupName converts a DN to a group name
func parseDnToGroupName(dn string) string {
// Extract the CN or OU from the DN
parts := strings.Split(dn, ",")
if len(parts) == 0 {
return ""
}
firstPart := parts[0]
// Extract value after = sign
if idx := strings.Index(firstPart, "="); idx != -1 {
return firstPart[idx+1:]
}
return firstPart
}
func AutoAdjustLdapUser(users []LdapUser) []LdapUser {
res := make([]LdapUser, len(users))
@@ -315,6 +417,7 @@ func AutoAdjustLdapUser(users []LdapUser) []LdapUser {
Address: util.ReturnAnyNotEmpty(user.Address, user.PostalAddress, user.RegisteredAddress),
Country: util.ReturnAnyNotEmpty(user.Country, user.CountryName),
CountryName: user.CountryName,
MemberOf: user.MemberOf,
Attributes: user.Attributes,
}
}
@@ -398,8 +501,22 @@ func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUser
}
formatUserPhone(newUser)
// Assign user to groups based on memberOf attribute
userGroups := []string{}
if ldap.DefaultGroup != "" {
newUser.Groups = []string{ldap.DefaultGroup}
userGroups = append(userGroups, ldap.DefaultGroup)
}
// Extract group names from memberOf DNs
for _, memberDn := range syncUser.MemberOf {
groupName := dnToGroupName(owner, memberDn)
if groupName != "" {
userGroups = append(userGroups, groupName)
}
}
if len(userGroups) > 0 {
newUser.Groups = userGroups
}
affected, err := AddUser(newUser, "en")
@@ -420,6 +537,179 @@ func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUser
return existUsers, failedUsers, err
}
// SyncLdapGroups syncs LDAP groups/OUs to Casdoor groups with hierarchy
func SyncLdapGroups(owner string, ldapGroups []LdapGroup, ldapId string) (newGroups int, updatedGroups int, err error) {
if len(ldapGroups) == 0 {
return 0, 0, nil
}
// Create a map of DN to group for quick lookup
dnToGroup := make(map[string]*LdapGroup)
for i := range ldapGroups {
dnToGroup[ldapGroups[i].Dn] = &ldapGroups[i]
}
// Get existing groups for this organization
existingGroups, err := GetGroups(owner)
if err != nil {
return 0, 0, err
}
existingGroupMap := make(map[string]*Group)
for _, group := range existingGroups {
existingGroupMap[group.Name] = group
}
ldap, err := GetLdap(ldapId)
if err != nil {
return 0, 0, err
}
// Process groups in hierarchical order (parents before children)
processedGroups := make(map[string]bool)
var processGroup func(ldapGroup *LdapGroup) error
processGroup = func(ldapGroup *LdapGroup) error {
if processedGroups[ldapGroup.Dn] {
return nil
}
// Generate group name from DN
groupName := dnToGroupName(owner, ldapGroup.Dn)
if groupName == "" {
return nil
}
// Determine parent
var parentId string
var isTopGroup bool
if ldapGroup.ParentDn == "" || ldapGroup.ParentDn == ldap.BaseDn {
isTopGroup = true
parentId = ""
} else {
// Process parent first
if parentGroup, exists := dnToGroup[ldapGroup.ParentDn]; exists {
err := processGroup(parentGroup)
if err != nil {
return err
}
parentId = dnToGroupName(owner, ldapGroup.ParentDn)
} else {
isTopGroup = true
}
}
// Check if group already exists
if existingGroup, exists := existingGroupMap[groupName]; exists {
// Update existing group
existingGroup.DisplayName = ldapGroup.Name
existingGroup.ParentId = parentId
existingGroup.IsTopGroup = isTopGroup
existingGroup.Type = "ldap-synced"
existingGroup.UpdatedTime = util.GetCurrentTime()
_, err := UpdateGroup(existingGroup.GetId(), existingGroup)
if err == nil {
updatedGroups++
}
} else {
// Create new group
newGroup := &Group{
Owner: owner,
Name: groupName,
CreatedTime: util.GetCurrentTime(),
UpdatedTime: util.GetCurrentTime(),
DisplayName: ldapGroup.Name,
ParentId: parentId,
IsTopGroup: isTopGroup,
Type: "ldap-synced",
IsEnabled: true,
}
_, err := AddGroup(newGroup)
if err == nil {
newGroups++
existingGroupMap[groupName] = newGroup
}
}
processedGroups[ldapGroup.Dn] = true
return nil
}
// Process all groups
for i := range ldapGroups {
err := processGroup(&ldapGroups[i])
if err != nil {
// Log error but continue processing other groups
continue
}
}
return newGroups, updatedGroups, nil
}
// dnToGroupName converts an LDAP DN to a Casdoor group name
func dnToGroupName(owner, dn string) string {
if dn == "" {
return ""
}
// Parse DN to extract meaningful components
parts := strings.Split(dn, ",")
// Build a hierarchical name from DN components (excluding DC parts)
var nameComponents []string
for _, part := range parts {
part = strings.TrimSpace(part)
lowerPart := strings.ToLower(part)
// Skip DC (domain component) parts
if strings.HasPrefix(lowerPart, "dc=") {
continue
}
// Extract value after = sign
if idx := strings.Index(part, "="); idx != -1 {
value := part[idx+1:]
nameComponents = append(nameComponents, value)
}
}
if len(nameComponents) == 0 {
return ""
}
// Reverse to get top-down hierarchy
for i, j := 0, len(nameComponents)-1; i < j; i, j = i+1, j-1 {
nameComponents[i], nameComponents[j] = nameComponents[j], nameComponents[i]
}
// Join with underscore to create a unique group name
groupName := strings.Join(nameComponents, "_")
// Sanitize group name - replace invalid characters with underscores
// Keep only alphanumeric characters, underscores, and hyphens
var sanitized strings.Builder
for _, r := range groupName {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' {
sanitized.WriteRune(r)
} else {
sanitized.WriteRune('_')
}
}
groupName = sanitized.String()
// Remove consecutive underscores and trim
for strings.Contains(groupName, "__") {
groupName = strings.ReplaceAll(groupName, "__", "_")
}
groupName = strings.Trim(groupName, "_")
return groupName
}
func GetExistUuids(owner string, uuids []string) ([]string, error) {
var existUuids []string

193
object/oauth_dcr.go Normal file
View File

@@ -0,0 +1,193 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"time"
"github.com/casdoor/casdoor/util"
)
// DynamicClientRegistrationRequest represents an RFC 7591 client registration request
type DynamicClientRegistrationRequest struct {
ClientName string `json:"client_name,omitempty"`
RedirectUris []string `json:"redirect_uris,omitempty"`
GrantTypes []string `json:"grant_types,omitempty"`
ResponseTypes []string `json:"response_types,omitempty"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
ApplicationType string `json:"application_type,omitempty"`
Contacts []string `json:"contacts,omitempty"`
LogoUri string `json:"logo_uri,omitempty"`
ClientUri string `json:"client_uri,omitempty"`
PolicyUri string `json:"policy_uri,omitempty"`
TosUri string `json:"tos_uri,omitempty"`
Scope string `json:"scope,omitempty"`
}
// DynamicClientRegistrationResponse represents an RFC 7591 client registration response
type DynamicClientRegistrationResponse struct {
ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
ClientIdIssuedAt int64 `json:"client_id_issued_at,omitempty"`
ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"`
ClientName string `json:"client_name,omitempty"`
RedirectUris []string `json:"redirect_uris,omitempty"`
GrantTypes []string `json:"grant_types,omitempty"`
ResponseTypes []string `json:"response_types,omitempty"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
ApplicationType string `json:"application_type,omitempty"`
Contacts []string `json:"contacts,omitempty"`
LogoUri string `json:"logo_uri,omitempty"`
ClientUri string `json:"client_uri,omitempty"`
PolicyUri string `json:"policy_uri,omitempty"`
TosUri string `json:"tos_uri,omitempty"`
Scope string `json:"scope,omitempty"`
RegistrationClientUri string `json:"registration_client_uri,omitempty"`
RegistrationAccessToken string `json:"registration_access_token,omitempty"`
}
// DcrError represents an RFC 7591 error response
type DcrError struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description,omitempty"`
}
// RegisterDynamicClient creates a new application based on DCR request
func RegisterDynamicClient(req *DynamicClientRegistrationRequest, organization string) (*DynamicClientRegistrationResponse, *DcrError, error) {
// Validate organization exists and has DCR enabled
org, err := GetOrganization(util.GetId("admin", organization))
if err != nil {
return nil, nil, err
}
if org == nil {
return nil, &DcrError{
Error: "invalid_client_metadata",
ErrorDescription: "organization not found",
}, nil
}
// Check if DCR is enabled for this organization
if org.DcrPolicy == "" || org.DcrPolicy == "disabled" {
return nil, &DcrError{
Error: "invalid_client_metadata",
ErrorDescription: "dynamic client registration is disabled for this organization",
}, nil
}
// Validate required fields
if len(req.RedirectUris) == 0 {
return nil, &DcrError{
Error: "invalid_redirect_uri",
ErrorDescription: "redirect_uris is required and must contain at least one URI",
}, nil
}
// Set defaults
if req.ClientName == "" {
clientIdPrefix := util.GenerateClientId()
if len(clientIdPrefix) > 8 {
clientIdPrefix = clientIdPrefix[:8]
}
req.ClientName = fmt.Sprintf("DCR Client %s", clientIdPrefix)
}
if len(req.GrantTypes) == 0 {
req.GrantTypes = []string{"authorization_code"}
}
if len(req.ResponseTypes) == 0 {
req.ResponseTypes = []string{"code"}
}
if req.TokenEndpointAuthMethod == "" {
req.TokenEndpointAuthMethod = "client_secret_basic"
}
if req.ApplicationType == "" {
req.ApplicationType = "web"
}
// Generate unique application name
randomName := util.GetRandomName()
appName := fmt.Sprintf("dcr_%s", randomName)
// Create Application object
// Note: DCR applications are created under "admin" owner by default
// This can be made configurable in future versions
clientId := util.GenerateClientId()
clientSecret := util.GenerateClientSecret()
createdTime := util.GetCurrentTime()
application := &Application{
Owner: "admin",
Name: appName,
Organization: organization,
CreatedTime: createdTime,
DisplayName: req.ClientName,
Category: "Agent",
Type: "MCP",
Scopes: []*ScopeItem{},
Logo: req.LogoUri,
HomepageUrl: req.ClientUri,
ClientId: clientId,
ClientSecret: clientSecret,
RedirectUris: req.RedirectUris,
GrantTypes: req.GrantTypes,
EnablePassword: false,
EnableSignUp: false,
DisableSignin: false,
EnableSigninSession: false,
EnableCodeSignin: true,
EnableAutoSignin: false,
TokenFormat: "JWT",
ExpireInHours: 168,
RefreshExpireInHours: 168,
CookieExpireInHours: 720,
FormOffset: 2,
Tags: []string{"dcr"},
TermsOfUse: req.TosUri,
}
// Add the application
affected, err := AddApplication(application)
if err != nil {
return nil, nil, err
}
if !affected {
return nil, &DcrError{
Error: "invalid_client_metadata",
ErrorDescription: "failed to create client application",
}, nil
}
// Build response
response := &DynamicClientRegistrationResponse{
ClientId: clientId,
ClientSecret: clientSecret,
ClientIdIssuedAt: time.Now().Unix(),
ClientSecretExpiresAt: 0, // Never expires
ClientName: req.ClientName,
RedirectUris: req.RedirectUris,
GrantTypes: req.GrantTypes,
ResponseTypes: req.ResponseTypes,
TokenEndpointAuthMethod: req.TokenEndpointAuthMethod,
ApplicationType: req.ApplicationType,
Contacts: req.Contacts,
LogoUri: req.LogoUri,
ClientUri: req.ClientUri,
PolicyUri: req.PolicyUri,
TosUri: req.TosUri,
Scope: req.Scope,
}
return response, nil, nil
}

View File

@@ -49,6 +49,7 @@ type Order struct {
type ProductInfo struct {
Owner string `json:"owner"`
Name string `json:"name"`
CreatedTime string `json:"createdTime,omitempty"`
DisplayName string `json:"displayName"`
Image string `json:"image,omitempty"`
Detail string `json:"detail,omitempty"`

View File

@@ -276,7 +276,7 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
OutOrderId: payResp.OrderId,
}
if provider.Type == "Dummy" || provider.Type == "Balance" {
if provider.Type == "Balance" {
payment.State = pp.PaymentStatePaid
}
@@ -351,7 +351,7 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
}
order.Payment = payment.Name
if provider.Type == "Dummy" || provider.Type == "Balance" {
if provider.Type == "Balance" {
order.State = "Paid"
order.Message = "Payment successful"
order.UpdateTime = util.GetCurrentTime()
@@ -364,7 +364,7 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
}
// Update product stock after order state is persisted (for instant payment methods)
if provider.Type == "Dummy" || provider.Type == "Balance" {
if provider.Type == "Balance" {
err = UpdateProductStock(orderProductInfos)
if err != nil {
return nil, nil, err

View File

@@ -92,6 +92,8 @@ type Organization struct {
AccountMenu string `xorm:"varchar(20)" json:"accountMenu"`
AccountItems []*AccountItem `xorm:"mediumtext" json:"accountItems"`
DcrPolicy string `xorm:"varchar(100)" json:"dcrPolicy"`
OrgBalance float64 `json:"orgBalance"`
UserBalance float64 `json:"userBalance"`
BalanceCredit float64 `json:"balanceCredit"`

View File

@@ -29,9 +29,9 @@ import (
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
xormadapter "github.com/casdoor/xorm-adapter/v3"
_ "github.com/denisenkom/go-mssqldb" // db = mssql
_ "github.com/go-sql-driver/mysql" // db = mysql
_ "github.com/lib/pq" // db = postgres
_ "github.com/go-sql-driver/mysql" // db = mysql
_ "github.com/lib/pq" // db = postgres
_ "github.com/microsoft/go-mssqldb" // db = mssql
"github.com/xorm-io/xorm"
"github.com/xorm-io/xorm/core"
"github.com/xorm-io/xorm/names"
@@ -62,6 +62,12 @@ func InitFlag() {
configPath = *configPathPtr
exportData = *exportDataPtr
exportFilePath = *exportFilePathPtr
// Load beego config from the specified config path
err := web.LoadAppConfig("ini", configPath)
if err != nil {
panic(fmt.Sprintf("failed to load config from %s: %v", configPath, err))
}
}
func ShouldExportData() bool {

View File

@@ -33,6 +33,8 @@ type Payment struct {
// Product Info
Products []string `xorm:"varchar(1000)" json:"products"`
ProductsDisplayName string `xorm:"varchar(1000)" json:"productsDisplayName"`
ProductName string `xorm:"varchar(1000)" json:"productName"`
ProductDisplayName string `xorm:"varchar(1000)" json:"productDisplayName"`
Detail string `xorm:"varchar(255)" json:"detail"`
Currency string `xorm:"varchar(100)" json:"currency"`
Price float64 `json:"price"`
@@ -303,7 +305,7 @@ func NotifyPayment(body []byte, owner string, paymentName string, lang string) (
order.Message = "Payment successful"
order.UpdateTime = util.GetCurrentTime()
} else if payment.State == pp.PaymentStateError {
order.State = "PaymentFailed"
order.State = "Failed"
order.Message = payment.Message
order.UpdateTime = util.GetCurrentTime()
} else if payment.State == pp.PaymentStateCanceled {

View File

@@ -53,7 +53,8 @@ type Provider struct {
Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"`
DisableSsl bool `json:"disableSsl"` // If the provider type is WeChat, DisableSsl means EnableQRCode, if type is Google, it means sync phone number
DisableSsl bool `json:"disableSsl"` // Deprecated: Use SslMode instead. If the provider type is WeChat, DisableSsl means EnableQRCode, if type is Google, it means sync phone number
SslMode string `xorm:"varchar(100)" json:"sslMode"` // "Auto" (empty means Auto), "Enable", "Disable"
Title string `xorm:"varchar(100)" json:"title"`
Content string `xorm:"varchar(2000)" json:"content"` // If provider type is WeChat, Content means QRCode string by Base64 encoding
Receiver string `xorm:"varchar(100)" json:"receiver"`
@@ -563,7 +564,7 @@ func providerChangeTrigger(oldName string, newName string) error {
return session.Commit()
}
func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.ProviderInfo {
func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) (*idp.ProviderInfo, error) {
providerInfo := &idp.ProviderInfo{
Type: provider.Type,
SubType: provider.SubType,
@@ -587,9 +588,19 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.Provid
}
} else if provider.Type == "ADFS" || provider.Type == "AzureAD" || provider.Type == "AzureADB2C" || provider.Type == "Casdoor" || provider.Type == "Okta" {
providerInfo.HostUrl = provider.Domain
} else if provider.Type == "Alipay" && provider.Cert != "" {
// For Alipay with certificate mode, load private key from certificate
cert, err := GetCert(util.GetId(provider.Owner, provider.Cert))
if err != nil {
return nil, fmt.Errorf("failed to load certificate for Alipay provider %s: %w", provider.Name, err)
}
if cert == nil {
return nil, fmt.Errorf("certificate not found for Alipay provider %s", provider.Name)
}
providerInfo.ClientSecret = cert.PrivateKey
}
return providerInfo
return providerInfo, nil
}
func GetIdvProviderFromProvider(provider *Provider) idv.IdvProvider {

View File

@@ -61,7 +61,6 @@ type Syncer struct {
SyncInterval int `json:"syncInterval"`
IsReadOnly bool `json:"isReadOnly"`
IsEnabled bool `json:"isEnabled"`
NameMapping string `xorm:"varchar(100)" json:"nameMapping"`
Ormer *Ormer `xorm:"-" json:"-"`
SshClient *ssh.Client `xorm:"-" json:"-"`

View File

@@ -408,37 +408,11 @@ func (p *DingtalkSyncerProvider) getDingtalkUsers() ([]*OriginalUser, error) {
// dingtalkUserToOriginalUser converts DingTalk user to Casdoor OriginalUser
func (p *DingtalkSyncerProvider) dingtalkUserToOriginalUser(dingtalkUser *DingtalkUser) *OriginalUser {
// Determine the userName based on the NameMapping configuration
// Default behavior (for backward compatibility): unionid, fallback to userId
var userName string
switch p.Syncer.NameMapping {
case "userid":
userName = dingtalkUser.UserId
case "email":
userName = dingtalkUser.Email
if userName == "" {
// Fallback to userId if email is empty
userName = dingtalkUser.UserId
}
case "mobile":
userName = dingtalkUser.Mobile
if userName == "" {
// Fallback to userId if mobile is empty
userName = dingtalkUser.UserId
}
case "unionid":
// Use unionid as name to be consistent with OAuth provider
// Fallback to userId if unionid is not available
userName := dingtalkUser.UserId
if dingtalkUser.UnionId != "" {
userName = dingtalkUser.UnionId
if userName == "" {
// Fallback to userId if unionid is empty
userName = dingtalkUser.UserId
}
default:
// Default behavior: prefer unionid, fallback to userId
userName = dingtalkUser.UserId
if dingtalkUser.UnionId != "" {
userName = dingtalkUser.UnionId
}
}
user := &OriginalUser{

View File

@@ -16,22 +16,6 @@ package object
import "fmt"
func getSyncerForUser(user *User) (*Syncer, error) {
syncers, err := GetSyncers("admin")
if err != nil {
return nil, err
}
for _, syncer := range syncers {
if syncer.Organization == user.Owner && syncer.IsEnabled {
return syncer, nil
}
}
return nil, nil
}
// Deprecated: Use getSyncerForUser instead. Maintained for backward compatibility.
// This function only returns database-type syncers.
func getDbSyncerForUser(user *User) (*Syncer, error) {
syncers, err := GetSyncers("admin")
if err != nil {

View File

@@ -43,6 +43,7 @@ type Token struct {
CodeChallenge string `xorm:"varchar(100)" json:"codeChallenge"`
CodeIsUsed bool `json:"codeIsUsed"`
CodeExpireIn int64 `json:"codeExpireIn"`
Resource string `xorm:"varchar(255)" json:"resource"` // RFC 8707 Resource Indicator
}
func GetTokenCount(owner, organization, field, value string) (int64, error) {

View File

@@ -509,7 +509,7 @@ func refineUser(user *User) *User {
return user
}
func generateJwtToken(application *Application, user *User, provider string, signinMethod string, nonce string, scope string, host string) (string, string, string, error) {
func generateJwtToken(application *Application, user *User, provider string, signinMethod string, nonce string, scope string, resource string, host string) (string, string, string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(time.Duration(application.ExpireInHours * float64(time.Hour)))
refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours * float64(time.Hour)))
@@ -553,7 +553,10 @@ func generateJwtToken(application *Application, user *User, provider string, sig
},
}
if application.IsShared {
// RFC 8707: Use resource as audience when provided
if resource != "" {
claims.Audience = []string{resource}
} else if application.IsShared {
claims.Audience = []string{application.ClientId + "-org-" + user.Owner}
}

View File

@@ -18,6 +18,7 @@ import (
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"strings"
"sync"
"time"
@@ -92,6 +93,26 @@ type DeviceAuthResponse struct {
Interval int `json:"interval"`
}
// validateResourceURI validates that the resource parameter is a valid absolute URI
// according to RFC 8707 Section 2
func validateResourceURI(resource string) error {
if resource == "" {
return nil // empty resource is allowed (backward compatibility)
}
parsedURL, err := url.Parse(resource)
if err != nil {
return fmt.Errorf("resource must be a valid URI")
}
// RFC 8707: The resource parameter must be an absolute URI
if !parsedURL.IsAbs() {
return fmt.Errorf("resource must be an absolute URI")
}
return nil
}
func ExpireTokenByAccessToken(accessToken string) (bool, *Application, *Token, error) {
token, err := GetTokenByAccessToken(accessToken)
if err != nil {
@@ -138,7 +159,7 @@ func CheckOAuthLogin(clientId string, responseType string, redirectUri string, s
return "", application, nil
}
func GetOAuthCode(userId string, clientId string, provider string, signinMethod string, responseType string, redirectUri string, scope string, state string, nonce string, challenge string, host string, lang string) (*Code, error) {
func GetOAuthCode(userId string, clientId string, provider string, signinMethod string, responseType string, redirectUri string, scope string, state string, nonce string, challenge string, resource string, host string, lang string) (*Code, error) {
user, err := GetUser(userId)
if err != nil {
return nil, err
@@ -169,11 +190,19 @@ func GetOAuthCode(userId string, clientId string, provider string, signinMethod
}, nil
}
// Validate resource parameter (RFC 8707)
if err := validateResourceURI(resource); err != nil {
return &Code{
Message: err.Error(),
Code: "",
}, nil
}
err = ExtendUserWithRolesAndPermissions(user)
if err != nil {
return nil, err
}
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, provider, signinMethod, nonce, scope, host)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, provider, signinMethod, nonce, scope, resource, host)
if err != nil {
return nil, err
}
@@ -198,6 +227,7 @@ func GetOAuthCode(userId string, clientId string, provider string, signinMethod
CodeChallenge: challenge,
CodeIsUsed: false,
CodeExpireIn: time.Now().Add(time.Minute * 5).Unix(),
Resource: resource,
}
_, err = AddToken(token)
if err != nil {
@@ -210,7 +240,7 @@ func GetOAuthCode(userId string, clientId string, provider string, signinMethod
}, nil
}
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, nonce string, username string, password string, host string, refreshToken string, tag string, avatar string, lang string, subjectToken string, subjectTokenType string, audience string) (interface{}, error) {
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, nonce string, username string, password string, host string, refreshToken string, tag string, avatar string, lang string, subjectToken string, subjectTokenType string, audience string, resource string) (interface{}, error) {
application, err := GetApplicationByClientId(clientId)
if err != nil {
return nil, err
@@ -236,7 +266,7 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
var tokenError *TokenError
switch grantType {
case "authorization_code": // Authorization Code Grant
token, tokenError, err = GetAuthorizationCodeToken(application, clientSecret, code, verifier)
token, tokenError, err = GetAuthorizationCodeToken(application, clientSecret, code, verifier, resource)
case "password": // Resource Owner Password Credentials Grant
token, tokenError, err = GetPasswordToken(application, username, password, scope, host)
case "client_credentials": // Client Credentials Grant
@@ -391,7 +421,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
return nil, err
}
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, host)
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, "", host)
if err != nil {
return &TokenError{
Error: EndpointError,
@@ -545,7 +575,7 @@ func createGuestUserToken(application *Application, clientSecret string, verifie
}
// Generate JWT token
accessToken, refreshToken, tokenName, err := generateJwtToken(application, guestUser, "", "", "", "", "")
accessToken, refreshToken, tokenName, err := generateJwtToken(application, guestUser, "", "", "", "", "", "")
if err != nil {
return nil, &TokenError{
Error: EndpointError,
@@ -595,7 +625,7 @@ func generateGuestUsername() string {
// GetAuthorizationCodeToken
// Authorization code flow
func GetAuthorizationCodeToken(application *Application, clientSecret string, code string, verifier string) (*Token, *TokenError, error) {
func GetAuthorizationCodeToken(application *Application, clientSecret string, code string, verifier string, resource string) (*Token, *TokenError, error) {
if code == "" {
return nil, &TokenError{
Error: InvalidRequest,
@@ -663,6 +693,14 @@ func GetAuthorizationCodeToken(application *Application, clientSecret string, co
}, nil
}
// RFC 8707: Validate resource parameter matches the one in the authorization request
if resource != token.Resource {
return nil, &TokenError{
Error: InvalidGrant,
ErrorDescription: fmt.Sprintf("resource parameter does not match authorization request, expected: [%s], got: [%s]", token.Resource, resource),
}, nil
}
nowUnix := time.Now().Unix()
if nowUnix > token.CodeExpireIn {
// code must be used within 5 minutes
@@ -719,7 +757,7 @@ func GetPasswordToken(application *Application, username string, password string
return nil, nil, err
}
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, host)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, "", host)
if err != nil {
return nil, &TokenError{
Error: EndpointError,
@@ -765,7 +803,7 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
Type: "application",
}
accessToken, _, tokenName, err := generateJwtToken(application, nullUser, "", "", "", scope, host)
accessToken, _, tokenName, err := generateJwtToken(application, nullUser, "", "", "", scope, "", host)
if err != nil {
return nil, &TokenError{
Error: EndpointError,
@@ -829,7 +867,7 @@ func GetTokenByUser(application *Application, user *User, scope string, nonce st
return nil, err
}
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", nonce, scope, host)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", nonce, scope, "", host)
if err != nil {
return nil, err
}
@@ -936,7 +974,7 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin
return nil, nil, err
}
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", "", host)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", "", "", host)
if err != nil {
return nil, &TokenError{
Error: EndpointError,
@@ -1110,7 +1148,7 @@ func GetTokenExchangeToken(application *Application, clientSecret string, subjec
}
// Generate new JWT token
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, host)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, "", host)
if err != nil {
return nil, &TokenError{
Error: EndpointError,

View File

@@ -20,7 +20,7 @@ import (
)
func calculateHash(user *User) (string, error) {
syncer, err := getSyncerForUser(user)
syncer, err := getDbSyncerForUser(user)
if err != nil {
return "", err
}

View File

@@ -405,7 +405,7 @@ func ClearUserOAuthProperties(user *User, providerType string) (bool, error) {
func userVisible(isAdmin bool, item *AccountItem) bool {
if item == nil {
return false
return true
}
if item.ViewRule == "Admin" && !isAdmin {
@@ -564,10 +564,11 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, allowDis
itemsChanged = append(itemsChanged, item)
}
}
if oldUser.SignupApplication != newUser.SignupApplication {
item := GetAccountItemByName("Signup application", organization)
if oldUser.Language != newUser.Language {
item := GetAccountItemByName("Language", organization)
if !userVisible(isAdmin, item) {
newUser.SignupApplication = oldUser.SignupApplication
newUser.Language = oldUser.Language
} else {
itemsChanged = append(itemsChanged, item)
}
@@ -600,6 +601,83 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, allowDis
}
}
if oldUser.Balance != newUser.Balance {
item := GetAccountItemByName("Balance", organization)
if !userVisible(isAdmin, item) {
newUser.Balance = oldUser.Balance
} else {
itemsChanged = append(itemsChanged, item)
}
}
if oldUser.BalanceCredit != newUser.BalanceCredit {
item := GetAccountItemByName("Balance credit", organization)
if !userVisible(isAdmin, item) {
newUser.BalanceCredit = oldUser.BalanceCredit
} else {
itemsChanged = append(itemsChanged, item)
}
}
if oldUser.BalanceCurrency != newUser.BalanceCurrency {
item := GetAccountItemByName("Balance currency", organization)
if !userVisible(isAdmin, item) {
newUser.BalanceCurrency = oldUser.BalanceCurrency
} else {
itemsChanged = append(itemsChanged, item)
}
}
oldUserCartJson, _ := json.Marshal(oldUser.Cart)
if newUser.Cart == nil {
newUser.Cart = []ProductInfo{}
}
newUserCartJson, _ := json.Marshal(newUser.Cart)
if string(oldUserCartJson) != string(newUserCartJson) {
item := GetAccountItemByName("Cart", organization)
if !userVisible(isAdmin, item) {
newUser.Cart = oldUser.Cart
} else {
itemsChanged = append(itemsChanged, item)
}
}
if oldUser.Score != newUser.Score {
item := GetAccountItemByName("Score", organization)
if !userVisible(isAdmin, item) {
newUser.Score = oldUser.Score
} else {
itemsChanged = append(itemsChanged, item)
}
}
if oldUser.Karma != newUser.Karma {
item := GetAccountItemByName("Karma", organization)
if !userVisible(isAdmin, item) {
newUser.Karma = oldUser.Karma
} else {
itemsChanged = append(itemsChanged, item)
}
}
if oldUser.Ranking != newUser.Ranking {
item := GetAccountItemByName("Ranking", organization)
if !userVisible(isAdmin, item) {
newUser.Ranking = oldUser.Ranking
} else {
itemsChanged = append(itemsChanged, item)
}
}
if oldUser.SignupApplication != newUser.SignupApplication {
item := GetAccountItemByName("Signup application", organization)
if !userVisible(isAdmin, item) {
newUser.SignupApplication = oldUser.SignupApplication
} else {
itemsChanged = append(itemsChanged, item)
}
}
if oldUser.IdCard != newUser.IdCard {
item := GetAccountItemByName("ID card", organization)
if !userVisible(isAdmin, item) {
@@ -728,51 +806,6 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, allowDis
}
}
if oldUser.Balance != newUser.Balance {
item := GetAccountItemByName("Balance", organization)
if !userVisible(isAdmin, item) {
newUser.Balance = oldUser.Balance
} else {
itemsChanged = append(itemsChanged, item)
}
}
if oldUser.Score != newUser.Score {
item := GetAccountItemByName("Score", organization)
if !userVisible(isAdmin, item) {
newUser.Score = oldUser.Score
} else {
itemsChanged = append(itemsChanged, item)
}
}
if oldUser.Karma != newUser.Karma {
item := GetAccountItemByName("Karma", organization)
if !userVisible(isAdmin, item) {
newUser.Karma = oldUser.Karma
} else {
itemsChanged = append(itemsChanged, item)
}
}
if oldUser.Language != newUser.Language {
item := GetAccountItemByName("Language", organization)
if !userVisible(isAdmin, item) {
newUser.Language = oldUser.Language
} else {
itemsChanged = append(itemsChanged, item)
}
}
if oldUser.Ranking != newUser.Ranking {
item := GetAccountItemByName("Ranking", organization)
if !userVisible(isAdmin, item) {
newUser.Ranking = oldUser.Ranking
} else {
itemsChanged = append(itemsChanged, item)
}
}
if oldUser.Currency != newUser.Currency {
item := GetAccountItemByName("Currency", organization)
if !userVisible(isAdmin, item) {
@@ -792,6 +825,11 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, allowDis
}
for _, accountItem := range itemsChanged {
// Skip nil items - these occur when a field doesn't have a corresponding
// account item configuration, meaning no validation rules apply
if accountItem == nil {
continue
}
if pass, err := CheckAccountItemModifyRule(accountItem, isAdmin, lang); !pass {
return pass, err

View File

@@ -21,7 +21,7 @@ import (
"net"
"time"
mssql "github.com/denisenkom/go-mssqldb"
mssql "github.com/microsoft/go-mssqldb"
"github.com/lib/pq"
"golang.org/x/crypto/ssh"

View File

@@ -0,0 +1,59 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
)
// OauthProtectedResourceMetadata represents RFC 9728 OAuth 2.0 Protected Resource Metadata
type OauthProtectedResourceMetadata struct {
Resource string `json:"resource"`
AuthorizationServers []string `json:"authorization_servers"`
BearerMethodsSupported []string `json:"bearer_methods_supported,omitempty"`
ScopesSupported []string `json:"scopes_supported,omitempty"`
ResourceSigningAlg []string `json:"resource_signing_alg_values_supported,omitempty"`
ResourceDocumentation string `json:"resource_documentation,omitempty"`
}
// GetOauthProtectedResourceMetadata returns RFC 9728 Protected Resource Metadata for global discovery
func GetOauthProtectedResourceMetadata(host string) OauthProtectedResourceMetadata {
_, originBackend := getOriginFromHost(host)
return OauthProtectedResourceMetadata{
Resource: originBackend,
AuthorizationServers: []string{originBackend},
BearerMethodsSupported: []string{"header"},
ScopesSupported: []string{"openid", "profile", "email", "read", "write"},
ResourceSigningAlg: []string{"RS256"},
}
}
// GetOauthProtectedResourceMetadataByApplication returns RFC 9728 Protected Resource Metadata for application-specific discovery
func GetOauthProtectedResourceMetadataByApplication(host string, applicationName string) OauthProtectedResourceMetadata {
_, originBackend := getOriginFromHost(host)
// For application-specific discovery, the resource identifier includes the application name
resourceIdentifier := fmt.Sprintf("%s/.well-known/%s", originBackend, applicationName)
authServer := fmt.Sprintf("%s/.well-known/%s", originBackend, applicationName)
return OauthProtectedResourceMetadata{
Resource: resourceIdentifier,
AuthorizationServers: []string{authServer},
BearerMethodsSupported: []string{"header"},
ScopesSupported: []string{"openid", "profile", "email", "read", "write"},
ResourceSigningAlg: []string{"RS256"},
}
}

View File

@@ -32,6 +32,7 @@ type OidcDiscovery struct {
TokenEndpoint string `json:"token_endpoint"`
UserinfoEndpoint string `json:"userinfo_endpoint"`
DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"`
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
JwksUri string `json:"jwks_uri"`
IntrospectionEndpoint string `json:"introspection_endpoint"`
ResponseTypesSupported []string `json:"response_types_supported"`
@@ -40,6 +41,7 @@ type OidcDiscovery struct {
SubjectTypesSupported []string `json:"subject_types_supported"`
IdTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
ScopesSupported []string `json:"scopes_supported"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
ClaimsSupported []string `json:"claims_supported"`
RequestParameterSupported bool `json:"request_parameter_supported"`
RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported"`
@@ -123,6 +125,23 @@ func GetOidcDiscovery(host string, applicationName string) OidcDiscovery {
jwksUri = fmt.Sprintf("%s/.well-known/jwks", originBackend)
}
// Default OIDC scopes
scopes := []string{"openid", "email", "profile", "address", "phone", "offline_access"}
// Merge application-specific custom scopes if application is provided
if applicationName != "" {
applicationId := util.GetId("admin", applicationName)
application, err := GetApplication(applicationId)
if err == nil && application != nil && len(application.Scopes) > 0 {
for _, scope := range application.Scopes {
// Add custom scope names to the scopes list
if scope.Name != "" {
scopes = append(scopes, scope.Name)
}
}
}
}
// Examples:
// https://login.okta.com/.well-known/openid-configuration
// https://auth0.auth0.com/.well-known/openid-configuration
@@ -134,6 +153,7 @@ func GetOidcDiscovery(host string, applicationName string) OidcDiscovery {
TokenEndpoint: fmt.Sprintf("%s/api/login/oauth/access_token", originBackend),
UserinfoEndpoint: fmt.Sprintf("%s/api/userinfo", originBackend),
DeviceAuthorizationEndpoint: fmt.Sprintf("%s/api/device-auth", originBackend),
RegistrationEndpoint: fmt.Sprintf("%s/api/oauth/register", originBackend),
JwksUri: jwksUri,
IntrospectionEndpoint: fmt.Sprintf("%s/api/login/oauth/introspect", originBackend),
ResponseTypesSupported: []string{"code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token", "none"},
@@ -141,7 +161,8 @@ func GetOidcDiscovery(host string, applicationName string) OidcDiscovery {
GrantTypesSupported: []string{"authorization_code", "implicit", "password", "client_credentials", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code", "urn:ietf:params:oauth:grant-type:token-exchange"},
SubjectTypesSupported: []string{"public"},
IdTokenSigningAlgValuesSupported: []string{"RS256", "RS512", "ES256", "ES384", "ES512"},
ScopesSupported: []string{"openid", "email", "profile", "address", "phone", "offline_access"},
ScopesSupported: scopes,
CodeChallengeMethodsSupported: []string{"S256"},
ClaimsSupported: []string{"iss", "ver", "sub", "aud", "iat", "exp", "id", "type", "displayName", "avatar", "permanentAvatar", "email", "phone", "location", "affiliation", "title", "homepage", "bio", "tag", "region", "language", "score", "ranking", "isOnline", "isAdmin", "isForbidden", "signupApplication", "ldap"},
RequestParameterSupported: true,
RequestObjectSigningAlgValuesSupported: []string{"HS256", "HS384", "HS512"},

View File

@@ -14,22 +14,59 @@
package pp
import (
"encoding/json"
"fmt"
)
type DummyPaymentProvider struct{}
type DummyOrderInfo struct {
Price float64 `json:"price"`
Currency string `json:"currency"`
ProductDisplayName string `json:"productDisplayName"`
}
func NewDummyPaymentProvider() (*DummyPaymentProvider, error) {
pp := &DummyPaymentProvider{}
return pp, nil
}
func (pp *DummyPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
// Encode payment information in OrderId for later retrieval in Notify.
// Note: This is a test/mock provider and the OrderId is only used internally for testing.
// Real payment providers would receive this information from their external payment gateway.
orderInfo := DummyOrderInfo{
Price: r.Price,
Currency: r.Currency,
ProductDisplayName: "",
}
orderInfoBytes, err := json.Marshal(orderInfo)
if err != nil {
return nil, fmt.Errorf("failed to encode order info: %w", err)
}
return &PayResp{
PayUrl: r.ReturnUrl,
PayUrl: r.ReturnUrl,
OrderId: string(orderInfoBytes),
}, nil
}
func (pp *DummyPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
// Decode payment information from OrderId
var orderInfo DummyOrderInfo
if orderId != "" {
err := json.Unmarshal([]byte(orderId), &orderInfo)
if err != nil {
return nil, fmt.Errorf("failed to decode order info: %w", err)
}
}
return &NotifyResult{
PaymentStatus: PaymentStatePaid,
PaymentStatus: PaymentStatePaid,
Price: orderInfo.Price,
Currency: orderInfo.Currency,
ProductDisplayName: orderInfo.ProductDisplayName,
}, nil
}

View File

@@ -94,6 +94,17 @@ func denyMcpRequest(ctx *context.Context) {
Data: T(ctx, "auth:Unauthorized operation"),
})
// Add WWW-Authenticate header per MCP Authorization spec (RFC 9728)
// Use the same logic as getOriginFromHost to determine the scheme
host := ctx.Request.Host
scheme := "https"
if !strings.Contains(host, ".") {
// localhost:8000 or computer-name:80
scheme = "http"
}
resourceMetadataUrl := fmt.Sprintf("%s://%s/.well-known/oauth-protected-resource", scheme, host)
ctx.Output.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"casdoor\", resource_metadata=\"%s\"", resourceMetadataUrl))
ctx.Output.SetStatus(http.StatusUnauthorized)
_ = ctx.Output.JSON(resp, true, false)
}

View File

@@ -298,6 +298,7 @@ func InitAPI() {
web.Router("/api/login/oauth/access_token", &controllers.ApiController{}, "POST:GetOAuthToken")
web.Router("/api/login/oauth/refresh_token", &controllers.ApiController{}, "POST:RefreshToken")
web.Router("/api/login/oauth/introspect", &controllers.ApiController{}, "POST:IntrospectToken")
web.Router("/api/oauth/register", &controllers.ApiController{}, "POST:DynamicClientRegister")
web.Router("/api/get-records", &controllers.ApiController{}, "GET:GetRecords")
web.Router("/api/get-records-filter", &controllers.ApiController{}, "POST:GetRecordsByFilter")
@@ -320,10 +321,14 @@ func InitAPI() {
web.Router("/.well-known/openid-configuration", &controllers.RootController{}, "GET:GetOidcDiscovery")
web.Router("/.well-known/:application/openid-configuration", &controllers.RootController{}, "GET:GetOidcDiscoveryByApplication")
web.Router("/.well-known/oauth-authorization-server", &controllers.RootController{}, "GET:GetOAuthServerMetadata")
web.Router("/.well-known/:application/oauth-authorization-server", &controllers.RootController{}, "GET:GetOAuthServerMetadataByApplication")
web.Router("/.well-known/jwks", &controllers.RootController{}, "*:GetJwks")
web.Router("/.well-known/:application/jwks", &controllers.RootController{}, "*:GetJwksByApplication")
web.Router("/.well-known/webfinger", &controllers.RootController{}, "GET:GetWebFinger")
web.Router("/.well-known/:application/webfinger", &controllers.RootController{}, "GET:GetWebFingerByApplication")
web.Router("/.well-known/oauth-protected-resource", &controllers.RootController{}, "GET:GetOauthProtectedResourceMetadata")
web.Router("/.well-known/:application/oauth-protected-resource", &controllers.RootController{}, "GET:GetOauthProtectedResourceMetadataByApplication")
web.Router("/cas/:organization/:application/serviceValidate", &controllers.RootController{}, "GET:CasServiceValidate")
web.Router("/cas/:organization/:application/proxyValidate", &controllers.RootController{}, "GET:CasProxyValidate")

View File

@@ -89,7 +89,7 @@ func fastAutoSignin(ctx *context.Context) (string, error) {
return "", nil
}
code, err := object.GetOAuthCode(userId, clientId, "", "autoSignin", responseType, redirectUri, scope, state, nonce, codeChallenge, ctx.Request.Host, getAcceptLanguage(ctx))
code, err := object.GetOAuthCode(userId, clientId, "", "autoSignin", responseType, redirectUri, scope, state, nonce, codeChallenge, "", ctx.Request.Host, getAcceptLanguage(ctx))
if err != nil {
return "", err
} else if code.Message != "" {

View File

@@ -158,7 +158,7 @@ class AdapterEditPage extends React.Component {
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} disabled={Setting.builtInObject(this.state.adapter)} style={{width: "100%"}} value={this.state.adapter.type} onChange={(value => {

View File

@@ -48,6 +48,7 @@ import ProviderTable from "./table/ProviderTable";
import SigninMethodTable from "./table/SigninMethodTable";
import SignupTable from "./table/SignupTable";
import SamlAttributeTable from "./table/SamlAttributeTable";
import ScopeTable from "./table/ScopeTable";
import PromptPage from "./auth/PromptPage";
import copy from "copy-to-clipboard";
import ThemeEditor from "./common/theme/ThemeEditor";
@@ -307,6 +308,61 @@ class ApplicationEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("general:Category"), i18next.t("general:Category - Tooltip"))} :
</Col>
<Col span={21} >
<Select
virtual={false}
style={{width: "100%"}}
value={this.state.application.category}
onChange={(value) => {
this.updateApplicationField("category", value);
if (value === "Agent") {
this.updateApplicationField("type", "MCP");
} else {
this.updateApplicationField("type", "All");
}
}}
>
<Option value="Default">Default</Option>
<Option value="Agent">Agent</Option>
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
</Col>
<Col span={21} >
<Select
virtual={false}
style={{width: "100%"}}
value={this.state.application.type}
onChange={(value) => {
this.updateApplicationField("type", value);
}}
>
{
(this.state.application.category === "Agent") ? (
<>
<Option value="MCP">MCP</Option>
<Option value="A2A">A2A</Option>
</>
) : (
<>
<Option value="All">All</Option>
<Option value="OIDC">OIDC</Option>
<Option value="OAuth">OAuth</Option>
<Option value="SAML">SAML</Option>
<Option value="CAS">CAS</Option>
</>
)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("general:Is shared"), i18next.t("general:Is shared - Tooltip"))} :
@@ -516,6 +572,22 @@ class ApplicationEditPage extends React.Component {
</Select>
</Col>
</Row>
{
(this.state.application.category === "Agent") ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("general:Scopes"), i18next.t("general:Scopes - Tooltip"))} :
</Col>
<Col span={21} >
<ScopeTable
title={i18next.t("general:Scopes")}
table={this.state.application.scopes}
onUpdateTable={(value) => {this.updateApplicationField("scopes", value);}}
/>
</Col>
</Row>
) : null
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Token format"), i18next.t("application:Token format - Tooltip"))} :
@@ -1301,6 +1373,68 @@ class ApplicationEditPage extends React.Component {
</Col>
</Row>
</React.Fragment>
)}
{this.state.activeMenuKey === "reverse-proxy" && (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
</Col>
<Col span={21} >
<Input value={this.state.application.domain} placeholder="e.g., blog.example.com" onChange={e => {
this.updateApplicationField("domain", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Other domains"), i18next.t("application:Other domains - Tooltip"))} :
</Col>
<Col span={21} >
<UrlTable
title={i18next.t("application:Other domains")}
table={this.state.application.otherDomains}
onUpdateTable={(value) => {this.updateApplicationField("otherDomains", value);}}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Upstream host"), i18next.t("application:Upstream host - Tooltip"))} :
</Col>
<Col span={21} >
<Input value={this.state.application.upstreamHost} placeholder="e.g., localhost:8080 or 192.168.1.100:3000" onChange={e => {
this.updateApplicationField("upstreamHost", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("provider:SSL mode"), i18next.t("provider:SSL mode - Tooltip"))} :
</Col>
<Col span={21} >
<Select virtual={false} style={{width: "100%"}} value={this.state.application.sslMode} onChange={(value => {this.updateApplicationField("sslMode", value);})}>
<Option value="">{i18next.t("general:None")}</Option>
<Option value="HTTP">HTTP</Option>
<Option value="HTTPS and HTTP">HTTPS and HTTP</Option>
<Option value="HTTPS Only">HTTPS Only</Option>
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:SSL cert"), i18next.t("application:SSL cert - Tooltip"))} :
</Col>
<Col span={21} >
<Select virtual={false} style={{width: "100%"}} value={this.state.application.sslCert} onChange={(value => {this.updateApplicationField("sslCert", value);})}>
<Option value="">{i18next.t("general:None")}</Option>
{
this.state.certs.map((cert, index) => <Option key={index} value={cert.name}>{cert.name}</Option>)
}
</Select>
</Col>
</Row>
</React.Fragment>
)}</>;
}
@@ -1313,11 +1447,12 @@ class ApplicationEditPage extends React.Component {
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitApplicationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteApplication()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
<Layout style={{background: "inherit"}}>
} style={{margin: (Setting.isMobile()) ? "5px" : {}, height: "calc(100vh - 145px - 48px)", overflow: "hidden"}}
styles={{body: {height: "100%"}}} type="inner">
<Layout style={{background: "inherit", height: "100%", overflow: "auto"}}>
{
this.state.menuMode === "horizontal" || !this.state.menuMode ? (
<Header style={{background: "inherit", padding: "0px"}}>
<Header style={{background: "inherit", padding: "0px", position: "sticky", top: 0}}>
<div className="demo-logo" />
<Tabs
onChange={(key) => {
@@ -1332,12 +1467,13 @@ class ApplicationEditPage extends React.Component {
{label: i18next.t("application:Providers"), key: "providers"},
{label: i18next.t("application:UI Customization"), key: "ui-customization"},
{label: i18next.t("application:Security"), key: "security"},
{label: i18next.t("application:Reverse Proxy"), key: "reverse-proxy"},
]}
/>
</Header>
) : null
}
<Layout style={{background: "inherit", maxHeight: "calc(70vh - 70px)", overflow: "auto"}}>
<Layout style={{background: "inherit", overflow: "auto"}}>
{
this.state.menuMode === "vertical" ? (
<Sider width={200} style={{background: "inherit", position: "sticky", top: 0}}>
@@ -1355,6 +1491,7 @@ class ApplicationEditPage extends React.Component {
<Menu.Item key="providers">{i18next.t("application:Providers")}</Menu.Item>
<Menu.Item key="ui-customization">{i18next.t("application:UI Customization")}</Menu.Item>
<Menu.Item key="security">{i18next.t("application:Security")}</Menu.Item>
<Menu.Item key="reverse-proxy">{i18next.t("application:Reverse Proxy")}</Menu.Item>
</Menu>
</Sider>) : null
}

View File

@@ -38,6 +38,9 @@ class ApplicationListPage extends BaseListPage {
organization: organizationName,
createdTime: moment().format(),
displayName: `New Application - ${randomName}`,
category: "Default",
type: "All",
scopes: [],
logo: `${Setting.StaticBaseUrl}/img/casdoor-logo_1185x256.png`,
enablePassword: true,
enableSignUp: true,
@@ -179,6 +182,36 @@ class ApplicationListPage extends BaseListPage {
sorter: true,
...this.getColumnSearchProps("displayName"),
},
{
title: i18next.t("general:Category"),
dataIndex: "category",
key: "category",
width: "120px",
sorter: true,
...this.getColumnSearchProps("category"),
render: (text, record, index) => {
if (!text) {
text = "Default";
}
if (text === "Agent") {
return Setting.getTag("success", text);
} else {
return Setting.getTag("default", text);
}
},
},
{
title: i18next.t("general:Type"),
dataIndex: "type",
key: "type",
width: "100px",
sorter: true,
...this.getColumnSearchProps("type"),
render: (text, record, index) => {
return text;
},
},
{
title: "Logo",
dataIndex: "logo",

View File

@@ -75,6 +75,11 @@ class CartListPage extends BaseListPage {
const owner = this.state.user?.owner || this.props.account.owner;
const carts = this.state.data || [];
const invalidCarts = carts.filter(item => item.isInvalid);
if (invalidCarts.length > 0) {
Setting.showMessage("error", i18next.t("product:Cart contains invalid products, please delete them before placing an order"));
return;
}
if (carts.length === 0) {
Setting.showMessage("error", i18next.t("product:Product list cannot be empty"));
return;
@@ -119,7 +124,7 @@ class CartListPage extends BaseListPage {
const index = user.cart.findIndex(item =>
item.name === record.name &&
(record.price !== null ? item.price === record.price : true) &&
(record.isRecharge ? item.price === record.price : true) &&
(item.pricingName || "") === (record.pricingName || "") &&
(item.planName || "") === (record.planName || ""));
if (index === -1) {
@@ -210,15 +215,17 @@ class CartListPage extends BaseListPage {
renderTable(carts) {
const isEmpty = carts === undefined || carts === null || carts.length === 0;
const hasInvalidItems = carts && carts.some(item => item.isInvalid);
const owner = this.state.user?.owner || this.props.account.owner;
let total = 0;
let currency = "";
if (carts && carts.length > 0) {
carts.forEach(item => {
const validCarts = carts.filter(item => !item.isInvalid);
validCarts.forEach(item => {
total += item.price * item.quantity;
});
currency = carts[0].currency;
currency = validCarts.length > 0 ? validCarts[0].currency : (carts[0].currency || "USD");
}
const columns = [
@@ -231,6 +238,9 @@ class CartListPage extends BaseListPage {
sorter: true,
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
if (record.isInvalid) {
return <span style={{color: "red"}}>{text}</span>;
}
return (
<Link to={`/products/${owner}/${text}`}>
{text}
@@ -244,6 +254,12 @@ class CartListPage extends BaseListPage {
key: "displayName",
width: "170px",
sorter: true,
render: (text, record) => {
if (record.isInvalid) {
return <span style={{color: "red"}}>{i18next.t("product:Invalid product")}</span>;
}
return text;
},
},
{
title: i18next.t("product:Image"),
@@ -277,6 +293,9 @@ class CartListPage extends BaseListPage {
sorter: true,
render: (text, record) => {
if (!text) {return null;}
if (record.isInvalid) {
return <span style={{color: "red"}}>{text}</span>;
}
return (
<Link to={`/pricings/${owner}/${text}`}>
{text}
@@ -292,6 +311,9 @@ class CartListPage extends BaseListPage {
sorter: true,
render: (text, record) => {
if (!text) {return null;}
if (record.isInvalid) {
return <span style={{color: "red"}}>{text}</span>;
}
return (
<Link to={`/plans/${owner}/${text}`}>
{text}
@@ -315,7 +337,7 @@ class CartListPage extends BaseListPage {
onIncrease={() => this.updateCartItemQuantity(record, text + 1)}
onDecrease={() => this.updateCartItemQuantity(record, text - 1)}
onChange={null}
disabled={isUpdating}
disabled={isUpdating || record.isInvalid}
/>
);
},
@@ -329,7 +351,11 @@ class CartListPage extends BaseListPage {
render: (text, record, index) => {
return (
<div style={{display: "flex", flexWrap: "wrap", gap: "8px"}}>
<Button type="primary" onClick={() => this.props.history.push(`/products/${owner}/${record.name}/buy`)}>
<Button
type="primary"
onClick={() => this.props.history.push(`/products/${owner}/${record.name}/buy`)}
disabled={record.isInvalid}
>
{i18next.t("general:Detail")}
</Button>
<PopconfirmModal
@@ -367,7 +393,7 @@ class CartListPage extends BaseListPage {
onConfirm={() => this.clearCart()}
disabled={isEmpty}
/>
<Button type="primary" size="small" onClick={() => this.placeOrder()} disabled={isEmpty || this.state.isPlacingOrder} loading={this.state.isPlacingOrder}>{i18next.t("general:Place Order")}</Button>
<Button type="primary" size="small" onClick={() => this.placeOrder()} disabled={isEmpty || hasInvalidItems || this.state.isPlacingOrder} loading={this.state.isPlacingOrder}>{i18next.t("general:Place Order")}</Button>
</div>
);
}}
@@ -388,7 +414,7 @@ class CartListPage extends BaseListPage {
size="large"
style={{height: "50px", fontSize: "20px", padding: "0 40px", borderRadius: "5px"}}
onClick={() => this.placeOrder()}
disabled={this.state.isPlacingOrder}
disabled={hasInvalidItems || this.state.isPlacingOrder}
loading={this.state.isPlacingOrder}
>
{i18next.t("general:Place Order")}
@@ -413,17 +439,33 @@ class CartListPage extends BaseListPage {
ProductBackend.getProduct(organizationName, item.name)
.then(pRes => {
if (pRes.status === "ok" && pRes.data) {
const isCurrencyChanged = item.currency && pRes.data.currency && item.currency !== pRes.data.currency;
if (isCurrencyChanged) {
Setting.showMessage("warning", i18next.t("product:Product not found or invalid") + `: ${item.name}`);
}
return {
...pRes.data,
createdTime: item.createdTime,
pricingName: item.pricingName,
planName: item.planName,
quantity: item.quantity,
price: pRes.data.isRecharge ? item.price : pRes.data.price,
isInvalid: isCurrencyChanged,
};
}
return item;
Setting.showMessage("warning", i18next.t("product:Product not found or invalid") + `: ${item.name}`);
return {
...item,
isInvalid: true,
};
})
.catch(() => {
Setting.showMessage("warning", i18next.t("product:Product not found or invalid") + `: ${item.name}`);
return {
...item,
isInvalid: true,
};
})
.catch(() => item)
);
const fullCartData = await Promise.all(productPromises);
@@ -441,6 +483,10 @@ class CartListPage extends BaseListPage {
const comparison = aValue > bValue ? 1 : -1;
return params.sortOrder === "ascend" ? comparison : -comparison;
});
} else {
sortedData.sort((a, b) => {
return b.createdTime - a.createdTime;
});
}
this.setState({
@@ -454,6 +500,11 @@ class CartListPage extends BaseListPage {
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
const invalidProducts = sortedData.filter(item => item.isInvalid);
invalidProducts.forEach(item => {
Setting.showMessage("error", i18next.t("product:Product not found or invalid") + `: ${item.name}`);
});
} else {
this.setState({loading: false});
Setting.showMessage("error", res.msg);

View File

@@ -133,7 +133,7 @@ class CertEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("cert:Scope - Tooltip"))} :
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.cert.scope} onChange={(value => {
@@ -149,7 +149,7 @@ class CertEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.cert.type} onChange={(value => {

View File

@@ -93,7 +93,7 @@ class FormEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={Setting.isMobile() ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
</Col>
<Col span={22}>
<Select

View File

@@ -148,7 +148,7 @@ class GroupEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select style={{width: "100%"}}

View File

@@ -236,6 +236,13 @@ class OrderListPage extends BaseListPage {
width: "120px",
sorter: true,
...this.getColumnSearchProps("state"),
render: (text, record, index) => {
return (
<Tooltip title={record.message || ""}>
<span>{text}</span>
</Tooltip>
);
},
},
{
title: i18next.t("general:Action"),
@@ -248,7 +255,7 @@ class OrderListPage extends BaseListPage {
return (
<div style={{display: "flex", flexWrap: "wrap", gap: "8px"}}>
<Button onClick={() => this.props.history.push(`/orders/${record.owner}/${record.name}/pay`)}>
{record.state === "Created" ? i18next.t("order:Pay") : i18next.t("general:Detail")}
{(record.state === "Created" || record.state === "Failed") ? i18next.t("order:Pay") : i18next.t("general:Detail")}
</Button>
<Button danger onClick={() => this.cancelOrder(record)} disabled={record.state !== "Created" || !isAdmin}>
{i18next.t("general:Cancel")}

View File

@@ -272,7 +272,7 @@ class OrderPayPage extends React.Component {
const updateTimeMap = {
Paid: i18next.t("order:Payment time"),
Canceled: i18next.t("order:Cancel time"),
PaymentFailed: i18next.t("order:Payment failed time"),
Failed: i18next.t("order:Payment failed time"),
Timeout: i18next.t("order:Timeout time"),
};
const updateTimeLabel = updateTimeMap[state] || i18next.t("general:Updated time");

View File

@@ -18,7 +18,6 @@ import {InfoCircleTwoTone} from "@ant-design/icons";
import * as PaymentBackend from "./backend/PaymentBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import * as ProductBackend from "./backend/ProductBackend";
const {Option} = Select;
@@ -30,7 +29,6 @@ class PaymentEditPage extends React.Component {
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
paymentName: props.match.params.paymentName,
payment: null,
products: [],
isModalVisible: false,
isInvoiceLoading: false,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
@@ -39,7 +37,6 @@ class PaymentEditPage extends React.Component {
UNSAFE_componentWillMount() {
this.getPayment();
this.getProducts();
}
getPayment() {
@@ -58,19 +55,6 @@ class PaymentEditPage extends React.Component {
});
}
getProducts() {
ProductBackend.getProducts(this.state.organizationName)
.then((res) => {
if (res.status === "ok") {
this.setState({
products: res.data,
});
} else {
Setting.showMessage("error", `Failed to get products: ${res.msg}`);
}
});
}
goToViewOrder() {
const payment = this.state.payment;
if (payment && payment.order) {
@@ -232,7 +216,7 @@ class PaymentEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Input disabled={true} value={this.state.payment.type} onChange={e => {
@@ -240,29 +224,6 @@ class PaymentEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Products"), i18next.t("payment:Products - Tooltip"))} :
</Col>
<Col span={22} >
<Select
mode="multiple"
style={{width: "100%"}}
value={this.state.payment?.products || []}
disabled={isViewMode}
allowClear
options={(this.state.products || [])
.map((p) => ({
label: Setting.getLanguageText(p?.displayName) || p?.name,
value: p?.name,
}))
.filter((o) => o.value)}
onChange={(value) => {
this.updatePaymentField("products", value);
}}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("order:Price"), i18next.t("plan:Price - Tooltip"))} :

View File

@@ -14,7 +14,7 @@
import React from "react";
import {Link} from "react-router-dom";
import {Button, List, Table, Tooltip} from "antd";
import {Button, Col, List, Row, Table, Tooltip} from "antd";
import moment from "moment";
import * as Setting from "./Setting";
import * as PaymentBackend from "./backend/PaymentBackend";
@@ -195,21 +195,31 @@ class PaymentListPage extends BaseListPage {
paddingBottom: 8,
}}
renderItem={(productInfo, i) => {
const price = productInfo.price * (productInfo.quantity || 1);
const price = productInfo.price || 0;
const number = productInfo.quantity || 1;
const currency = record.currency || "USD";
const productName = productInfo.displayName || productInfo.name;
return (
<List.Item>
<div style={{display: "inline"}}>
<Tooltip placement="topLeft" title={i18next.t("general:Edit")}>
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/products/${record.owner}/${productInfo.name}`)} />
</Tooltip>
<Link to={`/products/${record.owner}/${productInfo.name}`}>
{productInfo.displayName || productInfo.name}
</Link>
<span style={{marginLeft: "8px", color: "#666"}}>
{Setting.getPriceDisplay(price, currency)}
</span>
</div>
<Row style={{width: "100%"}} wrap={false} gutter={[12, 0]}>
<Col flex="auto" style={{minWidth: 0}}>
<div style={{display: "flex", alignItems: "center", minWidth: 0}}>
<Tooltip placement="topLeft" title={i18next.t("general:Edit")}>
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/products/${record.owner}/${productInfo.name}`)} />
</Tooltip>
<Tooltip placement="topLeft" title={productName}>
<Link to={`/products/${record.owner}/${productInfo.name}`} style={{display: "inline-block", maxWidth: "100%", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap"}}>
{productName}
</Link>
</Tooltip>
</div>
</Col>
<Col flex="none" style={{whiteSpace: "nowrap"}}>
<span style={{color: "#666"}}>
{Setting.getCurrencySymbol(currency)}{price} ({Setting.getCurrencyText(currency)}) × {number}
</span>
</Col>
</Row>
</List.Item>
);
}}

View File

@@ -122,7 +122,7 @@ class PaymentResultPage extends React.Component {
payment: payment,
});
if (payment.state === "Created") {
if (["PayPal", "Stripe", "AirWallex", "Alipay", "WeChat Pay", "Balance"].includes(payment.type)) {
if (["PayPal", "Stripe", "AirWallex", "Alipay", "WeChat Pay", "Balance", "Dummy"].includes(payment.type)) {
this.setState({
timeout: setTimeout(async() => {
await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);

View File

@@ -309,7 +309,7 @@ class PermissionEditPage extends React.Component {
}
const data = res.data.map((role) => Setting.getOption(`${role.owner}/${role.name}`, `${role.owner}/${role.name}`));
if (args?.[1] === 1 && Array.isArray(res?.data)) {
// res.data = [{owner: i18next.t("organization:All"), name: "*"}, ...res.data];
// res.data = [{owner: i18next.t("general:All"), name: "*"}, ...res.data];
res.data = [
Setting.getOption(i18next.t("general:All"), "*"),
...data,

View File

@@ -14,6 +14,7 @@
import React from "react";
import {Button, Descriptions, Divider, InputNumber, Radio, Space, Spin, Typography} from "antd";
import moment from "moment";
import i18next from "i18next";
import * as ProductBackend from "./backend/ProductBackend";
import * as PlanBackend from "./backend/PlanBackend";
@@ -207,6 +208,7 @@ class ProductBuyPage extends React.Component {
} else {
const newProductInfo = {
name: product.name,
createdTime: moment().format(),
price: cartPrice,
currency: product.currency,
pricingName: pricingName,

View File

@@ -14,6 +14,7 @@
import React from "react";
import {Button, Card, Col, Row, Tag, Typography} from "antd";
import moment from "moment";
import * as Setting from "./Setting";
import * as ProductBackend from "./backend/ProductBackend";
import * as UserBackend from "./backend/UserBackend";
@@ -140,6 +141,7 @@ class ProductStorePage extends React.Component {
} else {
const newCartProductInfo = {
name: product.name,
createdTime: moment().format(),
currency: product.currency,
pricingName: "",
planName: "",

View File

@@ -687,7 +687,7 @@ class ProviderEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Category"), i18next.t("provider:Category - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Category"), i18next.t("general:Category - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.category} onChange={(value => {
@@ -698,7 +698,7 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("type", "Default");
this.updateProviderField("host", "smtp.example.com");
this.updateProviderField("port", 465);
this.updateProviderField("disableSsl", false);
this.updateProviderField("sslMode", "Auto");
this.updateProviderField("title", "Casdoor Verification Code");
this.updateProviderField("content", Setting.getDefaultHtmlEmailContent());
this.updateProviderField("metadata", Setting.getDefaultInvitationHtmlEmailContent());
@@ -751,7 +751,7 @@ class ProviderEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} showSearch value={this.state.provider.type} onChange={(value => {
@@ -893,7 +893,7 @@ class ProviderEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("cert:Scope - Tooltip"))}
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))}
</Col>
<Col span={22} >
<Input value={this.state.provider.scopes} onChange={e => {
@@ -924,41 +924,47 @@ class ProviderEditPage extends React.Component {
</Col>
) : null
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:User mapping"), i18next.t("provider:User mapping - Tooltip"))} :
</Col>
<Col span={22} >
{this.renderUserMappingInput()}
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Favicon"), i18next.t("general:Favicon - Tooltip"))} :
</Col>
<Col span={22} >
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col>
<Col span={23} >
<Input prefix={<LinkOutlined />} value={this.state.provider.customLogo} onChange={e => {
this.updateProviderField("customLogo", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
{i18next.t("general:Preview")}:
</Col>
<Col span={23} >
<a target="_blank" rel="noreferrer" href={this.state.provider.customLogo}>
<img src={this.state.provider.customLogo} alt={this.state.provider.customLogo} height={90} style={{marginBottom: "20px"}} />
</a>
</Col>
</Row>
</Col>
</Row>
{
this.state.provider.type !== "Custom HTTP SMS" && this.state.provider.type !== "Custom HTTP Email" ? (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:User mapping"), i18next.t("provider:User mapping - Tooltip"))} :
</Col>
<Col span={22} >
{this.renderUserMappingInput()}
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Favicon"), i18next.t("general:Favicon - Tooltip"))} :
</Col>
<Col span={22} >
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col>
<Col span={23} >
<Input prefix={<LinkOutlined />} value={this.state.provider.customLogo} onChange={e => {
this.updateProviderField("customLogo", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
{i18next.t("general:Preview")}:
</Col>
<Col span={23} >
<a target="_blank" rel="noreferrer" href={this.state.provider.customLogo}>
<img src={this.state.provider.customLogo} alt={this.state.provider.customLogo} height={90} style={{marginBottom: "20px"}} />
</a>
</Col>
</Row>
</Col>
</Row>
</React.Fragment>
) : null
}
</React.Fragment>
) : null
}
@@ -969,7 +975,7 @@ class ProviderEditPage extends React.Component {
(this.state.provider.category === "Storage" && this.state.provider.type === "Local File System") ||
(this.state.provider.category === "SMS" && this.state.provider.type === "Custom HTTP SMS") ||
(this.state.provider.category === "Email" && this.state.provider.type === "Custom HTTP Email") ||
(this.state.provider.category === "Notification" && (this.state.provider.type === "Google Chat" || this.state.provider.type === "Custom HTTP") || this.state.provider.type === "Balance") ? null : (
(this.state.provider.category === "Notification" && (this.state.provider.type === "Google Chat" || this.state.provider.type === "Custom HTTP" || this.state.provider.type === "Balance")) ? null : (
<React.Fragment>
{
(this.state.provider.category === "Storage" && this.state.provider.type === "Google Cloud Storage") ||
@@ -1097,7 +1103,7 @@ class ProviderEditPage extends React.Component {
)
}
{
this.state.provider.type !== "ADFS" && this.state.provider.type !== "AzureAD" && this.state.provider.type !== "AzureADB2C" && (this.state.provider.type !== "Casdoor" && this.state.category !== "Storage") && this.state.provider.type !== "Okta" && this.state.provider.type !== "Nextcloud" ? null : (
this.state.provider.type !== "ADFS" && this.state.provider.type !== "AzureAD" && this.state.provider.type !== "AzureADB2C" && (this.state.provider.type !== "Casdoor" && this.state.provider.category !== "Storage") && this.state.provider.type !== "Okta" && this.state.provider.type !== "Nextcloud" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{this.getDomainLabel(this.state.provider)} :
@@ -1294,12 +1300,16 @@ class ProviderEditPage extends React.Component {
{["Azure ACS", "SendGrid"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Disable SSL"), i18next.t("provider:Disable SSL - Tooltip"))} :
{Setting.getLabel(i18next.t("provider:SSL mode"), i18next.t("provider:SSL mode - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.provider.disableSsl} onChange={checked => {
this.updateProviderField("disableSsl", checked);
}} />
<Col span={22} >
<Select virtual={false} style={{width: "200px"}} value={this.state.provider.sslMode || "Auto"} onChange={value => {
this.updateProviderField("sslMode", value);
}}>
<Option value="Auto">{i18next.t("general:Auto")}</Option>
<Option value="Enable">{i18next.t("general:Enable")}</Option>
<Option value="Disable">{i18next.t("general:Disable")}</Option>
</Select>
</Col>
</Row>
)}
@@ -1479,7 +1489,7 @@ class ProviderEditPage extends React.Component {
</React.Fragment>
) : ["SMS"].includes(this.state.provider.category) ? (
<React.Fragment>
{["Custom HTTP SMS", "Twilio SMS", "Amazon SNS", "Azure ACS", "Msg91 SMS", "Infobip SMS"].includes(this.state.provider.type) ?
{["Custom HTTP SMS", "Twilio SMS", "Amazon SNS", "Msg91 SMS", "Infobip SMS"].includes(this.state.provider.type) ?
null :
(<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
@@ -1493,7 +1503,7 @@ class ProviderEditPage extends React.Component {
</Row>
)
}
{["Infobip SMS"].includes(this.state.provider.type) ?
{["Infobip SMS", "Custom HTTP SMS"].includes(this.state.provider.type) ?
null :
(<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>

View File

@@ -139,7 +139,7 @@ class ProviderListPage extends BaseListPage {
...this.getColumnSearchProps("displayName"),
},
{
title: i18next.t("provider:Category"),
title: i18next.t("general:Category"),
dataIndex: "category",
key: "category",
filterMultiple: false,

View File

@@ -457,8 +457,8 @@ export const UserFields = ["owner", "name", "password", "display_name", "id", "t
"is_admin", "homepage", "birthday", "gender", "password_type", "password_salt", "external_id", "avatar", "first_name", "last_name",
"avatar_type", "permanent_avatar", "email_verified", "region", "location", "address",
"affiliation", "title", "id_card_type", "id_card", "real_name", "is_verified", "bio", "tag", "language",
"education", "score", "karma", "ranking", "balance", "currency", "is_default_avatar", "is_online",
"is_forbidden", "is_deleted", "signup_application", "hash", "pre_hash", "access_key", "access_secret", "access_token",
"education", "score", "karma", "ranking", "balance", "balance_credit", "balance_currency", "currency", "is_default_avatar", "is_online",
"is_forbidden", "is_deleted", "signup_application", "register_type", "register_source", "hash", "pre_hash", "access_key", "access_secret", "access_token",
"created_ip", "last_signin_time", "last_signin_ip", "github", "google", "qq", "wechat", "facebook", "dingtalk",
"weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs", "baidu", "alipay", "casdoor", "infoflow", "apple",
"azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "kwai", "line", "amazon", "auth0",
@@ -469,7 +469,7 @@ export const UserFields = ["owner", "name", "password", "display_name", "id", "t
"wepay", "xero", "yahoo", "yammer", "yandex", "zoom", "metamask", "web3onboard", "custom", "webauthnCredentials",
"preferred_mfa_type", "recovery_codes", "totp_secret", "mfa_phone_enabled", "mfa_email_enabled", "invitation",
"invitation_code", "face_ids", "ldap", "properties", "roles", "permissions", "groups", "last_change_password_time",
"last_signin_wrong_time", "signin_wrong_times", "managedAccounts", "mfaAccounts", "need_update_password",
"last_signin_wrong_time", "signin_wrong_times", "managedAccounts", "mfaAccounts", "mfaItems", "need_update_password",
"created_time", "updated_time", "deleted_time",
"ip_whitelist"];
@@ -500,6 +500,7 @@ export const GetTranslatedUserItems = () => {
{name: "Country/Region", label: i18next.t("user:Country/Region")},
{name: "Location", label: i18next.t("user:Location")},
{name: "Address", label: i18next.t("user:Address")},
{name: "Addresses", label: i18next.t("user:Addresses")},
{name: "Affiliation", label: i18next.t("user:Affiliation")},
{name: "Title", label: i18next.t("general:Title")},
{name: "ID card type", label: i18next.t("user:ID card type")},
@@ -517,11 +518,14 @@ export const GetTranslatedUserItems = () => {
{name: "Balance", label: i18next.t("user:Balance")},
{name: "Balance currency", label: i18next.t("organization:Balance currency")},
{name: "Balance credit", label: i18next.t("organization:Balance credit")},
{name: "Cart", label: i18next.t("general:Cart")},
{name: "Transactions", label: i18next.t("general:Transactions")},
{name: "Score", label: i18next.t("user:Score")},
{name: "Karma", label: i18next.t("user:Karma")},
{name: "Ranking", label: i18next.t("user:Ranking")},
{name: "Signup application", label: i18next.t("general:Signup application")},
{name: "Register type", label: i18next.t("user:Register type")},
{name: "Register source", label: i18next.t("user:Register source")},
{name: "API key", label: i18next.t("general:API key")},
{name: "Groups", label: i18next.t("general:Groups")},
{name: "Roles", label: i18next.t("general:Roles")},
@@ -536,6 +540,7 @@ export const GetTranslatedUserItems = () => {
{name: "IP whitelist", label: i18next.t("general:IP whitelist")},
{name: "Multi-factor authentication", label: i18next.t("mfa:Multi-factor authentication")},
{name: "WebAuthn credentials", label: i18next.t("user:WebAuthn credentials")},
{name: "Last change password time", label: i18next.t("user:Last change password time")},
{name: "Managed accounts", label: i18next.t("user:Managed accounts")},
{name: "Face ID", label: i18next.t("login:Face ID")},
{name: "MFA accounts", label: i18next.t("user:MFA accounts")},
@@ -553,6 +558,8 @@ export function getUserColumns() {
transField = "Country/Region";
} else if (field === "mfaAccounts") {
transField = "MFA accounts";
} else if (field === "mfaItems") {
transField = "MFA items";
} else if (field === "face_ids") {
transField = "Face ID";
} else if (field === "managedAccounts") {
@@ -2266,7 +2273,7 @@ export function getFormTypeItems(formType) {
{name: "owner", label: "general:Organization", visible: true, width: "150"},
{name: "createdTime", label: "general:Created time", visible: true, width: "180"},
{name: "displayName", label: "general:Display name", visible: true, width: "150"},
{name: "category", label: "provider:Category", visible: true, width: "110"},
{name: "category", label: "general:Category", visible: true, width: "110"},
{name: "type", label: "general:Type", visible: true, width: "110"},
{name: "clientId", label: "provider:Client ID", visible: true, width: "100"},
{name: "providerUrl", label: "provider:Provider URL", visible: true, width: "150"},

View File

@@ -826,7 +826,7 @@ class SyncerEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.syncer.type} onChange={(value => {
@@ -878,7 +878,7 @@ class SyncerEditPage extends React.Component {
this.state.syncer.databaseType !== "postgres" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:SSL mode"), i18next.t("syncer:SSL mode - Tooltip"))} :
{Setting.getLabel(i18next.t("provider:SSL mode"), i18next.t("provider:SSL mode - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.syncer.sslMode} onChange={(value => {this.updateSyncerField("sslMode", value);})}>
@@ -977,27 +977,6 @@ class SyncerEditPage extends React.Component {
}
</Col>
</Row>
{
this.state.syncer.type === "DingTalk" ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Name mapping"), i18next.t("syncer:Name mapping - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.syncer.nameMapping} onChange={(value => {this.updateSyncerField("nameMapping", value);})}>
{
[
{id: "unionid", name: "Union ID (default)"},
{id: "userid", name: "User ID"},
{id: "email", name: "Email"},
{id: "mobile", name: "Mobile"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
) : null
}
{
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" || this.state.syncer.type === "AWS IAM" ? null : (
<Row style={{marginTop: "20px"}} >

View File

@@ -158,7 +158,7 @@ class TokenEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("cert:Scope - Tooltip"))}
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))}
</Col>
<Col span={22} >
<Input value={this.state.token.scope} onChange={e => {

View File

@@ -261,7 +261,7 @@ class TransactionEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Category"), i18next.t("provider:Category - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Category"), i18next.t("general:Category - Tooltip"))} :
</Col>
<Col span={22} >
<Input disabled={true} value={this.state.transaction.category} />
@@ -269,7 +269,7 @@ class TransactionEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Input disabled={true} value={this.state.transaction.type} onChange={e => {

View File

@@ -48,6 +48,7 @@ import FaceIdTable from "./table/FaceIdTable";
import MfaAccountTable from "./table/MfaAccountTable";
import MfaTable from "./table/MfaTable";
import TransactionTable from "./table/TransactionTable";
import CartTable from "./table/CartTable";
import * as TransactionBackend from "./backend/TransactionBackend";
import {Content, Header} from "antd/es/layout/layout";
import Sider from "antd/es/layout/Sider";
@@ -861,6 +862,17 @@ class UserEditPage extends React.Component {
</Col>
</Row>
);
} else if (accountItem.name === "Cart") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Cart"), i18next.t("general:Cart"))} :
</Col>
<Col span={22}>
<CartTable cart={this.state.user.cart} />
</Col>
</Row>
);
} else if (accountItem.name === "Transactions") {
return (
<Row style={{marginTop: "20px"}} >

View File

@@ -44,20 +44,15 @@ function generateCodeChallenge(verifier) {
}
function storeCodeVerifier(state, verifier) {
localStorage.setItem("pkce_verifier", `${state}#${verifier}`);
localStorage.setItem(`pkce_verifier_${state}`, verifier);
}
export function getCodeVerifier(state) {
const verifierStore = localStorage.getItem("pkce_verifier");
const [storedState, verifier] = verifierStore ? verifierStore.split("#") : [null, null];
if (storedState !== state) {
return null;
}
return verifier;
return localStorage.getItem(`pkce_verifier_${state}`);
}
export function clearCodeVerifier(state) {
localStorage.removeItem("pkce_verifier");
localStorage.removeItem(`pkce_verifier_${state}`);
}
const authInfo = {

View File

@@ -70,6 +70,7 @@
"Enable signin session - Tooltip": "Ob Casdoor eine Sitzung aufrechterhält, nachdem man sich von der Anwendung aus bei Casdoor angemeldet hat",
"Enable signup": "Registrierung aktivieren",
"Enable signup - Tooltip": "Ob Benutzern erlaubt werden soll, ein neues Konto zu registrieren",
"Existing Field": "Existing Field",
"Failed signin frozen time": "Sperrzeit bei fehlgeschlagenem Login",
"Failed signin frozen time - Tooltip": "Zeit, für die das Konto nach fehlgeschlagenen Anmeldeversuchen gesperrt wird",
"Failed signin limit": "Limit für fehlgeschlagene Logins",
@@ -151,6 +152,7 @@
"Signup items - Tooltip": "Elemente für Benutzer, die beim Registrieren neuer Konten ausgefüllt werden müssen - Hinweis",
"Single Choice": "Einfachauswahl",
"Small icon": "Kleines Symbol",
"Static Value": "Static Value",
"String": "String",
"Tags - Tooltip": "Nur Benutzer mit einem Tag, das in den Anwendungstags aufgeführt ist, können sich anmelden",
"The application does not allow to sign up new account": "Die Anwendung erlaubt es nicht, ein neues Konto zu registrieren",
@@ -184,9 +186,7 @@
"Expire in years - Tooltip": "Gültigkeitsdauer des Zertifikats in Jahren",
"New Cert": "Neues Zertifikat",
"Private key": "Private-Key",
"Private key - Tooltip": "Privater Schlüssel, der zum öffentlichen Schlüsselzertifikat gehört",
"Scope - Tooltip": "Nutzungsszenarien des Zertifikats",
"Type - Tooltip": "Art des Zertifikats"
"Private key - Tooltip": "Privater Schlüssel, der zum öffentlichen Schlüsselzertifikat gehört"
},
"code": {
"Code you received": "Der Code, den Sie erhalten haben",
@@ -275,6 +275,7 @@
"Applications that require authentication": "Anwendungen, die eine Authentifizierung erfordern",
"Apps": "Anwendungen",
"Authorization": "Autorisierung",
"Auto": "Auto",
"Avatar": "Profilbild",
"Avatar - Tooltip": "Öffentliches Avatarbild für den Benutzer",
"Back": "Zurück",
@@ -283,6 +284,8 @@
"Cancel": "Abbrechen",
"Captcha": "Captcha",
"Cart": "Warenkorb",
"Category": "Category",
"Category - Tooltip": "Category - Tooltip",
"Cert": "Zertifikat",
"Cert - Tooltip": "Das Public-Key-Zertifikat, das vom Client-SDK, das mit dieser Anwendung korrespondiert, verifiziert werden muss",
"Certs": "Zertifikate",
@@ -476,6 +479,8 @@
"SSH type - Tooltip": "Der Authentifizierungstyp für SSH-Verbindungen",
"Save": "Speichern",
"Save & Exit": "Speichern und verlassen",
"Scopes": "Scopes",
"Scopes - Tooltip": "Scopes - Tooltip",
"Search": "Suchen",
"Send": "Senden",
"Session ID": "Session-ID",
@@ -530,6 +535,7 @@
"Transactions": "Transaktionen",
"True": "Wahr",
"Type": "Typ",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "URL-Link",
"Unknown application name": "Unbekannter Anwendungsname",
@@ -867,6 +873,8 @@
},
"plan": {
"Edit Plan": "Plan bearbeiten",
"Is exclusive": "Is exclusive",
"Is exclusive - Tooltip": "Is exclusive - Tooltip",
"New Plan": "Neuer Plan",
"Period": "Zeitraum",
"Period - Tooltip": "Zeitraum",
@@ -897,6 +905,7 @@
"Amount": "Betrag",
"Buy": "Kaufen",
"Buy Product": "Produkt kaufen",
"Cart contains invalid products, please delete them before placing an order": "Cart contains invalid products, please delete them before placing an order",
"Custom amount available": "Benutzerdefinierter Betrag verfügbar",
"Custom price should be greater than zero": "Benutzerdefinierter Preis muss größer als null sein",
"Detail - Tooltip": "Detail des Produkts",
@@ -909,9 +918,11 @@
"Image": "Bild",
"Image - Tooltip": "Bild des Produkts",
"Information": "Information",
"Invalid product": "Invalid product",
"Is recharge": "Ist Aufladung",
"Is recharge - Tooltip": "Ob das Produkt zum Aufladen des Guthabens dient",
"New Product": "Neues Produkt",
"No recharge options available": "No recharge options available",
"Order created successfully": "Bestellung erfolgreich erstellt",
"PayPal": "PayPal",
"Payment cancelled": "Zahlung storniert",
@@ -924,10 +935,12 @@
"Please select at least one payment provider": "Bitte wählen Sie mindestens einen Zahlungsanbieter aus",
"Processing payment...": "Zahlung wird verarbeitet...",
"Product list cannot be empty": "Produktliste darf nicht leer sein",
"Product not found or invalid": "Product not found or invalid",
"Quantity": "Menge",
"Quantity - Tooltip": "Menge des Produkts",
"Recharge options": "Aufladeoptionen",
"Recharge options - Tooltip": "Aufladeoptionen - Tooltip",
"Recharge products need to go to the product detail page to set custom amount": "Recharge products need to go to the product detail page to set custom amount",
"Return URL": "Rückkeht-URL",
"Return URL - Tooltip": "URL für die Rückkehr nach einem erfolgreichen Kauf",
"SKU": "SKU",
@@ -972,8 +985,6 @@
"Can signin": "Kann sich einloggen",
"Can signup": "Kann sich registrieren",
"Can unlink": "Entlinken möglich",
"Category": "Kategorie",
"Category - Tooltip": "Kennung zur Kategorisierung und Gruppierung von Elementen oder Inhalten, erleichtert Filterung und Verwaltung",
"Channel No.": "Kanal Nr.",
"Channel No. - Tooltip": "Eindeutige Nummer zur Identifizierung eines Kommunikations- oder Datenübertragungskanals, verwendet zur Unterscheidung verschiedener Übertragungswege",
"Chat ID": "Chat-ID",
@@ -990,8 +1001,6 @@
"Content - Tooltip": "Spezifische Informationen oder Daten in Nachrichten, Benachrichtigungen oder Dokumenten",
"DB test": "DB-Test",
"DB test - Tooltip": "DB-Test - Tooltip",
"Disable SSL": "SSL deaktivieren",
"Disable SSL - Tooltip": "Ob die Deaktivierung des SSL-Protokolls bei der Kommunikation mit dem STMP-Server erfolgen soll",
"Domain": "Domäne",
"Domain - Tooltip": "Benutzerdefinierte Domain für Objektspeicher",
"Edit Provider": "Provider bearbeiten",
@@ -1074,9 +1083,12 @@
"SP ACS URL": "SP-ACS-URL",
"SP ACS URL - Tooltip": "SP ACS URL",
"SP Entity ID": "SP-Entitäts-ID",
"SSL mode": "SSL mode",
"SSL mode - Tooltip": "SSL mode - Tooltip",
"Scene": "Szene",
"Scene - Tooltip": "Spezifisches Geschäftsszenario, in dem die Funktion oder Operation angewendet wird, verwendet zur Anpassung der logischen Verarbeitung für verschiedene Szenarien",
"Scope": "Umfang",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Secret-Access-Key",
"Secret access key - Tooltip": "Privater Schlüssel, der mit dem Zugriffsschlüssel gepaart ist, verwendet zum Signieren sensibler Operationen zur Verbesserung der Zugriffssicherheit",
"Secret key": "Secret-Key",
@@ -1238,6 +1250,9 @@
},
"syncer": {
"API Token / Password": "API Token / Password",
"AWS Access Key ID": "AWS Access Key ID",
"AWS Region": "AWS Region",
"AWS Secret Access Key": "AWS Secret Access Key",
"Admin Email": "Admin-E-Mail",
"Affiliation table": "Zuordnungstabelle",
"Affiliation table - Tooltip": "Datenbanktabellenname der Arbeitseinheit",
@@ -1269,8 +1284,6 @@
"SSH password": "SSH-Passwort",
"SSH port": "SSH-Port",
"SSH user": "SSH-Benutzer",
"SSL mode": "SSL-Modus",
"SSL mode - Tooltip": "SSL-Modus",
"Service account key": "Service-Account-Schlüssel",
"Sync interval": "Synchronisierungsintervall",
"Sync interval - Tooltip": "Einheit in Sekunden",

View File

@@ -70,6 +70,7 @@
"Enable signin session - Tooltip": "Whether Casdoor maintains a session after logging into Casdoor from the application",
"Enable signup": "Enable signup",
"Enable signup - Tooltip": "Whether to allow users to register a new account",
"Existing Field": "Existing Field",
"Failed signin frozen time": "Failed signin frozen time",
"Failed signin frozen time - Tooltip": "Waiting time after exceeding the number of failed login attempts. Users can only log in again after the waiting time expires. Default value is 15 minutes. The set value must be a positive integer",
"Failed signin limit": "Failed signin limit",
@@ -151,6 +152,7 @@
"Signup items - Tooltip": "Items for users to fill in when registering new accounts",
"Single Choice": "Single Choice",
"Small icon": "Small icon",
"Static Value": "Static Value",
"String": "String",
"Tags - Tooltip": "Only users with the tag that is listed in the application tags can login",
"The application does not allow to sign up new account": "The application does not allow to sign up new account",
@@ -184,9 +186,7 @@
"Expire in years - Tooltip": "Validity period of the certificate, in years",
"New Cert": "New Cert",
"Private key": "Private key",
"Private key - Tooltip": "Private key corresponding to the public key certificate",
"Scope - Tooltip": "Usage scenarios of the certificate",
"Type - Tooltip": "Type of certificate"
"Private key - Tooltip": "Private key corresponding to the public key certificate"
},
"code": {
"Code you received": "Code you received",
@@ -275,6 +275,7 @@
"Applications that require authentication": "Applications that require authentication",
"Apps": "Apps",
"Authorization": "Authorization",
"Auto": "Auto",
"Avatar": "Avatar",
"Avatar - Tooltip": "Public avatar image for the user",
"Back": "Back",
@@ -283,6 +284,8 @@
"Cancel": "Cancel",
"Captcha": "Captcha",
"Cart": "Cart",
"Category": "Category",
"Category - Tooltip": "Category - Tooltip",
"Cert": "Cert",
"Cert - Tooltip": "The public key certificate that needs to be verified by the client SDK corresponding to this application",
"Certs": "Certs",
@@ -476,6 +479,8 @@
"SSH type - Tooltip": "The auth type of SSH connection",
"Save": "Save",
"Save & Exit": "Save & Exit",
"Scopes": "Scopes",
"Scopes - Tooltip": "Scopes - Tooltip",
"Search": "Search",
"Send": "Send",
"Session ID": "Session ID",
@@ -530,6 +535,7 @@
"Transactions": "Transactions",
"True": "True",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "URL link",
"Unknown application name": "Unknown application name",
@@ -867,6 +873,8 @@
},
"plan": {
"Edit Plan": "Edit Plan",
"Is exclusive": "Is exclusive",
"Is exclusive - Tooltip": "Is exclusive - Tooltip",
"New Plan": "New Plan",
"Period": "Period",
"Period - Tooltip": "Period for the plan",
@@ -897,6 +905,7 @@
"Amount": "Amount",
"Buy": "Buy",
"Buy Product": "Buy Product",
"Cart contains invalid products, please delete them before placing an order": "Cart contains invalid products, please delete them before placing an order",
"Custom amount available": "Custom amount available",
"Custom price should be greater than zero": "Custom price should be greater than zero",
"Detail - Tooltip": "Detail of product",
@@ -909,9 +918,11 @@
"Image": "Image",
"Image - Tooltip": "Image of product",
"Information": "Information",
"Invalid product": "Invalid product",
"Is recharge": "Is recharge",
"Is recharge - Tooltip": "Whether the current product is to recharge balance",
"New Product": "New Product",
"No recharge options available": "No recharge options available",
"Order created successfully": "Order created successfully",
"PayPal": "PayPal",
"Payment cancelled": "Payment cancelled",
@@ -924,10 +935,12 @@
"Please select at least one payment provider": "Please select at least one payment provider",
"Processing payment...": "Processing payment...",
"Product list cannot be empty": "Product list cannot be empty",
"Product not found or invalid": "Product not found or invalid",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity of product",
"Recharge options": "Recharge options",
"Recharge options - Tooltip": "Preset recharge amounts",
"Recharge products need to go to the product detail page to set custom amount": "Recharge products need to go to the product detail page to set custom amount",
"Return URL": "Return URL",
"Return URL - Tooltip": "URL to return to after successful purchase",
"SKU": "SKU",
@@ -972,8 +985,6 @@
"Can signin": "Can signin",
"Can signup": "Can signup",
"Can unlink": "Can unlink",
"Category": "Category",
"Category - Tooltip": "Identifier for categorizing and grouping items or content, facilitating filtering and management",
"Channel No.": "Channel No.",
"Channel No. - Tooltip": "Unique number identifying a communication or data transmission channel, used to distinguish different transmission paths",
"Chat ID": "Chat ID",
@@ -990,8 +1001,6 @@
"Content - Tooltip": "Specific information or data contained in messages, notifications, or documents",
"DB test": "DB test",
"DB test - Tooltip": "DB test - Tooltip",
"Disable SSL": "Disable SSL",
"Disable SSL - Tooltip": "Whether to disable SSL protocol when communicating with STMP server",
"Domain": "Domain",
"Domain - Tooltip": "Custom domain for object storage",
"Edit Provider": "Edit Provider",
@@ -1074,9 +1083,12 @@
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL",
"SP Entity ID": "SP Entity ID",
"SSL mode": "SSL mode",
"SSL mode - Tooltip": "SSL mode - Tooltip",
"Scene": "Scene",
"Scene - Tooltip": "Specific business scenario where the function or operation applies, used to adapt logic processing for different scenarios",
"Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Secret access key",
"Secret access key - Tooltip": "Private key paired with the access key, used for signing sensitive operations to enhance access security",
"Secret key": "Secret key",
@@ -1238,6 +1250,9 @@
},
"syncer": {
"API Token / Password": "API Token / Password",
"AWS Access Key ID": "AWS Access Key ID",
"AWS Region": "AWS Region",
"AWS Secret Access Key": "AWS Secret Access Key",
"Admin Email": "Admin Email",
"Affiliation table": "Affiliation table",
"Affiliation table - Tooltip": "Database table name of the work unit",
@@ -1262,8 +1277,6 @@
"Is key": "Is key",
"Is read-only": "Is read-only",
"Is read-only - Tooltip": "Is read-only - Tooltip",
"Name mapping": "Name mapping",
"Name mapping - Tooltip": "Choose which DingTalk field to use as the username in Casdoor. Default is Union ID for compatibility with OAuth login.",
"New Syncer": "New Syncer",
"Paste your Google Workspace service account JSON key here": "Paste your Google Workspace service account JSON key here",
"SCIM Server URL": "SCIM Server URL",
@@ -1271,8 +1284,6 @@
"SSH password": "SSH password",
"SSH port": "SSH port",
"SSH user": "SSH user",
"SSL mode": "SSL mode",
"SSL mode - Tooltip": "The SSL mode used when connecting to the database",
"Service account key": "Service account key",
"Sync interval": "Sync interval",
"Sync interval - Tooltip": "Unit in seconds",

View File

@@ -70,6 +70,7 @@
"Enable signin session - Tooltip": "Si Casdoor mantiene una sesión después de iniciar sesión en Casdoor desde la aplicación",
"Enable signup": "Habilitar registro",
"Enable signup - Tooltip": "Ya sea permitir que los usuarios registren una nueva cuenta",
"Existing Field": "Existing Field",
"Failed signin frozen time": "Tiempo de congelación tras inicio fallido",
"Failed signin frozen time - Tooltip": "Tiempo durante el cual la cuenta está congelada después de intentos fallidos de inicio de sesión",
"Failed signin limit": "Límite de intentos fallidos de inicio",
@@ -134,7 +135,6 @@
"SAML metadata": "Metadatos de SAML",
"SAML metadata - Tooltip": "Los metadatos del protocolo SAML - Sugerencia",
"SAML reply URL": "URL de respuesta SAML",
"SAML reply URL - Tooltip": "Personalizar el código HTML del panel lateral de la página de inicio de sesión - Sugerencia",
"Security": "Seguridad",
"Select": "Seleccionar",
"Side panel HTML": "Panel lateral HTML",
@@ -152,6 +152,7 @@
"Signup items - Tooltip": "Elementos para que los usuarios completen al registrar nuevas cuentas - Sugerencia",
"Single Choice": "Opción única",
"Small icon": "Icono pequeño",
"Static Value": "Static Value",
"String": "Cadena",
"Tags - Tooltip": "Solo los usuarios con la etiqueta que esté listada en las etiquetas de la aplicación pueden iniciar sesión - Sugerencia",
"The application does not allow to sign up new account": "La aplicación no permite registrarse una cuenta nueva",
@@ -185,9 +186,7 @@
"Expire in years - Tooltip": "Período de validez del certificado, en años",
"New Cert": "Nuevo certificado",
"Private key": "Clave privada",
"Private key - Tooltip": "Clave privada correspondiente al certificado de clave pública",
"Scope - Tooltip": "Escenarios de uso del certificado",
"Type - Tooltip": "Tipo de certificado"
"Private key - Tooltip": "Clave privada correspondiente al certificado de clave pública"
},
"code": {
"Code you received": "Código que recibió",
@@ -276,6 +275,7 @@
"Applications that require authentication": "Aplicaciones que requieren autenticación",
"Apps": "Aplicaciones",
"Authorization": "Autorización",
"Auto": "Auto",
"Avatar": "Avatar",
"Avatar - Tooltip": "Imagen de avatar pública para el usuario",
"Back": "Atrás",
@@ -284,6 +284,8 @@
"Cancel": "Cancelar",
"Captcha": "Captcha",
"Cart": "Carrito",
"Category": "Category",
"Category - Tooltip": "Category - Tooltip",
"Cert": "Certificado",
"Cert - Tooltip": "El certificado de clave pública que necesita ser verificado por el SDK del cliente correspondiente a esta aplicación",
"Certs": "Certificaciones",
@@ -477,6 +479,8 @@
"SSH type - Tooltip": "El tipo de autenticación de conexión SSH",
"Save": "Guardar",
"Save & Exit": "Guardar y salir",
"Scopes": "Scopes",
"Scopes - Tooltip": "Scopes - Tooltip",
"Search": "Buscar",
"Send": "Enviar",
"Session ID": "ID de sesión",
@@ -531,6 +535,7 @@
"Transactions": "Transacciones",
"True": "Verdadero",
"Type": "Tipo",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "Enlace de URL",
"Unknown application name": "Nombre de aplicación desconocido",
@@ -868,6 +873,8 @@
},
"plan": {
"Edit Plan": "Editar plan",
"Is exclusive": "Is exclusive",
"Is exclusive - Tooltip": "Is exclusive - Tooltip",
"New Plan": "Nuevo plan",
"Period": "Período",
"Period - Tooltip": "Período",
@@ -898,6 +905,7 @@
"Amount": "Importe",
"Buy": "Comprar",
"Buy Product": "Comprar producto",
"Cart contains invalid products, please delete them before placing an order": "Cart contains invalid products, please delete them before placing an order",
"Custom amount available": "Importe personalizado disponible",
"Custom price should be greater than zero": "El precio personalizado debe ser mayor que cero",
"Detail - Tooltip": "Detalle del producto",
@@ -910,9 +918,11 @@
"Image": "Imagen",
"Image - Tooltip": "Imagen del producto",
"Information": "Información",
"Invalid product": "Invalid product",
"Is recharge": "Es recarga",
"Is recharge - Tooltip": "Indica si el producto actual es para recargar saldo",
"New Product": "Nuevo producto",
"No recharge options available": "No recharge options available",
"Order created successfully": "Pedido creado con éxito",
"PayPal": "PayPal",
"Payment cancelled": "Pago cancelado",
@@ -925,10 +935,12 @@
"Please select at least one payment provider": "Por favor, selecciona al menos un proveedor de pago",
"Processing payment...": "Procesando el pago...",
"Product list cannot be empty": "La lista de productos no puede estar vacía",
"Product not found or invalid": "Product not found or invalid",
"Quantity": "Cantidad",
"Quantity - Tooltip": "Cantidad de producto",
"Recharge options": "Opciones de recarga",
"Recharge options - Tooltip": "Opciones de recarga - Tooltip",
"Recharge products need to go to the product detail page to set custom amount": "Recharge products need to go to the product detail page to set custom amount",
"Return URL": "URL de retorno",
"Return URL - Tooltip": "URL para regresar después de una compra exitosa",
"SKU": "SKU",
@@ -973,8 +985,6 @@
"Can signin": "¿Puedes iniciar sesión?",
"Can signup": "Puede registrarse",
"Can unlink": "Desvincular",
"Category": "Categoría",
"Category - Tooltip": "Identificador para categorizar y agrupar elementos o contenido, facilitando el filtrado y la gestión",
"Channel No.": "Canal No.",
"Channel No. - Tooltip": "Número único que identifica un canal de comunicación o transmisión de datos, utilizado para distinguir diferentes rutas de transmisión",
"Chat ID": "ID de chat",
@@ -991,8 +1001,6 @@
"Content - Tooltip": "Contenido - Información adicional",
"DB test": "Prueba de BD",
"DB test - Tooltip": "Prueba de BD - Tooltip",
"Disable SSL": "Desactivar SSL",
"Disable SSL - Tooltip": "¿Hay que desactivar el protocolo SSL al comunicarse con el servidor STMP?",
"Domain": "Dominio",
"Domain - Tooltip": "Dominio personalizado para almacenamiento de objetos",
"Edit Provider": "Editar proveedor",
@@ -1075,9 +1083,12 @@
"SP ACS URL": "URL de ACS de SP",
"SP ACS URL - Tooltip": "URL del ACS de SP",
"SP Entity ID": "ID de entidad SP",
"SSL mode": "SSL mode",
"SSL mode - Tooltip": "SSL mode - Tooltip",
"Scene": "Escena",
"Scene - Tooltip": "Escena",
"Scope": "Alcance",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Clave de acceso secreta",
"Secret access key - Tooltip": "Clave de acceso secreta",
"Secret key": "Clave secreta",
@@ -1239,6 +1250,9 @@
},
"syncer": {
"API Token / Password": "Token API / Contraseña",
"AWS Access Key ID": "AWS Access Key ID",
"AWS Region": "AWS Region",
"AWS Secret Access Key": "AWS Secret Access Key",
"Admin Email": "Correo del administrador",
"Affiliation table": "Tabla de afiliación",
"Affiliation table - Tooltip": "Nombre de la tabla de base de datos de la unidad de trabajo",
@@ -1270,8 +1284,6 @@
"SSH password": "Contraseña SSH",
"SSH port": "Puerto SSH",
"SSH user": "Usuario SSH",
"SSL mode": "Modo SSL",
"SSL mode - Tooltip": "Modo SSL",
"Service account key": "Clave de la cuenta de servicio",
"Sync interval": "Intervalo de sincronización",
"Sync interval - Tooltip": "Unidad en segundos",

View File

@@ -70,6 +70,7 @@
"Enable signin session - Tooltip": "Conserver une session après la connexion à Casdoor à partir de l'application",
"Enable signup": "Activer l'inscription",
"Enable signup - Tooltip": "Autoriser la création de nouveaux comptes",
"Existing Field": "Existing Field",
"Failed signin frozen time": "Temps de blocage après échec de connexion",
"Failed signin frozen time - Tooltip": "Durée pendant laquelle le compte est gelé après des tentatives de connexion échouées",
"Failed signin limit": "Limite d'échecs de connexion",
@@ -151,6 +152,7 @@
"Signup items - Tooltip": "Éléments à remplir par les utilisateurs lors de la création de nouveaux comptes - Info-bulle",
"Single Choice": "Choix unique",
"Small icon": "Petite icône",
"Static Value": "Static Value",
"String": "String",
"Tags - Tooltip": "Seuls les utilisateurs avec le tag listé dans les tags de l'application peuvent se connecter - Info-bulle",
"The application does not allow to sign up new account": "L'application ne permet pas de créer un nouveau compte",
@@ -184,9 +186,7 @@
"Expire in years - Tooltip": "Période de validité du certificat, en années",
"New Cert": "Nouveau Certificat",
"Private key": "Clé privée",
"Private key - Tooltip": "Clé privée correspondant au certificat de la clé publique",
"Scope - Tooltip": "Scénarios d'utilisation du certificat",
"Type - Tooltip": "Type de certificat"
"Private key - Tooltip": "Clé privée correspondant au certificat de la clé publique"
},
"code": {
"Code you received": "Le code que vous avez reçu",
@@ -275,6 +275,7 @@
"Applications that require authentication": "Applications qui nécessitent une authentification",
"Apps": "Applications",
"Authorization": "Autorisation",
"Auto": "Auto",
"Avatar": "Avatar",
"Avatar - Tooltip": "Image d'avatar publique pour le compte",
"Back": "Retour",
@@ -283,6 +284,8 @@
"Cancel": "Annuler",
"Captcha": "Captcha",
"Cart": "Panier",
"Category": "Category",
"Category - Tooltip": "Category - Tooltip",
"Cert": "Certificat",
"Cert - Tooltip": "La clé publique du certificat qui doit être vérifiée par le kit de développement client correspondant à cette application",
"Certs": "Certificats",
@@ -476,6 +479,8 @@
"SSH type - Tooltip": "Type d'authentification de connexion SSH",
"Save": "Enregistrer",
"Save & Exit": "Enregistrer et quitter",
"Scopes": "Scopes",
"Scopes - Tooltip": "Scopes - Tooltip",
"Search": "Rechercher",
"Send": "Envoyer",
"Session ID": "Identifiant de session",
@@ -530,6 +535,7 @@
"Transactions": "Transactions",
"True": "Vrai",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "Lien de l'URL",
"Unknown application name": "Nom d'application inconnu",
@@ -867,6 +873,8 @@
},
"plan": {
"Edit Plan": "Modifier le plan",
"Is exclusive": "Is exclusive",
"Is exclusive - Tooltip": "Is exclusive - Tooltip",
"New Plan": "Nouveau plan",
"Period": "Période",
"Period - Tooltip": "Période",
@@ -897,6 +905,7 @@
"Amount": "Montant",
"Buy": "Acheter",
"Buy Product": "Acheter un produit",
"Cart contains invalid products, please delete them before placing an order": "Cart contains invalid products, please delete them before placing an order",
"Custom amount available": "Montant personnalisé disponible",
"Custom price should be greater than zero": "Le prix personnalisé doit être supérieur à zéro",
"Detail - Tooltip": "Détail du produit - Infobulle",
@@ -909,9 +918,11 @@
"Image": "Image",
"Image - Tooltip": "Image du produit",
"Information": "Informations",
"Invalid product": "Invalid product",
"Is recharge": "Est un rechargement",
"Is recharge - Tooltip": "Indique si le produit actuel permet de recharger le solde",
"New Product": "Nouveau produit",
"No recharge options available": "No recharge options available",
"Order created successfully": "Commande créée avec succès",
"PayPal": "PayPal",
"Payment cancelled": "Paiement annulé",
@@ -924,10 +935,12 @@
"Please select at least one payment provider": "Veuillez sélectionner au moins un fournisseur de paiement",
"Processing payment...": "Traitement du paiement...",
"Product list cannot be empty": "La liste des produits ne peut pas être vide",
"Product not found or invalid": "Product not found or invalid",
"Quantity": "Quantité",
"Quantity - Tooltip": "Quantité du produit",
"Recharge options": "Options de recharge",
"Recharge options - Tooltip": "Recharge options - Tooltip",
"Recharge products need to go to the product detail page to set custom amount": "Recharge products need to go to the product detail page to set custom amount",
"Return URL": "URL de retour",
"Return URL - Tooltip": "URL de retour après l'achat réussi",
"SKU": "SKU",
@@ -972,8 +985,6 @@
"Can signin": "Pouvez-vous vous connecter?",
"Can signup": "Peut s'inscrire",
"Can unlink": "Peut annuler le lien",
"Category": "Catégorie",
"Category - Tooltip": "Sélectionnez une catégorie",
"Channel No.": "chaîne n°",
"Channel No. - Tooltip": "Canal N°",
"Chat ID": "ID de chat",
@@ -990,8 +1001,6 @@
"Content - Tooltip": "Contenu - Infobulle",
"DB test": "Test BD",
"DB test - Tooltip": "Test BD - Infobulle",
"Disable SSL": "Désactiver SSL",
"Disable SSL - Tooltip": "Désactiver le protocole SSL lors de la communication avec le serveur STMP",
"Domain": "Domaine",
"Domain - Tooltip": "Domaine personnalisé pour le stockage d'objets",
"Edit Provider": "Modifier le fournisseur",
@@ -1074,9 +1083,12 @@
"SP ACS URL": "URL du SP ACS",
"SP ACS URL - Tooltip": "URL de l'ACS du fournisseur de service",
"SP Entity ID": "Identifiant d'entité SP",
"SSL mode": "SSL mode",
"SSL mode - Tooltip": "SSL mode - Tooltip",
"Scene": "Scène",
"Scene - Tooltip": "Scène",
"Scope": "Portée",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Clé d'accès secrète",
"Secret access key - Tooltip": "Clé d'accès secrète",
"Secret key": "Clé secrète",
@@ -1238,6 +1250,9 @@
},
"syncer": {
"API Token / Password": "Jeton API / Mot de passe",
"AWS Access Key ID": "AWS Access Key ID",
"AWS Region": "AWS Region",
"AWS Secret Access Key": "AWS Secret Access Key",
"Admin Email": "E-mail admin",
"Affiliation table": "Table d'affiliation",
"Affiliation table - Tooltip": "Nom de la table de la base de données de l'unité de travail",
@@ -1269,8 +1284,6 @@
"SSH password": "Mot de passe SSH",
"SSH port": "Port SSH",
"SSH user": "Utilisateur SSH",
"SSL mode": "Mode SSL",
"SSL mode - Tooltip": "Mode SSL",
"Service account key": "Clé du compte de service",
"Sync interval": "Intervalle de synchronisation",
"Sync interval - Tooltip": "Unité en secondes",

View File

@@ -70,6 +70,7 @@
"Enable signin session - Tooltip": "アプリケーションから Casdoor にログイン後、Casdoor がセッションを維持しているかどうか",
"Enable signup": "サインアップを有効にする",
"Enable signup - Tooltip": "新しいアカウントの登録をユーザーに許可するかどうか",
"Existing Field": "Existing Field",
"Failed signin frozen time": "サインイン失敗時の凍結時間",
"Failed signin frozen time - Tooltip": "サインイン失敗後にアカウントが凍結される時間",
"Failed signin limit": "サインイン失敗回数制限",
@@ -117,7 +118,6 @@
"Please input your organization!": "あなたの組織を入力してください!",
"Please select a HTML file": "HTMLファイルを選択してください",
"Pop up": "ポップアップ",
"Pop up - Tooltip": "ポップアップ - ヒント",
"Providers": "プロバイダー",
"Random": "ランダム",
"Real name": "本名",
@@ -135,7 +135,6 @@
"SAML metadata": "SAMLメタデータ",
"SAML metadata - Tooltip": "SAMLプロトコルのメタデータ - ヒント",
"SAML reply URL": "SAMLリプライURL",
"SAML reply URL - Tooltip": "SAMLリプライURL - ヒント",
"Security": "セキュリティ",
"Select": "選択",
"Side panel HTML": "サイドパネルのHTML",
@@ -153,6 +152,7 @@
"Signup items - Tooltip": "新しいアカウントを登録する際にユーザーが入力するアイテム",
"Single Choice": "単一選択",
"Small icon": "小さいアイコン",
"Static Value": "Static Value",
"String": "文字列",
"Tags - Tooltip": "アプリケーションタグに含まれるタグを持つユーザーのみログイン可能です",
"The application does not allow to sign up new account": "アプリケーションでは新しいアカウントの登録ができません",
@@ -186,9 +186,7 @@
"Expire in years - Tooltip": "証明書の有効期間、年数で",
"New Cert": "新しい証明書",
"Private key": "プライベートキー",
"Private key - Tooltip": "公開鍵証明書に対応する秘密鍵",
"Scope - Tooltip": "証明書の使用シナリオ",
"Type - Tooltip": "証明書の種類"
"Private key - Tooltip": "公開鍵証明書に対応する秘密鍵"
},
"code": {
"Code you received": "受け取ったコード",
@@ -277,6 +275,7 @@
"Applications that require authentication": "認証が必要なアプリケーション",
"Apps": "アプリ",
"Authorization": "認可",
"Auto": "Auto",
"Avatar": "アバター",
"Avatar - Tooltip": "ユーザーのパブリックアバター画像",
"Back": "戻る",
@@ -285,6 +284,8 @@
"Cancel": "キャンセルします",
"Captcha": "キャプチャ",
"Cart": "カート",
"Category": "Category",
"Category - Tooltip": "Category - Tooltip",
"Cert": "証明書",
"Cert - Tooltip": "このアプリケーションに対応するクライアントSDKによって検証する必要がある公開鍵証明書",
"Certs": "証明書",
@@ -478,6 +479,8 @@
"SSH type - Tooltip": "SSH接続の認証タイプ",
"Save": "保存",
"Save & Exit": "保存して終了",
"Scopes": "Scopes",
"Scopes - Tooltip": "Scopes - Tooltip",
"Search": "検索",
"Send": "送信",
"Session ID": "セッションID",
@@ -532,6 +535,7 @@
"Transactions": "取引",
"True": "真",
"Type": "タイプ",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "URLリンク",
"Unknown application name": "不明なアプリケーション名",
@@ -869,6 +873,8 @@
},
"plan": {
"Edit Plan": "プランを編集",
"Is exclusive": "Is exclusive",
"Is exclusive - Tooltip": "Is exclusive - Tooltip",
"New Plan": "新しいプラン",
"Period": "期間",
"Period - Tooltip": "期間",
@@ -899,6 +905,7 @@
"Amount": "金額",
"Buy": "購入",
"Buy Product": "製品を購入する",
"Cart contains invalid products, please delete them before placing an order": "Cart contains invalid products, please delete them before placing an order",
"Custom amount available": "任意金額を利用可能",
"Custom price should be greater than zero": "カスタム価格は0より大きくする必要があります",
"Detail - Tooltip": "製品の詳細",
@@ -911,9 +918,11 @@
"Image": "画像",
"Image - Tooltip": "製品のイメージ",
"Information": "情報",
"Invalid product": "Invalid product",
"Is recharge": "チャージ用か",
"Is recharge - Tooltip": "現在の製品が残高をチャージするためかどうか",
"New Product": "新製品",
"No recharge options available": "No recharge options available",
"Order created successfully": "注文が正常に作成されました",
"PayPal": "ペイパル",
"Payment cancelled": "支払いキャンセル",
@@ -926,10 +935,12 @@
"Please select at least one payment provider": "少なくとも1つの支払いプロバイダーを選択してください",
"Processing payment...": "支払い処理中...",
"Product list cannot be empty": "商品リストを空にできません",
"Product not found or invalid": "Product not found or invalid",
"Quantity": "量",
"Quantity - Tooltip": "製品の量",
"Recharge options": "チャージオプション",
"Recharge options - Tooltip": "チャージオプション - ツールチップ",
"Recharge products need to go to the product detail page to set custom amount": "Recharge products need to go to the product detail page to set custom amount",
"Return URL": "戻りURL",
"Return URL - Tooltip": "成功した購入後に戻るURL",
"SKU": "SKU",
@@ -974,8 +985,6 @@
"Can signin": "サインインできますか?",
"Can signup": "サインアップできますか?",
"Can unlink": "アンリンクすることができます",
"Category": "カテゴリー",
"Category - Tooltip": "カテゴリーを選択してください",
"Channel No.": "チャンネル番号",
"Channel No. - Tooltip": "チャンネル番号",
"Chat ID": "チャットID",
@@ -992,8 +1001,6 @@
"Content - Tooltip": "コンテンツ - ツールチップ",
"DB test": "DBテスト",
"DB test - Tooltip": "DBテスト - ツールチップ",
"Disable SSL": "SSLを無効にする",
"Disable SSL - Tooltip": "SMTPサーバーと通信する場合にSSLプロトコルを無効にするかどうか",
"Domain": "ドメイン",
"Domain - Tooltip": "オブジェクトストレージのカスタムドメイン",
"Edit Provider": "編集プロバイダー",
@@ -1076,9 +1083,12 @@
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL - ツールチップ",
"SP Entity ID": "SPエンティティID",
"SSL mode": "SSL mode",
"SSL mode - Tooltip": "SSL mode - Tooltip",
"Scene": "シーン",
"Scene - Tooltip": "シーン",
"Scope": "範囲",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "秘密のアクセスキー",
"Secret access key - Tooltip": "秘密のアクセスキー",
"Secret key": "秘密鍵",
@@ -1240,6 +1250,9 @@
},
"syncer": {
"API Token / Password": "APIトークン / パスワード",
"AWS Access Key ID": "AWS Access Key ID",
"AWS Region": "AWS Region",
"AWS Secret Access Key": "AWS Secret Access Key",
"Admin Email": "管理者メール",
"Affiliation table": "所属テーブル",
"Affiliation table - Tooltip": "作業単位のデータベーステーブル名",
@@ -1271,8 +1284,6 @@
"SSH password": "SSHパスワード",
"SSH port": "SSHポート",
"SSH user": "SSHユーザー",
"SSL mode": "SSLモード",
"SSL mode - Tooltip": "SSLモード",
"Service account key": "サービスアカウントキー",
"Sync interval": "同期の間隔",
"Sync interval - Tooltip": "単位は秒です",

View File

@@ -70,6 +70,7 @@
"Enable signin session - Tooltip": "Czy Casdoor utrzymuje sesję po zalogowaniu do Casdoor z poziomu aplikacji",
"Enable signup": "Włącz rejestrację",
"Enable signup - Tooltip": "Czy zezwolić użytkownikom na rejestrację nowych kont",
"Existing Field": "Existing Field",
"Failed signin frozen time": "Czas blokady po nieudanym logowaniu",
"Failed signin frozen time - Tooltip": "Czas w którym konto jest zablokowane po nieudanych próbach logowania - Podpowiedź",
"Failed signin limit": "Limit nieudanych logowań",
@@ -151,6 +152,7 @@
"Signup items - Tooltip": "Elementy, które użytkownicy muszą wypełnić podczas rejestracji nowych kont",
"Single Choice": "Jednokrotny wybór",
"Small icon": "Mała ikona",
"Static Value": "Static Value",
"String": "Ciąg",
"Tags - Tooltip": "Tylko użytkownicy z tagiem wymienionym w tagach aplikacji mogą się zalogować",
"The application does not allow to sign up new account": "Aplikacja nie zezwala na rejestrację nowego konta",
@@ -184,9 +186,7 @@
"Expire in years - Tooltip": "Okres ważności certyfikatu, w latach",
"New Cert": "Nowy certyfikat",
"Private key": "Klucz prywatny",
"Private key - Tooltip": "Klucz prywatny odpowiadający certyfikatowi klucza publicznego",
"Scope - Tooltip": "Scenariusze użycia certyfikatu",
"Type - Tooltip": "Typ certyfikatu"
"Private key - Tooltip": "Klucz prywatny odpowiadający certyfikatowi klucza publicznego"
},
"code": {
"Code you received": "Kod, który otrzymałeś",
@@ -275,6 +275,7 @@
"Applications that require authentication": "Aplikacje wymagające uwierzytelniania",
"Apps": "Aplikacje",
"Authorization": "Autoryzacja",
"Auto": "Auto",
"Avatar": "Awatar",
"Avatar - Tooltip": "Publiczny obraz awatara użytkownika",
"Back": "Wstecz",
@@ -283,6 +284,8 @@
"Cancel": "Anuluj",
"Captcha": "Captcha",
"Cart": "Koszyk",
"Category": "Category",
"Category - Tooltip": "Category - Tooltip",
"Cert": "Certyfikat",
"Cert - Tooltip": "Certyfikat klucza publicznego, który musi być zweryfikowany przez odpowiednią aplikację SDK po stronie klienta",
"Certs": "Certyfikaty",
@@ -476,6 +479,8 @@
"SSH type - Tooltip": "Typ uwierzytelniania połączenia SSH",
"Save": "Zapisz",
"Save & Exit": "Zapisz i wyjdź",
"Scopes": "Scopes",
"Scopes - Tooltip": "Scopes - Tooltip",
"Search": "Szukaj",
"Send": "Wyślij",
"Session ID": "ID sesji",
@@ -530,6 +535,7 @@
"Transactions": "Transakcje",
"True": "Prawda",
"Type": "Typ",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "Link URL",
"Unknown application name": "Nieznana nazwa aplikacji",
@@ -867,6 +873,8 @@
},
"plan": {
"Edit Plan": "Edytuj plan",
"Is exclusive": "Is exclusive",
"Is exclusive - Tooltip": "Is exclusive - Tooltip",
"New Plan": "Nowy plan",
"Period": "Okres",
"Period - Tooltip": "Okres",
@@ -897,6 +905,7 @@
"Amount": "Kwota",
"Buy": "Kup",
"Buy Product": "Kup produkt",
"Cart contains invalid products, please delete them before placing an order": "Cart contains invalid products, please delete them before placing an order",
"Custom amount available": "Dostępna kwota niestandardowa",
"Custom price should be greater than zero": "Cena niestandardowa musi być większa od zera",
"Detail - Tooltip": "Szczegóły produktu",
@@ -909,9 +918,11 @@
"Image": "Obrazek",
"Image - Tooltip": "Obrazek produktu",
"Information": "Informacje",
"Invalid product": "Invalid product",
"Is recharge": "Jest doładowaniem",
"Is recharge - Tooltip": "Czy bieżący produkt służy do doładowania salda",
"New Product": "Nowy produkt",
"No recharge options available": "No recharge options available",
"Order created successfully": "Zamówienie utworzone pomyślnie",
"PayPal": "PayPal",
"Payment cancelled": "Płatność anulowana",
@@ -924,10 +935,12 @@
"Please select at least one payment provider": "Wybierz co najmniej jednego dostawcę płatności",
"Processing payment...": "Przetwarzanie płatności...",
"Product list cannot be empty": "Lista produktów nie może być pusta",
"Product not found or invalid": "Product not found or invalid",
"Quantity": "Ilość",
"Quantity - Tooltip": "Ilość produktu",
"Recharge options": "Opcje doładowania",
"Recharge options - Tooltip": "Opcje doładowania - Podpowiedź",
"Recharge products need to go to the product detail page to set custom amount": "Recharge products need to go to the product detail page to set custom amount",
"Return URL": "Adres powrotu",
"Return URL - Tooltip": "Adres do powrotu po udanym zakupie",
"SKU": "SKU",
@@ -972,8 +985,10 @@
"Can signin": "Można się zalogować",
"Can signup": "Można się zarejestrować",
"Can unlink": "Można odłączyć",
"Category": "Kategoria",
"Category - Tooltip": "Wybierz kategorię",
"Channel No.": "Channel No.",
"Channel No. - Tooltip": "Channel No. - Tooltip",
"Chat ID": "Chat ID",
"Chat ID - Tooltip": "Chat ID - Tooltip",
"Client ID": "ID klienta",
"Client ID - Tooltip": "ID klienta",
"Client ID 2": "ID klienta 2",
@@ -986,6 +1001,37 @@
"Content - Tooltip": "Treść",
"DB test": "Test bazy danych",
"DB test - Tooltip": "Test bazy danych",
"Domain": "Domain",
"Domain - Tooltip": "Domain - Tooltip",
"Edit Provider": "Edit Provider",
"Email content": "Email content",
"Email content - Tooltip": "Email content - Tooltip",
"Email regex": "Email regex",
"Email regex - Tooltip": "Email regex - Tooltip",
"Email title": "Email title",
"Email title - Tooltip": "Email title - Tooltip",
"Enable PKCE": "Enable PKCE",
"Enable PKCE - Tooltip": "Enable PKCE - Tooltip",
"Enable proxy": "Enable proxy",
"Enable proxy - Tooltip": "Enable proxy - Tooltip",
"Endpoint": "Endpoint",
"Endpoint (Intranet)": "Endpoint (Intranet)",
"Endpoint - Tooltip": "Endpoint - Tooltip",
"Follow-up action": "Follow-up action",
"Follow-up action - Tooltip": "Follow-up action - Tooltip",
"From address": "From address",
"From address - Tooltip": "From address - Tooltip",
"From name": "From name",
"From name - Tooltip": "From name - Tooltip",
"Get phone number": "Get phone number",
"Get phone number - Tooltip": "Get phone number - Tooltip",
"HTTP body mapping": "HTTP body mapping",
"HTTP body mapping - Tooltip": "HTTP body mapping - Tooltip",
"HTTP header": "HTTP header",
"HTTP header - Tooltip": "HTTP header - Tooltip",
"Host": "Host",
"Host - Tooltip": "Host - Tooltip",
"IdP": "IdP",
"IdP certificate": "Certyfikat IdP",
"Internal": "Wewnętrzny",
"Issuer URL": "Adres URL wystawcy",
@@ -1037,9 +1083,12 @@
"SP ACS URL": "Adres URL SP ACS",
"SP ACS URL - Tooltip": "Adres URL SP ACS",
"SP Entity ID": "ID jednostki SP",
"SSL mode": "SSL mode",
"SSL mode - Tooltip": "SSL mode - Tooltip",
"Scene": "Scena",
"Scene - Tooltip": "Scena",
"Scope": "Zakres",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Tajny klucz dostępu",
"Secret access key - Tooltip": "Tajny klucz dostępu",
"Secret key": "Tajny klucz",
@@ -1201,6 +1250,9 @@
},
"syncer": {
"API Token / Password": "API Token / Password",
"AWS Access Key ID": "AWS Access Key ID",
"AWS Region": "AWS Region",
"AWS Secret Access Key": "AWS Secret Access Key",
"Admin Email": "Admin Email",
"Affiliation table": "Tabela przynależności",
"Affiliation table - Tooltip": "Nazwa tabeli bazy danych jednostki pracy",
@@ -1232,8 +1284,6 @@
"SSH password": "Hasło SSH",
"SSH port": "Port SSH",
"SSH user": "Użytkownik SSH",
"SSL mode": "Tryb SSL",
"SSL mode - Tooltip": "Tryb SSL - etykietka",
"Service account key": "Klucz konta usługi",
"Sync interval": "Interwał synchronizacji",
"Sync interval - Tooltip": "Jednostka w sekundach",

View File

@@ -70,6 +70,7 @@
"Enable signin session - Tooltip": "Se o Casdoor mantém uma sessão depois de fazer login no Casdoor a partir da aplicação",
"Enable signup": "Ativar registro",
"Enable signup - Tooltip": "Se permite que os usuários registrem uma nova conta",
"Existing Field": "Existing Field",
"Failed signin frozen time": "Tempo de bloqueio após falha de login",
"Failed signin frozen time - Tooltip": "Tempo em que a conta fica congelada após tentativas de login falhadas",
"Failed signin limit": "Limite de tentativas de login falhadas",
@@ -151,6 +152,7 @@
"Signup items - Tooltip": "Itens para os usuários preencherem ao fazer login - Dica",
"Single Choice": "Escolha única",
"Small icon": "Ícone pequeno",
"Static Value": "Static Value",
"String": "String",
"Tags - Tooltip": "Apenas usuários com a tag listada nas tags da aplicação podem fazer login - Dica",
"The application does not allow to sign up new account": "A aplicação não permite o registro de novas contas",
@@ -184,9 +186,7 @@
"Expire in years - Tooltip": "Período de validade do certificado, em anos",
"New Cert": "Novo Certificado",
"Private key": "Chave privada",
"Private key - Tooltip": "Chave privada correspondente ao certificado de chave pública",
"Scope - Tooltip": "Cenários de uso do certificado",
"Type - Tooltip": "Tipo de certificado"
"Private key - Tooltip": "Chave privada correspondente ao certificado de chave pública"
},
"code": {
"Code you received": "Código que você recebeu",
@@ -275,6 +275,7 @@
"Applications that require authentication": "Aplicações que requerem autenticação",
"Apps": "Aplicativos",
"Authorization": "Autorização",
"Auto": "Auto",
"Avatar": "Avatar",
"Avatar - Tooltip": "Imagem de avatar pública do usuário",
"Back": "Voltar",
@@ -283,6 +284,8 @@
"Cancel": "Cancelar",
"Captcha": "Captcha",
"Cart": "Carrinho",
"Category": "Category",
"Category - Tooltip": "Category - Tooltip",
"Cert": "Certificado",
"Cert - Tooltip": "O certificado da chave pública que precisa ser verificado pelo SDK do cliente correspondente a esta aplicação",
"Certs": "Certificados",
@@ -476,6 +479,8 @@
"SSH type - Tooltip": "Tipo de autenticação para conexão SSH",
"Save": "Salvar",
"Save & Exit": "Salvar e Sair",
"Scopes": "Scopes",
"Scopes - Tooltip": "Scopes - Tooltip",
"Search": "Buscar",
"Send": "Enviar",
"Session ID": "ID da sessão",
@@ -530,6 +535,7 @@
"Transactions": "Transações",
"True": "Verdadeiro",
"Type": "Tipo",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "Link da URL",
"Unknown application name": "Nome de aplicação desconhecido",
@@ -867,6 +873,8 @@
},
"plan": {
"Edit Plan": "Editar Plano",
"Is exclusive": "Is exclusive",
"Is exclusive - Tooltip": "Is exclusive - Tooltip",
"New Plan": "Novo Plano",
"Period": "Período",
"Period - Tooltip": "Período",
@@ -897,6 +905,7 @@
"Amount": "Valor",
"Buy": "Comprar",
"Buy Product": "Comprar Produto",
"Cart contains invalid products, please delete them before placing an order": "Cart contains invalid products, please delete them before placing an order",
"Custom amount available": "Valor personalizado disponível",
"Custom price should be greater than zero": "O preço personalizado deve ser maior que zero",
"Detail - Tooltip": "Detalhes do produto",
@@ -909,9 +918,11 @@
"Image": "Imagem",
"Image - Tooltip": "Imagem do produto",
"Information": "Informações",
"Invalid product": "Invalid product",
"Is recharge": "É recarga",
"Is recharge - Tooltip": "Se o produto atual é para recarregar saldo",
"New Product": "Novo Produto",
"No recharge options available": "No recharge options available",
"Order created successfully": "Pedido criado com sucesso",
"PayPal": "PayPal",
"Payment cancelled": "Pagamento cancelado",
@@ -924,10 +935,12 @@
"Please select at least one payment provider": "Por favor, selecione pelo menos um provedor de pagamento",
"Processing payment...": "Processando pagamento...",
"Product list cannot be empty": "A lista de produtos não pode estar vazia",
"Product not found or invalid": "Product not found or invalid",
"Quantity": "Quantidade",
"Quantity - Tooltip": "Quantidade do produto",
"Recharge options": "Opções de recarga",
"Recharge options - Tooltip": "Dica: opções de recarga",
"Recharge products need to go to the product detail page to set custom amount": "Recharge products need to go to the product detail page to set custom amount",
"Return URL": "URL de Retorno",
"Return URL - Tooltip": "URL para retornar após a compra bem-sucedida",
"SKU": "SKU",
@@ -972,8 +985,6 @@
"Can signin": "Pode fazer login",
"Can signup": "Pode se inscrever",
"Can unlink": "Pode desvincular",
"Category": "Categoria",
"Category - Tooltip": "Selecione uma categoria",
"Channel No.": "Número do canal",
"Channel No. - Tooltip": "Número do canal",
"Chat ID": "ID do chat",
@@ -990,8 +1001,6 @@
"Content - Tooltip": "Dica: conteúdo",
"DB test": "Teste do banco de dados",
"DB test - Tooltip": "Dica: teste do banco de dados",
"Disable SSL": "Desabilitar SSL",
"Disable SSL - Tooltip": "Se deve desabilitar o protocolo SSL ao comunicar com o servidor SMTP",
"Domain": "Domínio",
"Domain - Tooltip": "Domínio personalizado para armazenamento de objetos",
"Edit Provider": "Editar Provedor",
@@ -1074,9 +1083,12 @@
"SP ACS URL": "URL SP ACS",
"SP ACS URL - Tooltip": "URL SP ACS",
"SP Entity ID": "ID da Entidade SP",
"SSL mode": "SSL mode",
"SSL mode - Tooltip": "SSL mode - Tooltip",
"Scene": "Cenário",
"Scene - Tooltip": "Cenário",
"Scope": "Escopo",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Chave de acesso secreta",
"Secret access key - Tooltip": "Chave de acesso secreta",
"Secret key": "Chave secreta",
@@ -1238,6 +1250,9 @@
},
"syncer": {
"API Token / Password": "Token de API / Senha",
"AWS Access Key ID": "AWS Access Key ID",
"AWS Region": "AWS Region",
"AWS Secret Access Key": "AWS Secret Access Key",
"Admin Email": "E-mail do administrador",
"Affiliation table": "Tabela de Afiliação",
"Affiliation table - Tooltip": "Nome da tabela no banco de dados da unidade de trabalho",
@@ -1269,8 +1284,6 @@
"SSH password": "Senha SSH",
"SSH port": "Porta SSH",
"SSH user": "Usuário SSH",
"SSL mode": "Modo SSL",
"SSL mode - Tooltip": "Dica: modo SSL",
"Service account key": "Chave da conta de serviço",
"Sync interval": "Intervalo de sincronização",
"Sync interval - Tooltip": "Unidade em segundos",

View File

@@ -70,6 +70,7 @@
"Enable signin session - Tooltip": "Uygulamadan Casdoor'a giriş yaptıktan sonra Casdoor'un bir oturum sürdürüp sürdürmeyeceği",
"Enable signup": "Kayıtı Etkinleştir",
"Enable signup - Tooltip": "Kullanıcıların yeni bir hesap kaydetmesine izin verilip verilmeyeceği",
"Existing Field": "Existing Field",
"Failed signin frozen time": "Başarısız giriş dondurma süresi",
"Failed signin frozen time - Tooltip": "Başarısız giriş denemelerinden sonra hesabın dondurulduğu süre",
"Failed signin limit": "Başarısız giriş limiti",
@@ -151,6 +152,7 @@
"Signup items - Tooltip": "Kullanıcıların yeni hesaplar kaydederken doldurması gereken öğeler - İpucu",
"Single Choice": "Tek Seçim",
"Small icon": "Küçük simge",
"Static Value": "Static Value",
"String": "Dize",
"Tags - Tooltip": "Yalnızca uygulama etiketlerinde listelenen etikete sahip kullanıcılar giriş yapabilir - İpucu",
"The application does not allow to sign up new account": "Uygulama yeni hesap kaydetmeyi izin vermemektedir",
@@ -184,9 +186,7 @@
"Expire in years - Tooltip": "Sertifikanın geçerlilik süresi, yıllarda",
"New Cert": "Yeni Sertifika",
"Private key": "Özel anahtar",
"Private key - Tooltip": "Genel anahtar sertifikasına karşılık gelen özel anahtar",
"Scope - Tooltip": "Sertifikanın kullanım senaryoları",
"Type - Tooltip": "Sertifika türü"
"Private key - Tooltip": "Genel anahtar sertifikasına karşılık gelen özel anahtar"
},
"code": {
"Code you received": "Aldığınız kod",
@@ -275,6 +275,7 @@
"Applications that require authentication": "Kimlik doğrulaması gerektiren uygulamalar",
"Apps": "Uygulamalar",
"Authorization": "Yetkilendirme",
"Auto": "Auto",
"Avatar": "Avatar",
"Avatar - Tooltip": "Kullanıcı için genel avatar resmi",
"Back": "Geri",
@@ -283,6 +284,8 @@
"Cancel": "Vazgeç",
"Captcha": "Captcha",
"Cart": "Sepet",
"Category": "Category",
"Category - Tooltip": "Category - Tooltip",
"Cert": "Sertifika",
"Cert - Tooltip": "Bu uygulamaya karşılık gelen istemci SDK tarafından doğrulanması gereken genel anahtar sertifikası",
"Certs": "Sertifikalar",
@@ -476,6 +479,8 @@
"SSH type - Tooltip": "SSH bağlantısının kimlik doğrulama türü",
"Save": "Kaydet",
"Save & Exit": "Kaydet ve Çık",
"Scopes": "Scopes",
"Scopes - Tooltip": "Scopes - Tooltip",
"Search": "Ara",
"Send": "Gönder",
"Session ID": "Oturum ID",
@@ -530,6 +535,7 @@
"Transactions": "İşlemler",
"True": "Doğru",
"Type": "Tür",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "URL bağlantısı",
"Unknown application name": "Bilinmeyen uygulama adı",
@@ -867,6 +873,8 @@
},
"plan": {
"Edit Plan": "Planı Düzenle",
"Is exclusive": "Is exclusive",
"Is exclusive - Tooltip": "Is exclusive - Tooltip",
"New Plan": "Yeni Plan",
"Period": "Dönem",
"Period - Tooltip": "Dönem",
@@ -897,6 +905,7 @@
"Amount": "Tutar",
"Buy": "Satın Al",
"Buy Product": "Ürün Satın Al",
"Cart contains invalid products, please delete them before placing an order": "Cart contains invalid products, please delete them before placing an order",
"Custom amount available": "Özel tutar kullanılabilir",
"Custom price should be greater than zero": "Özel fiyat sıfırdan büyük olmalıdır",
"Detail - Tooltip": "Ürün detayı",
@@ -909,9 +918,11 @@
"Image": "Resim",
"Image - Tooltip": "Ürün resmi",
"Information": "Bilgi",
"Invalid product": "Invalid product",
"Is recharge": "Yeniden yükleme mi",
"Is recharge - Tooltip": "Mevcut ürün bakiye yeniden yüklemesi ise",
"New Product": "Yeni Ürün",
"No recharge options available": "No recharge options available",
"Order created successfully": "Sipariş başarıyla oluşturuldu",
"PayPal": "PayPal",
"Payment cancelled": "Ödeme iptal edildi",
@@ -924,10 +935,12 @@
"Please select at least one payment provider": "Lütfen en az bir ödeme sağlayıcısı seçin",
"Processing payment...": "Ödeme işleniyor...",
"Product list cannot be empty": "Ürün listesi boş olamaz",
"Product not found or invalid": "Product not found or invalid",
"Quantity": "Miktar",
"Quantity - Tooltip": "Ürün miktarı",
"Recharge options": "Yeniden yükleme seçenekleri",
"Recharge options - Tooltip": "Yeniden yükleme seçenekleri - Araç ipucu",
"Recharge products need to go to the product detail page to set custom amount": "Recharge products need to go to the product detail page to set custom amount",
"Return URL": "Dönüş URL'si",
"Return URL - Tooltip": "Satın alımdan sonra dönülecek URL",
"SKU": "SKU",
@@ -972,8 +985,6 @@
"Can signin": "Giriş yapabilir",
"Can signup": "Kayıt yapabilir",
"Can unlink": "Bağlantıyı kesebilir",
"Category": "Kategori",
"Category - Tooltip": "Bir kategori seçin",
"Channel No.": "Kanal Numarası",
"Channel No. - Tooltip": "Kanal Numarası",
"Chat ID": "Sohbet Kimliği",
@@ -990,8 +1001,6 @@
"Content - Tooltip": "İçerik - Araç ipucu",
"DB test": "Veritabanı testi",
"DB test - Tooltip": "Veritabanı testi - Araç ipucu",
"Disable SSL": "SSL'yi Devre Dışı Bırak",
"Disable SSL - Tooltip": "STMP sunucusu ile iletişim kurarken SSL protokolünü devre dışı bırakıp bırakmayacağı",
"Domain": "Alan adı",
"Domain - Tooltip": "Nesne depolama için özel alan adı",
"Edit Provider": "Sağlayıcıyı Düzenle",
@@ -1074,9 +1083,12 @@
"SP ACS URL": "SP ACS URL'si",
"SP ACS URL - Tooltip": "SP ACS URL'si",
"SP Entity ID": "SP Varlık ID'si",
"SSL mode": "SSL mode",
"SSL mode - Tooltip": "SSL mode - Tooltip",
"Scene": "Senaryo",
"Scene - Tooltip": "Senaryo",
"Scope": "Kapsam",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Gizli erişim anahtarı",
"Secret access key - Tooltip": "Gizli erişim anahtarı",
"Secret key": "Gizli anahtar",
@@ -1238,6 +1250,9 @@
},
"syncer": {
"API Token / Password": "API Token / Password",
"AWS Access Key ID": "AWS Access Key ID",
"AWS Region": "AWS Region",
"AWS Secret Access Key": "AWS Secret Access Key",
"Admin Email": "Admin Email",
"Affiliation table": "İlişki tablosu",
"Affiliation table - Tooltip": "Çalışma biriminin veritabanı tablo adı",
@@ -1269,8 +1284,6 @@
"SSH password": "SSH şifresi",
"SSH port": "SSH portu",
"SSH user": "SSH kullanıcısı",
"SSL mode": "SSL modu",
"SSL mode - Tooltip": "SSL modu - İpucu",
"Service account key": "Service account key",
"Sync interval": "Senkronizasyon aralığı",
"Sync interval - Tooltip": "Birimi saniye cinsinden",

View File

@@ -70,6 +70,7 @@
"Enable signin session - Tooltip": "Чи підтримує Casdoor сеанс після входу в Casdoor із програми",
"Enable signup": "Увімкнути реєстрацію",
"Enable signup - Tooltip": "Чи дозволяти користувачам реєструвати новий обліковий запис",
"Existing Field": "Existing Field",
"Failed signin frozen time": "Помилка входу заморожений час",
"Failed signin frozen time - Tooltip": "Час після якого обліковий запис заморожується після невдалих спроб входу - Підказка",
"Failed signin limit": "Обмеження невдалого входу",
@@ -151,6 +152,7 @@
"Signup items - Tooltip": "Пункти, які користувачі повинні заповнити під час реєстрації нових облікових записів",
"Single Choice": "Один варіант",
"Small icon": "Маленький значок",
"Static Value": "Static Value",
"String": "Рядок",
"Tags - Tooltip": "Увійти можуть лише користувачі з тегом, указаним у тегах програми",
"The application does not allow to sign up new account": "Програма не дозволяє зареєструвати новий обліковий запис",
@@ -184,9 +186,7 @@
"Expire in years - Tooltip": "Термін дії сертифіката, років",
"New Cert": "Новий сертифікат",
"Private key": "Приватний ключ",
"Private key - Tooltip": "Закритий ключ, що відповідає сертифікату відкритого ключа",
"Scope - Tooltip": "Сценарії використання сертифіката",
"Type - Tooltip": "Тип сертифіката"
"Private key - Tooltip": "Закритий ключ, що відповідає сертифікату відкритого ключа"
},
"code": {
"Code you received": "Код, який ви отримали",
@@ -275,6 +275,7 @@
"Applications that require authentication": "Програми, які потребують автентифікації",
"Apps": "програми",
"Authorization": "Авторизація",
"Auto": "Auto",
"Avatar": "Аватар",
"Avatar - Tooltip": "Публічний аватар користувача",
"Back": "Назад",
@@ -283,6 +284,8 @@
"Cancel": "Скасувати",
"Captcha": "Капча",
"Cart": "Кошик",
"Category": "Category",
"Category - Tooltip": "Category - Tooltip",
"Cert": "сертифікат",
"Cert - Tooltip": "Сертифікат відкритого ключа, який потрібно перевірити клієнтським SDK, що відповідає цій програмі",
"Certs": "Сертифікати",
@@ -476,6 +479,8 @@
"SSH type - Tooltip": "Тип авторизації підключення SSH",
"Save": "зберегти",
"Save & Exit": "зберегти",
"Scopes": "Scopes",
"Scopes - Tooltip": "Scopes - Tooltip",
"Search": "Пошук",
"Send": "Надіслати",
"Session ID": "Ідентифікатор сеансу",
@@ -530,6 +535,7 @@
"Transactions": "транзакції",
"True": "Так",
"Type": "Тип",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "URL-посилання",
"Unknown application name": "Невідома назва програми",
@@ -737,48 +743,59 @@
"New Order": "Нове замовлення",
"Order not found": "Замовлення не знайдено",
"Pay": "Оплатити",
"Account menu - Tooltip": "Меню облікового запису - підказка",
"Admin navbar items": "Пункти панелі навігації адміністратора",
"Admin navbar items - Tooltip": "Пункти панелі навігації адміністратора - підказка",
"Balance credit": "Баланс кредиту",
"Balance credit - Tooltip": "Баланс кредиту - підказка",
"Balance currency": "Валюта балансу",
"Balance currency - Tooltip": "Валюта балансу - підказка",
"Edit Organization": "Редагувати організацію",
"Follow global theme": "Дотримуйтеся глобальної теми",
"Has privilege consent": "Має згоду на привілеї",
"Has privilege consent - Tooltip": "Заборонити додавання користувачів до вбудованої організації, якщо HasPrivilegeConsent встановлено в false",
"Has privilege consent warning": "Додавання нового користувача до організації «built-in» (вбудованої) на даний момент вимкнено. Зауважте: усі користувачі в організації «built-in» є глобальними адміністраторами в Casdoor. Дивіться документацію: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. Якщо ви все ще хочете створити користувача для організації «built-in», перейдіть на сторінку налаштувань організації та увімкніть опцію «Має згоду на привілеї».",
"Init score": "Початкова оцінка",
"Init score - Tooltip": "Початкові бали, нараховані користувачам під час реєстрації",
"Is profile public": "Профіль загальнодоступний",
"Is profile public - Tooltip": "Після закриття лише глобальні адміністратори або користувачі в одній організації можуть отримати доступ до сторінки профілю користувача",
"Modify rule": "Змінити правило",
"New Organization": "Нова організація",
"Optional": "Додатково",
"Org balance": "Баланс організації",
"Org balance - Tooltip": "Баланс організації - підказка",
"Password expire days": "Кількість днів дії паролю",
"Password expire days - Tooltip": "Кількість днів дії паролю - підказка",
"Prompt": "Підкажіть",
"Required": "вимагається",
"Soft deletion": "М'яке видалення",
"Soft deletion - Tooltip": "Якщо ввімкнено, видалення користувачів не призведе до їх повного видалення з бази даних. ",
"Tags": "Теги",
"Use Email as username": "Використовувати Email як ім'я користувача",
"Use Email as username - Tooltip": "Використовувати Email як ім'я користувача, якщо поле імені користувача не відображається під час реєстрації",
"User balance": "Баланс користувача",
"User balance - Tooltip": "Баланс користувача - підказка",
"User navbar items": "Пункти панелі навігації користувача",
"User navbar items - Tooltip": "Пункти панелі навігації користувача - підказка",
"User types": "Типи користувачів",
"User types - Tooltip": "Типи користувачів - підказка",
"View rule": "Переглянути правило",
"Visible": "Видно",
"Website URL": "адреса вебсайту",
"Website URL - Tooltip": "URL-адреса домашньої сторінки організації. ",
"Widget items": "Елементи віджета",
"Widget items - Tooltip": "Елементи віджета - підказка"
"Payment failed time": "Payment failed time",
"Payment time": "Payment time",
"Price": "Price",
"Return to Order List": "Return to Order List",
"Timeout time": "Timeout time",
"View Order": "View Order"
},
"organization": {
"Account items": "Account items",
"Account items - Tooltip": "Account items - Tooltip",
"Account menu": "Account menu",
"Account menu - Tooltip": "Account menu - Tooltip",
"Admin navbar items": "Admin navbar items",
"Admin navbar items - Tooltip": "Admin navbar items - Tooltip",
"Balance credit": "Balance credit",
"Balance credit - Tooltip": "Balance credit - Tooltip",
"Balance currency": "Balance currency",
"Balance currency - Tooltip": "Balance currency - Tooltip",
"Edit Organization": "Edit Organization",
"Follow global theme": "Follow global theme",
"Has privilege consent": "Has privilege consent",
"Has privilege consent - Tooltip": "Has privilege consent - Tooltip",
"Has privilege consent warning": "Has privilege consent warning",
"Init score": "Init score",
"Init score - Tooltip": "Init score - Tooltip",
"Is profile public": "Is profile public",
"Is profile public - Tooltip": "Is profile public - Tooltip",
"Modify rule": "Modify rule",
"New Organization": "New Organization",
"Optional": "Optional",
"Org balance": "Org balance",
"Org balance - Tooltip": "Org balance - Tooltip",
"Password expire days": "Password expire days",
"Password expire days - Tooltip": "Password expire days - Tooltip",
"Prompt": "Prompt",
"Required": "Required",
"Soft deletion": "Soft deletion",
"Soft deletion - Tooltip": "Soft deletion - Tooltip",
"Tags": "Tags",
"Use Email as username": "Use Email as username",
"Use Email as username - Tooltip": "Use Email as username - Tooltip",
"User balance": "User balance",
"User balance - Tooltip": "User balance - Tooltip",
"User navbar items": "User navbar items",
"User navbar items - Tooltip": "User navbar items - Tooltip",
"User types": "User types",
"User types - Tooltip": "User types - Tooltip",
"View rule": "View rule",
"Visible": "Visible",
"Website URL": "Website URL",
"Website URL - Tooltip": "Website URL - Tooltip",
"Widget items": "Widget items",
"Widget items - Tooltip": "Widget items - Tooltip"
},
"payment": {
"Confirm your invoice information": "Підтвердьте інформацію про рахунок",
@@ -856,6 +873,8 @@
},
"plan": {
"Edit Plan": "Редагувати план",
"Is exclusive": "Is exclusive",
"Is exclusive - Tooltip": "Is exclusive - Tooltip",
"New Plan": "Новий план",
"Period": "Крапка",
"Period - Tooltip": "Період",
@@ -886,6 +905,7 @@
"Amount": "Amount",
"Buy": "купити",
"Buy Product": "Купити товар",
"Cart contains invalid products, please delete them before placing an order": "Cart contains invalid products, please delete them before placing an order",
"Custom amount available": "Custom amount available",
"Custom price should be greater than zero": "Custom price should be greater than zero",
"Detail - Tooltip": "Деталь продукту",
@@ -898,9 +918,11 @@
"Image": "Зображення",
"Image - Tooltip": "Зображення товару",
"Information": "Information",
"Invalid product": "Invalid product",
"Is recharge": "Чи є поповненням",
"Is recharge - Tooltip": "Чи є поточний продукт для поповнення балансу",
"New Product": "Новий продукт",
"No recharge options available": "No recharge options available",
"Order created successfully": "Order created successfully",
"PayPal": "Пейпал",
"Payment cancelled": "Платіж скасовано",
@@ -913,10 +935,12 @@
"Please select at least one payment provider": "Please select at least one payment provider",
"Processing payment...": "Processing payment...",
"Product list cannot be empty": "Product list cannot be empty",
"Product not found or invalid": "Product not found or invalid",
"Quantity": "Кількість",
"Quantity - Tooltip": "Кількість товару",
"Recharge options": "Recharge options",
"Recharge options - Tooltip": "Варіанти поповнення - Підказка",
"Recharge products need to go to the product detail page to set custom amount": "Recharge products need to go to the product detail page to set custom amount",
"Return URL": "Повернута URL-адреса",
"Return URL - Tooltip": "URL-адреса для повернення після успішної покупки",
"SKU": "SKU",
@@ -961,8 +985,6 @@
"Can signin": "Можна ввійти",
"Can signup": "Можна записатися",
"Can unlink": "Можна від’єднати",
"Category": "Категорія",
"Category - Tooltip": "Ідентифікатор для категоризації та групування елементів або контенту, що полегшує фільтрацію та управління",
"Channel No.": "Номер каналу",
"Channel No. - Tooltip": "Унікальний номер, що ідентифікує канал зв'язку або передачі даних, використовується для розрізнення різних шляхів передачі",
"Chat ID": "Ідентифікатор чату",
@@ -979,8 +1001,6 @@
"Content - Tooltip": "Вміст підказка",
"DB test": "Тест БД",
"DB test - Tooltip": "Тест бази даних - підказка",
"Disable SSL": "Вимкнути SSL",
"Disable SSL - Tooltip": "Чи вимикати протокол SSL під час зв’язку із сервером STMP",
"Domain": "Домен",
"Domain - Tooltip": "Спеціальний домен для зберігання об'єктів",
"Edit Provider": "Редагувати постачальника",
@@ -1063,9 +1083,12 @@
"SP ACS URL": "URL ACS СП",
"SP ACS URL - Tooltip": "URL ACS СП",
"SP Entity ID": "Ідентифікатор особи SP",
"SSL mode": "SSL mode",
"SSL mode - Tooltip": "SSL mode - Tooltip",
"Scene": "Сцена",
"Scene - Tooltip": "Сцена",
"Scope": "Область застосування",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Секретний ключ доступу",
"Secret access key - Tooltip": "Секретний ключ доступу",
"Secret key": "Секретний ключ",
@@ -1227,6 +1250,9 @@
},
"syncer": {
"API Token / Password": "API Token / Password",
"AWS Access Key ID": "AWS Access Key ID",
"AWS Region": "AWS Region",
"AWS Secret Access Key": "AWS Secret Access Key",
"Admin Email": "Admin Email",
"Affiliation table": "Таблиця приналежності",
"Affiliation table - Tooltip": "Назва робочої одиниці таблиці бази даних",
@@ -1258,8 +1284,6 @@
"SSH password": "пароль SSH",
"SSH port": "порт SSH",
"SSH user": "Користувач SSH",
"SSL mode": "Режим SSL",
"SSL mode - Tooltip": "Режим SSL підказка",
"Service account key": "Service account key",
"Sync interval": "Інтервал синхронізації",
"Sync interval - Tooltip": "Одиниця в секундах",

View File

@@ -70,6 +70,7 @@
"Enable signin session - Tooltip": "Có phải Casdoor duy trì phiên sau khi đăng nhập vào Casdoor từ ứng dụng không?",
"Enable signup": "Kích hoạt đăng ký",
"Enable signup - Tooltip": "Có cho phép người dùng đăng ký tài khoản mới không?",
"Existing Field": "Existing Field",
"Failed signin frozen time": "Thời gian khóa khi đăng nhập thất bại",
"Failed signin frozen time - Tooltip": "Thời gian tài khoản bị đóng băng sau các lần đăng nhập thất bại",
"Failed signin limit": "Giới hạn đăng nhập thất bại",
@@ -151,6 +152,7 @@
"Signup items - Tooltip": "Mục cho người dùng đề điền khi đăng nhập - Gợi ý",
"Single Choice": "Lựa chọn đơn",
"Small icon": "Biểu tượng nhỏ",
"Static Value": "Static Value",
"String": "Chuỗi",
"Tags - Tooltip": "Chỉ người dùng có thẻ được liệt kê trong thẻ ứng dụng mới có thể đăng nhập - Gợi ý",
"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",
@@ -184,9 +186,7 @@
"Expire in years - Tooltip": "Thời hạn hiệu lực của chứng chỉ, tính bằng năm",
"New Cert": "Chứng chỉ mới",
"Private key": "Khóa bí mật",
"Private key - Tooltip": "Khóa riêng tương ứng với chứng thư khóa công khai",
"Scope - Tooltip": "Các kịch bản sử dụng của giấy chứng nhận",
"Type - Tooltip": "Loại chứng chỉ"
"Private key - Tooltip": "Khóa riêng tương ứng với chứng thư khóa công khai"
},
"code": {
"Code you received": "Mã bạn nhận được",
@@ -275,6 +275,7 @@
"Applications that require authentication": "Các ứng dụng yêu cầu xác thực",
"Apps": "Ứng dụng",
"Authorization": "Ủy quyền",
"Auto": "Auto",
"Avatar": "Ảnh đại diện",
"Avatar - Tooltip": "Ảnh đại diện công khai cho người dùng",
"Back": "Quay lại",
@@ -283,6 +284,8 @@
"Cancel": "Hủy bỏ",
"Captcha": "Mã xác nhận",
"Cart": "Giỏ hàng",
"Category": "Category",
"Category - Tooltip": "Category - Tooltip",
"Cert": "Chứng chỉ",
"Cert - Tooltip": "Chứng chỉ khóa công khai cần được xác minh bởi SDK khách hàng tương ứng với ứng dụng này",
"Certs": "Chứng chỉ",
@@ -476,6 +479,8 @@
"SSH type - Tooltip": "Loại xác thực kết nối SSH",
"Save": "Lưu",
"Save & Exit": "Lưu và Thoát",
"Scopes": "Scopes",
"Scopes - Tooltip": "Scopes - Tooltip",
"Search": "Tìm kiếm",
"Send": "Gửi",
"Session ID": "ID phiên làm việc",
@@ -530,6 +535,7 @@
"Transactions": "Giao dịch",
"True": "Đúng",
"Type": "Loại",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "Đường dẫn URL",
"Unknown application name": "Tên ứng dụng không xác định",
@@ -867,6 +873,8 @@
},
"plan": {
"Edit Plan": "Chỉnh sửa gói",
"Is exclusive": "Is exclusive",
"Is exclusive - Tooltip": "Is exclusive - Tooltip",
"New Plan": "Gói mới",
"Period": "Kỳ",
"Period - Tooltip": "Thời kỳ",
@@ -897,6 +905,7 @@
"Amount": "Số tiền",
"Buy": "Mua",
"Buy Product": "Mua sản phẩm",
"Cart contains invalid products, please delete them before placing an order": "Cart contains invalid products, please delete them before placing an order",
"Custom amount available": "Số tiền tùy chỉnh có sẵn",
"Custom price should be greater than zero": "Giá tùy chỉnh phải lớn hơn không",
"Detail - Tooltip": "Chi tiết sản phẩm",
@@ -909,9 +918,11 @@
"Image": "Ảnh",
"Image - Tooltip": "Hình ảnh sản phẩm",
"Information": "Thông tin",
"Invalid product": "Invalid product",
"Is recharge": "Là nạp tiền",
"Is recharge - Tooltip": "Sản phẩm hiện tại có phải để nạp số dư",
"New Product": "Sản phẩm mới",
"No recharge options available": "No recharge options available",
"Order created successfully": "Tạo đơn hàng thành công",
"PayPal": "PayPal",
"Payment cancelled": "Thanh toán đã bị hủy",
@@ -924,10 +935,12 @@
"Please select at least one payment provider": "Vui lòng chọn ít nhất một nhà cung cấp thanh toán",
"Processing payment...": "Đang xử lý thanh toán...",
"Product list cannot be empty": "Danh sách sản phẩm không thể trống",
"Product not found or invalid": "Product not found or invalid",
"Quantity": "Số lượng",
"Quantity - Tooltip": "Số lượng sản phẩm",
"Recharge options": "Tùy chọn nạp tiền",
"Recharge options - Tooltip": "Tùy chọn nạp tiền - Gợi ý",
"Recharge products need to go to the product detail page to set custom amount": "Recharge products need to go to the product detail page to set custom amount",
"Return URL": "Địa chỉ URL trở lại",
"Return URL - Tooltip": "URL để quay lại sau khi mua hàng thành công",
"SKU": "SKU",
@@ -972,8 +985,6 @@
"Can signin": "Đăng nhập được không?",
"Can signup": "Đăng ký có thể được thực hiện",
"Can unlink": "Không liên kết được",
"Category": "Thể loại",
"Category - Tooltip": "Chọn một danh mục",
"Channel No.": "Kênh số.",
"Channel No. - Tooltip": "Kênh Số.",
"Chat ID": "ID trò chuyện",
@@ -990,8 +1001,6 @@
"Content - Tooltip": "Gợi ý nội dung",
"DB test": "DB test",
"DB test - Tooltip": "Kiểm tra DB - Gợi ý",
"Disable SSL": "Vô hiệu hóa SSL",
"Disable SSL - Tooltip": "Có nên vô hiệu hóa giao thức SSL khi giao tiếp với máy chủ STMP hay không?",
"Domain": "Miền",
"Domain - Tooltip": "Tên miền tùy chỉnh cho lưu trữ đối tượng",
"Edit Provider": "Chỉnh sửa nhà cung cấp",
@@ -1074,9 +1083,12 @@
"SP ACS URL": "SP ACC URL",
"SP ACS URL - Tooltip": "URL ACS của SP - Gợi ý",
"SP Entity ID": "SP Entity ID: Định danh thực thể SP",
"SSL mode": "SSL mode",
"SSL mode - Tooltip": "SSL mode - Tooltip",
"Scene": "Cảnh",
"Scene - Tooltip": "Cảnh",
"Scope": "Phạm vi",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "Chìa khóa truy cập bí mật",
"Secret access key - Tooltip": "Khóa truy cập bí mật",
"Secret key": "Chìa khóa bí mật",
@@ -1238,6 +1250,9 @@
},
"syncer": {
"API Token / Password": "Mã thông báo API / Mật khẩu",
"AWS Access Key ID": "AWS Access Key ID",
"AWS Region": "AWS Region",
"AWS Secret Access Key": "AWS Secret Access Key",
"Admin Email": "Email quản trị viên",
"Affiliation table": "Bảng liên kết",
"Affiliation table - Tooltip": "Bảng liên kết - Gợi ý",
@@ -1245,11 +1260,8 @@
"Avatar base URL - Tooltip": "Địa chỉ cơ sở URL ảnh đại diện - Gợi ý",
"Bind DN": "Kết nối DN",
"Casdoor column": "Cột Casdoor",
"Casdoor column - Tooltip": "Tên cột trong bảng Casdoor tương ứng",
"Column name": "Tên cột",
"Column name - Tooltip": "Tên cột trong bảng cơ sở dữ liệu",
"Column type": "Loại cột",
"Column type - Tooltip": "Kiểu dữ liệu của cột",
"Connect successfully": "Kết nối thành công",
"Corp ID": "Mã doanh nghiệp",
"Corp secret": "Bí mật doanh nghiệp",
@@ -1268,15 +1280,11 @@
"New Syncer": "Đồng bộ mới",
"Paste your Google Workspace service account JSON key here": "Dán khóa JSON tài khoản dịch vụ Google Workspace của bạn tại đây",
"SCIM Server URL": "URL máy chủ SCIM",
"SCIM Server URL - Tooltip": "Địa chỉ URL của máy chủ SCIM",
"SSH host": "Máy chủ SSH",
"SSH password": "Mật khẩu SSH",
"SSH port": "Cổng SSH",
"SSH user": "Người dùng SSH",
"SSL mode": "Chế độ SSL",
"SSL mode - Tooltip": "Chế độ kết nối SSL với cơ sở dữ liệu",
"Service account key": "Khóa tài khoản dịch vụ",
"Service account key - Tooltip": "Khóa JSON của tài khoản dịch vụ",
"Sync interval": "Khoảng thời gian đồng bộ",
"Sync interval - Tooltip": "Khoảng thời gian giữa các lần đồng bộ (tính bằng giây)",
"Table": "Bảng",
@@ -1291,7 +1299,6 @@
"API Latency": "Độ trễ API",
"API Throughput": "Thông lượng API",
"About Casdoor": "Về Casdoor",
"About Casdoor - Tooltip": "Thông tin về nền tảng Casdoor",
"An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS": "Một nền tảng Quản lý Danh tính và Truy cập (IAM) / Đăng nhập Một lần (SSO) với giao diện người dùng web hỗ trợ OAuth 2.0, OIDC, SAML và CAS",
"CPU Usage": "Sử dụng CPU",
"Community": "Cộng đồng",

View File

@@ -70,6 +70,7 @@
"Enable signin session - Tooltip": "从应用登录Casdoor后Casdoor是否保持会话",
"Enable signup": "启用注册",
"Enable signup - Tooltip": "是否允许用户注册",
"Existing Field": "Existing Field",
"Failed signin frozen time": "登入重试等待时间",
"Failed signin frozen time - Tooltip": "超过登入错误重试次数后的等待时间只有超过等待时间后用户才能重新登入默认值为15分钟设置的值需为正整数",
"Failed signin limit": "登入错误次数限制",
@@ -151,6 +152,7 @@
"Signup items - Tooltip": "注册用户注册时需要填写的项目",
"Single Choice": "单选",
"Small icon": "小图标",
"Static Value": "Static Value",
"String": "字符串",
"Tags - Tooltip": "用户的标签在应用的标签集合中时,用户才可以登录该应用",
"The application does not allow to sign up new account": "该应用不允许注册新账户",
@@ -184,9 +186,7 @@
"Expire in years - Tooltip": "公钥证书的有效期,以年为单位",
"New Cert": "添加证书",
"Private key": "私钥",
"Private key - Tooltip": "公钥证书对应的私钥",
"Scope - Tooltip": "公钥证书的使用场景",
"Type - Tooltip": "公钥证书的类型"
"Private key - Tooltip": "公钥证书对应的私钥"
},
"code": {
"Code you received": "验证码",
@@ -275,6 +275,7 @@
"Applications that require authentication": "需要认证和鉴权的应用",
"Apps": "应用列表",
"Authorization": "Casbin权限管理",
"Auto": "Auto",
"Avatar": "头像",
"Avatar - Tooltip": "公开展示的用户头像",
"Back": "返回",
@@ -283,6 +284,8 @@
"Cancel": "取消",
"Captcha": "人机验证码",
"Cart": "购物车",
"Category": "Category",
"Category - Tooltip": "Category - Tooltip",
"Cert": "证书",
"Cert - Tooltip": "该应用所对应的客户端SDK需要验证的公钥证书",
"Certs": "证书",
@@ -476,6 +479,8 @@
"SSH type - Tooltip": "SSH连接的认证类型",
"Save": "保存",
"Save & Exit": "保存 & 退出",
"Scopes": "Scopes",
"Scopes - Tooltip": "Scopes - Tooltip",
"Search": "搜索",
"Send": "发送",
"Session ID": "会话ID",
@@ -530,6 +535,7 @@
"Transactions": "交易",
"True": "真",
"Type": "类型",
"Type - Tooltip": "Type - Tooltip",
"URL": "链接",
"URL - Tooltip": "URL链接",
"Unknown application name": "未知的应用程序名称",
@@ -867,6 +873,8 @@
},
"plan": {
"Edit Plan": "编辑计划",
"Is exclusive": "Is exclusive",
"Is exclusive - Tooltip": "Is exclusive - Tooltip",
"New Plan": "添加计划",
"Period": "期限",
"Period - Tooltip": "计划对应的期限",
@@ -897,6 +905,7 @@
"Amount": "金额",
"Buy": "购买",
"Buy Product": "购买商品",
"Cart contains invalid products, please delete them before placing an order": "Cart contains invalid products, please delete them before placing an order",
"Custom amount available": "可自定义金额",
"Custom price should be greater than zero": "自定义价格必须大于零",
"Detail - Tooltip": "商品详情",
@@ -909,9 +918,11 @@
"Image": "图片",
"Image - Tooltip": "商品图片",
"Information": "信息",
"Invalid product": "Invalid product",
"Is recharge": "充值",
"Is recharge - Tooltip": "当前商品是否为充值商品",
"New Product": "添加商品",
"No recharge options available": "No recharge options available",
"Order created successfully": "订单创建成功",
"PayPal": "PayPal",
"Payment cancelled": "支付取消",
@@ -924,10 +935,12 @@
"Please select at least one payment provider": "请至少选择一个支付提供商",
"Processing payment...": "正在处理支付...",
"Product list cannot be empty": "商品列表不能为空",
"Product not found or invalid": "Product not found or invalid",
"Quantity": "库存",
"Quantity - Tooltip": "库存的数量",
"Recharge options": "充值选项",
"Recharge options - Tooltip": "预设充值金额",
"Recharge products need to go to the product detail page to set custom amount": "Recharge products need to go to the product detail page to set custom amount",
"Return URL": "返回URL",
"Return URL - Tooltip": "购买成功后返回的URL",
"SKU": "货号",
@@ -972,8 +985,6 @@
"Can signin": "可用于登录",
"Can signup": "可用于注册",
"Can unlink": "可解绑定",
"Category": "分类",
"Category - Tooltip": "用于对项目或内容进行归类分组的标识",
"Channel No.": "Channel号码",
"Channel No. - Tooltip": "标识通信或数据传输通道的唯一编号",
"Chat ID": "聊天ID",
@@ -990,8 +1001,6 @@
"Content - Tooltip": "消息、通知或文档中包含的具体信息或数据内容",
"DB test": "数据库测试",
"DB test - Tooltip": "测试数据库连接是否正常",
"Disable SSL": "禁用SSL",
"Disable SSL - Tooltip": "与STMP服务器通信时是否禁用SSL协议",
"Domain": "域名",
"Domain - Tooltip": "对象存储的自定义域名",
"Edit Provider": "编辑提供商",
@@ -1074,9 +1083,12 @@
"SP ACS URL": "SP ACS 网址",
"SP ACS URL - Tooltip": "服务提供商SP的断言消费者服务ACS地址",
"SP Entity ID": "SP 实体 ID",
"SSL mode": "SSL mode",
"SSL mode - Tooltip": "SSL mode - Tooltip",
"Scene": "场景",
"Scene - Tooltip": "表示功能或操作适用的具体业务场景,用于适配不同场景下的逻辑处理",
"Scope": "范围",
"Scope - Tooltip": "Scope - Tooltip",
"Secret access key": "秘密访问密钥",
"Secret access key - Tooltip": "与访问密钥配套的私密密钥",
"Secret key": "密钥",
@@ -1238,6 +1250,9 @@
},
"syncer": {
"API Token / Password": "API Token / Password",
"AWS Access Key ID": "AWS Access Key ID",
"AWS Region": "AWS Region",
"AWS Secret Access Key": "AWS Secret Access Key",
"Admin Email": "Admin Email",
"Affiliation table": "工作单位表",
"Affiliation table - Tooltip": "工作单位的数据库表名",
@@ -1269,8 +1284,6 @@
"SSH password": "SSH密码",
"SSH port": "SSH端口",
"SSH user": "SSH用户",
"SSL mode": "SSL模式",
"SSL mode - Tooltip": "连接数据库采用哪种SSL模式",
"Service account key": "Service account key",
"Sync interval": "同步间隔",
"Sync interval - Tooltip": "单位为秒",

View File

@@ -0,0 +1,81 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Table} from "antd";
import i18next from "i18next";
import * as Setting from "../Setting";
class CartTable extends React.Component {
render() {
const columns = [
{
title: i18next.t("general:Name"),
dataIndex: "displayName",
key: "displayName",
width: "200px",
},
{
title: i18next.t("product:Image"),
dataIndex: "image",
key: "image",
width: "80px",
render: (text, record, index) => {
if (!text) {
return null;
}
return (
<a target="_blank" rel="noreferrer" href={text}>
<img src={text} alt={record.displayName} width={40} />
</a>
);
},
},
{
title: i18next.t("order:Price"),
dataIndex: "price",
key: "price",
width: "120px",
render: (text, record, index) => {
return Setting.getCurrencySymbol(record.currency) + text;
},
},
{
title: i18next.t("product:Quantity"),
dataIndex: "quantity",
key: "quantity",
width: "100px",
},
{
title: i18next.t("general:Detail"),
dataIndex: "detail",
key: "detail",
},
];
const cart = this.props.cart || [];
return (
<Table
columns={columns}
dataSource={cart}
rowKey={(record) => `${record.owner}/${record.name}`}
size="small"
pagination={false}
/>
);
}
}
export default CartTable;

View File

@@ -104,7 +104,7 @@ class ProviderTable extends React.Component {
},
},
{
title: i18next.t("provider:Category"),
title: i18next.t("general:Category"),
dataIndex: "category",
key: "category",
width: "100px",

166
web/src/table/ScopeTable.js Normal file
View File

@@ -0,0 +1,166 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Input, Table, Tooltip} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
class ScopeTable extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
};
}
updateTable(table) {
this.props.onUpdateTable(table);
}
updateField(table, index, key, value) {
table[index][key] = value;
this.updateTable(table);
}
addRow(table) {
const row = {name: "", displayName: "", description: ""};
if (table === undefined) {
table = [];
}
table = Setting.addRow(table, row);
this.updateTable(table);
}
deleteRow(table, i) {
table = Setting.deleteRow(table, i);
this.updateTable(table);
}
upRow(table, i) {
table = Setting.swapRow(table, i - 1, i);
this.updateTable(table);
}
downRow(table, i) {
table = Setting.swapRow(table, i, i + 1);
this.updateTable(table);
}
renderTable(table) {
if (table === null) {
return null;
}
const columns = [
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "25%",
render: (text, record, index) => {
return (
<Input
value={text}
placeholder="e.g., files:read"
onChange={e => {
this.updateField(table, index, "name", e.target.value);
}}
/>
);
},
},
{
title: i18next.t("general:Display name"),
dataIndex: "displayName",
key: "displayName",
width: "25%",
render: (text, record, index) => {
return (
<Input
value={text}
placeholder="e.g., Read Files"
onChange={e => {
this.updateField(table, index, "displayName", e.target.value);
}}
/>
);
},
},
{
title: i18next.t("general:Description"),
dataIndex: "description",
key: "description",
width: "40%",
render: (text, record, index) => {
return (
<Input
value={text}
placeholder="e.g., Allow reading your files and documents"
onChange={e => {
this.updateField(table, index, "description", e.target.value);
}}
/>
);
},
},
{
title: i18next.t("general:Action"),
key: "action",
width: "10%",
render: (text, record, index) => {
return (
<div>
<Tooltip placement="bottomLeft" title={i18next.t("general:Up")}>
<Button style={{marginRight: "5px"}} disabled={index === 0} icon={<UpOutlined />} size="small" onClick={() => this.upRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={i18next.t("general:Down")}>
<Button style={{marginRight: "5px"}} disabled={index === table.length - 1} icon={<DownOutlined />} size="small" onClick={() => this.downRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={i18next.t("general:Delete")}>
<Button icon={<DeleteOutlined />} size="small" onClick={() => this.deleteRow(table, index)} />
</Tooltip>
</div>
);
},
},
];
return (
<div>
<Table scroll={{x: "max-content"}} rowKey={(record, index) => index} columns={columns} dataSource={table} size="middle" bordered pagination={false}
title={() => (
<div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
</div>
)}
/>
</div>
);
}
render() {
return (
<div>
{
this.renderTable(this.props.table)
}
</div>
);
}
}
export default ScopeTable;

View File

@@ -168,7 +168,7 @@ export function getTransactionTableColumns(options = {}) {
});
columns.push({
title: i18next.t("provider:Category"),
title: i18next.t("general:Category"),
dataIndex: "category",
key: "category",
width: "120px",