forked from casdoor/casdoor
Compare commits
5 Commits
copilot/ad
...
copilot/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
136a193020 | ||
|
|
1f6809f93f | ||
|
|
cacab01b9a | ||
|
|
ab1a623c61 | ||
|
|
5886a52aed |
@@ -59,7 +59,6 @@ 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, *, *
|
||||
|
||||
@@ -30,8 +30,6 @@ ldapsServerPort = 636
|
||||
radiusServerPort = 1812
|
||||
radiusDefaultOrganization = "built-in"
|
||||
radiusSecret = "secret"
|
||||
proxyHttpPort =
|
||||
proxyHttpsPort =
|
||||
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
|
||||
logConfig = {"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
|
||||
initDataNewOnly = false
|
||||
|
||||
@@ -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,51 +688,6 @@ 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())
|
||||
|
||||
@@ -161,13 +161,12 @@ 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, resource, c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||
code, err := object.GetOAuthCode(userId, clientId, form.Provider, form.SigninMethod, responseType, redirectUri, scope, state, nonce, codeChallenge, c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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()
|
||||
}
|
||||
@@ -137,29 +137,3 @@ 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()
|
||||
}
|
||||
@@ -176,7 +176,6 @@ 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()
|
||||
@@ -232,9 +231,6 @@ func (c *ApiController) GetOAuthToken() {
|
||||
if audience == "" {
|
||||
audience = tokenRequest.Audience
|
||||
}
|
||||
if resource == "" {
|
||||
resource = tokenRequest.Resource
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +275,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, resource)
|
||||
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage(), subjectToken, subjectTokenType, audience)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
||||
@@ -30,5 +30,4 @@ 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
|
||||
}
|
||||
|
||||
@@ -151,14 +151,39 @@ func (c *ApiController) SendVerificationCode() {
|
||||
return
|
||||
}
|
||||
|
||||
application, err := object.GetApplication(vform.ApplicationId)
|
||||
provider, err := object.GetCaptchaProviderByApplication(vform.ApplicationId, "false", c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), vform.ApplicationId))
|
||||
if provider != nil {
|
||||
if vform.CaptchaType != provider.Type {
|
||||
c.ResponseError(c.T("verification:Turing test failed."))
|
||||
return
|
||||
}
|
||||
|
||||
if provider.Type != "Default" {
|
||||
vform.ClientSecret = provider.ClientSecret
|
||||
}
|
||||
|
||||
if vform.CaptchaType != "none" {
|
||||
if captchaProvider := captcha.GetCaptchaProvider(vform.CaptchaType); captchaProvider == nil {
|
||||
c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType)
|
||||
return
|
||||
} else if isHuman, err := captchaProvider.VerifyCaptcha(vform.CaptchaToken, provider.ClientId, vform.ClientSecret, provider.ClientId2); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
} else if !isHuman {
|
||||
c.ResponseError(c.T("verification:Turing test failed."))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
application, err := object.GetApplication(vform.ApplicationId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -189,7 +214,6 @@ 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
|
||||
@@ -207,86 +231,18 @@ func (c *ApiController) SendVerificationCode() {
|
||||
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
|
||||
return
|
||||
}
|
||||
} else if mfaUserSession := c.getMfaUserSession(); mfaUserSession != "" {
|
||||
// mfaUserSession != "", means method is MfaAuthVerification
|
||||
}
|
||||
|
||||
// mfaUserSession != "", means method is MfaAuthVerification
|
||||
if mfaUserSession := c.getMfaUserSession(); mfaUserSession != "" {
|
||||
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:
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"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()
|
||||
}
|
||||
@@ -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, sslMode string, 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, disableSsl bool, 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, sslMode, enableProxy)
|
||||
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl, enableProxy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,20 +25,13 @@ type SmtpEmailProvider struct {
|
||||
Dialer *gomail.Dialer
|
||||
}
|
||||
|
||||
func NewSmtpEmailProvider(userName string, password string, host string, port int, typ string, sslMode string, enableProxy bool) *SmtpEmailProvider {
|
||||
func NewSmtpEmailProvider(userName string, password string, host string, port int, typ string, disableSsl bool, enableProxy bool) *SmtpEmailProvider {
|
||||
dialer := gomail.NewDialer(host, port, userName, password)
|
||||
if typ == "SUBMAIL" {
|
||||
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
// 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
|
||||
dialer.SSL = !disableSsl
|
||||
|
||||
if enableProxy {
|
||||
socks5Proxy := conf.GetConfigString("socks5Proxy")
|
||||
|
||||
5
go.mod
5
go.mod
@@ -30,6 +30,7 @@ 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
|
||||
@@ -49,7 +50,6 @@ 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,8 +161,7 @@ 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-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // 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
24
go.sum
@@ -627,16 +627,6 @@ 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=
|
||||
@@ -652,8 +642,6 @@ 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=
|
||||
@@ -929,6 +917,7 @@ 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=
|
||||
@@ -1101,11 +1090,8 @@ 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=
|
||||
@@ -1364,8 +1350,6 @@ 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=
|
||||
@@ -1430,8 +1414,6 @@ 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=
|
||||
@@ -1514,8 +1496,6 @@ 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=
|
||||
|
||||
@@ -86,18 +86,6 @@
|
||||
{"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"},
|
||||
|
||||
2
main.go
2
main.go
@@ -72,7 +72,6 @@ func main() {
|
||||
object.InitFromFile()
|
||||
object.InitCasvisorConfig()
|
||||
object.InitCleanupTokens()
|
||||
object.InitApplicationMap()
|
||||
|
||||
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
|
||||
util.SafeGoroutine(func() { controllers.InitCLIDownloader() })
|
||||
@@ -126,7 +125,6 @@ func main() {
|
||||
go ldap.StartLdapServer()
|
||||
go radius.StartRadiusServer()
|
||||
go object.ClearThroughputPerSecond()
|
||||
go proxy.StartProxyServer()
|
||||
|
||||
web.Run(fmt.Sprintf(":%v", port))
|
||||
}
|
||||
|
||||
56
mcp/auth.go
56
mcp/auth.go
@@ -15,7 +15,6 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@@ -121,58 +120,3 @@ 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, " ")
|
||||
}
|
||||
|
||||
211
mcp/base.go
211
mcp/base.go
@@ -268,160 +268,7 @@ func (c *McpController) handlePing(req McpRequest) {
|
||||
}
|
||||
|
||||
func (c *McpController) handleToolsList(req McpRequest) {
|
||||
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, ¶ms)
|
||||
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{
|
||||
tools := []McpTool{
|
||||
{
|
||||
Name: "get_applications",
|
||||
Description: "Get all applications for a specific owner",
|
||||
@@ -497,4 +344,60 @@ func (c *McpController) getAllTools() []McpTool {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := McpListToolsResult{
|
||||
Tools: tools,
|
||||
}
|
||||
|
||||
c.McpResponseOk(req.ID, result)
|
||||
}
|
||||
|
||||
func (c *McpController) handleToolsCall(req McpRequest) {
|
||||
var params McpCallToolParams
|
||||
err := json.Unmarshal(req.Params, ¶ms)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package 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 ""
|
||||
}
|
||||
@@ -67,22 +67,12 @@ 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"`
|
||||
@@ -154,13 +144,6 @@ 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) {
|
||||
@@ -173,16 +156,6 @@ func GetOrganizationApplicationCount(owner, organization, field, value string) (
|
||||
return session.Where("organization = ? or is_shared = ? ", organization, true).Count(&Application{})
|
||||
}
|
||||
|
||||
func GetGlobalApplications() ([]*Application, error) {
|
||||
applications := []*Application{}
|
||||
err := ormer.Engine.Desc("created_time").Find(&applications)
|
||||
if err != nil {
|
||||
return applications, err
|
||||
}
|
||||
|
||||
return applications, nil
|
||||
}
|
||||
|
||||
func GetApplications(owner string) ([]*Application, error) {
|
||||
applications := []*Application{}
|
||||
err := ormer.Engine.Desc("created_time").Find(&applications, &Application{Owner: owner})
|
||||
@@ -768,12 +741,6 @@ func UpdateApplication(id string, application *Application, isGlobalAdmin bool,
|
||||
return false, err
|
||||
}
|
||||
|
||||
if affected != 0 {
|
||||
if err := RefreshApplicationCache(); err != nil {
|
||||
fmt.Printf("Failed to refresh application cache after update: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
@@ -825,12 +792,6 @@ func AddApplication(application *Application) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if affected != 0 {
|
||||
if err := RefreshApplicationCache(); err != nil {
|
||||
fmt.Printf("Failed to refresh application cache after add: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
@@ -840,12 +801,6 @@ func deleteApplication(application *Application) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if affected != 0 {
|
||||
if err := RefreshApplicationCache(); err != nil {
|
||||
fmt.Printf("Failed to refresh application cache after delete: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
)
|
||||
|
||||
var (
|
||||
applicationMap = make(map[string]*Application)
|
||||
applicationMapMutex sync.RWMutex
|
||||
)
|
||||
|
||||
func InitApplicationMap() error {
|
||||
// Set up the application lookup function for the proxy package
|
||||
proxy.SetApplicationLookup(func(domain string) *proxy.Application {
|
||||
app := GetApplicationByDomain(domain)
|
||||
if app == nil {
|
||||
return nil
|
||||
}
|
||||
return &proxy.Application{
|
||||
Owner: app.Owner,
|
||||
Name: app.Name,
|
||||
UpstreamHost: app.UpstreamHost,
|
||||
}
|
||||
})
|
||||
|
||||
return refreshApplicationMap()
|
||||
}
|
||||
|
||||
func refreshApplicationMap() error {
|
||||
applications, err := GetGlobalApplications()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get global applications: %w", err)
|
||||
}
|
||||
|
||||
newApplicationMap := make(map[string]*Application)
|
||||
for _, app := range applications {
|
||||
if app.Domain != "" {
|
||||
newApplicationMap[strings.ToLower(app.Domain)] = app
|
||||
}
|
||||
for _, domain := range app.OtherDomains {
|
||||
if domain != "" {
|
||||
newApplicationMap[strings.ToLower(domain)] = app
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applicationMapMutex.Lock()
|
||||
applicationMap = newApplicationMap
|
||||
applicationMapMutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetApplicationByDomain(domain string) *Application {
|
||||
applicationMapMutex.RLock()
|
||||
defer applicationMapMutex.RUnlock()
|
||||
|
||||
domain = strings.ToLower(domain)
|
||||
if app, ok := applicationMap[domain]; ok {
|
||||
return app
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func RefreshApplicationCache() error {
|
||||
return refreshApplicationMap()
|
||||
}
|
||||
@@ -20,8 +20,7 @@ import "github.com/casdoor/casdoor/email"
|
||||
|
||||
// TestSmtpServer Test the SMTP server
|
||||
func TestSmtpServer(provider *Provider) error {
|
||||
sslMode := getSslMode(provider)
|
||||
smtpEmailProvider := email.NewSmtpEmailProvider(provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.Type, sslMode, provider.EnableProxy)
|
||||
smtpEmailProvider := email.NewSmtpEmailProvider(provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.Type, provider.DisableSsl, provider.EnableProxy)
|
||||
sender, err := smtpEmailProvider.Dialer.Dial()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -32,8 +31,7 @@ func TestSmtpServer(provider *Provider) error {
|
||||
}
|
||||
|
||||
func SendEmail(provider *Provider, title string, content string, dest []string, sender string) error {
|
||||
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)
|
||||
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)
|
||||
|
||||
fromAddress := provider.ClientId2
|
||||
if fromAddress == "" {
|
||||
@@ -47,19 +45,3 @@ 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"
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -50,12 +49,7 @@ func GetDashboard(owner string) (*map[string][]int64, error) {
|
||||
dashboard[tableName+"Counts"] = make([]int64, 31)
|
||||
tableFullName := tableNamePrefix + tableName
|
||||
go func(ch chan error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ch <- fmt.Errorf("panic in dashboard goroutine: %v", r)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
defer wg.Done()
|
||||
dashboardDateItems := []DashboardDateItem{}
|
||||
var countResult int64
|
||||
|
||||
|
||||
@@ -72,18 +72,6 @@ func getBuiltInAccountItems() []*AccountItem {
|
||||
{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"},
|
||||
@@ -132,7 +120,6 @@ func initBuiltInOrganization() bool {
|
||||
IsProfilePublic: false,
|
||||
UseEmailAsUsername: false,
|
||||
EnableTour: true,
|
||||
DcrPolicy: "open",
|
||||
}
|
||||
_, err = AddOrganization(organization)
|
||||
if err != nil {
|
||||
@@ -198,9 +185,6 @@ 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",
|
||||
|
||||
@@ -37,9 +37,8 @@ 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"`
|
||||
EnableGroups bool `xorm:"bool" json:"enableGroups"`
|
||||
AutoSync int `json:"autoSync"`
|
||||
LastSync string `xorm:"varchar(100)" json:"lastSync"`
|
||||
}
|
||||
|
||||
func AddLdap(ldap *Ldap) (bool, error) {
|
||||
@@ -153,7 +152,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", "enable_groups").Update(ldap)
|
||||
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type", "allow_self_signed_cert", "custom_attributes").Update(ldap)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -91,28 +91,13 @@ func (l *LdapAutoSynchronizer) syncRoutine(ldap *Ldap, stopChan chan struct{}) e
|
||||
return err
|
||||
}
|
||||
|
||||
// fetch all users and groups
|
||||
// fetch all users
|
||||
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()
|
||||
@@ -131,7 +116,7 @@ func (l *LdapAutoSynchronizer) syncRoutine(ldap *Ldap, stopChan chan struct{}) e
|
||||
logs.Warning(fmt.Sprintf("ldap autosync,%d new users,but %d user failed during :", len(users)-len(existed)-len(failed), len(failed)), failed)
|
||||
logs.Warning(err.Error())
|
||||
} else {
|
||||
logs.Info(fmt.Sprintf("ldap autosync success, %d new users, %d existing users", len(users)-len(existed), len(existed)))
|
||||
logs.Info(fmt.Sprintf("ldap autosync success, %d new users, %d existing users (groups synchronized)", len(users)-len(existed), len(existed)))
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/core/logs"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@@ -31,6 +32,10 @@ import (
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
LdapGroupType = "ldap-group"
|
||||
)
|
||||
|
||||
// formatUserPhone processes phone number for a user based on their CountryCode
|
||||
func formatUserPhone(u *User) {
|
||||
if u.Phone == "" {
|
||||
@@ -87,19 +92,11 @@ type LdapUser struct {
|
||||
|
||||
GroupId string `json:"groupId"`
|
||||
Address string `json:"address"`
|
||||
MemberOf []string `json:"memberOf"`
|
||||
MemberOf string `json:"memberOf"`
|
||||
MemberOfs []string `json:"memberOfs"`
|
||||
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{
|
||||
@@ -256,7 +253,8 @@ func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]LdapUser, error) {
|
||||
case "co":
|
||||
user.CountryName = attribute.Values[0]
|
||||
case "memberOf":
|
||||
user.MemberOf = attribute.Values
|
||||
user.MemberOf = attribute.Values[0]
|
||||
user.MemberOfs = attribute.Values
|
||||
default:
|
||||
if propName, ok := ldapServer.CustomAttributes[attribute.Name]; ok {
|
||||
if user.Attributes == nil {
|
||||
@@ -272,135 +270,42 @@ func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]LdapUser, error) {
|
||||
return ldapUsers, 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
|
||||
}
|
||||
// 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
|
||||
// }
|
||||
|
||||
func AutoAdjustLdapUser(users []LdapUser) []LdapUser {
|
||||
res := make([]LdapUser, len(users))
|
||||
@@ -418,12 +323,103 @@ func AutoAdjustLdapUser(users []LdapUser) []LdapUser {
|
||||
Country: util.ReturnAnyNotEmpty(user.Country, user.CountryName),
|
||||
CountryName: user.CountryName,
|
||||
MemberOf: user.MemberOf,
|
||||
MemberOfs: user.MemberOfs,
|
||||
Attributes: user.Attributes,
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// parseGroupNameFromDN extracts the CN (Common Name) from an LDAP DN
|
||||
// e.g., "CN=GroupName,OU=Groups,DC=example,DC=com" -> "GroupName"
|
||||
func parseGroupNameFromDN(dn string) string {
|
||||
if dn == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Split by comma and find the CN component
|
||||
parts := strings.Split(dn, ",")
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if strings.HasPrefix(strings.ToLower(part), "cn=") {
|
||||
return part[3:] // Return everything after "cn="
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractGroupNamesFromMemberOf extracts group names from memberOf DNs
|
||||
func extractGroupNamesFromMemberOf(memberOfs []string) []string {
|
||||
var groupNames []string
|
||||
for _, dn := range memberOfs {
|
||||
groupName := parseGroupNameFromDN(dn)
|
||||
if groupName != "" {
|
||||
groupNames = append(groupNames, groupName)
|
||||
}
|
||||
}
|
||||
return groupNames
|
||||
}
|
||||
|
||||
// ensureGroupExists creates a group if it doesn't exist
|
||||
func ensureGroupExists(owner, groupName string) error {
|
||||
if groupName == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
existingGroup, err := getGroup(owner, groupName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if existingGroup != nil {
|
||||
return nil // Group already exists
|
||||
}
|
||||
|
||||
// Create the group
|
||||
newGroup := &Group{
|
||||
Owner: owner,
|
||||
Name: groupName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
UpdatedTime: util.GetCurrentTime(),
|
||||
DisplayName: groupName,
|
||||
Type: LdapGroupType,
|
||||
IsEnabled: true,
|
||||
IsTopGroup: true,
|
||||
}
|
||||
|
||||
_, err = AddGroup(newGroup)
|
||||
return err
|
||||
}
|
||||
|
||||
// updateUserGroups updates an existing user's group memberships from LDAP
|
||||
func updateUserGroups(owner string, syncUser LdapUser, ldapGroupNames []string, defaultGroup string) error {
|
||||
// Find the user by LDAP UUID
|
||||
user := &User{}
|
||||
has, err := ormer.Engine.Where("owner = ? AND ldap = ?", owner, syncUser.Uuid).Get(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return fmt.Errorf("user with LDAP UUID %s not found in database (may be a new user)", syncUser.Uuid)
|
||||
}
|
||||
|
||||
// Prepare new group list with preallocated capacity
|
||||
capacity := len(ldapGroupNames)
|
||||
if defaultGroup != "" {
|
||||
capacity++
|
||||
}
|
||||
newGroups := make([]string, 0, capacity)
|
||||
if defaultGroup != "" {
|
||||
newGroups = append(newGroups, defaultGroup)
|
||||
}
|
||||
newGroups = append(newGroups, ldapGroupNames...)
|
||||
|
||||
// Update user groups
|
||||
user.Groups = newGroups
|
||||
_, err = UpdateUser(user.GetId(), user, []string{"groups"}, false)
|
||||
return err
|
||||
}
|
||||
|
||||
func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUsers []LdapUser, failedUsers []LdapUser, err error) {
|
||||
var uuids []string
|
||||
for _, user := range syncUsers {
|
||||
@@ -459,12 +455,32 @@ func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUser
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Extract group names from LDAP memberOf attributes
|
||||
ldapGroupNames := extractGroupNamesFromMemberOf(syncUser.MemberOfs)
|
||||
|
||||
// Ensure all LDAP groups exist in Casdoor
|
||||
for _, groupName := range ldapGroupNames {
|
||||
err := ensureGroupExists(owner, groupName)
|
||||
if err != nil {
|
||||
// Log warning but continue processing
|
||||
logs.Warning("Failed to create LDAP group %s: %v", groupName, err)
|
||||
}
|
||||
}
|
||||
|
||||
found := false
|
||||
if len(existUuids) > 0 {
|
||||
for _, existUuid := range existUuids {
|
||||
if syncUser.Uuid == existUuid {
|
||||
existUsers = append(existUsers, syncUser)
|
||||
found = true
|
||||
|
||||
// Update existing user's group memberships
|
||||
if len(ldapGroupNames) > 0 {
|
||||
err := updateUserGroups(owner, syncUser, ldapGroupNames, ldap.DefaultGroup)
|
||||
if err != nil {
|
||||
logs.Warning("Failed to update groups for user %s: %v", syncUser.Uuid, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -480,6 +496,18 @@ func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUser
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Prepare group assignments for new user with preallocated capacity
|
||||
capacity := len(ldapGroupNames)
|
||||
if ldap.DefaultGroup != "" {
|
||||
capacity++
|
||||
}
|
||||
userGroups := make([]string, 0, capacity)
|
||||
if ldap.DefaultGroup != "" {
|
||||
userGroups = append(userGroups, ldap.DefaultGroup)
|
||||
}
|
||||
// Add LDAP groups
|
||||
userGroups = append(userGroups, ldapGroupNames...)
|
||||
|
||||
newUser := &User{
|
||||
Owner: owner,
|
||||
Name: name,
|
||||
@@ -498,27 +526,10 @@ func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUser
|
||||
Score: score,
|
||||
Ldap: syncUser.Uuid,
|
||||
Properties: syncUser.Attributes,
|
||||
Groups: userGroups,
|
||||
}
|
||||
formatUserPhone(newUser)
|
||||
|
||||
// Assign user to groups based on memberOf attribute
|
||||
userGroups := []string{}
|
||||
if 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")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -537,179 +548,6 @@ 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
|
||||
|
||||
|
||||
143
object/ldap_conn_test.go
Normal file
143
object/ldap_conn_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseGroupNameFromDN(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
dn string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Standard Active Directory DN",
|
||||
dn: "CN=Domain Admins,OU=Groups,DC=example,DC=com",
|
||||
expected: "Domain Admins",
|
||||
},
|
||||
{
|
||||
name: "OpenLDAP DN",
|
||||
dn: "cn=developers,ou=groups,dc=example,dc=org",
|
||||
expected: "developers",
|
||||
},
|
||||
{
|
||||
name: "DN with spaces",
|
||||
dn: "CN=Project Managers,OU=Marketing,DC=company,DC=net",
|
||||
expected: "Project Managers",
|
||||
},
|
||||
{
|
||||
name: "DN with special characters",
|
||||
dn: "CN=IT-Support-Team,OU=IT,DC=domain,DC=local",
|
||||
expected: "IT-Support-Team",
|
||||
},
|
||||
{
|
||||
name: "Empty DN",
|
||||
dn: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "DN without CN",
|
||||
dn: "OU=Groups,DC=example,DC=com",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "DN with extra spaces",
|
||||
dn: "CN=Engineers,OU=Tech,DC=example,DC=com",
|
||||
expected: "Engineers",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := parseGroupNameFromDN(tc.dn)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractGroupNamesFromMemberOf(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
memberOfs []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "Multiple groups from Active Directory",
|
||||
memberOfs: []string{
|
||||
"CN=Domain Admins,OU=Groups,DC=example,DC=com",
|
||||
"CN=IT Support,OU=Groups,DC=example,DC=com",
|
||||
"CN=Developers,OU=Engineering,DC=example,DC=com",
|
||||
},
|
||||
expected: []string{"Domain Admins", "IT Support", "Developers"},
|
||||
},
|
||||
{
|
||||
name: "Single group from OpenLDAP",
|
||||
memberOfs: []string{
|
||||
"cn=developers,ou=groups,dc=example,dc=org",
|
||||
},
|
||||
expected: []string{"developers"},
|
||||
},
|
||||
{
|
||||
name: "Empty memberOf list",
|
||||
memberOfs: []string{},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "Mixed valid and invalid DNs",
|
||||
memberOfs: []string{
|
||||
"CN=Valid Group,OU=Groups,DC=example,DC=com",
|
||||
"OU=InvalidDN,DC=example,DC=com",
|
||||
"CN=Another Valid,OU=Teams,DC=example,DC=com",
|
||||
},
|
||||
expected: []string{"Valid Group", "Another Valid"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := extractGroupNamesFromMemberOf(tc.memberOfs)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoAdjustLdapUser_WithMemberOf(t *testing.T) {
|
||||
// Test that AutoAdjustLdapUser properly preserves MemberOfs
|
||||
users := []LdapUser{
|
||||
{
|
||||
Uid: "testuser",
|
||||
Cn: "Test User",
|
||||
DisplayName: "Test User",
|
||||
Email: "test@example.com",
|
||||
MemberOf: "CN=Admins,OU=Groups,DC=example,DC=com",
|
||||
MemberOfs: []string{
|
||||
"CN=Admins,OU=Groups,DC=example,DC=com",
|
||||
"CN=Developers,OU=Groups,DC=example,DC=com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := AutoAdjustLdapUser(users)
|
||||
|
||||
assert.Len(t, result, 1)
|
||||
assert.Equal(t, "CN=Admins,OU=Groups,DC=example,DC=com", result[0].MemberOf)
|
||||
assert.Len(t, result[0].MemberOfs, 2)
|
||||
assert.Equal(t, "CN=Admins,OU=Groups,DC=example,DC=com", result[0].MemberOfs[0])
|
||||
assert.Equal(t, "CN=Developers,OU=Groups,DC=example,DC=com", result[0].MemberOfs[1])
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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
|
||||
}
|
||||
@@ -32,7 +32,6 @@ 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"`
|
||||
@@ -41,7 +40,6 @@ 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"`
|
||||
@@ -125,23 +123,6 @@ 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
|
||||
@@ -153,7 +134,6 @@ 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"},
|
||||
@@ -161,8 +141,7 @@ 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: scopes,
|
||||
CodeChallengeMethodsSupported: []string{"S256"},
|
||||
ScopesSupported: []string{"openid", "email", "profile", "address", "phone", "offline_access"},
|
||||
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"},
|
||||
@@ -49,7 +49,6 @@ 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"`
|
||||
|
||||
@@ -276,7 +276,7 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
|
||||
OutOrderId: payResp.OrderId,
|
||||
}
|
||||
|
||||
if provider.Type == "Balance" {
|
||||
if provider.Type == "Dummy" || 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 == "Balance" {
|
||||
if provider.Type == "Dummy" || 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 == "Balance" {
|
||||
if provider.Type == "Dummy" || provider.Type == "Balance" {
|
||||
err = UpdateProductStock(orderProductInfos)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
||||
@@ -92,8 +92,6 @@ 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"`
|
||||
|
||||
@@ -29,9 +29,9 @@ import (
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
xormadapter "github.com/casdoor/xorm-adapter/v3"
|
||||
_ "github.com/go-sql-driver/mysql" // db = mysql
|
||||
_ "github.com/lib/pq" // db = postgres
|
||||
_ "github.com/microsoft/go-mssqldb" // db = mssql
|
||||
_ "github.com/denisenkom/go-mssqldb" // db = mssql
|
||||
_ "github.com/go-sql-driver/mysql" // db = mysql
|
||||
_ "github.com/lib/pq" // db = postgres
|
||||
"github.com/xorm-io/xorm"
|
||||
"github.com/xorm-io/xorm/core"
|
||||
"github.com/xorm-io/xorm/names"
|
||||
|
||||
@@ -303,7 +303,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 = "Failed"
|
||||
order.State = "PaymentFailed"
|
||||
order.Message = payment.Message
|
||||
order.UpdateTime = util.GetCurrentTime()
|
||||
} else if payment.State == pp.PaymentStateCanceled {
|
||||
|
||||
@@ -53,8 +53,7 @@ type Provider struct {
|
||||
|
||||
Host string `xorm:"varchar(100)" json:"host"`
|
||||
Port int `json:"port"`
|
||||
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"
|
||||
DisableSsl bool `json:"disableSsl"` // If the provider type is WeChat, DisableSsl means EnableQRCode, if type is Google, it means sync phone number
|
||||
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"`
|
||||
|
||||
@@ -43,7 +43,6 @@ 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) {
|
||||
|
||||
@@ -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, resource string, host string) (string, string, string, error) {
|
||||
func generateJwtToken(application *Application, user *User, provider string, signinMethod string, nonce string, scope 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,10 +553,7 @@ func generateJwtToken(application *Application, user *User, provider string, sig
|
||||
},
|
||||
}
|
||||
|
||||
// RFC 8707: Use resource as audience when provided
|
||||
if resource != "" {
|
||||
claims.Audience = []string{resource}
|
||||
} else if application.IsShared {
|
||||
if application.IsShared {
|
||||
claims.Audience = []string{application.ClientId + "-org-" + user.Owner}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -93,26 +92,6 @@ 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 {
|
||||
@@ -159,7 +138,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, resource 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, host string, lang string) (*Code, error) {
|
||||
user, err := GetUser(userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -190,19 +169,11 @@ 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, resource, host)
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, provider, signinMethod, nonce, scope, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -227,7 +198,6 @@ 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 {
|
||||
@@ -240,7 +210,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, resource 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) (interface{}, error) {
|
||||
application, err := GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -266,7 +236,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, resource)
|
||||
token, tokenError, err = GetAuthorizationCodeToken(application, clientSecret, code, verifier)
|
||||
case "password": // Resource Owner Password Credentials Grant
|
||||
token, tokenError, err = GetPasswordToken(application, username, password, scope, host)
|
||||
case "client_credentials": // Client Credentials Grant
|
||||
@@ -421,7 +391,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,
|
||||
@@ -575,7 +545,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,
|
||||
@@ -625,7 +595,7 @@ func generateGuestUsername() string {
|
||||
|
||||
// GetAuthorizationCodeToken
|
||||
// Authorization code flow
|
||||
func GetAuthorizationCodeToken(application *Application, clientSecret string, code string, verifier string, resource string) (*Token, *TokenError, error) {
|
||||
func GetAuthorizationCodeToken(application *Application, clientSecret string, code string, verifier string) (*Token, *TokenError, error) {
|
||||
if code == "" {
|
||||
return nil, &TokenError{
|
||||
Error: InvalidRequest,
|
||||
@@ -693,14 +663,6 @@ 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
|
||||
@@ -757,7 +719,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,
|
||||
@@ -803,7 +765,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,
|
||||
@@ -867,7 +829,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
|
||||
}
|
||||
@@ -974,7 +936,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,
|
||||
@@ -1148,7 +1110,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,
|
||||
|
||||
@@ -405,7 +405,7 @@ func ClearUserOAuthProperties(user *User, providerType string) (bool, error) {
|
||||
|
||||
func userVisible(isAdmin bool, item *AccountItem) bool {
|
||||
if item == nil {
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
if item.ViewRule == "Admin" && !isAdmin {
|
||||
@@ -564,11 +564,10 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, allowDis
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
}
|
||||
}
|
||||
|
||||
if oldUser.Language != newUser.Language {
|
||||
item := GetAccountItemByName("Language", organization)
|
||||
if oldUser.SignupApplication != newUser.SignupApplication {
|
||||
item := GetAccountItemByName("Signup application", organization)
|
||||
if !userVisible(isAdmin, item) {
|
||||
newUser.Language = oldUser.Language
|
||||
newUser.SignupApplication = oldUser.SignupApplication
|
||||
} else {
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
}
|
||||
@@ -601,83 +600,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.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) {
|
||||
@@ -806,6 +728,51 @@ 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) {
|
||||
@@ -825,11 +792,6 @@ 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
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
mssql "github.com/microsoft/go-mssqldb"
|
||||
mssql "github.com/denisenkom/go-mssqldb"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 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"},
|
||||
}
|
||||
}
|
||||
41
pp/dummy.go
41
pp/dummy.go
@@ -14,59 +14,22 @@
|
||||
|
||||
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,
|
||||
OrderId: string(orderInfoBytes),
|
||||
PayUrl: r.ReturnUrl,
|
||||
}, 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,
|
||||
Price: orderInfo.Price,
|
||||
Currency: orderInfo.Currency,
|
||||
ProductDisplayName: orderInfo.ProductDisplayName,
|
||||
PaymentStatus: PaymentStatePaid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/core/logs"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
)
|
||||
|
||||
// Application represents a simplified application structure for reverse proxy
|
||||
type Application struct {
|
||||
Owner string
|
||||
Name string
|
||||
UpstreamHost string
|
||||
}
|
||||
|
||||
// ApplicationLookupFunc is a function type for looking up applications by domain
|
||||
type ApplicationLookupFunc func(domain string) *Application
|
||||
|
||||
var applicationLookup ApplicationLookupFunc
|
||||
|
||||
// SetApplicationLookup sets the function to use for looking up applications by domain
|
||||
func SetApplicationLookup(lookupFunc ApplicationLookupFunc) {
|
||||
applicationLookup = lookupFunc
|
||||
}
|
||||
|
||||
// getDomainWithoutPort removes the port from a domain string
|
||||
func getDomainWithoutPort(domain string) string {
|
||||
if !strings.Contains(domain, ":") {
|
||||
return domain
|
||||
}
|
||||
|
||||
tokens := strings.SplitN(domain, ":", 2)
|
||||
if len(tokens) > 1 {
|
||||
return tokens[0]
|
||||
}
|
||||
return domain
|
||||
}
|
||||
|
||||
// forwardHandler creates and configures a reverse proxy for the given target URL
|
||||
func forwardHandler(targetUrl string, writer http.ResponseWriter, request *http.Request) {
|
||||
target, err := url.Parse(targetUrl)
|
||||
if err != nil {
|
||||
logs.Error("Failed to parse target URL %s: %v", targetUrl, err)
|
||||
http.Error(writer, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||
|
||||
// Configure the Director to set proper headers
|
||||
proxy.Director = func(r *http.Request) {
|
||||
r.URL.Scheme = target.Scheme
|
||||
r.URL.Host = target.Host
|
||||
r.Host = target.Host
|
||||
|
||||
// Set X-Real-IP and X-Forwarded-For headers
|
||||
if clientIP, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
r.Header.Set("X-Forwarded-For", fmt.Sprintf("%s, %s", xff, clientIP))
|
||||
} else {
|
||||
r.Header.Set("X-Forwarded-For", clientIP)
|
||||
}
|
||||
r.Header.Set("X-Real-IP", clientIP)
|
||||
}
|
||||
|
||||
// Set X-Forwarded-Proto header
|
||||
if r.TLS != nil {
|
||||
r.Header.Set("X-Forwarded-Proto", "https")
|
||||
} else {
|
||||
r.Header.Set("X-Forwarded-Proto", "http")
|
||||
}
|
||||
|
||||
// Set X-Forwarded-Host header
|
||||
r.Header.Set("X-Forwarded-Host", request.Host)
|
||||
}
|
||||
|
||||
// Handle ModifyResponse for security enhancements
|
||||
proxy.ModifyResponse = func(resp *http.Response) error {
|
||||
// Add Secure flag to all Set-Cookie headers in HTTPS responses
|
||||
if request.TLS != nil {
|
||||
// Add HSTS header for HTTPS responses if not already set by backend
|
||||
if resp.Header.Get("Strict-Transport-Security") == "" {
|
||||
resp.Header.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||||
}
|
||||
|
||||
cookies := resp.Header["Set-Cookie"]
|
||||
if len(cookies) > 0 {
|
||||
// Clear existing Set-Cookie headers
|
||||
resp.Header.Del("Set-Cookie")
|
||||
// Add them back with Secure flag if not already present
|
||||
for _, cookie := range cookies {
|
||||
// Check if Secure attribute is already present (case-insensitive)
|
||||
cookieLower := strings.ToLower(cookie)
|
||||
hasSecure := strings.Contains(cookieLower, ";secure;") ||
|
||||
strings.Contains(cookieLower, "; secure;") ||
|
||||
strings.HasSuffix(cookieLower, ";secure") ||
|
||||
strings.HasSuffix(cookieLower, "; secure")
|
||||
if !hasSecure {
|
||||
cookie = cookie + "; Secure"
|
||||
}
|
||||
resp.Header.Add("Set-Cookie", cookie)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(writer, request)
|
||||
}
|
||||
|
||||
// HandleReverseProxy handles incoming requests and forwards them to the appropriate upstream
|
||||
func HandleReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||
domain := getDomainWithoutPort(r.Host)
|
||||
|
||||
if applicationLookup == nil {
|
||||
logs.Error("Application lookup function not set")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Lookup the application by domain
|
||||
app := applicationLookup(domain)
|
||||
if app == nil {
|
||||
logs.Info("No application found for domain: %s", domain)
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the application has an upstream host configured
|
||||
if app.UpstreamHost == "" {
|
||||
logs.Warn("Application %s/%s has no upstream host configured", app.Owner, app.Name)
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Build the target URL - just use the upstream host, the actual path/query will be set by the proxy Director
|
||||
targetUrl := app.UpstreamHost
|
||||
if !strings.HasPrefix(targetUrl, "http://") && !strings.HasPrefix(targetUrl, "https://") {
|
||||
targetUrl = "http://" + targetUrl
|
||||
}
|
||||
|
||||
logs.Debug("Forwarding request from %s%s to %s", r.Host, r.RequestURI, targetUrl)
|
||||
forwardHandler(targetUrl, w, r)
|
||||
}
|
||||
|
||||
// StartProxyServer starts the HTTP and HTTPS proxy servers based on configuration
|
||||
func StartProxyServer() {
|
||||
proxyHttpPort := conf.GetConfigString("proxyHttpPort")
|
||||
proxyHttpsPort := conf.GetConfigString("proxyHttpsPort")
|
||||
|
||||
if proxyHttpPort == "" && proxyHttpsPort == "" {
|
||||
logs.Info("Reverse proxy not enabled (proxyHttpPort and proxyHttpsPort are empty)")
|
||||
return
|
||||
}
|
||||
|
||||
serverMux := http.NewServeMux()
|
||||
serverMux.HandleFunc("/", HandleReverseProxy)
|
||||
|
||||
// Start HTTP proxy if configured
|
||||
if proxyHttpPort != "" {
|
||||
go func() {
|
||||
addr := fmt.Sprintf(":%s", proxyHttpPort)
|
||||
logs.Info("Starting reverse proxy HTTP server on %s", addr)
|
||||
err := http.ListenAndServe(addr, serverMux)
|
||||
if err != nil {
|
||||
logs.Error("Failed to start HTTP proxy server: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Start HTTPS proxy if configured
|
||||
if proxyHttpsPort != "" {
|
||||
go func() {
|
||||
addr := fmt.Sprintf(":%s", proxyHttpsPort)
|
||||
|
||||
// For now, HTTPS will need certificate configuration
|
||||
// This can be enhanced later to use Application's SslCert field
|
||||
logs.Info("HTTPS proxy server on %s requires certificate configuration - not implemented yet", addr)
|
||||
|
||||
// When implemented, use code like:
|
||||
// server := &http.Server{
|
||||
// Handler: serverMux,
|
||||
// Addr: addr,
|
||||
// TLSConfig: &tls.Config{
|
||||
// MinVersion: tls.VersionTLS12,
|
||||
// PreferServerCipherSuites: true,
|
||||
// CipherSuites: []uint16{
|
||||
// tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
// tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
// tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
// tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
// tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
// tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
// },
|
||||
// CurvePreferences: []tls.CurveID{
|
||||
// tls.X25519,
|
||||
// tls.CurveP256,
|
||||
// tls.CurveP384,
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// err := server.ListenAndServeTLS("", "")
|
||||
// if err != nil {
|
||||
// logs.Error("Failed to start HTTPS proxy server: %v", err)
|
||||
// }
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestReverseProxyIntegration tests the reverse proxy with a real backend server
|
||||
func TestReverseProxyIntegration(t *testing.T) {
|
||||
// Create a test backend server that echoes the request path
|
||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify headers
|
||||
headers := []string{
|
||||
"X-Forwarded-For",
|
||||
"X-Forwarded-Proto",
|
||||
"X-Real-IP",
|
||||
"X-Forwarded-Host",
|
||||
}
|
||||
|
||||
for _, header := range headers {
|
||||
if r.Header.Get(header) == "" {
|
||||
t.Errorf("Expected header %s to be set", header)
|
||||
}
|
||||
}
|
||||
|
||||
// Echo the path and query
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Path: " + r.URL.Path + "\n"))
|
||||
w.Write([]byte("Query: " + r.URL.RawQuery + "\n"))
|
||||
w.Write([]byte("Host: " + r.Host + "\n"))
|
||||
}))
|
||||
defer backend.Close()
|
||||
|
||||
// Set up the application lookup
|
||||
SetApplicationLookup(func(domain string) *Application {
|
||||
if domain == "myapp.example.com" {
|
||||
return &Application{
|
||||
Owner: "test-owner",
|
||||
Name: "my-app",
|
||||
UpstreamHost: backend.URL,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Test various request paths
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
query string
|
||||
expected string
|
||||
}{
|
||||
{"Simple path", "/", "", "Path: /\n"},
|
||||
{"Path with segments", "/api/v1/users", "", "Path: /api/v1/users\n"},
|
||||
{"Path with query", "/search", "q=test&limit=10", "Query: q=test&limit=10\n"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
url := "http://myapp.example.com" + tt.path
|
||||
if tt.query != "" {
|
||||
url += "?" + tt.query
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", url, nil)
|
||||
req.Host = "myapp.example.com"
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
HandleReverseProxy(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(w.Body)
|
||||
bodyStr := string(body)
|
||||
|
||||
if !strings.Contains(bodyStr, tt.expected) {
|
||||
t.Errorf("Expected response to contain %q, got %q", tt.expected, bodyStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestReverseProxyWebSocket tests that WebSocket upgrade headers are preserved
|
||||
func TestReverseProxyWebSocket(t *testing.T) {
|
||||
// Note: WebSocket upgrade through httptest.ResponseRecorder has limitations
|
||||
// This test verifies that WebSocket headers are passed through, but
|
||||
// full WebSocket functionality would need integration testing with real servers
|
||||
|
||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify WebSocket headers are present
|
||||
if r.Header.Get("Upgrade") == "websocket" &&
|
||||
r.Header.Get("Connection") != "" &&
|
||||
r.Header.Get("Sec-WebSocket-Version") != "" &&
|
||||
r.Header.Get("Sec-WebSocket-Key") != "" {
|
||||
// Headers are present - this is what we're testing
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("WebSocket headers received"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Missing WebSocket headers"))
|
||||
}
|
||||
}))
|
||||
defer backend.Close()
|
||||
|
||||
SetApplicationLookup(func(domain string) *Application {
|
||||
if domain == "ws.example.com" {
|
||||
return &Application{
|
||||
Owner: "test-owner",
|
||||
Name: "ws-app",
|
||||
UpstreamHost: backend.URL,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "http://ws.example.com/ws", nil)
|
||||
req.Host = "ws.example.com"
|
||||
req.Header.Set("Upgrade", "websocket")
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
req.Header.Set("Sec-WebSocket-Version", "13")
|
||||
req.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
HandleReverseProxy(w, req)
|
||||
|
||||
body, _ := io.ReadAll(w.Body)
|
||||
bodyStr := string(body)
|
||||
|
||||
// We expect the headers to be passed through to the backend
|
||||
if !strings.Contains(bodyStr, "WebSocket headers received") {
|
||||
t.Errorf("WebSocket headers were not properly forwarded. Got: %s", bodyStr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReverseProxyUpstreamHostVariations tests different UpstreamHost formats
|
||||
func TestReverseProxyUpstreamHostVariations(t *testing.T) {
|
||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
}))
|
||||
defer backend.Close()
|
||||
|
||||
// Parse backend URL to get host
|
||||
backendURL, err := url.Parse(backend.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse backend URL: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
upstreamHost string
|
||||
shouldWork bool
|
||||
}{
|
||||
{"Full URL", backend.URL, true},
|
||||
{"Host only", backendURL.Host, true},
|
||||
{"Empty", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
SetApplicationLookup(func(domain string) *Application {
|
||||
if domain == "test.example.com" {
|
||||
return &Application{
|
||||
Owner: "test-owner",
|
||||
Name: "test-app",
|
||||
UpstreamHost: tt.upstreamHost,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "http://test.example.com/", nil)
|
||||
req.Host = "test.example.com"
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
HandleReverseProxy(w, req)
|
||||
|
||||
if tt.shouldWork {
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", w.Code)
|
||||
}
|
||||
} else {
|
||||
if w.Code == http.StatusOK {
|
||||
t.Errorf("Expected failure, but got status 200")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetDomainWithoutPort(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"example.com", "example.com"},
|
||||
{"example.com:8080", "example.com"},
|
||||
{"localhost:3000", "localhost"},
|
||||
{"subdomain.example.com:443", "subdomain.example.com"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := getDomainWithoutPort(test.input)
|
||||
if result != test.expected {
|
||||
t.Errorf("getDomainWithoutPort(%s) = %s; want %s", test.input, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleReverseProxy(t *testing.T) {
|
||||
// Create a test backend server
|
||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check that headers are set correctly
|
||||
if r.Header.Get("X-Forwarded-For") == "" {
|
||||
t.Error("X-Forwarded-For header not set")
|
||||
}
|
||||
if r.Header.Get("X-Forwarded-Proto") == "" {
|
||||
t.Error("X-Forwarded-Proto header not set")
|
||||
}
|
||||
if r.Header.Get("X-Real-IP") == "" {
|
||||
t.Error("X-Real-IP header not set")
|
||||
}
|
||||
if r.Header.Get("X-Forwarded-Host") == "" {
|
||||
t.Error("X-Forwarded-Host header not set")
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, "Backend response")
|
||||
}))
|
||||
defer backend.Close()
|
||||
|
||||
// Set up a mock application lookup function
|
||||
SetApplicationLookup(func(domain string) *Application {
|
||||
if domain == "test.example.com" {
|
||||
return &Application{
|
||||
Owner: "test-owner",
|
||||
Name: "test-app",
|
||||
UpstreamHost: backend.URL,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Test successful proxy
|
||||
req := httptest.NewRequest("GET", "http://test.example.com/path", nil)
|
||||
req.Host = "test.example.com"
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
HandleReverseProxy(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
// Test domain not found
|
||||
req = httptest.NewRequest("GET", "http://unknown.example.com/path", nil)
|
||||
req.Host = "unknown.example.com"
|
||||
w = httptest.NewRecorder()
|
||||
|
||||
HandleReverseProxy(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("Expected status 404 for unknown domain, got %d", w.Code)
|
||||
}
|
||||
|
||||
// Test application without upstream host
|
||||
SetApplicationLookup(func(domain string) *Application {
|
||||
if domain == "no-upstream.example.com" {
|
||||
return &Application{
|
||||
Owner: "test-owner",
|
||||
Name: "test-app-no-upstream",
|
||||
UpstreamHost: "",
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
req = httptest.NewRequest("GET", "http://no-upstream.example.com/path", nil)
|
||||
req.Host = "no-upstream.example.com"
|
||||
w = httptest.NewRecorder()
|
||||
|
||||
HandleReverseProxy(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("Expected status 404 for app without upstream, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationLookup(t *testing.T) {
|
||||
// Test setting and using the application lookup function
|
||||
called := false
|
||||
SetApplicationLookup(func(domain string) *Application {
|
||||
called = true
|
||||
return &Application{
|
||||
Owner: "test",
|
||||
Name: "app",
|
||||
UpstreamHost: "http://localhost:8080",
|
||||
}
|
||||
})
|
||||
|
||||
if applicationLookup == nil {
|
||||
t.Error("applicationLookup should not be nil after SetApplicationLookup")
|
||||
}
|
||||
|
||||
app := applicationLookup("test.com")
|
||||
if !called {
|
||||
t.Error("applicationLookup function was not called")
|
||||
}
|
||||
if app == nil {
|
||||
t.Error("applicationLookup should return non-nil application")
|
||||
}
|
||||
if app.Owner != "test" {
|
||||
t.Errorf("Expected owner 'test', got '%s'", app.Owner)
|
||||
}
|
||||
}
|
||||
@@ -94,17 +94,6 @@ 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)
|
||||
}
|
||||
|
||||
@@ -298,7 +298,6 @@ 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")
|
||||
@@ -321,14 +320,10 @@ 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")
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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("general:Type - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert: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 => {
|
||||
|
||||
@@ -48,7 +48,6 @@ 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";
|
||||
@@ -308,61 +307,6 @@ 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"))} :
|
||||
@@ -572,22 +516,6 @@ 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"))} :
|
||||
@@ -1373,68 +1301,6 @@ 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>
|
||||
)}</>;
|
||||
}
|
||||
|
||||
@@ -1447,12 +1313,11 @@ 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={{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"}}>
|
||||
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
|
||||
<Layout style={{background: "inherit"}}>
|
||||
{
|
||||
this.state.menuMode === "horizontal" || !this.state.menuMode ? (
|
||||
<Header style={{background: "inherit", padding: "0px", position: "sticky", top: 0}}>
|
||||
<Header style={{background: "inherit", padding: "0px"}}>
|
||||
<div className="demo-logo" />
|
||||
<Tabs
|
||||
onChange={(key) => {
|
||||
@@ -1467,13 +1332,12 @@ 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", overflow: "auto"}}>
|
||||
<Layout style={{background: "inherit", maxHeight: "calc(70vh - 70px)", overflow: "auto"}}>
|
||||
{
|
||||
this.state.menuMode === "vertical" ? (
|
||||
<Sider width={200} style={{background: "inherit", position: "sticky", top: 0}}>
|
||||
@@ -1491,7 +1355,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -38,9 +38,6 @@ 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,
|
||||
@@ -182,36 +179,6 @@ 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",
|
||||
|
||||
@@ -75,11 +75,6 @@ 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;
|
||||
@@ -124,7 +119,7 @@ class CartListPage extends BaseListPage {
|
||||
|
||||
const index = user.cart.findIndex(item =>
|
||||
item.name === record.name &&
|
||||
(record.isRecharge ? item.price === record.price : true) &&
|
||||
(record.price !== null ? item.price === record.price : true) &&
|
||||
(item.pricingName || "") === (record.pricingName || "") &&
|
||||
(item.planName || "") === (record.planName || ""));
|
||||
if (index === -1) {
|
||||
@@ -215,17 +210,15 @@ 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) {
|
||||
const validCarts = carts.filter(item => !item.isInvalid);
|
||||
validCarts.forEach(item => {
|
||||
carts.forEach(item => {
|
||||
total += item.price * item.quantity;
|
||||
});
|
||||
currency = validCarts.length > 0 ? validCarts[0].currency : (carts[0].currency || "USD");
|
||||
currency = carts[0].currency;
|
||||
}
|
||||
|
||||
const columns = [
|
||||
@@ -238,9 +231,6 @@ 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}
|
||||
@@ -254,12 +244,6 @@ 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"),
|
||||
@@ -293,9 +277,6 @@ 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}
|
||||
@@ -311,9 +292,6 @@ 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}
|
||||
@@ -337,7 +315,7 @@ class CartListPage extends BaseListPage {
|
||||
onIncrease={() => this.updateCartItemQuantity(record, text + 1)}
|
||||
onDecrease={() => this.updateCartItemQuantity(record, text - 1)}
|
||||
onChange={null}
|
||||
disabled={isUpdating || record.isInvalid}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -351,11 +329,7 @@ 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`)}
|
||||
disabled={record.isInvalid}
|
||||
>
|
||||
<Button type="primary" onClick={() => this.props.history.push(`/products/${owner}/${record.name}/buy`)}>
|
||||
{i18next.t("general:Detail")}
|
||||
</Button>
|
||||
<PopconfirmModal
|
||||
@@ -393,7 +367,7 @@ class CartListPage extends BaseListPage {
|
||||
onConfirm={() => this.clearCart()}
|
||||
disabled={isEmpty}
|
||||
/>
|
||||
<Button type="primary" size="small" onClick={() => this.placeOrder()} disabled={isEmpty || hasInvalidItems || this.state.isPlacingOrder} loading={this.state.isPlacingOrder}>{i18next.t("general:Place Order")}</Button>
|
||||
<Button type="primary" size="small" onClick={() => this.placeOrder()} disabled={isEmpty || this.state.isPlacingOrder} loading={this.state.isPlacingOrder}>{i18next.t("general:Place Order")}</Button>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
@@ -414,7 +388,7 @@ class CartListPage extends BaseListPage {
|
||||
size="large"
|
||||
style={{height: "50px", fontSize: "20px", padding: "0 40px", borderRadius: "5px"}}
|
||||
onClick={() => this.placeOrder()}
|
||||
disabled={hasInvalidItems || this.state.isPlacingOrder}
|
||||
disabled={this.state.isPlacingOrder}
|
||||
loading={this.state.isPlacingOrder}
|
||||
>
|
||||
{i18next.t("general:Place Order")}
|
||||
@@ -439,33 +413,17 @@ 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,
|
||||
};
|
||||
}
|
||||
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,
|
||||
};
|
||||
return item;
|
||||
})
|
||||
.catch(() => item)
|
||||
);
|
||||
|
||||
const fullCartData = await Promise.all(productPromises);
|
||||
@@ -483,10 +441,6 @@ 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({
|
||||
@@ -500,11 +454,6 @@ 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);
|
||||
|
||||
@@ -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("provider:Scope - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("cert: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("general:Type - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.cert.type} onChange={(value => {
|
||||
|
||||
@@ -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("general:Type - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Select
|
||||
|
||||
@@ -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("general:Type - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select style={{width: "100%"}}
|
||||
|
||||
@@ -236,13 +236,6 @@ 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"),
|
||||
@@ -255,7 +248,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" || record.state === "Failed") ? i18next.t("order:Pay") : i18next.t("general:Detail")}
|
||||
{record.state === "Created" ? i18next.t("order:Pay") : i18next.t("general:Detail")}
|
||||
</Button>
|
||||
<Button danger onClick={() => this.cancelOrder(record)} disabled={record.state !== "Created" || !isAdmin}>
|
||||
{i18next.t("general:Cancel")}
|
||||
|
||||
@@ -272,7 +272,7 @@ class OrderPayPage extends React.Component {
|
||||
const updateTimeMap = {
|
||||
Paid: i18next.t("order:Payment time"),
|
||||
Canceled: i18next.t("order:Cancel time"),
|
||||
Failed: i18next.t("order:Payment failed time"),
|
||||
PaymentFailed: i18next.t("order:Payment failed time"),
|
||||
Timeout: i18next.t("order:Timeout time"),
|
||||
};
|
||||
const updateTimeLabel = updateTimeMap[state] || i18next.t("general:Updated time");
|
||||
|
||||
@@ -232,7 +232,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("general:Type - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.payment.type} onChange={e => {
|
||||
|
||||
@@ -122,7 +122,7 @@ class PaymentResultPage extends React.Component {
|
||||
payment: payment,
|
||||
});
|
||||
if (payment.state === "Created") {
|
||||
if (["PayPal", "Stripe", "AirWallex", "Alipay", "WeChat Pay", "Balance", "Dummy"].includes(payment.type)) {
|
||||
if (["PayPal", "Stripe", "AirWallex", "Alipay", "WeChat Pay", "Balance"].includes(payment.type)) {
|
||||
this.setState({
|
||||
timeout: setTimeout(async() => {
|
||||
await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);
|
||||
|
||||
@@ -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("general:All"), name: "*"}, ...res.data];
|
||||
// res.data = [{owner: i18next.t("organization:All"), name: "*"}, ...res.data];
|
||||
res.data = [
|
||||
Setting.getOption(i18next.t("general:All"), "*"),
|
||||
...data,
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
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";
|
||||
@@ -208,7 +207,6 @@ class ProductBuyPage extends React.Component {
|
||||
} else {
|
||||
const newProductInfo = {
|
||||
name: product.name,
|
||||
createdTime: moment().format(),
|
||||
price: cartPrice,
|
||||
currency: product.currency,
|
||||
pricingName: pricingName,
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
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";
|
||||
@@ -141,7 +140,6 @@ class ProductStorePage extends React.Component {
|
||||
} else {
|
||||
const newCartProductInfo = {
|
||||
name: product.name,
|
||||
createdTime: moment().format(),
|
||||
currency: product.currency,
|
||||
pricingName: "",
|
||||
planName: "",
|
||||
|
||||
@@ -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("general:Category"), i18next.t("general:Category - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("provider:Category"), i18next.t("provider: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("sslMode", "Auto");
|
||||
this.updateProviderField("disableSsl", false);
|
||||
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("general:Type - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert: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("provider:Scope - Tooltip"))}
|
||||
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("cert:Scope - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.provider.scopes} onChange={e => {
|
||||
@@ -1294,16 +1294,12 @@ 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:SSL mode"), i18next.t("provider:SSL mode - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("provider:Disable SSL"), i18next.t("provider:Disable SSL - Tooltip"))} :
|
||||
</Col>
|
||||
<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 span={1} >
|
||||
<Switch checked={this.state.provider.disableSsl} onChange={checked => {
|
||||
this.updateProviderField("disableSsl", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
@@ -139,7 +139,7 @@ class ProviderListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("displayName"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Category"),
|
||||
title: i18next.t("provider:Category"),
|
||||
dataIndex: "category",
|
||||
key: "category",
|
||||
filterMultiple: false,
|
||||
|
||||
@@ -517,7 +517,6 @@ 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")},
|
||||
@@ -2267,7 +2266,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: "general:Category", visible: true, width: "110"},
|
||||
{name: "category", label: "provider: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"},
|
||||
|
||||
@@ -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("general:Type - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert: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("provider:SSL mode"), i18next.t("provider:SSL mode - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("syncer:SSL mode"), i18next.t("syncer:SSL mode - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.syncer.sslMode} onChange={(value => {this.updateSyncerField("sslMode", value);})}>
|
||||
|
||||
@@ -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("provider:Scope - Tooltip"))}
|
||||
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("cert:Scope - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.token.scope} onChange={e => {
|
||||
|
||||
@@ -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("general:Category"), i18next.t("general:Category - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("provider:Category"), i18next.t("provider: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("general:Type - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.type} onChange={e => {
|
||||
|
||||
@@ -48,7 +48,6 @@ 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";
|
||||
@@ -862,17 +861,6 @@ 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"}} >
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
"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",
|
||||
@@ -152,7 +151,6 @@
|
||||
"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",
|
||||
@@ -186,7 +184,9 @@
|
||||
"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"
|
||||
"Private key - Tooltip": "Privater Schlüssel, der zum öffentlichen Schlüsselzertifikat gehört",
|
||||
"Scope - Tooltip": "Nutzungsszenarien des Zertifikats",
|
||||
"Type - Tooltip": "Art des Zertifikats"
|
||||
},
|
||||
"code": {
|
||||
"Code you received": "Der Code, den Sie erhalten haben",
|
||||
@@ -275,7 +275,6 @@
|
||||
"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",
|
||||
@@ -284,8 +283,6 @@
|
||||
"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",
|
||||
@@ -479,8 +476,6 @@
|
||||
"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",
|
||||
@@ -535,7 +530,6 @@
|
||||
"Transactions": "Transaktionen",
|
||||
"True": "Wahr",
|
||||
"Type": "Typ",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
"URL": "URL",
|
||||
"URL - Tooltip": "URL-Link",
|
||||
"Unknown application name": "Unbekannter Anwendungsname",
|
||||
@@ -873,8 +867,6 @@
|
||||
},
|
||||
"plan": {
|
||||
"Edit Plan": "Plan bearbeiten",
|
||||
"Is exclusive": "Is exclusive",
|
||||
"Is exclusive - Tooltip": "Is exclusive - Tooltip",
|
||||
"New Plan": "Neuer Plan",
|
||||
"Period": "Zeitraum",
|
||||
"Period - Tooltip": "Zeitraum",
|
||||
@@ -905,7 +897,6 @@
|
||||
"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",
|
||||
@@ -918,11 +909,9 @@
|
||||
"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",
|
||||
@@ -935,12 +924,10 @@
|
||||
"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",
|
||||
@@ -985,6 +972,8 @@
|
||||
"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",
|
||||
@@ -1001,6 +990,8 @@
|
||||
"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",
|
||||
@@ -1083,12 +1074,9 @@
|
||||
"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",
|
||||
@@ -1250,9 +1238,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -1284,6 +1269,8 @@
|
||||
"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",
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
"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",
|
||||
@@ -152,7 +151,6 @@
|
||||
"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",
|
||||
@@ -186,7 +184,9 @@
|
||||
"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"
|
||||
"Private key - Tooltip": "Private key corresponding to the public key certificate",
|
||||
"Scope - Tooltip": "Usage scenarios of the certificate",
|
||||
"Type - Tooltip": "Type of certificate"
|
||||
},
|
||||
"code": {
|
||||
"Code you received": "Code you received",
|
||||
@@ -275,7 +275,6 @@
|
||||
"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",
|
||||
@@ -284,8 +283,6 @@
|
||||
"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",
|
||||
@@ -479,8 +476,6 @@
|
||||
"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",
|
||||
@@ -535,7 +530,6 @@
|
||||
"Transactions": "Transactions",
|
||||
"True": "True",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
"URL": "URL",
|
||||
"URL - Tooltip": "URL link",
|
||||
"Unknown application name": "Unknown application name",
|
||||
@@ -873,8 +867,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -905,7 +897,6 @@
|
||||
"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",
|
||||
@@ -918,11 +909,9 @@
|
||||
"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",
|
||||
@@ -935,12 +924,10 @@
|
||||
"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",
|
||||
@@ -985,6 +972,8 @@
|
||||
"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",
|
||||
@@ -1001,6 +990,8 @@
|
||||
"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",
|
||||
@@ -1083,12 +1074,9 @@
|
||||
"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",
|
||||
@@ -1250,9 +1238,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -1284,6 +1269,8 @@
|
||||
"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",
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
"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",
|
||||
@@ -135,6 +134,7 @@
|
||||
"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,7 +152,6 @@
|
||||
"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",
|
||||
@@ -186,7 +185,9 @@
|
||||
"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"
|
||||
"Private key - Tooltip": "Clave privada correspondiente al certificado de clave pública",
|
||||
"Scope - Tooltip": "Escenarios de uso del certificado",
|
||||
"Type - Tooltip": "Tipo de certificado"
|
||||
},
|
||||
"code": {
|
||||
"Code you received": "Código que recibió",
|
||||
@@ -275,7 +276,6 @@
|
||||
"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,8 +284,6 @@
|
||||
"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",
|
||||
@@ -479,8 +477,6 @@
|
||||
"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",
|
||||
@@ -535,7 +531,6 @@
|
||||
"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",
|
||||
@@ -873,8 +868,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -905,7 +898,6 @@
|
||||
"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",
|
||||
@@ -918,11 +910,9 @@
|
||||
"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",
|
||||
@@ -935,12 +925,10 @@
|
||||
"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",
|
||||
@@ -985,6 +973,8 @@
|
||||
"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",
|
||||
@@ -1001,6 +991,8 @@
|
||||
"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",
|
||||
@@ -1083,12 +1075,9 @@
|
||||
"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",
|
||||
@@ -1250,9 +1239,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -1284,6 +1270,8 @@
|
||||
"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",
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
"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",
|
||||
@@ -152,7 +151,6 @@
|
||||
"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",
|
||||
@@ -186,7 +184,9 @@
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"code": {
|
||||
"Code you received": "Le code que vous avez reçu",
|
||||
@@ -275,7 +275,6 @@
|
||||
"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",
|
||||
@@ -284,8 +283,6 @@
|
||||
"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",
|
||||
@@ -479,8 +476,6 @@
|
||||
"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",
|
||||
@@ -535,7 +530,6 @@
|
||||
"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",
|
||||
@@ -873,8 +867,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -905,7 +897,6 @@
|
||||
"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",
|
||||
@@ -918,11 +909,9 @@
|
||||
"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é",
|
||||
@@ -935,12 +924,10 @@
|
||||
"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",
|
||||
@@ -985,6 +972,8 @@
|
||||
"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",
|
||||
@@ -1001,6 +990,8 @@
|
||||
"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",
|
||||
@@ -1083,12 +1074,9 @@
|
||||
"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",
|
||||
@@ -1250,9 +1238,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -1284,6 +1269,8 @@
|
||||
"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",
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
"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": "サインイン失敗回数制限",
|
||||
@@ -118,6 +117,7 @@
|
||||
"Please input your organization!": "あなたの組織を入力してください!",
|
||||
"Please select a HTML file": "HTMLファイルを選択してください",
|
||||
"Pop up": "ポップアップ",
|
||||
"Pop up - Tooltip": "ポップアップ - ヒント",
|
||||
"Providers": "プロバイダー",
|
||||
"Random": "ランダム",
|
||||
"Real name": "本名",
|
||||
@@ -135,6 +135,7 @@
|
||||
"SAML metadata": "SAMLメタデータ",
|
||||
"SAML metadata - Tooltip": "SAMLプロトコルのメタデータ - ヒント",
|
||||
"SAML reply URL": "SAMLリプライURL",
|
||||
"SAML reply URL - Tooltip": "SAMLリプライURL - ヒント",
|
||||
"Security": "セキュリティ",
|
||||
"Select": "選択",
|
||||
"Side panel HTML": "サイドパネルのHTML",
|
||||
@@ -152,7 +153,6 @@
|
||||
"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,7 +186,9 @@
|
||||
"Expire in years - Tooltip": "証明書の有効期間、年数で",
|
||||
"New Cert": "新しい証明書",
|
||||
"Private key": "プライベートキー",
|
||||
"Private key - Tooltip": "公開鍵証明書に対応する秘密鍵"
|
||||
"Private key - Tooltip": "公開鍵証明書に対応する秘密鍵",
|
||||
"Scope - Tooltip": "証明書の使用シナリオ",
|
||||
"Type - Tooltip": "証明書の種類"
|
||||
},
|
||||
"code": {
|
||||
"Code you received": "受け取ったコード",
|
||||
@@ -275,7 +277,6 @@
|
||||
"Applications that require authentication": "認証が必要なアプリケーション",
|
||||
"Apps": "アプリ",
|
||||
"Authorization": "認可",
|
||||
"Auto": "Auto",
|
||||
"Avatar": "アバター",
|
||||
"Avatar - Tooltip": "ユーザーのパブリックアバター画像",
|
||||
"Back": "戻る",
|
||||
@@ -284,8 +285,6 @@
|
||||
"Cancel": "キャンセルします",
|
||||
"Captcha": "キャプチャ",
|
||||
"Cart": "カート",
|
||||
"Category": "Category",
|
||||
"Category - Tooltip": "Category - Tooltip",
|
||||
"Cert": "証明書",
|
||||
"Cert - Tooltip": "このアプリケーションに対応するクライアントSDKによって検証する必要がある公開鍵証明書",
|
||||
"Certs": "証明書",
|
||||
@@ -479,8 +478,6 @@
|
||||
"SSH type - Tooltip": "SSH接続の認証タイプ",
|
||||
"Save": "保存",
|
||||
"Save & Exit": "保存して終了",
|
||||
"Scopes": "Scopes",
|
||||
"Scopes - Tooltip": "Scopes - Tooltip",
|
||||
"Search": "検索",
|
||||
"Send": "送信",
|
||||
"Session ID": "セッションID",
|
||||
@@ -535,7 +532,6 @@
|
||||
"Transactions": "取引",
|
||||
"True": "真",
|
||||
"Type": "タイプ",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
"URL": "URL",
|
||||
"URL - Tooltip": "URLリンク",
|
||||
"Unknown application name": "不明なアプリケーション名",
|
||||
@@ -873,8 +869,6 @@
|
||||
},
|
||||
"plan": {
|
||||
"Edit Plan": "プランを編集",
|
||||
"Is exclusive": "Is exclusive",
|
||||
"Is exclusive - Tooltip": "Is exclusive - Tooltip",
|
||||
"New Plan": "新しいプラン",
|
||||
"Period": "期間",
|
||||
"Period - Tooltip": "期間",
|
||||
@@ -905,7 +899,6 @@
|
||||
"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": "製品の詳細",
|
||||
@@ -918,11 +911,9 @@
|
||||
"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": "支払いキャンセル",
|
||||
@@ -935,12 +926,10 @@
|
||||
"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",
|
||||
@@ -985,6 +974,8 @@
|
||||
"Can signin": "サインインできますか?",
|
||||
"Can signup": "サインアップできますか?",
|
||||
"Can unlink": "アンリンクすることができます",
|
||||
"Category": "カテゴリー",
|
||||
"Category - Tooltip": "カテゴリーを選択してください",
|
||||
"Channel No.": "チャンネル番号",
|
||||
"Channel No. - Tooltip": "チャンネル番号",
|
||||
"Chat ID": "チャットID",
|
||||
@@ -1001,6 +992,8 @@
|
||||
"Content - Tooltip": "コンテンツ - ツールチップ",
|
||||
"DB test": "DBテスト",
|
||||
"DB test - Tooltip": "DBテスト - ツールチップ",
|
||||
"Disable SSL": "SSLを無効にする",
|
||||
"Disable SSL - Tooltip": "SMTPサーバーと通信する場合にSSLプロトコルを無効にするかどうか",
|
||||
"Domain": "ドメイン",
|
||||
"Domain - Tooltip": "オブジェクトストレージのカスタムドメイン",
|
||||
"Edit Provider": "編集プロバイダー",
|
||||
@@ -1083,12 +1076,9 @@
|
||||
"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": "秘密鍵",
|
||||
@@ -1250,9 +1240,6 @@
|
||||
},
|
||||
"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": "作業単位のデータベーステーブル名",
|
||||
@@ -1284,6 +1271,8 @@
|
||||
"SSH password": "SSHパスワード",
|
||||
"SSH port": "SSHポート",
|
||||
"SSH user": "SSHユーザー",
|
||||
"SSL mode": "SSLモード",
|
||||
"SSL mode - Tooltip": "SSLモード",
|
||||
"Service account key": "サービスアカウントキー",
|
||||
"Sync interval": "同期の間隔",
|
||||
"Sync interval - Tooltip": "単位は秒です",
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
"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ń",
|
||||
@@ -152,7 +151,6 @@
|
||||
"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",
|
||||
@@ -186,7 +184,9 @@
|
||||
"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"
|
||||
"Private key - Tooltip": "Klucz prywatny odpowiadający certyfikatowi klucza publicznego",
|
||||
"Scope - Tooltip": "Scenariusze użycia certyfikatu",
|
||||
"Type - Tooltip": "Typ certyfikatu"
|
||||
},
|
||||
"code": {
|
||||
"Code you received": "Kod, który otrzymałeś",
|
||||
@@ -275,7 +275,6 @@
|
||||
"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",
|
||||
@@ -284,8 +283,6 @@
|
||||
"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",
|
||||
@@ -479,8 +476,6 @@
|
||||
"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",
|
||||
@@ -535,7 +530,6 @@
|
||||
"Transactions": "Transakcje",
|
||||
"True": "Prawda",
|
||||
"Type": "Typ",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
"URL": "URL",
|
||||
"URL - Tooltip": "Link URL",
|
||||
"Unknown application name": "Nieznana nazwa aplikacji",
|
||||
@@ -873,8 +867,6 @@
|
||||
},
|
||||
"plan": {
|
||||
"Edit Plan": "Edytuj plan",
|
||||
"Is exclusive": "Is exclusive",
|
||||
"Is exclusive - Tooltip": "Is exclusive - Tooltip",
|
||||
"New Plan": "Nowy plan",
|
||||
"Period": "Okres",
|
||||
"Period - Tooltip": "Okres",
|
||||
@@ -905,7 +897,6 @@
|
||||
"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",
|
||||
@@ -918,11 +909,9 @@
|
||||
"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",
|
||||
@@ -935,12 +924,10 @@
|
||||
"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",
|
||||
@@ -985,10 +972,8 @@
|
||||
"Can signin": "Można się zalogować",
|
||||
"Can signup": "Można się zarejestrować",
|
||||
"Can unlink": "Można odłączyć",
|
||||
"Channel No.": "Channel No.",
|
||||
"Channel No. - Tooltip": "Channel No. - Tooltip",
|
||||
"Chat ID": "Chat ID",
|
||||
"Chat ID - Tooltip": "Chat ID - Tooltip",
|
||||
"Category": "Kategoria",
|
||||
"Category - Tooltip": "Wybierz kategorię",
|
||||
"Client ID": "ID klienta",
|
||||
"Client ID - Tooltip": "ID klienta",
|
||||
"Client ID 2": "ID klienta 2",
|
||||
@@ -1001,37 +986,6 @@
|
||||
"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",
|
||||
@@ -1083,12 +1037,9 @@
|
||||
"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",
|
||||
@@ -1250,9 +1201,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -1284,6 +1232,8 @@
|
||||
"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",
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
"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",
|
||||
@@ -152,7 +151,6 @@
|
||||
"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",
|
||||
@@ -186,7 +184,9 @@
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"code": {
|
||||
"Code you received": "Código que você recebeu",
|
||||
@@ -275,7 +275,6 @@
|
||||
"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",
|
||||
@@ -284,8 +283,6 @@
|
||||
"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",
|
||||
@@ -479,8 +476,6 @@
|
||||
"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",
|
||||
@@ -535,7 +530,6 @@
|
||||
"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",
|
||||
@@ -873,8 +867,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -905,7 +897,6 @@
|
||||
"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",
|
||||
@@ -918,11 +909,9 @@
|
||||
"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",
|
||||
@@ -935,12 +924,10 @@
|
||||
"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",
|
||||
@@ -985,6 +972,8 @@
|
||||
"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",
|
||||
@@ -1001,6 +990,8 @@
|
||||
"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",
|
||||
@@ -1083,12 +1074,9 @@
|
||||
"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",
|
||||
@@ -1250,9 +1238,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -1284,6 +1269,8 @@
|
||||
"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",
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
"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",
|
||||
@@ -152,7 +151,6 @@
|
||||
"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",
|
||||
@@ -186,7 +184,9 @@
|
||||
"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"
|
||||
"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ü"
|
||||
},
|
||||
"code": {
|
||||
"Code you received": "Aldığınız kod",
|
||||
@@ -275,7 +275,6 @@
|
||||
"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",
|
||||
@@ -284,8 +283,6 @@
|
||||
"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",
|
||||
@@ -479,8 +476,6 @@
|
||||
"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",
|
||||
@@ -535,7 +530,6 @@
|
||||
"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ı",
|
||||
@@ -873,8 +867,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -905,7 +897,6 @@
|
||||
"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ı",
|
||||
@@ -918,11 +909,9 @@
|
||||
"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",
|
||||
@@ -935,12 +924,10 @@
|
||||
"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",
|
||||
@@ -985,6 +972,8 @@
|
||||
"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",
|
||||
@@ -1001,6 +990,8 @@
|
||||
"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",
|
||||
@@ -1083,12 +1074,9 @@
|
||||
"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",
|
||||
@@ -1250,9 +1238,6 @@
|
||||
},
|
||||
"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ı",
|
||||
@@ -1284,6 +1269,8 @@
|
||||
"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",
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
"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": "Обмеження невдалого входу",
|
||||
@@ -152,7 +151,6 @@
|
||||
"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,7 +184,9 @@
|
||||
"Expire in years - Tooltip": "Термін дії сертифіката, років",
|
||||
"New Cert": "Новий сертифікат",
|
||||
"Private key": "Приватний ключ",
|
||||
"Private key - Tooltip": "Закритий ключ, що відповідає сертифікату відкритого ключа"
|
||||
"Private key - Tooltip": "Закритий ключ, що відповідає сертифікату відкритого ключа",
|
||||
"Scope - Tooltip": "Сценарії використання сертифіката",
|
||||
"Type - Tooltip": "Тип сертифіката"
|
||||
},
|
||||
"code": {
|
||||
"Code you received": "Код, який ви отримали",
|
||||
@@ -275,7 +275,6 @@
|
||||
"Applications that require authentication": "Програми, які потребують автентифікації",
|
||||
"Apps": "програми",
|
||||
"Authorization": "Авторизація",
|
||||
"Auto": "Auto",
|
||||
"Avatar": "Аватар",
|
||||
"Avatar - Tooltip": "Публічний аватар користувача",
|
||||
"Back": "Назад",
|
||||
@@ -284,8 +283,6 @@
|
||||
"Cancel": "Скасувати",
|
||||
"Captcha": "Капча",
|
||||
"Cart": "Кошик",
|
||||
"Category": "Category",
|
||||
"Category - Tooltip": "Category - Tooltip",
|
||||
"Cert": "сертифікат",
|
||||
"Cert - Tooltip": "Сертифікат відкритого ключа, який потрібно перевірити клієнтським SDK, що відповідає цій програмі",
|
||||
"Certs": "Сертифікати",
|
||||
@@ -479,8 +476,6 @@
|
||||
"SSH type - Tooltip": "Тип авторизації підключення SSH",
|
||||
"Save": "зберегти",
|
||||
"Save & Exit": "зберегти",
|
||||
"Scopes": "Scopes",
|
||||
"Scopes - Tooltip": "Scopes - Tooltip",
|
||||
"Search": "Пошук",
|
||||
"Send": "Надіслати",
|
||||
"Session ID": "Ідентифікатор сеансу",
|
||||
@@ -535,7 +530,6 @@
|
||||
"Transactions": "транзакції",
|
||||
"True": "Так",
|
||||
"Type": "Тип",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
"URL": "URL",
|
||||
"URL - Tooltip": "URL-посилання",
|
||||
"Unknown application name": "Невідома назва програми",
|
||||
@@ -743,59 +737,48 @@
|
||||
"New Order": "Нове замовлення",
|
||||
"Order not found": "Замовлення не знайдено",
|
||||
"Pay": "Оплатити",
|
||||
"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"
|
||||
"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": {
|
||||
"Confirm your invoice information": "Підтвердьте інформацію про рахунок",
|
||||
@@ -873,8 +856,6 @@
|
||||
},
|
||||
"plan": {
|
||||
"Edit Plan": "Редагувати план",
|
||||
"Is exclusive": "Is exclusive",
|
||||
"Is exclusive - Tooltip": "Is exclusive - Tooltip",
|
||||
"New Plan": "Новий план",
|
||||
"Period": "Крапка",
|
||||
"Period - Tooltip": "Період",
|
||||
@@ -905,7 +886,6 @@
|
||||
"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": "Деталь продукту",
|
||||
@@ -918,11 +898,9 @@
|
||||
"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": "Платіж скасовано",
|
||||
@@ -935,12 +913,10 @@
|
||||
"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",
|
||||
@@ -985,6 +961,8 @@
|
||||
"Can signin": "Можна ввійти",
|
||||
"Can signup": "Можна записатися",
|
||||
"Can unlink": "Можна від’єднати",
|
||||
"Category": "Категорія",
|
||||
"Category - Tooltip": "Ідентифікатор для категоризації та групування елементів або контенту, що полегшує фільтрацію та управління",
|
||||
"Channel No.": "Номер каналу",
|
||||
"Channel No. - Tooltip": "Унікальний номер, що ідентифікує канал зв'язку або передачі даних, використовується для розрізнення різних шляхів передачі",
|
||||
"Chat ID": "Ідентифікатор чату",
|
||||
@@ -1001,6 +979,8 @@
|
||||
"Content - Tooltip": "Вміст – підказка",
|
||||
"DB test": "Тест БД",
|
||||
"DB test - Tooltip": "Тест бази даних - підказка",
|
||||
"Disable SSL": "Вимкнути SSL",
|
||||
"Disable SSL - Tooltip": "Чи вимикати протокол SSL під час зв’язку із сервером STMP",
|
||||
"Domain": "Домен",
|
||||
"Domain - Tooltip": "Спеціальний домен для зберігання об'єктів",
|
||||
"Edit Provider": "Редагувати постачальника",
|
||||
@@ -1083,12 +1063,9 @@
|
||||
"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": "Секретний ключ",
|
||||
@@ -1250,9 +1227,6 @@
|
||||
},
|
||||
"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": "Назва робочої одиниці таблиці бази даних",
|
||||
@@ -1284,6 +1258,8 @@
|
||||
"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": "Одиниця в секундах",
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
"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",
|
||||
@@ -152,7 +151,6 @@
|
||||
"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",
|
||||
@@ -186,7 +184,9 @@
|
||||
"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"
|
||||
"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ỉ"
|
||||
},
|
||||
"code": {
|
||||
"Code you received": "Mã bạn nhận được",
|
||||
@@ -275,7 +275,6 @@
|
||||
"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",
|
||||
@@ -284,8 +283,6 @@
|
||||
"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ỉ",
|
||||
@@ -479,8 +476,6 @@
|
||||
"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",
|
||||
@@ -535,7 +530,6 @@
|
||||
"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",
|
||||
@@ -873,8 +867,6 @@
|
||||
},
|
||||
"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ỳ",
|
||||
@@ -905,7 +897,6 @@
|
||||
"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",
|
||||
@@ -918,11 +909,9 @@
|
||||
"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",
|
||||
@@ -935,12 +924,10 @@
|
||||
"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",
|
||||
@@ -985,6 +972,8 @@
|
||||
"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",
|
||||
@@ -1001,6 +990,8 @@
|
||||
"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",
|
||||
@@ -1083,12 +1074,9 @@
|
||||
"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",
|
||||
@@ -1250,9 +1238,6 @@
|
||||
},
|
||||
"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 ý",
|
||||
@@ -1260,8 +1245,11 @@
|
||||
"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",
|
||||
@@ -1280,11 +1268,15 @@
|
||||
"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",
|
||||
@@ -1299,6 +1291,7 @@
|
||||
"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",
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
"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": "登入错误次数限制",
|
||||
@@ -152,7 +151,6 @@
|
||||
"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,7 +184,9 @@
|
||||
"Expire in years - Tooltip": "公钥证书的有效期,以年为单位",
|
||||
"New Cert": "添加证书",
|
||||
"Private key": "私钥",
|
||||
"Private key - Tooltip": "公钥证书对应的私钥"
|
||||
"Private key - Tooltip": "公钥证书对应的私钥",
|
||||
"Scope - Tooltip": "公钥证书的使用场景",
|
||||
"Type - Tooltip": "公钥证书的类型"
|
||||
},
|
||||
"code": {
|
||||
"Code you received": "验证码",
|
||||
@@ -275,7 +275,6 @@
|
||||
"Applications that require authentication": "需要认证和鉴权的应用",
|
||||
"Apps": "应用列表",
|
||||
"Authorization": "Casbin权限管理",
|
||||
"Auto": "Auto",
|
||||
"Avatar": "头像",
|
||||
"Avatar - Tooltip": "公开展示的用户头像",
|
||||
"Back": "返回",
|
||||
@@ -284,8 +283,6 @@
|
||||
"Cancel": "取消",
|
||||
"Captcha": "人机验证码",
|
||||
"Cart": "购物车",
|
||||
"Category": "Category",
|
||||
"Category - Tooltip": "Category - Tooltip",
|
||||
"Cert": "证书",
|
||||
"Cert - Tooltip": "该应用所对应的客户端SDK需要验证的公钥证书",
|
||||
"Certs": "证书",
|
||||
@@ -479,8 +476,6 @@
|
||||
"SSH type - Tooltip": "SSH连接的认证类型",
|
||||
"Save": "保存",
|
||||
"Save & Exit": "保存 & 退出",
|
||||
"Scopes": "Scopes",
|
||||
"Scopes - Tooltip": "Scopes - Tooltip",
|
||||
"Search": "搜索",
|
||||
"Send": "发送",
|
||||
"Session ID": "会话ID",
|
||||
@@ -535,7 +530,6 @@
|
||||
"Transactions": "交易",
|
||||
"True": "真",
|
||||
"Type": "类型",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
"URL": "链接",
|
||||
"URL - Tooltip": "URL链接",
|
||||
"Unknown application name": "未知的应用程序名称",
|
||||
@@ -873,8 +867,6 @@
|
||||
},
|
||||
"plan": {
|
||||
"Edit Plan": "编辑计划",
|
||||
"Is exclusive": "Is exclusive",
|
||||
"Is exclusive - Tooltip": "Is exclusive - Tooltip",
|
||||
"New Plan": "添加计划",
|
||||
"Period": "期限",
|
||||
"Period - Tooltip": "计划对应的期限",
|
||||
@@ -905,7 +897,6 @@
|
||||
"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": "商品详情",
|
||||
@@ -918,11 +909,9 @@
|
||||
"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": "支付取消",
|
||||
@@ -935,12 +924,10 @@
|
||||
"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": "货号",
|
||||
@@ -985,6 +972,8 @@
|
||||
"Can signin": "可用于登录",
|
||||
"Can signup": "可用于注册",
|
||||
"Can unlink": "可解绑定",
|
||||
"Category": "分类",
|
||||
"Category - Tooltip": "用于对项目或内容进行归类分组的标识",
|
||||
"Channel No.": "Channel号码",
|
||||
"Channel No. - Tooltip": "标识通信或数据传输通道的唯一编号",
|
||||
"Chat ID": "聊天ID",
|
||||
@@ -1001,6 +990,8 @@
|
||||
"Content - Tooltip": "消息、通知或文档中包含的具体信息或数据内容",
|
||||
"DB test": "数据库测试",
|
||||
"DB test - Tooltip": "测试数据库连接是否正常",
|
||||
"Disable SSL": "禁用SSL",
|
||||
"Disable SSL - Tooltip": "与STMP服务器通信时是否禁用SSL协议",
|
||||
"Domain": "域名",
|
||||
"Domain - Tooltip": "对象存储的自定义域名",
|
||||
"Edit Provider": "编辑提供商",
|
||||
@@ -1083,12 +1074,9 @@
|
||||
"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": "密钥",
|
||||
@@ -1250,9 +1238,6 @@
|
||||
},
|
||||
"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": "工作单位的数据库表名",
|
||||
@@ -1284,6 +1269,8 @@
|
||||
"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": "单位为秒",
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {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;
|
||||
@@ -104,7 +104,7 @@ class ProviderTable extends React.Component {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Category"),
|
||||
title: i18next.t("provider:Category"),
|
||||
dataIndex: "category",
|
||||
key: "category",
|
||||
width: "100px",
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {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}
|
||||
<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;
|
||||
@@ -168,7 +168,7 @@ export function getTransactionTableColumns(options = {}) {
|
||||
});
|
||||
|
||||
columns.push({
|
||||
title: i18next.t("general:Category"),
|
||||
title: i18next.t("provider:Category"),
|
||||
dataIndex: "category",
|
||||
key: "category",
|
||||
width: "120px",
|
||||
|
||||
Reference in New Issue
Block a user