Compare commits

..

36 Commits

Author SHA1 Message Date
Yang Luo
7ba660fd7f feat: fix normal users blocked from /product-store (#5195) 2026-03-05 22:24:36 +08:00
Tomáš Karela Procházka
b1c31a4a9d feat: add Resend email provider (#5200) 2026-03-05 20:55:23 +08:00
Yang Luo
90d7add503 fix: remove useless returnUrl field from ProductEditPage (#5190) 2026-03-04 21:48:47 +08:00
Yang Luo
c961e75ad3 feat: fall back to English when unsupported Accept-Language locale is requested (#5177) 2026-03-04 21:41:10 +08:00
Br1an
547189a034 feat: add missing "min" param for Cloud PNVS SMS provider (#5180) 2026-03-03 09:08:31 +08:00
DacongDA
be725eda74 feat: merge CasWAF's cert related code into Casdoor's cert code (#5171) 2026-02-27 01:36:07 +08:00
Ke Wang
0765b352c9 fix: respect application's ID signup rule in WeChat Mini Program login (#5168) 2026-02-24 21:21:18 +08:00
Yang Luo
a2a8b582d9 feat: make DingTalk syncer respect TableColumns field mapping configuration (#5073) 2026-02-24 12:55:40 +08:00
Sriram-B-Srivatsa
0973652be4 fix: reduce code duplication in Logout logic (#5163) 2026-02-24 12:53:31 +08:00
Yang Luo
fef75715bf fix(web): prevent dashboard graph overlap when y-axis values increase 2026-02-23 15:24:05 +08:00
hikarukimi
4f78d56e31 feat: add OAuth consent page 2026-02-23 15:16:04 +08:00
hikarukimi
712bc756bc fix: improve code format 2026-02-23 15:09:57 +08:00
DacongDA
1c9952e3d9 feat: support JWT Profile for OAuth 2.0 Client Grants (RFC 7523) (#5124) 2026-02-23 14:44:34 +08:00
Yang Luo
bbaa28133f feat: apply application.DefaultGroup for OAuth signups (#5157) 2026-02-22 01:06:18 +08:00
Yang Luo
baef7680ea feat: validate OAuth scopes against Application config; return invalid_scope per RFC 6749 (#5153) 2026-02-21 17:44:26 +08:00
Yang Luo
d15b66177c feat: add missing Telegram field to User struct (#5151) 2026-02-21 17:21:31 +08:00
Yang Luo
5ce6bac529 fix: improve provider table links 2026-02-21 01:36:00 +08:00
Yang Luo
0621f35665 fix: improve tabs height UI in app edit page 2026-02-21 01:16:36 +08:00
Yang Luo
1ac2490419 fix: add OIDC and SAML tabs in application edit page 2026-02-21 01:13:54 +08:00
DacongDA
8c50ada494 feat: refactor provider edit page into different JS files (#5141) 2026-02-21 00:57:38 +08:00
Yang Luo
22da90576e feat: can free input in "Tag" in Addresses table 2026-02-20 16:49:50 +08:00
Yang Luo
b00404cb3a fix: fix RegionSelect cannot save value bug in Addresses table 2026-02-20 16:45:43 +08:00
Yang Luo
2ed27f4f0a fix: improve tables UI in my account page 2026-02-20 16:35:29 +08:00
Yang Luo
bf538d5260 fix: update UpdateUser() columns for missing User fields 2026-02-20 11:02:52 +08:00
Yang Luo
13ee5fd150 feat: sync newOrganization() accountItems with getBuiltInAccountItems() (#5146) 2026-02-20 10:47:02 +08:00
Yang Luo
04cdd5a012 feat: add missing user fields to GetTranslatedUserItems, getBuiltInAccountItems, init_data template, and UserFields (#5144) 2026-02-20 10:37:51 +08:00
Yang Luo
7b4873734b feat: fix "--config" flag to actually load specified configuration file (#5139) 2026-02-19 02:13:29 +08:00
Yang Luo
8d2290944a fix: add back Payment.ProductName and ProductDisplayName fields for backward compatibility 2026-02-18 19:28:14 +08:00
Yang Luo
6a2bba1627 feat: fix field visibility logic for provider types in ProviderEditPage (#5134) 2026-02-18 15:22:28 +08:00
Yang Luo
07554bbbe5 feat: fix Alipay OAuth provider by loading private key from cert object (#5119) 2026-02-17 14:42:21 +08:00
karatekaneen
a050403ee5 feat: fix bug that PKCE fails when multiple custom OAuth providers are configured (#5117) 2026-02-16 23:32:07 +08:00
IsAurora6
118eb0af80 feat: Optimize the display of payment products. (#5115) 2026-02-16 16:32:02 +08:00
Yang Luo
c16aebe642 fix: update README slogan 2026-02-16 02:33:45 +08:00
Yang Luo
3b8e7c9da2 fix: extend application with reverse proxy fields (#5113) 2026-02-16 02:23:47 +08:00
Yang Luo
4d5de767b0 fix: sync frontend i18n strings 2026-02-16 02:01:48 +08:00
Yang Luo
54bf8eae5c fix: improve category column UI in app list page 2026-02-16 01:46:06 +08:00
116 changed files with 4515 additions and 1843 deletions

View File

@@ -1,5 +1,5 @@
<h1 align="center" style="border-bottom: none;">📦⚡️ Casdoor</h1>
<h3 align="center">An open-source UI-first Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML, CAS, LDAP, SCIM, WebAuthn, TOTP, MFA and RADIUS</h3>
<h3 align="center">An open-source AI-first Identity and Access Management (IAM) /AI MCP gateway and auth server with web UI supporting MCP, A2A, OAuth 2.1, OIDC, SAML, CAS, LDAP, SCIM, WebAuthn, TOTP, MFA, Face ID, Google Workspace, Azure AD</h3>
<p align="center">
<a href="#badge">
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">

View File

@@ -68,6 +68,7 @@ p, *, *, POST, /api/upload-users, *, *
p, *, *, GET, /api/get-resources, *, *
p, *, *, GET, /api/get-records, *, *
p, *, *, GET, /api/get-product, *, *
p, *, *, GET, /api/get-products, *, *
p, *, *, GET, /api/get-order, *, *
p, *, *, GET, /api/get-orders, *, *
p, *, *, GET, /api/get-user-orders, *, *

107
certificate/account.go Normal file
View File

@@ -0,0 +1,107 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certificate
import (
"crypto"
"github.com/casbin/lego/v4/acme"
"github.com/casbin/lego/v4/certcrypto"
"github.com/casbin/lego/v4/lego"
"github.com/casbin/lego/v4/registration"
"github.com/casdoor/casdoor/proxy"
)
type Account struct {
Email string
Registration *registration.Resource
Key crypto.PrivateKey
}
/** Implementation of the registration.User interface **/
// GetEmail returns the email address for the account.
func (a *Account) GetEmail() string {
return a.Email
}
// GetPrivateKey returns the private RSA account key.
func (a *Account) GetPrivateKey() crypto.PrivateKey {
return a.Key
}
// GetRegistration returns the server registration.
func (a *Account) GetRegistration() *registration.Resource {
return a.Registration
}
func getLegoClientAndAccount(email string, privateKey string, devMode bool) (*lego.Client, *Account, error) {
key, err := decodeEccKey(privateKey)
if err != nil {
return nil, nil, err
}
account := &Account{
Email: email,
Key: key,
}
config := lego.NewConfig(account)
if devMode {
config.CADirURL = lego.LEDirectoryStaging
} else {
config.CADirURL = lego.LEDirectoryProduction
}
config.Certificate.KeyType = certcrypto.RSA2048
config.HTTPClient = proxy.ProxyHttpClient
client, err := lego.NewClient(config)
if err != nil {
return nil, nil, err
}
return client, account, err
}
// GetAcmeClient Incoming an email ,a privatekey and a Boolean value that controls the opening of the test environment
// When this function is started for the first time, it will initialize the account-related configuration,
// After initializing the configuration, It will try to obtain an account based on the private key,
// if it fails, it will create an account based on the private key.
// This account will be used during the running of the program
func GetAcmeClient(email string, privateKey string, devMode bool) (*lego.Client, error) {
// Create a user. New accounts need an email and private key to start.
client, account, err := getLegoClientAndAccount(email, privateKey, devMode)
// try to obtain an account based on the private key
account.Registration, err = client.Registration.ResolveAccountByKey()
if err != nil {
acmeError, ok := err.(*acme.ProblemDetails)
if !ok {
return nil, err
}
if acmeError.Type != "urn:ietf:params:acme:error:accountDoesNotExist" {
return nil, acmeError
}
// Failed to get account, so create an account based on the private key.
account.Registration, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
return nil, err
}
}
return client, nil
}

View File

@@ -0,0 +1,47 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package certificate
import (
"testing"
"github.com/beego/beego/v2/server/web"
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/util"
"github.com/stretchr/testify/assert"
)
func TestGetClient(t *testing.T) {
err := web.LoadAppConfig("ini", "../conf/app.conf")
if err != nil {
panic(err)
}
proxy.InitHttpClient()
eccKey := util.ReadStringFromPath("acme_account.key")
println(eccKey)
client, err := GetAcmeClient("acme2@casbin.org", eccKey, false)
assert.Nil(t, err)
pem, key, err := ObtainCertificateAli(client, "casbin.com", accessKeyId, accessKeySecret)
assert.Nil(t, err)
println(pem)
println()
println(key)
}

20
certificate/conf.go Normal file
View File

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

151
certificate/dns.go Normal file
View File

@@ -0,0 +1,151 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certificate
import (
"fmt"
"time"
"github.com/casbin/lego/v4/certificate"
"github.com/casbin/lego/v4/challenge/dns01"
"github.com/casbin/lego/v4/cmd"
"github.com/casbin/lego/v4/lego"
"github.com/casbin/lego/v4/providers/dns/alidns"
"github.com/casbin/lego/v4/providers/dns/godaddy"
)
type AliConf struct {
Domains []string // The domain names for which you want to apply for a certificate
AccessKey string // Aliyun account's AccessKey, if this is not empty, Secret is required.
Secret string
RAMRole string // Use Ramrole to control aliyun account
SecurityToken string // Optional
Path string // The path to store cert file
Timeout int // Maximum waiting time for certificate application, in minutes
}
type GodaddyConf struct {
Domains []string // The domain names for which you want to apply for a certificate
APIKey string // GoDaddy account's API Key
APISecret string
Path string // The path to store cert file
Timeout int // Maximum waiting time for certificate application, in minutes
}
// getCert Verify domain ownership, then obtain a certificate, and finally store it locally.
// Need to pass in an AliConf struct, some parameters are required, other parameters can be left blank
func getAliCert(client *lego.Client, conf AliConf) (string, string, error) {
if conf.Timeout <= 0 {
conf.Timeout = 3
}
config := alidns.NewDefaultConfig()
config.PropagationTimeout = time.Duration(conf.Timeout) * time.Minute
config.APIKey = conf.AccessKey
config.SecretKey = conf.Secret
config.RAMRole = conf.RAMRole
config.SecurityToken = conf.SecurityToken
dnsProvider, err := alidns.NewDNSProvider(config)
if err != nil {
return "", "", err
}
// Choose a local DNS service provider to increase the authentication speed
servers := []string{"223.5.5.5:53"}
err = client.Challenge.SetDNS01Provider(dnsProvider, dns01.CondOption(len(servers) > 0, dns01.AddRecursiveNameservers(dns01.ParseNameservers(servers))), dns01.DisableCompletePropagationRequirement())
if err != nil {
return "", "", err
}
// Obtain the certificate
request := certificate.ObtainRequest{
Domains: conf.Domains,
Bundle: true,
}
cert, err := client.Certificate.Obtain(request)
if err != nil {
return "", "", err
}
return string(cert.Certificate), string(cert.PrivateKey), nil
}
func getGoDaddyCert(client *lego.Client, conf GodaddyConf) (string, string, error) {
if conf.Timeout <= 0 {
conf.Timeout = 3
}
config := godaddy.NewDefaultConfig()
config.PropagationTimeout = time.Duration(conf.Timeout) * time.Minute
config.PollingInterval = time.Duration(conf.Timeout) * time.Minute / 9
config.APIKey = conf.APIKey
config.APISecret = conf.APISecret
dnsProvider, err := godaddy.NewDNSProvider(config)
if err != nil {
return "", "", err
}
// Choose a local DNS service provider to increase the authentication speed
servers := []string{"223.5.5.5:53"}
err = client.Challenge.SetDNS01Provider(dnsProvider, dns01.CondOption(len(servers) > 0, dns01.AddRecursiveNameservers(dns01.ParseNameservers(servers))), dns01.DisableCompletePropagationRequirement())
if err != nil {
return "", "", err
}
// Obtain the certificate
request := certificate.ObtainRequest{
Domains: conf.Domains,
Bundle: true,
}
cert, err := client.Certificate.Obtain(request)
if err != nil {
return "", "", err
}
return string(cert.Certificate), string(cert.PrivateKey), nil
}
func ObtainCertificateAli(client *lego.Client, domain string, accessKey string, accessSecret string) (string, string, error) {
conf := AliConf{
Domains: []string{fmt.Sprintf("*.%s", domain), domain},
AccessKey: accessKey,
Secret: accessSecret,
RAMRole: "",
SecurityToken: "",
Path: "",
Timeout: 3,
}
return getAliCert(client, conf)
}
func ObtainCertificateGoDaddy(client *lego.Client, domain string, accessKey string, accessSecret string) (string, string, error) {
conf := GodaddyConf{
Domains: []string{fmt.Sprintf("*.%s", domain), domain},
APIKey: accessKey,
APISecret: accessSecret,
Path: "",
Timeout: 3,
}
return getGoDaddyCert(client, conf)
}
func SaveCert(path, filename string, cert *certificate.Resource) {
// Store the certificate file locally
certsStorage := cmd.NewCertificatesStorageLib(path, filename, true)
certsStorage.CreateRootFolder()
certsStorage.SaveResource(cert)
}

55
certificate/ecc.go Normal file
View File

@@ -0,0 +1,55 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certificate
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
)
// generateEccKey generates a public and private key pair.(NIST P-256)
func generateEccKey() (*ecdsa.PrivateKey, error) {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}
// encodeEccKey Return the input private key object as string type private key
func encodeEccKey(privateKey *ecdsa.PrivateKey) (string, error) {
x509Encoded, err := x509.MarshalECPrivateKey(privateKey)
if err != nil {
return "", err
}
pemEncoded := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: x509Encoded})
return string(pemEncoded), nil
}
// decodeEccKey Return the entered private key string as a private key object that can be used
func decodeEccKey(pemEncoded string) (*ecdsa.PrivateKey, error) {
block, _ := pem.Decode([]byte(pemEncoded))
if block == nil {
return nil, fmt.Errorf("invalid PEM-encoded EC private key")
}
x509Encoded := block.Bytes
privateKey, err := x509.ParseECPrivateKey(x509Encoded)
if err != nil {
return nil, err
}
return privateKey, nil
}

34
certificate/ecc_test.go Normal file
View File

@@ -0,0 +1,34 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package certificate
import (
"testing"
"github.com/casdoor/casdoor/util"
"github.com/stretchr/testify/assert"
)
func TestGenerateEccKey(t *testing.T) {
eccKey, err := generateEccKey()
assert.Nil(t, err)
eccKeyStr, err := encodeEccKey(eccKey)
assert.Nil(t, err)
println(eccKeyStr)
util.WriteStringToPath(eccKeyStr, "acme_account.key")
}

View File

@@ -323,6 +323,17 @@ func (c *ApiController) Signup() {
// If OAuth parameters are present, generate OAuth code and return it
if clientId != "" && responseType == ResponseTypeCode {
consentRequired, err := object.CheckConsentRequired(user, application, scope)
if err != nil {
c.ResponseError(err.Error())
return
}
if consentRequired {
c.ResponseOk(map[string]bool{"required": true})
return
}
code, err := object.GetOAuthCode(userId, clientId, "", "password", responseType, redirectUri, scope, state, nonce, codeChallenge, "", c.Ctx.Request.Host, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error(), nil)
@@ -364,18 +375,11 @@ func (c *ApiController) Logout() {
c.ClearUserSession()
c.ClearTokenSession()
owner, username, err := util.GetOwnerAndNameFromIdWithError(user)
if err != nil {
c.ResponseError(err.Error())
return
}
_, err = object.DeleteSessionId(util.GetSessionId(owner, username, object.CasdoorApplication), c.Ctx.Input.CruSession.SessionID(context.Background()))
if err != nil {
c.ResponseError(err.Error())
return
}
util.LogInfo(c.Ctx, "API: [%s] logged out", user)
if err := c.deleteUserSession(user); err != nil {
c.ResponseError(err.Error())
return
}
application := c.GetSessionApplication()
if application == nil || application.Name == "app-built-in" || application.HomepageUrl == "" {
@@ -415,21 +419,13 @@ func (c *ApiController) Logout() {
c.ClearUserSession()
c.ClearTokenSession()
// TODO https://github.com/casdoor/casdoor/pull/1494#discussion_r1095675265
owner, username, err := util.GetOwnerAndNameFromIdWithError(user)
if err != nil {
if err := c.deleteUserSession(user); err != nil {
c.ResponseError(err.Error())
return
}
_, err = object.DeleteSessionId(util.GetSessionId(owner, username, object.CasdoorApplication), c.Ctx.Input.CruSession.SessionID(context.Background()))
if err != nil {
c.ResponseError(err.Error())
return
}
util.LogInfo(c.Ctx, "API: [%s] logged out", user)
if redirectUri == "" {
c.ResponseOk()
return
@@ -766,3 +762,24 @@ func (c *ApiController) GetCaptcha() {
c.ResponseOk(Captcha{Type: "none"})
}
func (c *ApiController) deleteUserSession(user string) error {
owner, username, err := util.GetOwnerAndNameFromIdWithError(user)
if err != nil {
return err
}
// Casdoor session ID derived from owner, username, and application
sessionId := util.GetSessionId(owner, username, object.CasdoorApplication)
// Explicitly get the Beego session ID from the context
beegoSessionId := c.Ctx.Input.CruSession.SessionID(context.Background())
_, err = object.DeleteSessionId(sessionId, beegoSessionId)
if err != nil {
return err
}
util.LogInfo(c.Ctx, "API: [%s] logged out", user)
return nil
}

View File

@@ -167,6 +167,19 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
c.ResponseError(c.T("auth:Challenge method should be S256"))
return
}
consentRequired, err := object.CheckConsentRequired(user, application, scope)
if err != nil {
c.ResponseError(err.Error())
return
}
if consentRequired {
resp = &Response{Status: "ok", Data: map[string]bool{"required": true}}
resp.Data3 = user.NeedUpdatePassword
return
}
code, err := object.GetOAuthCode(userId, clientId, form.Provider, form.SigninMethod, responseType, redirectUri, scope, state, nonce, codeChallenge, resource, c.Ctx.Request.Host, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error(), nil)
@@ -185,10 +198,14 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
} else {
scope := c.Ctx.Input.Query("scope")
nonce := c.Ctx.Input.Query("nonce")
token, _ := object.GetTokenByUser(application, user, scope, nonce, c.Ctx.Request.Host)
resp = tokenToResponse(token)
if !object.IsScopeValid(scope, application) {
resp = &Response{Status: "error", Msg: "error: invalid_scope", Data: ""}
} else {
token, _ := object.GetTokenByUser(application, user, scope, nonce, c.Ctx.Request.Host)
resp = tokenToResponse(token)
resp.Data3 = user.NeedUpdatePassword
resp.Data3 = user.NeedUpdatePassword
}
}
} else if form.Type == ResponseTypeDevice {
authCache, ok := object.DeviceAuthMap.LoadAndDelete(form.UserCode)
@@ -739,7 +756,11 @@ func (c *ApiController) Login() {
}
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
// OAuth
idpInfo := object.FromProviderToIdpInfo(c.Ctx, provider)
idpInfo, err := object.FromProviderToIdpInfo(c.Ctx, provider)
if err != nil {
c.ResponseError(err.Error())
return
}
idpInfo.CodeVerifier = authForm.CodeVerifier
var idProvider idp.IdProvider
idProvider, err = idp.GetIdProvider(idpInfo, authForm.RedirectUri)
@@ -950,11 +971,13 @@ func (c *ApiController) Login() {
RegisterSource: fmt.Sprintf("%s/%s", application.Organization, application.Name),
}
// Set group from invitation code if available, otherwise use provider's signup group
// Set group from invitation code if available, otherwise use provider's signup group or application's default group
if invitation != nil && invitation.SignupGroup != "" {
user.Groups = []string{invitation.SignupGroup}
} else if providerItem.SignupGroup != "" {
user.Groups = []string{providerItem.SignupGroup}
} else if application.DefaultGroup != "" {
user.Groups = []string{application.DefaultGroup}
}
var affected bool

View File

@@ -183,3 +183,40 @@ func (c *ApiController) DeleteCert() {
c.Data["json"] = wrapActionResponse(object.DeleteCert(&cert))
c.ServeJSON()
}
// UpdateCertDomainExpire
// @Title UpdateCertDomainExpire
// @Tag Cert API
// @Description update cert domain expire time
// @Param id query string true "The ID of the cert"
// @Success 200 {object} controllers.Response The Response object
// @router /update-cert-domain-expire [post]
func (c *ApiController) UpdateCertDomainExpire() {
if _, ok := c.RequireSignedIn(); !ok {
return
}
id := c.Ctx.Input.Query("id")
cert, err := object.GetCert(id)
if err != nil {
c.ResponseError(err.Error())
return
}
domainExpireTime, err := object.GetDomainExpireTime(cert.Name)
if err != nil {
c.ResponseError(err.Error())
return
}
if domainExpireTime == "" {
c.ResponseError("Failed to determine domain expiration time for domain " + cert.Name +
". Please verify that the domain is valid, publicly resolvable, and has a retrievable expiration date, " +
"or update the domain expiration time manually.")
return
}
cert.DomainExpireTime = domainExpireTime
c.Data["json"] = wrapActionResponse(object.UpdateCert(id, cert))
c.ServeJSON()
}

226
controllers/consent.go Normal file
View File

@@ -0,0 +1,226 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/casdoor/casdoor/object"
)
// RevokeConsent revokes a consent record
// @Title RevokeConsent
// @Tag Consent API
// @Description revoke a consent record
// @Param body body object.ConsentRecord true "The consent object"
// @Success 200 {object} controllers.Response The Response object
// @router /revoke-consent [post]
func (c *ApiController) RevokeConsent() {
userId := c.GetSessionUsername()
if userId == "" {
c.ResponseError(c.T("general:Please login first"))
return
}
var consent object.ConsentRecord
err := json.Unmarshal(c.Ctx.Input.RequestBody, &consent)
if err != nil {
c.ResponseError(err.Error())
return
}
// Validate that consent.Application is not empty
if consent.Application == "" {
c.ResponseError(c.T("general:Application cannot be empty"))
return
}
// Validate that GrantedScopes is not empty when scope-specific revoke is requested
if len(consent.GrantedScopes) == 0 {
c.ResponseError(c.T("general:Granted scopes cannot be empty"))
return
}
userObj, err := object.GetUser(userId)
if err != nil {
c.ResponseError(err.Error())
return
}
if userObj == nil {
c.ResponseError(c.T("general:The user doesn't exist"))
return
}
newScopes := []object.ConsentRecord{}
for _, record := range userObj.ApplicationScopes {
if record.Application != consent.Application {
// skip other applications
newScopes = append(newScopes, record)
continue
}
// revoke specified scopes
revokeSet := make(map[string]bool)
for _, s := range consent.GrantedScopes {
revokeSet[s] = true
}
remaining := []string{}
for _, s := range record.GrantedScopes {
if !revokeSet[s] {
remaining = append(remaining, s)
}
}
if len(remaining) > 0 {
// still have remaining scopes, keep the record and update
record.GrantedScopes = remaining
newScopes = append(newScopes, record)
}
// otherwise the application authorization is revoked, delete the whole record
}
userObj.ApplicationScopes = newScopes
success, err := object.UpdateUser(userObj.GetId(), userObj, nil, false)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(success)
}
// GrantConsent grants consent for an OAuth application and returns authorization code
// @Title GrantConsent
// @Tag Consent API
// @Description grant consent for an OAuth application and get authorization code
// @Param body body object.ConsentRecord true "The consent object with OAuth parameters"
// @Success 200 {object} controllers.Response The Response object
// @router /grant-consent [post]
func (c *ApiController) GrantConsent() {
userId := c.GetSessionUsername()
if userId == "" {
c.ResponseError(c.T("general:Please login first"))
return
}
var request struct {
Application string `json:"application"`
Scopes []string `json:"grantedScopes"`
ClientId string `json:"clientId"`
Provider string `json:"provider"`
SigninMethod string `json:"signinMethod"`
ResponseType string `json:"responseType"`
RedirectUri string `json:"redirectUri"`
Scope string `json:"scope"`
State string `json:"state"`
Nonce string `json:"nonce"`
Challenge string `json:"challenge"`
Resource string `json:"resource"`
}
err := json.Unmarshal(c.Ctx.Input.RequestBody, &request)
if err != nil {
c.ResponseError(err.Error())
return
}
// Validate application by clientId
application, err := object.GetApplicationByClientId(request.ClientId)
if err != nil {
c.ResponseError(err.Error())
return
}
if application == nil {
c.ResponseError(c.T("general:Invalid client_id"))
return
}
// Verify that request.Application matches the application's actual ID
if request.Application != application.GetId() {
c.ResponseError(c.T("general:Invalid application"))
return
}
// Update user's ApplicationScopes
userObj, err := object.GetUser(userId)
if err != nil {
c.ResponseError(err.Error())
return
}
if userObj == nil {
c.ResponseError(c.T("general:User not found"))
return
}
appId := application.GetId()
found := false
// Insert new scope into existing applicationScopes
for i, record := range userObj.ApplicationScopes {
if record.Application == appId {
existing := make(map[string]bool)
for _, s := range userObj.ApplicationScopes[i].GrantedScopes {
existing[s] = true
}
for _, s := range request.Scopes {
if !existing[s] {
userObj.ApplicationScopes[i].GrantedScopes = append(userObj.ApplicationScopes[i].GrantedScopes, s)
existing[s] = true
}
}
found = true
break
}
}
// create a new applicationScopes if not found
if !found {
uniqueScopes := []string{}
existing := make(map[string]bool)
for _, s := range request.Scopes {
if !existing[s] {
uniqueScopes = append(uniqueScopes, s)
existing[s] = true
}
}
userObj.ApplicationScopes = append(userObj.ApplicationScopes, object.ConsentRecord{
Application: appId,
GrantedScopes: uniqueScopes,
})
}
_, err = object.UpdateUser(userObj.GetId(), userObj, []string{"application_scopes"}, false)
if err != nil {
c.ResponseError(err.Error())
return
}
// Now get the OAuth code
code, err := object.GetOAuthCode(
userId,
request.ClientId,
request.Provider,
request.SigninMethod,
request.ResponseType,
request.RedirectUri,
request.Scope,
request.State,
request.Nonce,
request.Challenge,
request.Resource,
c.Ctx.Request.Host,
c.GetAcceptLanguage(),
)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(code.Code)
}

View File

@@ -162,6 +162,9 @@ func (c *ApiController) DeleteToken() {
func (c *ApiController) GetOAuthToken() {
clientId := c.Ctx.Input.Query("client_id")
clientSecret := c.Ctx.Input.Query("client_secret")
assertion := c.Ctx.Input.Query("assertion")
clientAssertion := c.Ctx.Input.Query("client_assertion")
clientAssertionType := c.Ctx.Input.Query("client_assertion_type")
grantType := c.Ctx.Input.Query("grant_type")
code := c.Ctx.Input.Query("code")
verifier := c.Ctx.Input.Query("code_verifier")
@@ -193,6 +196,12 @@ func (c *ApiController) GetOAuthToken() {
if clientSecret == "" {
clientSecret = tokenRequest.ClientSecret
}
if clientAssertion == "" {
clientAssertion = tokenRequest.ClientAssertion
}
if clientAssertionType == "" {
clientAssertionType = tokenRequest.ClientAssertionType
}
if grantType == "" {
grantType = tokenRequest.GrantType
}
@@ -235,9 +244,13 @@ func (c *ApiController) GetOAuthToken() {
if resource == "" {
resource = tokenRequest.Resource
}
if assertion == "" {
assertion = tokenRequest.Assertion
}
}
}
host := c.Ctx.Request.Host
if deviceCode != "" {
deviceAuthCache, ok := object.DeviceAuthMap.Load(deviceCode)
if !ok {
@@ -278,8 +291,7 @@ func (c *ApiController) GetOAuthToken() {
username = deviceAuthCacheCast.UserName
}
host := c.Ctx.Request.Host
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage(), 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, assertion, clientAssertion, clientAssertionType, audience, resource)
if err != nil {
c.ResponseError(err.Error())
return
@@ -323,7 +335,12 @@ func (c *ApiController) RefreshToken() {
}
}
refreshToken2, err := object.RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
ok, application, clientId, _, err := c.ValidateOAuth(true)
if err != nil || !ok {
return
}
refreshToken2, err := object.RefreshToken(application, grantType, refreshToken, scope, clientId, clientSecret, host)
if err != nil {
c.ResponseError(err.Error())
return
@@ -334,14 +351,79 @@ func (c *ApiController) RefreshToken() {
c.ServeJSON()
}
func (c *ApiController) ResponseTokenError(errorMsg string) {
func (c *ApiController) ResponseTokenError(errorMsg string, errorDescription string) {
c.Data["json"] = &object.TokenError{
Error: errorMsg,
Error: errorMsg,
ErrorDescription: errorDescription,
}
c.SetTokenErrorHttpStatus()
c.ServeJSON()
}
func (c *ApiController) ValidateOAuth(ignoreValidSecret bool) (ok bool, application *object.Application, clientId, clientSecret string, err error) {
reqClientId := c.Ctx.Input.Query("client_id")
reqClientSecret := c.Ctx.Input.Query("client_secret")
clientAssertion := c.Ctx.Input.Query("client_assertion")
clientAssertionType := c.Ctx.Input.Query("client_assertion_type")
if reqClientId == "" && clientAssertionType == "" {
var tokenRequest TokenRequest
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest); err == nil {
reqClientId = tokenRequest.ClientId
reqClientSecret = tokenRequest.ClientSecret
clientAssertion = tokenRequest.ClientAssertion
clientAssertionType = tokenRequest.ClientAssertionType
}
}
if clientAssertionType == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" {
ok, application, err = object.ValidateClientAssertion(clientAssertion, c.Ctx.Request.Host)
if err != nil {
c.ResponseTokenError(object.InvalidClient, err.Error())
return
}
if !ok || application == nil {
c.ResponseTokenError(object.InvalidClient, "client_assertion is invalid")
return
}
clientSecret = application.ClientSecret
clientId = application.ClientId
ok = true
return
}
if reqClientId == "" && reqClientSecret == "" {
clientId, clientSecret, ok = c.Ctx.Request.BasicAuth()
if !ok {
clientId = c.Ctx.Input.Query("client_id")
clientSecret = c.Ctx.Input.Query("client_secret")
if clientId == "" || clientSecret == "" {
c.ResponseTokenError(object.InvalidRequest, "")
return
}
}
} else {
clientId = reqClientId
clientSecret = reqClientSecret
}
application, err = object.GetApplicationByClientId(clientId)
if err != nil {
c.ResponseTokenError(object.InvalidClient, err.Error())
return
}
if application == nil || (application.ClientSecret != clientSecret && !ignoreValidSecret) {
c.ResponseTokenError(object.InvalidClient, c.T("token:Invalid application or wrong clientSecret"))
return
}
ok = true
return
}
// IntrospectToken
// @Title IntrospectToken
// @Tag Login API
@@ -349,7 +431,7 @@ func (c *ApiController) ResponseTokenError(errorMsg string) {
// parameter representing an OAuth 2.0 token and returns a JSON document
// representing the meta information surrounding the
// token, including whether this token is currently active.
// This endpoint only support Basic Authorization.
// This endpoint support Basic Authorization and authorization defined in RFC 7523.
//
// @Param token formData string true "access_token's value or refresh_token's value"
// @Param token_type_hint formData string true "the token type access_token or refresh_token"
@@ -359,24 +441,9 @@ func (c *ApiController) ResponseTokenError(errorMsg string) {
// @router /login/oauth/introspect [post]
func (c *ApiController) IntrospectToken() {
tokenValue := c.Ctx.Input.Query("token")
clientId, clientSecret, ok := c.Ctx.Request.BasicAuth()
if !ok {
clientId = c.Ctx.Input.Query("client_id")
clientSecret = c.Ctx.Input.Query("client_secret")
if clientId == "" || clientSecret == "" {
c.ResponseTokenError(object.InvalidRequest)
return
}
}
application, err := object.GetApplicationByClientId(clientId)
if err != nil {
c.ResponseTokenError(err.Error())
return
}
if application == nil || application.ClientSecret != clientSecret {
c.ResponseTokenError(c.T("token:Invalid application or wrong clientSecret"))
ok, application, clientId, _, err := c.ValidateOAuth(false)
if err != nil || !ok {
return
}
@@ -390,7 +457,7 @@ func (c *ApiController) IntrospectToken() {
if tokenTypeHint != "" {
token, err = object.GetTokenByTokenValue(tokenValue, tokenTypeHint)
if err != nil {
c.ResponseTokenError(err.Error())
c.ResponseTokenError(object.InvalidRequest, err.Error())
return
}
if token == nil || token.ExpiresIn <= 0 {
@@ -467,7 +534,7 @@ func (c *ApiController) IntrospectToken() {
if tokenTypeHint == "" {
token, err = object.GetTokenByTokenValue(tokenValue, introspectionResponse.TokenType)
if err != nil {
c.ResponseTokenError(err.Error())
c.ResponseTokenError(object.InvalidRequest, err.Error())
return
}
if token == nil || token.ExpiresIn <= 0 {
@@ -479,7 +546,7 @@ func (c *ApiController) IntrospectToken() {
if token != nil {
application, err = object.GetApplication(fmt.Sprintf("%s/%s", token.Owner, token.Application))
if err != nil {
c.ResponseTokenError(err.Error())
c.ResponseTokenError(object.InvalidClient, err.Error())
return
}
if application == nil {

View File

@@ -15,20 +15,23 @@
package controllers
type TokenRequest struct {
ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"`
GrantType string `json:"grant_type"`
Code string `json:"code"`
Verifier string `json:"code_verifier"`
Scope string `json:"scope"`
Nonce string `json:"nonce"`
Username string `json:"username"`
Password string `json:"password"`
Tag string `json:"tag"`
Avatar string `json:"avatar"`
RefreshToken string `json:"refresh_token"`
SubjectToken string `json:"subject_token"`
SubjectTokenType string `json:"subject_token_type"`
Audience string `json:"audience"`
Resource string `json:"resource"` // RFC 8707 Resource Indicator
Assertion string `json:"assertion"`
ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"`
ClientAssertion string `json:"client_assertion"`
ClientAssertionType string `json:"client_assertion_type"`
GrantType string `json:"grant_type"`
Code string `json:"code"`
Verifier string `json:"code_verifier"`
Scope string `json:"scope"`
Nonce string `json:"nonce"`
Username string `json:"username"`
Password string `json:"password"`
Tag string `json:"tag"`
Avatar string `json:"avatar"`
RefreshToken string `json:"refresh_token"`
SubjectToken string `json:"subject_token"`
SubjectTokenType string `json:"subject_token_type"`
Audience string `json:"audience"`
Resource string `json:"resource"` // RFC 8707 Resource Indicator
}

View File

@@ -19,13 +19,16 @@ type EmailProvider interface {
}
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, sslMode string, endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string, enableProxy bool) EmailProvider {
if typ == "Azure ACS" {
switch typ {
case "Azure ACS":
return NewAzureACSEmailProvider(clientSecret, host)
} else if typ == "Custom HTTP Email" {
case "Custom HTTP Email":
return NewHttpEmailProvider(endpoint, method, httpHeaders, bodyMapping, contentType)
} else if typ == "SendGrid" {
case "SendGrid":
return NewSendgridEmailProvider(clientSecret, host, endpoint)
} else {
case "Resend":
return NewResendEmailProvider(clientSecret)
default:
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, sslMode, enableProxy)
}
}

48
email/resend.go Normal file
View File

@@ -0,0 +1,48 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package email
import (
"fmt"
"github.com/resend/resend-go/v3"
)
type ResendEmailProvider struct {
Client *resend.Client
}
func NewResendEmailProvider(apiKey string) *ResendEmailProvider {
client := resend.NewClient(apiKey)
client.UserAgent += " Casdoor"
return &ResendEmailProvider{Client: client}
}
func (s *ResendEmailProvider) Send(fromAddress string, fromName string, toAddresses []string, subject string, content string) error {
from := fromAddress
if fromName != "" {
from = fmt.Sprintf("%s <%s>", fromName, fromAddress)
}
params := &resend.SendEmailRequest{
From: from,
To: toAddresses,
Subject: subject,
Html: content,
}
if _, err := s.Client.Emails.Send(params); err != nil {
return err
}
return nil
}

12
go.mod
View File

@@ -22,6 +22,7 @@ require (
github.com/beego/beego/v2 v2.3.8
github.com/beevik/etree v1.1.0
github.com/casbin/casbin/v2 v2.77.2
github.com/casbin/lego/v4 v4.5.4
github.com/casdoor/go-sms-sender v0.25.0
github.com/casdoor/gomail/v2 v2.2.0
github.com/casdoor/ldapserver v1.2.0
@@ -47,6 +48,8 @@ require (
github.com/json-iterator/go v1.1.12
github.com/lestrrat-go/jwx v1.2.29
github.com/lib/pq v1.10.9
github.com/likexian/whois v1.15.1
github.com/likexian/whois-parser v1.24.9
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3
github.com/markbates/goth v1.82.0
github.com/microsoft/go-mssqldb v1.9.0
@@ -57,6 +60,7 @@ require (
github.com/prometheus/client_golang v1.19.0
github.com/prometheus/client_model v0.6.0
github.com/qiangmzsx/string-adapter/v2 v2.1.0
github.com/resend/resend-go/v3 v3.1.0
github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/gosaml2 v0.9.0
github.com/russellhaering/goxmldsig v1.2.0
@@ -129,6 +133,7 @@ require (
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -188,6 +193,7 @@ require (
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/likexian/gokit v0.25.13 // indirect
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/markbates/going v1.0.0 // indirect
@@ -195,6 +201,7 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-ieproxy v0.0.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.43 // indirect
github.com/mileusna/viber v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -211,14 +218,17 @@ require (
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/qiniu/go-sdk/v7 v7.12.1 // indirect
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 // indirect
github.com/redis/go-redis/v9 v9.5.5 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rs/zerolog v1.33.0 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/scim2/filter-parser/v2 v2.2.0 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
@@ -238,6 +248,7 @@ require (
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/twilio/twilio-go v1.13.0 // indirect
github.com/ucloud/ucloud-sdk-go v0.22.5 // indirect
github.com/urfave/cli v1.22.5 // indirect
github.com/utahta/go-linenotify v0.5.0 // indirect
github.com/volcengine/volc-sdk-golang v1.0.117 // indirect
github.com/x448/float16 v0.8.4 // indirect
@@ -272,6 +283,7 @@ require (
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect

30
go.sum
View File

@@ -778,6 +778,7 @@ github.com/alibabacloud-go/tea-xml v1.1.1/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCE
github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1183/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible h1:9gWa46nstkJ9miBReJcN8Gq34cBFbzSpQZVVT9N09TM=
@@ -850,6 +851,8 @@ github.com/casbin/casbin/v2 v2.28.3/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRt
github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/casbin/casbin/v2 v2.77.2 h1:yQinn/w9x8AswiwqwtrXz93VU48R1aYTXdHEx4RI3jM=
github.com/casbin/casbin/v2 v2.77.2/go.mod h1:mzGx0hYW9/ksOSpw3wNjk3NRAroq5VMFYUQ6G43iGPk=
github.com/casbin/lego/v4 v4.5.4 h1:WdVEj1A5KmKZheNuFNLF/5+UUkpXLt9mEOrLX3E81Vo=
github.com/casbin/lego/v4 v4.5.4/go.mod h1:JjTyJgN5pyrDPcg3+aAM1NtFQIXl8zDgsoSS1TnVpJ8=
github.com/casdoor/casdoor-go-sdk v0.50.0 h1:bUYbz/MzJuWfLKJbJM0+U0YpYewAur+THp5TKnufWZM=
github.com/casdoor/casdoor-go-sdk v0.50.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
github.com/casdoor/go-sms-sender v0.25.0 h1:eF4cOCSbjVg7+0uLlJQnna/FQ0BWW+Fp/x4cXhzQu1Y=
@@ -911,6 +914,8 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb h1:7X9nrm+LNWdxzQOiCjy0G51rNUxbH35IDHCjAMvogyM=
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb/go.mod h1:RfQ9wji3fjcSEsQ+uFCtIh3+BXgcZum8Kt3JxvzYzlk=
@@ -1308,6 +1313,7 @@ github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aW
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko=
github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
@@ -1321,7 +1327,9 @@ github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUB
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -1390,6 +1398,12 @@ github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/likexian/gokit v0.25.13 h1:p2Uw3+6fGG53CwdU2Dz0T6bOycdb2+bAFAa3ymwWVkM=
github.com/likexian/gokit v0.25.13/go.mod h1:qQhEWFBEfqLCO3/vOEo2EDKd+EycekVtUK4tex+l2H4=
github.com/likexian/whois v1.15.1 h1:6vTMI8n9s1eJdmcO4R9h1x99aQWIZZX1CD3am68gApU=
github.com/likexian/whois v1.15.1/go.mod h1:/nxmQ6YXvLz+qTxC/QFtEJNAt0zLuRxJrKiWpBJX8X0=
github.com/likexian/whois-parser v1.24.9 h1:BT6fzO3lj3F07yzVv0YXoaj+K4Ush0/cF+Yp6tvJJgk=
github.com/likexian/whois-parser v1.24.9/go.mod h1:b6STMHHDaSKbd4PzGrP50wWE5NzeBUETa/hT9gI0G9I=
github.com/line/line-bot-sdk-go v7.8.0+incompatible h1:Uf9/OxV0zCVfqyvwZPH8CrdiHXXmMRa/L91G3btQblQ=
github.com/line/line-bot-sdk-go v7.8.0+incompatible/go.mod h1:0RjLjJEAU/3GIcHkC3av6O4jInAbt25nnZVmOFUgDBg=
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q=
@@ -1434,6 +1448,7 @@ github.com/microsoft/go-mssqldb v1.9.0 h1:5Vq+u2f4LDujJNeZn62Z4kBDEC9MjLv0ukRzOu
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 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/mileusna/viber v1.0.1 h1:gWB6/lKoWYVxkH0Jb8jRnGIRZ/9DEM7RBZRJHRfdYWs=
github.com/mileusna/viber v1.0.1/go.mod h1:Pxu/iPMnYjnHgu+bEp3SiKWHWmlf/kDp/yOX8XUdYrQ=
@@ -1572,12 +1587,16 @@ github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdk
github.com/qiniu/go-sdk/v7 v7.12.1 h1:FZG5dhs2MZBV/mHVhmHnsgsQ+j1gSE0RqIoA2WwEDwY=
github.com/qiniu/go-sdk/v7 v7.12.1/go.mod h1:btsaOc8CA3hdVloULfFdDgDc+g4f3TDZEFsDY0BLE+w=
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 h1:dq90+d51/hQRaHEqRAsQ1rE/pC1GUS4sc2rCbbFsAIY=
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.5.5 h1:51VEyMF8eOO+NUHFm8fpg+IOc1xFuFOhxs3R+kPu1FM=
github.com/redis/go-redis/v9 v9.5.5/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/resend/resend-go/v3 v3.1.0 h1:bJpU5gYCDcczLdhCo37oy9mOmdtSVlOzM6IfWX9zhMw=
github.com/resend/resend-go/v3 v3.1.0/go.mod h1:iI7VA0NoGjWvsNii5iNC5Dy0llsI3HncXPejhniYzwE=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
@@ -1595,6 +1614,7 @@ github.com/russellhaering/gosaml2 v0.9.0 h1:CNMnH42z/GirrKjdmNrSS6bAAs47F9bPdl4P
github.com/russellhaering/gosaml2 v0.9.0/go.mod h1:byViER/1YPUa0Puj9ROZblpoq2jsE7h/CJmitzX0geU=
github.com/russellhaering/goxmldsig v1.2.0 h1:Y6GTTc9Un5hCxSzVz4UIWQ/zuVwDvzJk80guqzwx6Vg=
github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
@@ -1616,6 +1636,7 @@ github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaK
github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 h1:xT+JlYxNGqyT+XcU8iUrN18JYed2TvG9yN5ULG2jATM=
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
@@ -1636,7 +1657,9 @@ github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQ
github.com/slack-go/slack v0.15.0 h1:LE2lj2y9vqqiOf+qIIy0GvEoxgF1N5yLGZffmEZykt0=
github.com/slack-go/slack v0.15.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sony/sonyflake v1.0.0 h1:MpU6Ro7tfXwgn2l5eluf9xQvQJDROTBImNCfRXn/YeM=
@@ -1653,6 +1676,7 @@ github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5J
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
@@ -1712,6 +1736,8 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ucloud/ucloud-sdk-go v0.22.5 h1:GIltVwMDUqQj4iPL/emsZAMhEYWjLTwZqpOxdkdDrM8=
github.com/ucloud/ucloud-sdk-go v0.22.5/go.mod h1:dyLmFHmUfgb4RZKYQP9IArlvQ2pxzFthfhwxRzOEPIw=
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/utahta/go-linenotify v0.5.0 h1:E1tJaB/XhqRY/iz203FD0MaHm10DjQPOq5/Mem2A3Gs=
github.com/utahta/go-linenotify v0.5.0/go.mod h1:KsvBXil2wx+ByaCR0e+IZKTbp4pDesc7yjzRigLf6pE=
@@ -2632,13 +2658,17 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=

View File

@@ -109,7 +109,6 @@
"Insufficient balance: new balance %v would be below credit limit %v": "Unzureichendes Guthaben: neues Guthaben %v wäre unter dem Kreditlimit %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Unzureichendes Guthaben: neues Organisationsguthaben %v wäre unter dem Kreditlimit %v",
"Missing parameter": "Fehlender Parameter",
"Multiple captcha providers are not allowed in the same application: %s": "Multiple captcha providers are not allowed in the same application: %s",
"Only admin user can specify user": "Nur Administrator kann Benutzer angeben",
"Please login first": "Bitte zuerst einloggen",
"The LDAP: %s does not exist": "Das LDAP: %s existiert nicht",

View File

@@ -109,7 +109,6 @@
"Insufficient balance: new balance %v would be below credit limit %v": "Insufficient balance: new balance %v would be below credit limit %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Insufficient balance: new organization balance %v would be below credit limit %v",
"Missing parameter": "Missing parameter",
"Multiple captcha providers are not allowed in the same application: %s": "Multiple captcha providers are not allowed in the same application: %s",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
"The LDAP: %s does not exist": "The LDAP: %s does not exist",

View File

@@ -109,7 +109,6 @@
"Insufficient balance: new balance %v would be below credit limit %v": "Saldo insuficiente: el nuevo saldo %v estaría por debajo del límite de crédito %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Saldo insuficiente: el nuevo saldo de la organización %v estaría por debajo del límite de crédito %v",
"Missing parameter": "Parámetro faltante",
"Multiple captcha providers are not allowed in the same application: %s": "Multiple captcha providers are not allowed in the same application: %s",
"Only admin user can specify user": "Solo el usuario administrador puede especificar usuario",
"Please login first": "Por favor, inicia sesión primero",
"The LDAP: %s does not exist": "El LDAP: %s no existe",

View File

@@ -109,7 +109,6 @@
"Insufficient balance: new balance %v would be below credit limit %v": "Solde insuffisant : le nouveau solde %v serait inférieur à la limite de crédit %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Solde insuffisant : le nouveau solde de l'organisation %v serait inférieur à la limite de crédit %v",
"Missing parameter": "Paramètre manquant",
"Multiple captcha providers are not allowed in the same application: %s": "Multiple captcha providers are not allowed in the same application: %s",
"Only admin user can specify user": "Seul un administrateur peut désigner un utilisateur",
"Please login first": "Veuillez d'abord vous connecter",
"The LDAP: %s does not exist": "Le LDAP : %s n'existe pas",

View File

@@ -109,7 +109,6 @@
"Insufficient balance: new balance %v would be below credit limit %v": "残高不足:新しい残高 %v がクレジット制限 %v を下回ります",
"Insufficient balance: new organization balance %v would be below credit limit %v": "残高不足:新しい組織残高 %v がクレジット制限 %v を下回ります",
"Missing parameter": "不足しているパラメーター",
"Multiple captcha providers are not allowed in the same application: %s": "Multiple captcha providers are not allowed in the same application: %s",
"Only admin user can specify user": "管理者ユーザーのみがユーザーを指定できます",
"Please login first": "最初にログインしてください",
"The LDAP: %s does not exist": "LDAP%s は存在しません",

View File

@@ -109,7 +109,6 @@
"Insufficient balance: new balance %v would be below credit limit %v": "Niewystarczające saldo: nowe saldo %v byłoby poniżej limitu kredytowego %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Niewystarczające saldo: nowe saldo organizacji %v byłoby poniżej limitu kredytowego %v",
"Missing parameter": "Brakujący parametr",
"Multiple captcha providers are not allowed in the same application: %s": "Multiple captcha providers are not allowed in the same application: %s",
"Only admin user can specify user": "Tylko administrator może wskazać użytkownika",
"Please login first": "Najpierw się zaloguj",
"The LDAP: %s does not exist": "LDAP: %s nie istnieje",

View File

@@ -109,7 +109,6 @@
"Insufficient balance: new balance %v would be below credit limit %v": "Saldo insuficiente: o novo saldo %v estaria abaixo do limite de crédito %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Saldo insuficiente: o novo saldo da organização %v estaria abaixo do limite de crédito %v",
"Missing parameter": "Parâmetro ausente",
"Multiple captcha providers are not allowed in the same application: %s": "Multiple captcha providers are not allowed in the same application: %s",
"Only admin user can specify user": "Apenas um administrador pode especificar um usuário",
"Please login first": "Por favor, faça login primeiro",
"The LDAP: %s does not exist": "O LDAP: %s não existe",

View File

@@ -109,7 +109,6 @@
"Insufficient balance: new balance %v would be below credit limit %v": "Yetersiz bakiye: yeni bakiye %v kredi limitinin altında olacak %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Yetersiz bakiye: yeni organizasyon bakiyesi %v kredi limitinin altında olacak %v",
"Missing parameter": "Eksik parametre",
"Multiple captcha providers are not allowed in the same application: %s": "Multiple captcha providers are not allowed in the same application: %s",
"Only admin user can specify user": "Yalnızca yönetici kullanıcı kullanıcı belirleyebilir",
"Please login first": "Lütfen önce giriş yapın",
"The LDAP: %s does not exist": "LDAP: %s mevcut değil",

View File

@@ -109,7 +109,6 @@
"Insufficient balance: new balance %v would be below credit limit %v": "Недостатній баланс: новий баланс %v буде нижче кредитного ліміту %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Недостатній баланс: новий баланс організації %v буде нижче кредитного ліміту %v",
"Missing parameter": "Відсутній параметр",
"Multiple captcha providers are not allowed in the same application: %s": "Multiple captcha providers are not allowed in the same application: %s",
"Only admin user can specify user": "Лише адміністратор може вказати користувача",
"Please login first": "Спочатку увійдіть",
"The LDAP: %s does not exist": "LDAP: %s не існує",

View File

@@ -109,7 +109,6 @@
"Insufficient balance: new balance %v would be below credit limit %v": "Số dư không đủ: số dư mới %v sẽ thấp hơn giới hạn tín dụng %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Số dư không đủ: số dư tổ chức mới %v sẽ thấp hơn giới hạn tín dụng %v",
"Missing parameter": "Thiếu tham số",
"Multiple captcha providers are not allowed in the same application: %s": "Multiple captcha providers are not allowed in the same application: %s",
"Only admin user can specify user": "Chỉ người dùng quản trị mới có thể chỉ định người dùng",
"Please login first": "Vui lòng đăng nhập trước",
"The LDAP: %s does not exist": "LDAP: %s không tồn tại",

View File

@@ -109,7 +109,6 @@
"Insufficient balance: new balance %v would be below credit limit %v": "余额不足:新余额 %v 将低于信用限额 %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "余额不足:新组织余额 %v 将低于信用限额 %v",
"Missing parameter": "缺少参数",
"Multiple captcha providers are not allowed in the same application: %s": "Multiple captcha providers are not allowed in the same application: %s",
"Only admin user can specify user": "仅管理员用户可以指定用户",
"Please login first": "请先登录",
"The LDAP: %s does not exist": "LDAP: %s 不存在",

View File

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

View File

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

View File

@@ -67,6 +67,8 @@
{"name": "ID", "visible": true, "viewRule": "Public", "modifyRule": "Immutable"},
{"name": "Name", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
{"name": "Display name", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "First name", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Last name", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "Avatar", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "User type", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
{"name": "Password", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
@@ -81,6 +83,7 @@
{"name": "Title", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "ID card type", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "ID card", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "ID card info", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "Real name", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
{"name": "ID verification", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "Homepage", "visible": true, "viewRule": "Public", "modifyRule": "Self"},
@@ -101,6 +104,7 @@
{"name": "Signup application", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
{"name": "Register type", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
{"name": "Register source", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
{"name": "API key", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "Roles", "visible": true, "viewRule": "Public", "modifyRule": "Immutable"},
{"name": "Permissions", "visible": true, "viewRule": "Public", "modifyRule": "Immutable"},
{"name": "Groups", "visible": true, "viewRule": "Public", "modifyRule": "Admin"},
@@ -110,9 +114,14 @@
{"name": "Is forbidden", "visible": true, "viewRule": "Admin", "modifyRule": "Admin"},
{"name": "Is deleted", "visible": true, "viewRule": "Admin", "modifyRule": "Admin"},
{"name": "Multi-factor authentication", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "MFA items", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "WebAuthn credentials", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "Last change password time", "visible": true, "viewRule": "Admin", "modifyRule": "Admin"},
{"name": "Managed accounts", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "MFA accounts", "visible": true, "viewRule": "Self", "modifyRule": "Self"}
{"name": "Face ID", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "MFA accounts", "visible": true, "viewRule": "Self", "modifyRule": "Self"},
{"name": "Need update password", "visible": true, "viewRule": "Admin", "modifyRule": "Admin"},
{"name": "IP whitelist", "visible": true, "viewRule": "Admin", "modifyRule": "Admin"}
]
}
],

View File

@@ -125,6 +125,7 @@ type Application struct {
ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
ClientCert string `xorm:"varchar(100)" json:"clientCert"`
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
ForcedRedirectOrigin string `xorm:"varchar(100)" json:"forcedRedirectOrigin"`
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
@@ -155,9 +156,11 @@ type Application struct {
FailedSigninFrozenTime int `json:"failedSigninFrozenTime"`
CodeResendTimeout int `json:"codeResendTimeout"`
// Reverse Proxy fields
CustomScopes []*ScopeDescription `xorm:"mediumtext" json:"customScopes"`
// Reverse proxy fields
Domain string `xorm:"varchar(100)" json:"domain"`
OtherDomains []string `xorm:"mediumtext" json:"otherDomains"`
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"`
@@ -745,6 +748,11 @@ func UpdateApplication(id string, application *Application, isGlobalAdmin bool,
return false, err
}
err = validateCustomScopes(application.CustomScopes, lang)
if err != nil {
return false, err
}
for _, providerItem := range application.Providers {
providerItem.Provider = nil
}
@@ -800,6 +808,11 @@ func AddApplication(application *Application) (bool, error) {
return false, err
}
err = validateCustomScopes(application.CustomScopes, "en")
if err != nil {
return false, err
}
for _, providerItem := range application.Providers {
providerItem.Provider = nil
}

View File

@@ -17,6 +17,7 @@ package object
import (
"fmt"
"github.com/casdoor/casdoor/certificate"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
)
@@ -33,6 +34,13 @@ type Cert struct {
BitSize int `json:"bitSize"`
ExpireInYears int `json:"expireInYears"`
ExpireTime string `xorm:"varchar(100)" json:"expireTime"`
DomainExpireTime string `xorm:"varchar(100)" json:"domainExpireTime"`
Provider string `xorm:"varchar(100)" json:"provider"`
Account string `xorm:"varchar(100)" json:"account"`
AccessKey string `xorm:"varchar(100)" json:"accessKey"`
AccessSecret string `xorm:"varchar(100)" json:"accessSecret"`
Certificate string `xorm:"mediumtext" json:"certificate"`
PrivateKey string `xorm:"mediumtext" json:"privateKey"`
}
@@ -224,6 +232,20 @@ func (p *Cert) populateContent() error {
return nil
}
if p.Type == "SSL" {
if p.Certificate != "" {
expireTime, err := util.GetCertExpireTime(p.Certificate)
if err != nil {
return err
}
p.ExpireTime = expireTime
} else {
p.ExpireTime = ""
}
return nil
}
if len(p.CryptoAlgorithm) < 3 {
err := fmt.Errorf("populateContent() error, unsupported crypto algorithm: %s", p.CryptoAlgorithm)
return err
@@ -258,6 +280,42 @@ func (p *Cert) populateContent() error {
return nil
}
func RenewCert(cert *Cert) (bool, error) {
useProxy := false
if cert.Provider == "GoDaddy" {
useProxy = true
}
client, err := GetAcmeClient(useProxy)
if err != nil {
return false, err
}
var certStr, privateKey string
if cert.Provider == "Aliyun" {
certStr, privateKey, err = certificate.ObtainCertificateAli(client, cert.Name, cert.AccessKey, cert.AccessSecret)
} else if cert.Provider == "GoDaddy" {
certStr, privateKey, err = certificate.ObtainCertificateGoDaddy(client, cert.Name, cert.AccessKey, cert.AccessSecret)
} else {
return false, fmt.Errorf("unknown provider: %s", cert.Provider)
}
if err != nil {
return false, err
}
expireTime, err := util.GetCertExpireTime(certStr)
if err != nil {
return false, err
}
cert.ExpireTime = expireTime
cert.Certificate = certStr
cert.PrivateKey = privateKey
return UpdateCert(cert.GetId(), cert)
}
func getCertByApplication(application *Application) (*Cert, error) {
if application.Cert != "" {
return getCertByName(application.Cert)

75
object/cert_whois.go Normal file
View File

@@ -0,0 +1,75 @@
// Copyright 2023 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"strings"
"time"
"github.com/casdoor/casdoor/util"
"github.com/likexian/whois"
whoisparser "github.com/likexian/whois-parser"
)
func getDomainExpireTime(domainName string) (string, error) {
domainName, err := util.GetBaseDomain(domainName)
if err != nil {
return "", err
}
server := ""
if strings.HasSuffix(domainName, ".com") || strings.HasSuffix(domainName, ".net") {
server = "whois.verisign-grs.com"
} else if strings.HasSuffix(domainName, ".org") {
server = "whois.pir.org"
} else if strings.HasSuffix(domainName, ".io") {
server = "whois.nic.io"
} else if strings.HasSuffix(domainName, ".co") {
server = "whois.nic.co"
} else if strings.HasSuffix(domainName, ".cn") {
server = "whois.cnnic.cn"
} else if strings.HasSuffix(domainName, ".run") {
server = "whois.nic.run"
} else {
server = "grs-whois.hichina.com" // com, net, cc, tv
}
client := whois.NewClient()
//if server != "whois.cnnic.cn" && server != "grs-whois.hichina.com" {
// dialer := proxy.GetProxyDialer()
// if dialer != nil {
// client.SetDialer(dialer)
// }
//}
data, err := client.Whois(domainName, server)
if err != nil {
if !strings.HasSuffix(domainName, ".run") || data == "" {
return "", err
}
}
whoisInfo, err := whoisparser.Parse(data)
if err != nil {
return "", err
}
res := whoisInfo.Domain.ExpirationDateInTime.Local().Format(time.RFC3339)
return res, nil
}
func GetDomainExpireTime(domainName string) (string, error) {
return getDomainExpireTime(domainName)
}

View File

@@ -53,6 +53,8 @@ func getBuiltInAccountItems() []*AccountItem {
{Name: "ID", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Name", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Display name", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "First name", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Last name", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Avatar", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "User type", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Password", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
@@ -67,6 +69,7 @@ func getBuiltInAccountItems() []*AccountItem {
{Name: "Title", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "ID card type", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "ID card", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "ID card info", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Real name", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "ID verification", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Homepage", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
@@ -87,18 +90,25 @@ func getBuiltInAccountItems() []*AccountItem {
{Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Register type", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Register source", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "API key", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Roles", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Permissions", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Groups", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Consents", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Properties", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "MFA items", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Last change password time", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Face ID", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "MFA accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Need update password", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "IP whitelist", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
}
}

View File

@@ -62,6 +62,12 @@ func InitFlag() {
configPath = *configPathPtr
exportData = *exportDataPtr
exportFilePath = *exportFilePathPtr
// Load beego config from the specified config path
err := web.LoadAppConfig("ini", configPath)
if err != nil {
panic(fmt.Sprintf("failed to load config from %s: %v", configPath, err))
}
}
func ShouldExportData() bool {

View File

@@ -33,6 +33,8 @@ type Payment struct {
// Product Info
Products []string `xorm:"varchar(1000)" json:"products"`
ProductsDisplayName string `xorm:"varchar(1000)" json:"productsDisplayName"`
ProductName string `xorm:"varchar(1000)" json:"productName"`
ProductDisplayName string `xorm:"varchar(1000)" json:"productDisplayName"`
Detail string `xorm:"varchar(255)" json:"detail"`
Currency string `xorm:"varchar(100)" json:"currency"`
Price float64 `json:"price"`

View File

@@ -564,7 +564,7 @@ func providerChangeTrigger(oldName string, newName string) error {
return session.Commit()
}
func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.ProviderInfo {
func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) (*idp.ProviderInfo, error) {
providerInfo := &idp.ProviderInfo{
Type: provider.Type,
SubType: provider.SubType,
@@ -588,9 +588,19 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.Provid
}
} else if provider.Type == "ADFS" || provider.Type == "AzureAD" || provider.Type == "AzureADB2C" || provider.Type == "Casdoor" || provider.Type == "Okta" {
providerInfo.HostUrl = provider.Domain
} else if provider.Type == "Alipay" && provider.Cert != "" {
// For Alipay with certificate mode, load private key from certificate
cert, err := GetCert(util.GetId(provider.Owner, provider.Cert))
if err != nil {
return nil, fmt.Errorf("failed to load certificate for Alipay provider %s: %w", provider.Name, err)
}
if cert == nil {
return nil, fmt.Errorf("certificate not found for Alipay provider %s", provider.Name)
}
providerInfo.ClientSecret = cert.PrivateKey
}
return providerInfo
return providerInfo, nil
}
func GetIdvProviderFromProvider(provider *Provider) idv.IdvProvider {

103
object/site_cert_account.go Normal file
View File

@@ -0,0 +1,103 @@
// Copyright 2023 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"github.com/casbin/lego/v4/acme"
"github.com/casbin/lego/v4/certcrypto"
"github.com/casbin/lego/v4/lego"
"github.com/casbin/lego/v4/registration"
"github.com/casdoor/casdoor/certificate"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/proxy"
)
func getLegoClientAndAccount(email string, privateKey string, devMode bool, useProxy bool) (*lego.Client, *certificate.Account, error) {
eccKey, err := decodeEccKey(privateKey)
if err != nil {
return nil, nil, err
}
account := &certificate.Account{
Email: email,
Key: eccKey,
}
config := lego.NewConfig(account)
if devMode {
config.CADirURL = lego.LEDirectoryStaging
} else {
config.CADirURL = lego.LEDirectoryProduction
}
config.Certificate.KeyType = certcrypto.RSA2048
if useProxy {
config.HTTPClient = proxy.ProxyHttpClient
} else {
config.HTTPClient = proxy.DefaultHttpClient
}
client, err := lego.NewClient(config)
if err != nil {
return nil, nil, err
}
return client, account, nil
}
func getAcmeClient(email string, privateKey string, devMode bool, useProxy bool) (*lego.Client, error) {
// Create a user. New accounts need an email and private key to start.
client, account, err := getLegoClientAndAccount(email, privateKey, devMode, useProxy)
if err != nil {
return nil, err
}
// try to obtain an account based on the private key
account.Registration, err = client.Registration.ResolveAccountByKey()
if err != nil {
acmeError, ok := err.(*acme.ProblemDetails)
if !ok {
return nil, err
}
if acmeError.Type != "urn:ietf:params:acme:error:accountDoesNotExist" {
return nil, err
}
// Failed to get account, so create an account based on the private key.
account.Registration, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
return nil, err
}
}
return client, nil
}
func GetAcmeClient(useProxy bool) (*lego.Client, error) {
acmeEmail := conf.GetConfigString("acmeEmail")
acmePrivateKey := conf.GetConfigString("acmePrivateKey")
if acmeEmail == "" {
return nil, fmt.Errorf("acmeEmail should not be empty")
}
if acmePrivateKey == "" {
return nil, fmt.Errorf("acmePrivateKey should not be empty")
}
return getAcmeClient(acmeEmail, acmePrivateKey, false, useProxy)
}

59
object/site_cert_ecc.go Normal file
View File

@@ -0,0 +1,59 @@
// Copyright 2023 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
"strings"
)
// generateEccKey generates a public and private key pair.(NIST P-256)
func generateEccKey() *ecdsa.PrivateKey {
privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
return privateKey
}
// encodeEccKey Return the input private key object as string type private key
func encodeEccKey(privateKey *ecdsa.PrivateKey) string {
x509Encoded, err := x509.MarshalECPrivateKey(privateKey)
if err != nil {
panic(err)
}
pemEncoded := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: x509Encoded})
return string(pemEncoded)
}
// decodeEccKey Return the entered private key string as a private key object that can be used
func decodeEccKey(pemEncoded string) (*ecdsa.PrivateKey, error) {
pemEncoded = strings.ReplaceAll(pemEncoded, "\\n", "\n")
block, _ := pem.Decode([]byte(pemEncoded))
if block == nil {
return nil, fmt.Errorf("decodeEccKey() error, block should not be nil")
}
x509Encoded := block.Bytes
privateKey, err := x509.ParseECPrivateKey(x509Encoded)
if err != nil {
return nil, err
}
return privateKey, nil
}

View File

@@ -15,8 +15,10 @@
package object
import (
"strconv"
"strings"
"github.com/casdoor/casdoor/conf"
sender "github.com/casdoor/go-sms-sender"
)
@@ -61,6 +63,13 @@ func SendSms(provider *Provider, content string, phoneNumbers ...string) error {
params["0"] = content
} else {
params["code"] = content
if provider.Type == "Alibaba Cloud PNVS SMS" {
timeoutInMinutes, err := conf.GetConfigInt64("verificationCodeTimeout")
if err != nil || timeoutInMinutes <= 0 {
timeoutInMinutes = 10
}
params["min"] = strconv.FormatInt(timeoutInMinutes, 10)
}
}
err = client.SendMessage(params, phoneNumbers...)

View File

@@ -406,27 +406,61 @@ func (p *DingtalkSyncerProvider) getDingtalkUsers() ([]*OriginalUser, error) {
return originalUsers, nil
}
// getDingtalkUserFieldValue extracts a field value from DingtalkUser by field name
func (p *DingtalkSyncerProvider) getDingtalkUserFieldValue(dingtalkUser *DingtalkUser, fieldName string) string {
switch fieldName {
case "userid":
return dingtalkUser.UserId
case "unionid":
return dingtalkUser.UnionId
case "name":
return dingtalkUser.Name
case "email":
return dingtalkUser.Email
case "mobile":
return dingtalkUser.Mobile
case "avatar":
return dingtalkUser.Avatar
case "title":
return dingtalkUser.Position
case "job_number":
return dingtalkUser.JobNumber
case "active":
// Invert the boolean because active=true means NOT forbidden
return util.BoolToString(!dingtalkUser.Active)
default:
return ""
}
}
// dingtalkUserToOriginalUser converts DingTalk user to Casdoor OriginalUser
func (p *DingtalkSyncerProvider) dingtalkUserToOriginalUser(dingtalkUser *DingtalkUser) *OriginalUser {
// Use unionid as name to be consistent with OAuth provider
// Fallback to userId if unionid is not available
userName := dingtalkUser.UserId
if dingtalkUser.UnionId != "" {
userName = dingtalkUser.UnionId
user := &OriginalUser{
Address: []string{},
Properties: map[string]string{},
Groups: []string{},
DingTalk: dingtalkUser.UserId, // Link DingTalk provider account
}
user := &OriginalUser{
Id: dingtalkUser.UserId,
Name: userName,
DisplayName: dingtalkUser.Name,
Email: dingtalkUser.Email,
Phone: dingtalkUser.Mobile,
Avatar: dingtalkUser.Avatar,
Title: dingtalkUser.Position,
Address: []string{},
Properties: map[string]string{},
Groups: []string{},
DingTalk: dingtalkUser.UserId, // Link DingTalk provider account
// Apply TableColumns mapping if configured
if len(p.Syncer.TableColumns) > 0 {
for _, tableColumn := range p.Syncer.TableColumns {
value := p.getDingtalkUserFieldValue(dingtalkUser, tableColumn.Name)
p.Syncer.setUserByKeyValue(user, tableColumn.CasdoorName, value)
}
} else {
// Fallback to default mapping for backward compatibility
user.Id = dingtalkUser.UserId
user.Name = dingtalkUser.UserId
if dingtalkUser.UnionId != "" {
user.Name = dingtalkUser.UnionId
}
user.DisplayName = dingtalkUser.Name
user.Email = dingtalkUser.Email
user.Phone = dingtalkUser.Mobile
user.Avatar = dingtalkUser.Avatar
user.Title = dingtalkUser.Position
user.IsForbidden = !dingtalkUser.Active
}
// Add department IDs to Groups field
@@ -434,9 +468,6 @@ func (p *DingtalkSyncerProvider) dingtalkUserToOriginalUser(dingtalkUser *Dingta
user.Groups = append(user.Groups, fmt.Sprintf("%d", deptId))
}
// Set IsForbidden based on active status (active=false means user is forbidden)
user.IsForbidden = !dingtalkUser.Active
// Set CreatedTime to current time if not set
if user.CreatedTime == "" {
user.CreatedTime = util.GetCurrentTime()

View File

@@ -660,6 +660,15 @@ func generateJwtToken(application *Application, user *User, provider string, sig
return tokenString, refreshTokenString, name, err
}
func ParseJwtTokenWithoutValidation(token string) (*jwt.Token, error) {
t, _, err := jwt.NewParser().ParseUnverified(token, &Claims{})
if err != nil {
return nil, err
}
return t, nil
}
func ParseJwtToken(token string, cert *Cert) (*Claims, error) {
t, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
var (

View File

@@ -19,6 +19,7 @@ import (
"encoding/base64"
"fmt"
"net/url"
"slices"
"strings"
"sync"
"time"
@@ -154,6 +155,10 @@ func CheckOAuthLogin(clientId string, responseType string, redirectUri string, s
return fmt.Sprintf(i18n.Translate(lang, "token:Redirect URI: %s doesn't exist in the allowed Redirect URI list"), redirectUri), application, nil
}
if !IsScopeValid(scope, application) {
return i18n.Translate(lang, "token:Invalid scope"), application, nil
}
// Mask application for /api/get-app-login
application.ClientSecret = ""
return "", application, nil
@@ -240,10 +245,33 @@ 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) {
application, err := GetApplicationByClientId(clientId)
if err != nil {
return nil, err
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, assertion string, clientAssertion string, clientAssertionType string, audience string, resource string) (interface{}, error) {
var (
application *Application
err error
ok bool
)
if clientAssertionType == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" {
ok, application, err = ValidateClientAssertion(clientAssertion, host)
if err != nil {
return nil, err
}
if !ok || application == nil {
return &TokenError{
Error: InvalidClient,
ErrorDescription: "client_assertion is invalid",
}, nil
}
clientSecret = application.ClientSecret
clientId = application.ClientId
} else {
application, err = GetApplicationByClientId(clientId)
if err != nil {
return nil, err
}
}
if application == nil {
@@ -273,12 +301,14 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
token, tokenError, err = GetClientCredentialsToken(application, clientSecret, scope, host)
case "token", "id_token": // Implicit Grant
token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host)
case "urn:ietf:params:oauth:grant-type:jwt-bearer":
token, tokenError, err = GetJwtBearerToken(application, assertion, scope, nonce, host)
case "urn:ietf:params:oauth:grant-type:device_code":
token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host)
case "urn:ietf:params:oauth:grant-type:token-exchange": // Token Exchange Grant (RFC 8693)
token, tokenError, err = GetTokenExchangeToken(application, clientSecret, subjectToken, subjectTokenType, audience, scope, host)
case "refresh_token":
refreshToken2, err := RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
refreshToken2, err := RefreshToken(application, grantType, refreshToken, scope, clientId, clientSecret, host)
if err != nil {
return nil, err
}
@@ -320,7 +350,7 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
return tokenWrapper, nil
}
func RefreshToken(grantType string, refreshToken string, scope string, clientId string, clientSecret string, host string) (interface{}, error) {
func RefreshToken(application *Application, grantType string, refreshToken string, scope string, clientId string, clientSecret string, host string) (interface{}, error) {
// check parameters
if grantType != "refresh_token" {
return &TokenError{
@@ -328,16 +358,20 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
ErrorDescription: "grant_type should be refresh_token",
}, nil
}
application, err := GetApplicationByClientId(clientId)
if err != nil {
return nil, err
}
var err error
if application == nil {
return &TokenError{
Error: InvalidClient,
ErrorDescription: "client_id is invalid",
}, nil
application, err = GetApplicationByClientId(clientId)
if err != nil {
return nil, err
}
if application == nil {
return &TokenError{
Error: InvalidClient,
ErrorDescription: "client_id is invalid",
}, nil
}
}
if clientSecret != "" && application.ClientSecret != clientSecret {
@@ -486,6 +520,28 @@ func IsGrantTypeValid(method string, grantTypes []string) bool {
return false
}
// IsScopeValid checks whether all space-separated scopes in the scope string
// are defined in the application's Scopes list.
// If the application has no defined scopes, every scope is considered valid
// (backward-compatible behaviour).
func IsScopeValid(scope string, application *Application) bool {
if len(application.Scopes) == 0 || scope == "" {
return true
}
allowed := make(map[string]bool, len(application.Scopes))
for _, s := range application.Scopes {
allowed[s.Name] = true
}
for _, s := range strings.Fields(scope) {
if !allowed[s] {
return false
}
}
return true
}
// createGuestUserToken creates a new guest user and returns a token for them
func createGuestUserToken(application *Application, clientSecret string, verifier string) (*Token, *TokenError, error) {
// Verify client secret if provided
@@ -526,12 +582,19 @@ func createGuestUserToken(application *Application, clientSecret string, verifie
}, nil
}
// Generate a unique user ID within the confines of the application
newUserId, idErr := GenerateIdForNewUser(application)
if idErr != nil {
// If we fail to generate a unique user ID, we can fallback to a random ID
newUserId = util.GenerateId()
}
// Create the guest user
guestUser := &User{
Owner: application.Organization,
Name: guestUsername,
CreatedTime: util.GetCurrentTime(),
Id: util.GenerateId(),
Id: newUserId,
Type: "normal-user",
Password: guestPassword,
Tag: "guest-user",
@@ -715,6 +778,13 @@ func GetAuthorizationCodeToken(application *Application, clientSecret string, co
// GetPasswordToken
// Resource Owner Password Credentials flow
func GetPasswordToken(application *Application, username string, password string, scope string, host string) (*Token, *TokenError, error) {
if !IsScopeValid(scope, application) {
return nil, &TokenError{
Error: InvalidScope,
ErrorDescription: "the requested scope is invalid or not defined in the application",
}, nil
}
user, err := GetUserByFields(application.Organization, username)
if err != nil {
return nil, nil, err
@@ -796,6 +866,12 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
ErrorDescription: "client_secret is invalid",
}, nil
}
if !IsScopeValid(scope, application) {
return nil, &TokenError{
Error: InvalidScope,
ErrorDescription: "the requested scope is invalid or not defined in the application",
}, nil
}
nullUser := &User{
Owner: application.Owner,
Id: application.GetId(),
@@ -835,6 +911,13 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
// GetImplicitToken
// Implicit flow
func GetImplicitToken(application *Application, username string, scope string, nonce string, host string) (*Token, *TokenError, error) {
if !IsScopeValid(scope, application) {
return nil, &TokenError{
Error: InvalidScope,
ErrorDescription: "the requested scope is invalid or not defined in the application",
}, nil
}
user, err := GetUserByFields(application.Organization, username)
if err != nil {
return nil, nil, err
@@ -859,6 +942,84 @@ func GetImplicitToken(application *Application, username string, scope string, n
return token, nil, nil
}
// GetJwtBearerToken
// RFC 7523
func GetJwtBearerToken(application *Application, assertion string, scope string, nonce string, host string) (*Token, *TokenError, error) {
ok, claims, err := ValidateJwtAssertion(assertion, application, host)
if err != nil || !ok {
if err != nil {
return nil, &TokenError{
Error: InvalidGrant,
ErrorDescription: err.Error(),
}, err
}
return nil, &TokenError{
Error: InvalidGrant,
ErrorDescription: fmt.Sprintf("assertion (JWT) is invalid for application: [%s]", application.GetId()),
}, nil
}
return GetImplicitToken(application, claims.Subject, scope, nonce, host)
}
func ValidateJwtAssertion(clientAssertion string, application *Application, host string) (bool, *Claims, error) {
_, originBackend := getOriginFromHost(host)
clientCert, err := getCert(application.Owner, application.ClientCert)
if err != nil {
return false, nil, err
}
if clientCert == nil {
return false, nil, fmt.Errorf("client certificate is not configured for application: [%s]", application.GetId())
}
claims, err := ParseJwtToken(clientAssertion, clientCert)
if err != nil {
return false, nil, err
}
if !slices.Contains(application.RedirectUris, claims.Issuer) {
return false, nil, nil
}
if !slices.Contains(claims.Audience, fmt.Sprintf("%s/api/login/oauth/access_token", originBackend)) {
return false, nil, nil
}
return true, claims, nil
}
func ValidateClientAssertion(clientAssertion string, host string) (bool, *Application, error) {
token, err := ParseJwtTokenWithoutValidation(clientAssertion)
if err != nil {
return false, nil, err
}
clientId, err := token.Claims.GetSubject()
if err != nil {
return false, nil, err
}
application, err := GetApplicationByClientId(clientId)
if err != nil {
return false, nil, err
}
if application == nil {
return false, nil, fmt.Errorf("application not found for client: [%s]", clientId)
}
ok, _, err := ValidateJwtAssertion(clientAssertion, application, host)
if err != nil {
return false, application, err
}
if !ok {
return false, application, nil
}
return true, application, nil
}
// GetTokenByUser
// Implicit flow
func GetTokenByUser(application *Application, user *User, scope string, nonce string, host string) (*Token, error) {
@@ -946,9 +1107,16 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin
name = fmt.Sprintf("wechat-%s", openId)
}
// Generate a unique user ID within the confines of the application
newUserId, idErr := GenerateIdForNewUser(application)
if idErr != nil {
// If we fail to generate a unique user ID, we can fallback to a random ID
newUserId = util.GenerateId()
}
user = &User{
Owner: application.Organization,
Id: util.GenerateId(),
Id: newUserId,
Name: name,
Avatar: avatar,
SignupApplication: application.Name,

View File

@@ -180,6 +180,7 @@ type User struct {
Spotify string `xorm:"spotify varchar(100)" json:"spotify"`
Strava string `xorm:"strava varchar(100)" json:"strava"`
Stripe string `xorm:"stripe varchar(100)" json:"stripe"`
Telegram string `xorm:"telegram varchar(100)" json:"telegram"`
TikTok string `xorm:"tiktok varchar(100)" json:"tiktok"`
Tumblr string `xorm:"tumblr varchar(100)" json:"tumblr"`
Twitch string `xorm:"twitch varchar(100)" json:"twitch"`
@@ -241,6 +242,7 @@ type User struct {
MfaRememberDeadline string `xorm:"varchar(100)" json:"mfaRememberDeadline"`
NeedUpdatePassword bool `json:"needUpdatePassword"`
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
ApplicationScopes []ConsentRecord `xorm:"mediumtext" json:"applicationScopes"`
}
type Userinfo struct {
@@ -860,17 +862,17 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
if len(columns) == 0 {
columns = []string{
"owner", "display_name", "avatar", "first_name", "last_name",
"location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
"signin_wrong_times", "last_change_password_time", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled", "email_verified",
"location", "address", "addresses", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application", "register_type", "register_source",
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "mfa_items", "last_change_password_time", "managedAccounts", "face_ids", "mfaAccounts",
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled", "email_verified",
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "kwai", "line", "amazon",
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
"spotify", "strava", "stripe", "type", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo",
"yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist", "mfa_items", "mfa_remember_deadline",
"cart",
"spotify", "strava", "stripe", "type", "telegram", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo",
"yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist", "mfa_remember_deadline",
"cart", "application_scopes",
}
}
if isAdmin {
@@ -954,6 +956,13 @@ func UpdateUserForAllFields(id string, user *User) (bool, error) {
user.UpdatedTime = util.GetCurrentTime()
if len(user.Groups) > 0 {
_, err = userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
if err != nil {
return false, err
}
}
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(user)
if err != nil {
return false, err

119
object/user_scope.go Normal file
View File

@@ -0,0 +1,119 @@
// 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"
"strings"
"github.com/casdoor/casdoor/i18n"
)
// ConsentRecord represents the data for OAuth consent API requests/responses
type ConsentRecord struct {
// owner/name
Application string `json:"application"`
GrantedScopes []string `json:"grantedScopes"`
}
// ScopeDescription represents a human-readable description of an OAuth scope
type ScopeDescription struct {
Scope string `json:"scope"`
DisplayName string `json:"displayName"`
Description string `json:"description"`
}
// parseScopes converts a space-separated scope string to a slice
func parseScopes(scopeStr string) []string {
if scopeStr == "" {
return []string{}
}
scopes := strings.Split(scopeStr, " ")
var result []string
for _, scope := range scopes {
trimmed := strings.TrimSpace(scope)
if trimmed != "" {
result = append(result, trimmed)
}
}
return result
}
// CheckConsentRequired checks if user consent is required for the OAuth flow
func CheckConsentRequired(userObj *User, application *Application, scopeStr string) (bool, error) {
// Skip consent when no custom scopes are configured
if len(application.CustomScopes) == 0 {
return false, nil
}
// Once policy: check if consent already granted
requestedScopes := parseScopes(scopeStr)
appId := application.GetId()
// Filter requestedScopes to only include scopes defined in application.CustomScopes
customScopesMap := make(map[string]bool)
for _, customScope := range application.CustomScopes {
if customScope.Scope != "" {
customScopesMap[customScope.Scope] = true
}
}
validRequestedScopes := []string{}
for _, scope := range requestedScopes {
if customScopesMap[scope] {
validRequestedScopes = append(validRequestedScopes, scope)
}
}
// If no valid requested scopes, no consent required
if len(validRequestedScopes) == 0 {
return false, nil
}
for _, record := range userObj.ApplicationScopes {
if record.Application == appId {
// Check if grantedScopes contains all validRequestedScopes
grantedMap := make(map[string]bool)
for _, scope := range record.GrantedScopes {
grantedMap[scope] = true
}
allGranted := true
for _, scope := range validRequestedScopes {
if !grantedMap[scope] {
allGranted = false
break
}
}
if allGranted {
// Consent already granted for all valid requested scopes
return false, nil
}
}
}
// Consent required
return true, nil
}
func validateCustomScopes(customScopes []*ScopeDescription, lang string) error {
for _, scope := range customScopes {
if scope == nil || strings.TrimSpace(scope.Scope) == "" {
return fmt.Errorf("%s: custom scope name", i18n.Translate(lang, "general:Missing parameter"))
}
}
return nil
}

View File

@@ -132,6 +132,7 @@ func InitAPI() {
web.Router("/api/update-cert", &controllers.ApiController{}, "POST:UpdateCert")
web.Router("/api/add-cert", &controllers.ApiController{}, "POST:AddCert")
web.Router("/api/delete-cert", &controllers.ApiController{}, "POST:DeleteCert")
web.Router("/api/update-cert-domain-expire", &controllers.ApiController{}, "POST:UpdateCertDomainExpire")
web.Router("/api/get-roles", &controllers.ApiController{}, "GET:GetRoles")
web.Router("/api/get-role", &controllers.ApiController{}, "GET:GetRole")
@@ -319,6 +320,9 @@ func InitAPI() {
web.Router("/api/delete-mfa", &controllers.ApiController{}, "POST:DeleteMfa")
web.Router("/api/set-preferred-mfa", &controllers.ApiController{}, "POST:SetPreferredMfa")
web.Router("/api/grant-consent", &controllers.ApiController{}, "POST:GrantConsent")
web.Router("/api/revoke-consent", &controllers.ApiController{}, "POST:RevokeConsent")
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")

View File

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

50
util/cert.go Normal file
View File

@@ -0,0 +1,50 @@
// 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 util
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"time"
"golang.org/x/net/publicsuffix"
)
func GetCertExpireTime(s string) (string, error) {
block, _ := pem.Decode([]byte(s))
if block == nil {
return "", errors.New("getCertExpireTime() error, block should not be nil")
} else if block.Type != "CERTIFICATE" {
return "", fmt.Errorf("getCertExpireTime() error, block.Type should be \"CERTIFICATE\" instead of %s", block.Type)
}
certificate, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return "", err
}
t := certificate.NotAfter
return t.Local().Format(time.RFC3339), nil
}
func GetBaseDomain(domain string) (string, error) {
// abc.com -> abc.com
// abc.com.it -> abc.com.it
// subdomain.abc.io -> abc.io
// subdomain.abc.org.us -> abc.org.us
return publicsuffix.EffectiveTLDPlusOne(domain)
}

View File

@@ -16,8 +16,18 @@ package util
import (
"net"
"os"
)
func GetHostname() string {
name, err := os.Hostname()
if err != nil {
panic(err)
}
return name
}
func IsInternetIp(ip string) bool {
ipStr, _, err := net.SplitHostPort(ip)
if err != nil {

View File

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

View File

@@ -487,7 +487,7 @@ class App extends Component {
: (
Conf.CustomFooter !== null ? Conf.CustomFooter : (
<React.Fragment>
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={logo} /></a>
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={logo} /></a>
</React.Fragment>
)
)
@@ -539,15 +539,16 @@ class App extends Component {
isEntryPages() {
return window.location.pathname.startsWith("/signup") ||
window.location.pathname.startsWith("/login") ||
window.location.pathname.startsWith("/forget") ||
window.location.pathname.startsWith("/prompt") ||
window.location.pathname.startsWith("/result") ||
window.location.pathname.startsWith("/cas") ||
window.location.pathname.startsWith("/select-plan") ||
window.location.pathname.startsWith("/buy-plan") ||
window.location.pathname.startsWith("/qrcode") ||
window.location.pathname.startsWith("/captcha");
window.location.pathname.startsWith("/login") ||
window.location.pathname.startsWith("/forget") ||
window.location.pathname.startsWith("/prompt") ||
window.location.pathname.startsWith("/result") ||
window.location.pathname.startsWith("/cas") ||
window.location.pathname.startsWith("/select-plan") ||
window.location.pathname.startsWith("/buy-plan") ||
window.location.pathname.startsWith("/qrcode") ||
window.location.pathname.startsWith("/consent") ||
window.location.pathname.startsWith("/captcha");
}
onClick = ({key}) => {
@@ -656,7 +657,7 @@ class App extends Component {
menuVisible={this.state.menuVisible}
logo={this.state.logo}
onChangeTheme={this.setTheme}
onClick = {this.onClick}
onClick={this.onClick}
onfinish={() => {
this.setState({requiredEnableMfa: false});
}}

View File

@@ -175,10 +175,6 @@ class ApplicationEditPage extends React.Component {
application.tags = [];
}
if (application.otherDomains === null || application.otherDomains === undefined) {
application.otherDomains = [];
}
this.setState({
application: application,
});
@@ -252,6 +248,33 @@ class ApplicationEditPage extends React.Component {
return value;
}
trimCustomScopes(customScopes) {
if (!Array.isArray(customScopes)) {
return [];
}
return customScopes.map((item) => {
const scope = (item?.scope || "").trim();
const displayName = (item?.displayName || "").trim();
const description = (item?.description || "").trim();
return {
...item,
scope: scope,
displayName: displayName,
description: description,
};
});
}
validateCustomScopes(customScopes) {
const trimmed = this.trimCustomScopes(customScopes);
for (const item of trimmed) {
if (!item || !item.scope || item.scope === "") {
return {ok: false, scopes: trimmed};
}
}
return {ok: true, scopes: trimmed};
}
updateApplicationField(key, value) {
value = this.parseApplicationField(key, value);
const application = this.state.application;
@@ -510,157 +533,6 @@ class ApplicationEditPage extends React.Component {
)}
{this.state.activeMenuKey === "authentication" && (
<React.Fragment>
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"))} :
</Col>
<Col span={21} >
<Input value={this.state.application.clientId} onChange={e => {
this.updateApplicationField("clientId", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"))} :
</Col>
<Col span={21} >
<Input value={this.state.application.clientSecret} onChange={e => {
this.updateApplicationField("clientSecret", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Redirect URLs"), i18next.t("application:Redirect URLs - Tooltip"))} :
</Col>
<Col span={21} >
<UrlTable
title={i18next.t("application:Redirect URLs")}
table={this.state.application.redirectUris}
onUpdateTable={(value) => {this.updateApplicationField("redirectUris", value);}}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Forced redirect origin"), i18next.t("general:Forced redirect origin - Tooltip"))} :
</Col>
<Col span={21} >
<Input prefix={<LinkOutlined />} value={this.state.application.forcedRedirectOrigin} onChange={e => {
this.updateApplicationField("forcedRedirectOrigin", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Grant types"), i18next.t("application:Grant types - Tooltip"))} :
</Col>
<Col span={21} >
<Select virtual={false} mode="multiple" style={{width: "100%"}}
value={this.state.application.grantTypes}
onChange={(value => {
this.updateApplicationField("grantTypes", value);
})} >
{
[
{id: "authorization_code", name: "Authorization Code"},
{id: "password", name: "Password"},
{id: "client_credentials", name: "Client Credentials"},
{id: "token", name: "Token"},
{id: "id_token", name: "ID Token"},
{id: "refresh_token", name: "Refresh Token"},
{id: "urn:ietf:params:oauth:grant-type:device_code", name: "Device Code"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</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"))} :
</Col>
<Col span={21} >
<Select virtual={false} style={{width: "100%"}} value={this.state.application.tokenFormat} onChange={(value => {this.updateApplicationField("tokenFormat", value);})}
options={["JWT", "JWT-Empty", "JWT-Custom", "JWT-Standard"].map((item) => Setting.getOption(item, item))}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Token signing method"), i18next.t("application:Token signing method - Tooltip"))} :
</Col>
<Col span={21} >
<Select virtual={false} style={{width: "100%"}} value={this.state.application.tokenSigningMethod === "" ? "RS256" : this.state.application.tokenSigningMethod} onChange={(value => {this.updateApplicationField("tokenSigningMethod", value);})}
options={["RS256", "RS512", "ES256", "ES512", "ES384"].map((item) => Setting.getOption(item, item))}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Token fields"), i18next.t("application:Token fields - Tooltip"))} :
</Col>
<Col span={21} >
<Select virtual={false} disabled={this.state.application.tokenFormat !== "JWT-Custom"} mode="tags" showSearch style={{width: "100%"}} value={this.state.application.tokenFields} onChange={(value => {this.updateApplicationField("tokenFields", value);})}>
<Option key={"signinMethod"} value={"signinMethod"}>{"SigninMethod"}</Option>
<Option key={"provider"} value={"provider"}>{"Provider"}</Option>
{
[...Setting.getUserCommonFields(), "permissionNames"].map((item, index) => <Option key={index} value={item}>{item}</Option>)
}
</Select>
</Col>
</Row>
{
this.state.application.tokenFormat === "JWT-Custom" ? (<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Token attributes"), i18next.t("general:Token attributes - Tooltip"))} :
</Col>
<Col span={22} >
<TokenAttributeTable
title={i18next.t("general:Token attributes")}
table={this.state.application.tokenAttributes}
application={this.state.application}
onUpdateTable={(value) => {this.updateApplicationField("tokenAttributes", value);}}
/>
</Col>
</Row>) : null
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Token expire"), i18next.t("application:Token expire - Tooltip"))} :
</Col>
<Col span={21} >
<InputNumber style={{width: "150px"}} value={this.state.application.expireInHours} min={0.01} step={1} precision={2} addonAfter="Hours" onChange={value => {
this.updateApplicationField("expireInHours", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Refresh token expire"), i18next.t("application:Refresh token expire - Tooltip"))} :
</Col>
<Col span={21} >
<InputNumber style={{width: "150px"}} value={this.state.application.refreshExpireInHours} min={0.01} step={1} precision={2} addonAfter="Hours" onChange={value => {
this.updateApplicationField("refreshExpireInHours", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Cookie expire"), i18next.t("application:Cookie expire - Tooltip"))} :
@@ -811,7 +683,167 @@ class ApplicationEditPage extends React.Component {
}} />
</Col>
</Row>
</React.Fragment>
)}
{this.state.activeMenuKey === "oidc-oauth" && (
<React.Fragment>
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"))} :
</Col>
<Col span={21} >
<Input value={this.state.application.clientId} onChange={e => {
this.updateApplicationField("clientId", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"))} :
</Col>
<Col span={21} >
<Input value={this.state.application.clientSecret} onChange={e => {
this.updateApplicationField("clientSecret", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Redirect URLs"), i18next.t("application:Redirect URLs - Tooltip"))} :
</Col>
<Col span={21} >
<UrlTable
title={i18next.t("application:Redirect URLs")}
table={this.state.application.redirectUris}
onUpdateTable={(value) => {this.updateApplicationField("redirectUris", value);}}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Forced redirect origin"), i18next.t("general:Forced redirect origin - Tooltip"))} :
</Col>
<Col span={21} >
<Input prefix={<LinkOutlined />} value={this.state.application.forcedRedirectOrigin} onChange={e => {
this.updateApplicationField("forcedRedirectOrigin", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Grant types"), i18next.t("application:Grant types - Tooltip"))} :
</Col>
<Col span={21} >
<Select virtual={false} mode="multiple" style={{width: "100%"}}
value={this.state.application.grantTypes}
onChange={(value => {
this.updateApplicationField("grantTypes", value);
})} >
{
[
{id: "authorization_code", name: "Authorization Code"},
{id: "password", name: "Password"},
{id: "client_credentials", name: "Client Credentials"},
{id: "token", name: "Token"},
{id: "id_token", name: "ID Token"},
{id: "refresh_token", name: "Refresh Token"},
{id: "urn:ietf:params:oauth:grant-type:device_code", name: "Device Code"},
{id: "urn:ietf:params:oauth:grant-type:jwt-bearer", name: "JWT Bearer"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</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"))} :
</Col>
<Col span={21} >
<Select virtual={false} style={{width: "100%"}} value={this.state.application.tokenFormat} onChange={(value => {this.updateApplicationField("tokenFormat", value);})}
options={["JWT", "JWT-Empty", "JWT-Custom", "JWT-Standard"].map((item) => Setting.getOption(item, item))}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Token signing method"), i18next.t("application:Token signing method - Tooltip"))} :
</Col>
<Col span={21} >
<Select virtual={false} style={{width: "100%"}} value={this.state.application.tokenSigningMethod === "" ? "RS256" : this.state.application.tokenSigningMethod} onChange={(value => {this.updateApplicationField("tokenSigningMethod", value);})}
options={["RS256", "RS512", "ES256", "ES512", "ES384"].map((item) => Setting.getOption(item, item))}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Token fields"), i18next.t("application:Token fields - Tooltip"))} :
</Col>
<Col span={21} >
<Select virtual={false} disabled={this.state.application.tokenFormat !== "JWT-Custom"} mode="tags" showSearch style={{width: "100%"}} value={this.state.application.tokenFields} onChange={(value => {this.updateApplicationField("tokenFields", value);})}>
<Option key={"signinMethod"} value={"signinMethod"}>{"SigninMethod"}</Option>
<Option key={"provider"} value={"provider"}>{"Provider"}</Option>
{
[...Setting.getUserCommonFields(), "permissionNames"].map((item, index) => <Option key={index} value={item}>{item}</Option>)
}
</Select>
</Col>
</Row>
{
this.state.application.tokenFormat === "JWT-Custom" ? (<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Token attributes"), i18next.t("general:Token attributes - Tooltip"))} :
</Col>
<Col span={22} >
<TokenAttributeTable
title={i18next.t("general:Token attributes")}
table={this.state.application.tokenAttributes}
application={this.state.application}
onUpdateTable={(value) => {this.updateApplicationField("tokenAttributes", value);}}
/>
</Col>
</Row>) : null
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Token expire"), i18next.t("application:Token expire - Tooltip"))} :
</Col>
<Col span={21} >
<InputNumber style={{width: "150px"}} value={this.state.application.expireInHours} min={0.01} step={1} precision={2} addonAfter="Hours" onChange={value => {
this.updateApplicationField("expireInHours", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Refresh token expire"), i18next.t("application:Refresh token expire - Tooltip"))} :
</Col>
<Col span={21} >
<InputNumber style={{width: "150px"}} value={this.state.application.refreshExpireInHours} min={0.01} step={1} precision={2} addonAfter="Hours" onChange={value => {
this.updateApplicationField("refreshExpireInHours", value);
}} />
</Col>
</Row>
</React.Fragment>
)}
{this.state.activeMenuKey === "saml" && (
<React.Fragment>
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:SAML reply URL"), i18next.t("application:Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip"))} :
</Col>
@@ -1312,7 +1344,7 @@ class ApplicationEditPage extends React.Component {
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("general:Cert"), i18next.t("general:Cert - Tooltip"))} :
{Setting.getLabel(i18next.t("application:Token cert"), i18next.t("application:Token cert - Tooltip"))} :
</Col>
<Col span={21} >
<Select virtual={false} style={{width: "100%"}} value={this.state.application.cert} onChange={(value => {this.updateApplicationField("cert", value);})}>
@@ -1322,6 +1354,18 @@ class ApplicationEditPage extends React.Component {
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Client cert"), i18next.t("application:Client cert - Tooltip"))} :
</Col>
<Col span={21} >
<Select virtual={false} style={{width: "100%"}} value={this.state.application.clientCert} onChange={(value => {this.updateApplicationField("clientCert", value);})}>
{
this.state.certs.map((cert, index) => <Option key={index} value={cert.name}>{cert.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Failed signin limit"), i18next.t("application:Failed signin limit - Tooltip"))} :
@@ -1357,7 +1401,7 @@ class ApplicationEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:IP whitelist"), i18next.t("general:IP whitelist - Tooltip"))} :
</Col>
<Col span={21} >
<Input placeholder = {this.state.application.organizationObj?.ipWhitelist} value={this.state.application.ipWhitelist} onChange={e => {
<Input placeholder={this.state.application.organizationObj?.ipWhitelist} value={this.state.application.ipWhitelist} onChange={e => {
this.updateApplicationField("ipWhitelist", e.target.value);
}} />
</Col>
@@ -1382,47 +1426,44 @@ class ApplicationEditPage extends React.Component {
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel("Domain", "The public-facing domain for this application (e.g., blog.example.com)")} :
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
</Col>
<Col span={21} >
<Input value={this.state.application.domain} placeholder="blog.example.com" onChange={e => {
<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("Other domains", "Additional domains that should also route to this application")} :
{Setting.getLabel(i18next.t("application:Other domains"), i18next.t("application:Other domains - Tooltip"))} :
</Col>
<Col span={21} >
<Select
mode="tags"
style={{width: "100%"}}
value={this.state.application.otherDomains}
onChange={(value) => {
this.updateApplicationField("otherDomains", value);
}}
placeholder="Please input additional domains"
<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("Upstream host", "The upstream backend address to forward requests to (e.g., localhost:8080 or 192.168.1.100)")} :
{Setting.getLabel(i18next.t("application:Upstream host"), i18next.t("application:Upstream host - Tooltip"))} :
</Col>
<Col span={21} >
<Input value={this.state.application.upstreamHost} placeholder="localhost:8080" onChange={e => {
<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("SSL mode", "SSL/TLS mode for the reverse proxy")} :
{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>
@@ -1430,7 +1471,7 @@ class ApplicationEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel("SSL cert", "Certificate to use for TLS termination")} :
{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);})}>
@@ -1459,7 +1500,7 @@ class ApplicationEditPage extends React.Component {
<Layout style={{background: "inherit", height: "100%", overflow: "auto"}}>
{
this.state.menuMode === "horizontal" || !this.state.menuMode ? (
<Header style={{background: "inherit", padding: "0px", position: "sticky", top: 0}}>
<Header style={{background: "inherit", padding: "0px", position: "sticky", top: 0, height: 38, minHeight: 38}}>
<div className="demo-logo" />
<Tabs
onChange={(key) => {
@@ -1468,13 +1509,16 @@ class ApplicationEditPage extends React.Component {
}}
type="card"
activeKey={this.state.activeMenuKey}
tabBarStyle={{marginBottom: 0}}
items={[
{label: i18next.t("application:Basic"), key: "basic"},
{label: i18next.t("application:Authentication"), key: "authentication"},
{label: "OIDC/OAuth", key: "oidc-oauth"},
{label: "SAML", key: "saml"},
{label: i18next.t("application:Providers"), key: "providers"},
{label: i18next.t("application:UI Customization"), key: "ui-customization"},
{label: i18next.t("application:Security"), key: "security"},
{label: "Reverse Proxy", key: "reverse-proxy"},
{label: i18next.t("application:Reverse Proxy"), key: "reverse-proxy"},
]}
/>
</Header>
@@ -1495,10 +1539,12 @@ class ApplicationEditPage extends React.Component {
>
<Menu.Item key="basic">{i18next.t("application:Basic")}</Menu.Item>
<Menu.Item key="authentication">{i18next.t("application:Authentication")}</Menu.Item>
<Menu.Item key="oidc-oauth">OIDC/OAuth</Menu.Item>
<Menu.Item key="saml">SAML</Menu.Item>
<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">Reverse Proxy</Menu.Item>
<Menu.Item key="reverse-proxy">{i18next.t("application:Reverse Proxy")}</Menu.Item>
</Menu>
</Sider>) : null
}
@@ -1554,11 +1600,11 @@ class ApplicationEditPage extends React.Component {
{
Setting.isPasswordEnabled(this.state.application) ? (
<div className="loginBackground" style={{backgroundImage: `url(${this.state.application?.formBackgroundUrl})`, overflow: "auto"}}>
<SignupPage application={this.state.application} preview = "auto" />
<SignupPage application={this.state.application} preview="auto" />
</div>
) : (
<div className="loginBackground" style={{backgroundImage: `url(${this.state.application?.formBackgroundUrl})`, overflow: "auto"}}>
<LoginPage type={"login"} mode={"signup"} application={this.state.application} preview = "auto" />
<LoginPage type={"login"} mode={"signup"} application={this.state.application} preview="auto" />
</div>
)
}
@@ -1584,7 +1630,7 @@ class ApplicationEditPage extends React.Component {
}}>
<div style={{position: "relative", width: previewWidth, border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", overflow: "auto"}}>
<div className="loginBackground" style={{backgroundImage: `url(${this.state.application?.formBackgroundUrl})`, overflow: "auto"}}>
<LoginPage type={"login"} mode={"signin"} application={this.state.application} preview = "auto" />
<LoginPage type={"login"} mode={"signin"} application={this.state.application} preview="auto" />
</div>
<div style={{overflow: "auto", ...maskStyle}} />
</div>
@@ -1628,6 +1674,12 @@ class ApplicationEditPage extends React.Component {
const application = Setting.deepCopy(this.state.application);
application.providers = application.providers?.filter(provider => this.state.providers.map(provider => provider.name).includes(provider.name));
application.signinMethods = application.signinMethods?.filter(signinMethod => ["Password", "Verification code", "WebAuthn", "LDAP", "Face ID", "WeChat"].includes(signinMethod.name));
const customScopeValidation = this.validateCustomScopes(application.customScopes);
application.customScopes = customScopeValidation.scopes;
if (!customScopeValidation.ok) {
Setting.showMessage("error", `${i18next.t("general:Name")}: ${i18next.t("provider:This field is required")}`);
return;
}
ApplicationBackend.updateApplication("admin", this.state.applicationName, application)
.then((res) => {

View File

@@ -190,19 +190,15 @@ class ApplicationListPage extends BaseListPage {
sorter: true,
...this.getColumnSearchProps("category"),
render: (text, record, index) => {
const category = text;
const tagColor = category === "Agent" ? "green" : "blue";
return (
<span style={{
padding: "4px 8px",
borderRadius: "4px",
backgroundColor: tagColor,
color: "white",
fontWeight: "500",
}}>
{category}
</span>
);
if (!text) {
text = "Default";
}
if (text === "Agent") {
return Setting.getTag("success", text);
} else {
return Setting.getTag("default", text);
}
},
},
{

View File

@@ -81,6 +81,24 @@ class CertEditPage extends React.Component {
value = this.parseCertField(key, value);
const cert = this.state.cert;
const previousType = cert.type;
if (key === "type") {
if (value === "SSL") {
cert.cryptoAlgorithm = "RSA";
cert.certificate = "";
cert.privateKey = "";
} else if (previousType === "SSL" && value !== "SSL") {
// Clear SSL-specific sensitive and derived fields when leaving SSL type
cert.provider = "";
cert.account = "";
cert.accessKey = "";
cert.accessSecret = "";
cert.certificate = "";
cert.privateKey = "";
cert.expireTime = "";
cert.domainExpireTime = "";
}
}
cert[key] = value;
this.setState({
cert: cert,
@@ -133,7 +151,7 @@ class CertEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("cert:Scope - Tooltip"))} :
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.cert.scope} onChange={(value => {
@@ -149,7 +167,7 @@ class CertEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.cert.type} onChange={(value => {
@@ -157,6 +175,7 @@ class CertEditPage extends React.Component {
})}>
{
[
{id: "SSL", name: "SSL"},
{id: "x509", name: "x509"},
{id: "Payment", name: "Payment"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
@@ -184,7 +203,10 @@ class CertEditPage extends React.Component {
this.updateCertField("privateKey", "");
})}>
{
[
(this.state.cert.type === "SSL" ? [
{id: "RSA", name: "RSA"},
{id: "ECC", name: "ECC"},
] : [
{id: "RS256", name: "RS256 (RSA + SHA256)"},
{id: "RS384", name: "RS384 (RSA + SHA384)"},
{id: "RS512", name: "RS512 (RSA + SHA512)"},
@@ -194,13 +216,13 @@ class CertEditPage extends React.Component {
{id: "PS256", name: "PS256 (RSASSA-PSS using SHA256 and MGF1 with SHA256)"},
{id: "PS384", name: "PS384 (RSASSA-PSS using SHA384 and MGF1 with SHA384)"},
{id: "PS512", name: "PS512 (RSASSA-PSS using SHA512 and MGF1 with SHA512)"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
]).map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
{
this.state.cert.cryptoAlgorithm.startsWith("ES") ? null : (
this.state.cert.cryptoAlgorithm.startsWith("ES") || this.state.cert.type === "SSL" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Bit size"), i18next.t("cert:Bit size - Tooltip"))} :
@@ -219,16 +241,91 @@ class CertEditPage extends React.Component {
</Row>
)
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Expire in years"), i18next.t("cert:Expire in years - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.cert.expireInYears} onChange={value => {
this.updateCertField("expireInYears", value);
}} />
</Col>
</Row>
{
this.state.cert.type === "SSL" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Expire in years"), i18next.t("cert:Expire in years - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.cert.expireInYears} onChange={value => {
this.updateCertField("expireInYears", value);
}} />
</Col>
</Row>
)
}
{
this.state.cert.type === "SSL" ? (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("cert:Expire time")}:
</Col>
<Col span={22} >
<Input disabled={true} value={Setting.getFormattedDate(this.state.cert.expireTime)} onChange={e => {
this.updateCertField("expireTime", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("cert:Domain expire")}:
</Col>
<Col span={22} >
<Input disabled={true} value={Setting.getFormattedDate(this.state.cert.domainExpireTime)} onChange={e => {
this.updateCertField("domainExpireTime", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("cert:Provider")}:
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.cert.provider} onChange={(value => {this.updateCertField("provider", value);})}>
{
[
{id: "GoDaddy", name: "GoDaddy"},
{id: "Aliyun", name: "Aliyun"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("cert:Account")}:
</Col>
<Col span={22} >
<Input value={this.state.cert.account} onChange={e => {
this.updateCertField("account", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("cert:Access key")}:
</Col>
<Col span={22} >
<Input value={this.state.cert.accessKey} onChange={e => {
this.updateCertField("accessKey", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{i18next.t("cert:Access secret")}:
</Col>
<Col span={22} >
<Input.Password value={this.state.cert.accessSecret} onChange={e => {
this.updateCertField("accessSecret", e.target.value);
}} />
</Col>
</Row>
</React.Fragment>
) : null
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Certificate"), i18next.t("cert:Certificate - Tooltip"))} :

View File

@@ -88,6 +88,28 @@ class CertListPage extends BaseListPage {
});
}
refreshCert(i) {
const cert = this.state.data[i];
CertBackend.refreshDomainExpire(cert.owner, cert.name)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", `Failed to refresh domain expire: ${res.msg}`);
} else {
Setting.showMessage("success", "Domain expire refreshed successfully");
this.fetch({
pagination: {
...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
});
}
}
)
.catch(error => {
Setting.showMessage("error", `Domain expire failed to refresh: ${error}`);
});
}
renderTable(certs) {
const columns = [
{
@@ -194,6 +216,12 @@ class CertListPage extends BaseListPage {
render: (text, record, index) => {
return (
<div>
{
record.type === "SSL" ? (
<Button disabled={!Setting.isAdminUser(this.props.account) && (record.owner !== this.props.account.owner)} style={{margin: "10px 10px 10px 0"}} type="default" onClick={() => this.refreshCert(index)}>{i18next.t("general:Refresh")}
</Button>
) : null
}
<Button disabled={!Setting.isAdminUser(this.props.account) && (record.owner !== this.props.account.owner)} style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/certs/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal
disabled={!Setting.isAdminUser(this.props.account) && (record.owner !== this.props.account.owner)}

View File

@@ -26,6 +26,7 @@ import LoginPage from "./auth/LoginPage";
import SelfForgetPage from "./auth/SelfForgetPage";
import ForgetPage from "./auth/ForgetPage";
import PromptPage from "./auth/PromptPage";
import ConsentPage from "./auth/ConsentPage";
import ResultPage from "./auth/ResultPage";
import CasLogout from "./auth/CasLogout";
import {authConfig} from "./auth/Auth";
@@ -125,6 +126,7 @@ class EntryPage extends React.Component {
<Route exact path="/forget/:applicationName" render={(props) => <ForgetPage {...this.props} account={this.props.account} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/prompt" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/prompt/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/consent/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<ConsentPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/result" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/result/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />

View File

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

View File

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

View File

@@ -193,12 +193,12 @@ function ManagementPage(props) {
{
renderAvatar()
}
&nbsp;
&nbsp;
&nbsp;
&nbsp;
{Setting.isMobile() ? null : Setting.getShortText(Setting.getNameAtLeast(props.account.displayName), 30)} &nbsp; <DownOutlined />
&nbsp;
&nbsp;
&nbsp;
&nbsp;
&nbsp;
&nbsp;
</div>
</Dropdown>
);
@@ -252,15 +252,15 @@ function ManagementPage(props) {
{renderRightDropdown()}
{renderWidgets()}
{Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) &&
<OrganizationSelect
initValue={Setting.getOrganization()}
withAll={true}
className="org-select"
style={{display: Setting.isMobile() ? "none" : "flex"}}
onChange={(value) => {
Setting.setOrganization(value);
}}
/>
<OrganizationSelect
initValue={Setting.getOrganization()}
withAll={true}
className="org-select"
style={{display: Setting.isMobile() ? "none" : "flex"}}
onChange={(value) => {
Setting.setOrganization(value);
}}
/>
}
</React.Fragment>
);
@@ -287,9 +287,9 @@ function ManagementPage(props) {
!Setting.isMobile() ? res.push({
label:
<Link to="/">
<img className="logo" src={logo ?? props.logo} alt="logo" />
</Link>,
<Link to="/">
<img className="logo" src={logo ?? props.logo} alt="logo" />
</Link>,
disabled: true, key: "logo",
style: {
padding: 0,

View File

@@ -57,6 +57,8 @@ class OrganizationListPage extends BaseListPage {
{name: "ID", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "Name", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Display name", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "First name", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Last name", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Avatar", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "User type", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Password", visible: true, viewRule: "Self", modifyRule: "Self"},
@@ -66,6 +68,7 @@ class OrganizationListPage extends BaseListPage {
{name: "Country/Region", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Location", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Address", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Addresses", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Affiliation", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Title", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "ID card type", visible: true, viewRule: "Public", modifyRule: "Self"},
@@ -86,6 +89,8 @@ class OrganizationListPage extends BaseListPage {
{name: "Balance", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Balance credit", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Balance currency", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Cart", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "Transactions", visible: true, viewRule: "Self", 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"},
@@ -93,16 +98,22 @@ class OrganizationListPage extends BaseListPage {
{name: "Groups", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Roles", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "Permissions", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "Consents", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "3rd-party logins", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "Properties", visible: false, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is online", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is admin", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is forbidden", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is deleted", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "MFA accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{name: "Multi-factor authentication", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "MFA items", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "WebAuthn credentials", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "Last change password time", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Managed accounts", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "Face ID", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "MFA accounts", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "Need update password", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "IP whitelist", visible: true, viewRule: "Admin", modifyRule: "Admin"},
],
};
}

View File

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

View File

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

View File

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

View File

@@ -307,16 +307,6 @@ class ProductEditPage extends React.Component {
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Return URL"), i18next.t("product:Return URL - Tooltip"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.product.returnUrl} disabled={isViewMode} onChange={e => {
this.updateProductField("returnUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Success URL"), i18next.t("product:Success URL - Tooltip"))} :

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -137,7 +137,7 @@ class RecordListPage extends BaseListPage {
title: i18next.t("record:Status code"),
dataIndex: "statusCode",
key: "statusCode",
width: "120px",
width: "140px",
sorter: true,
...this.getColumnSearchProps("statusCode"),
},

View File

@@ -182,6 +182,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_default.png`,
url: "https://casdoor.org/docs/provider/email/overview",
},
"Resend": {
logo: `${StaticBaseUrl}/img/email_resend.png`,
url: "https://resend.com/",
},
},
Storage: {
"Local File System": {
@@ -457,8 +461,8 @@ export const UserFields = ["owner", "name", "password", "display_name", "id", "t
"is_admin", "homepage", "birthday", "gender", "password_type", "password_salt", "external_id", "avatar", "first_name", "last_name",
"avatar_type", "permanent_avatar", "email_verified", "region", "location", "address",
"affiliation", "title", "id_card_type", "id_card", "real_name", "is_verified", "bio", "tag", "language",
"education", "score", "karma", "ranking", "balance", "currency", "is_default_avatar", "is_online",
"is_forbidden", "is_deleted", "signup_application", "hash", "pre_hash", "access_key", "access_secret", "access_token",
"education", "score", "karma", "ranking", "balance", "balance_credit", "balance_currency", "currency", "is_default_avatar", "is_online",
"is_forbidden", "is_deleted", "signup_application", "register_type", "register_source", "hash", "pre_hash", "access_key", "access_secret", "access_token",
"created_ip", "last_signin_time", "last_signin_ip", "github", "google", "qq", "wechat", "facebook", "dingtalk",
"weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs", "baidu", "alipay", "casdoor", "infoflow", "apple",
"azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "kwai", "line", "amazon", "auth0",
@@ -469,7 +473,7 @@ export const UserFields = ["owner", "name", "password", "display_name", "id", "t
"wepay", "xero", "yahoo", "yammer", "yandex", "zoom", "metamask", "web3onboard", "custom", "webauthnCredentials",
"preferred_mfa_type", "recovery_codes", "totp_secret", "mfa_phone_enabled", "mfa_email_enabled", "invitation",
"invitation_code", "face_ids", "ldap", "properties", "roles", "permissions", "groups", "last_change_password_time",
"last_signin_wrong_time", "signin_wrong_times", "managedAccounts", "mfaAccounts", "need_update_password",
"last_signin_wrong_time", "signin_wrong_times", "managedAccounts", "mfaAccounts", "mfaItems", "need_update_password",
"created_time", "updated_time", "deleted_time",
"ip_whitelist"];
@@ -500,6 +504,7 @@ export const GetTranslatedUserItems = () => {
{name: "Country/Region", label: i18next.t("user:Country/Region")},
{name: "Location", label: i18next.t("user:Location")},
{name: "Address", label: i18next.t("user:Address")},
{name: "Addresses", label: i18next.t("user:Addresses")},
{name: "Affiliation", label: i18next.t("user:Affiliation")},
{name: "Title", label: i18next.t("general:Title")},
{name: "ID card type", label: i18next.t("user:ID card type")},
@@ -523,6 +528,8 @@ export const GetTranslatedUserItems = () => {
{name: "Karma", label: i18next.t("user:Karma")},
{name: "Ranking", label: i18next.t("user:Ranking")},
{name: "Signup application", label: i18next.t("general:Signup application")},
{name: "Register type", label: i18next.t("user:Register type")},
{name: "Register source", label: i18next.t("user:Register source")},
{name: "API key", label: i18next.t("general:API key")},
{name: "Groups", label: i18next.t("general:Groups")},
{name: "Roles", label: i18next.t("general:Roles")},
@@ -537,6 +544,7 @@ export const GetTranslatedUserItems = () => {
{name: "IP whitelist", label: i18next.t("general:IP whitelist")},
{name: "Multi-factor authentication", label: i18next.t("mfa:Multi-factor authentication")},
{name: "WebAuthn credentials", label: i18next.t("user:WebAuthn credentials")},
{name: "Last change password time", label: i18next.t("user:Last change password time")},
{name: "Managed accounts", label: i18next.t("user:Managed accounts")},
{name: "Face ID", label: i18next.t("login:Face ID")},
{name: "MFA accounts", label: i18next.t("user:MFA accounts")},
@@ -554,6 +562,8 @@ export function getUserColumns() {
transField = "Country/Region";
} else if (field === "mfaAccounts") {
transField = "MFA accounts";
} else if (field === "mfaItems") {
transField = "MFA items";
} else if (field === "face_ids") {
transField = "Face ID";
} else if (field === "managedAccounts") {
@@ -1291,6 +1301,7 @@ export function getProviderTypeOptions(category) {
{id: "Azure ACS", name: "Azure ACS"},
{id: "SendGrid", name: "SendGrid"},
{id: "Custom HTTP Email", name: "Custom HTTP Email"},
{id: "Resend", name: "Resend"},
]
);
} else if (category === "SMS") {
@@ -2267,7 +2278,7 @@ export function getFormTypeItems(formType) {
{name: "owner", label: "general:Organization", visible: true, width: "150"},
{name: "createdTime", label: "general:Created time", visible: true, width: "180"},
{name: "displayName", label: "general:Display name", visible: true, width: "150"},
{name: "category", label: "provider:Category", visible: true, width: "110"},
{name: "category", label: "general:Category", visible: true, width: "110"},
{name: "type", label: "general:Type", visible: true, width: "110"},
{name: "clientId", label: "provider:Client ID", visible: true, width: "100"},
{name: "providerUrl", label: "provider:Provider URL", visible: true, width: "150"},

View File

@@ -389,6 +389,13 @@ class SyncerEditPage extends React.Component {
"isHashed": true,
"values": [],
},
{
"name": "unionid",
"type": "string",
"casdoorName": "Name",
"isHashed": true,
"values": [],
},
{
"name": "name",
"type": "string",
@@ -424,13 +431,6 @@ class SyncerEditPage extends React.Component {
"isHashed": true,
"values": [],
},
{
"name": "job_number",
"type": "string",
"casdoorName": "Name",
"isHashed": true,
"values": [],
},
{
"name": "active",
"type": "boolean",
@@ -826,7 +826,7 @@ class SyncerEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.syncer.type} onChange={(value => {
@@ -878,7 +878,7 @@ class SyncerEditPage extends React.Component {
this.state.syncer.databaseType !== "postgres" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:SSL mode"), i18next.t("syncer:SSL mode - Tooltip"))} :
{Setting.getLabel(i18next.t("provider:SSL mode"), i18next.t("provider:SSL mode - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.syncer.sslMode} onChange={(value => {this.updateSyncerField("sslMode", value);})}>

View File

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

View File

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

View File

@@ -50,6 +50,7 @@ import MfaTable from "./table/MfaTable";
import TransactionTable from "./table/TransactionTable";
import CartTable from "./table/CartTable";
import * as TransactionBackend from "./backend/TransactionBackend";
import ConsentTable from "./table/ConsentTable";
import {Content, Header} from "antd/es/layout/layout";
import Sider from "antd/es/layout/Sider";
@@ -73,6 +74,7 @@ class UserEditPage extends React.Component {
idCardInfo: ["ID card front", "ID card back", "ID card with person"],
openFaceRecognitionModal: false,
transactions: [],
consents: [],
activeMenuKey: window.location.hash?.slice(1) || "",
menuMode: "Horizontal",
};
@@ -110,6 +112,7 @@ class UserEditPage extends React.Component {
this.setState({
user: res.data,
multiFactorAuths: res.data?.multiFactorAuths ?? [],
consents: res.data?.applicationScopes ?? [],
loading: false,
});
@@ -274,7 +277,7 @@ class UserEditPage extends React.Component {
// Fallback to comparing by owner and name
return (this.state.user.owner === this.props.account.owner &&
this.state.user.name === this.props.account.name);
this.state.user.name === this.props.account.name);
}
isSelfOrAdmin() {
@@ -609,13 +612,20 @@ class UserEditPage extends React.Component {
);
} else if (accountItem.name === "Addresses") {
return (
<AddressTable
title={i18next.t("user:Addresses")}
table={this.state.user.addresses}
onUpdateTable={(value) => {
this.updateUserField("addresses", value);
}}
/>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Addresses"), i18next.t("user:Addresses"))} :
</Col>
<Col span={22} >
<AddressTable
title={i18next.t("user:Addresses")}
table={this.state.user.addresses}
onUpdateTable={(value) => {
this.updateUserField("addresses", value);
}}
/>
</Col>
</Row>
);
} else if (accountItem.name === "Affiliation") {
return (
@@ -880,7 +890,7 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Transactions"), i18next.t("general:Transactions"))} :
</Col>
<Col span={22}>
<TransactionTable transactions={this.state.transactions} hideTag={true} />
<TransactionTable title={i18next.t("general:Transactions")} transactions={this.state.transactions} hideTag={true} />
</Col>
</Row>
);
@@ -1122,6 +1132,21 @@ class UserEditPage extends React.Component {
/>
</Col>
</Row>);
} else if (accountItem.name === "Consents") {
return (
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={Setting.isMobile() ? 22 : 2}>
{Setting.getLabel(i18next.t("consent:Consents"), i18next.t("consent:Consents - Tooltip"))} :
</Col>
<Col span={22}>
<ConsentTable
title={i18next.t("consent:Consents")}
table={this.state.consents}
onUpdateTable={() => this.getUser()}
/>
</Col>
</Row>
);
} else if (accountItem.name === "Multi-factor authentication") {
return (
!this.isSelfOrAdmin() ? null : (
@@ -1130,15 +1155,21 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("mfa:Multi-factor authentication"), i18next.t("mfa:Multi-factor authentication - Tooltip "))} :
</Col>
<Col span={22} >
<Card size="small" title={i18next.t("mfa:Multi-factor methods")}
extra={this.state.multiFactorAuths?.some(mfaProps => mfaProps.enabled) ?
<PopconfirmModal
text={i18next.t("general:Disable")}
title={i18next.t("general:Sure to disable") + "?"}
onConfirm={() => this.deleteMfa()}
/> : null
}>
<Card size="small" title={
<div>
{i18next.t("mfa:Multi-factor methods")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.multiFactorAuths?.some(mfaProps => mfaProps.enabled) ?
<PopconfirmModal
text={i18next.t("general:Disable")}
title={i18next.t("general:Sure to disable") + "?"}
onConfirm={() => this.deleteMfa()}
size="small"
/> : null
}
</div>
}>
<List
size="small"
rowKey="mfaType"
itemLayout="horizontal"
dataSource={this.state.multiFactorAuths}

260
web/src/auth/ConsentPage.js Normal file
View File

@@ -0,0 +1,260 @@
// 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 {Button, Card, List, Result, Space} from "antd";
import {CheckOutlined, LockOutlined} from "@ant-design/icons";
import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as ConsentBackend from "../backend/ConsentBackend";
import * as Setting from "../Setting";
import i18next from "i18next";
import {withRouter} from "react-router-dom";
import * as Util from "./Util";
class ConsentPage extends React.Component {
constructor(props) {
super(props);
const params = new URLSearchParams(window.location.search);
this.state = {
applicationName: props.match?.params?.applicationName || params.get("application"),
scopeDescriptions: [],
granting: false,
oAuthParams: Util.getOAuthGetParameters(),
};
}
getApplicationObj() {
return this.props.application;
}
componentDidMount() {
this.getApplication();
this.loadScopeDescriptions();
}
componentDidUpdate(prevProps) {
if (this.props.application !== prevProps.application) {
this.loadScopeDescriptions();
}
}
getApplication() {
if (!this.state.applicationName) {
return;
}
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.props.onUpdateApplication(res.data);
});
}
loadScopeDescriptions() {
const {oAuthParams} = this.state;
const application = this.getApplicationObj();
if (!oAuthParams?.scope || !application) {
return;
}
// Check if urlPar scope is within application scopes
const scopes = oAuthParams.scope.split(" ").map(s => s.trim()).filter(Boolean);
const customScopes = application.customScopes || [];
const customScopesMap = {};
customScopes.forEach(s => {
if (s?.scope) {
customScopesMap[s.scope] = s;
}
});
const scopeDescriptions = scopes
.map(scope => {
const item = customScopesMap[scope];
if (item) {
return {
...item,
displayName: item.displayName || item.scope,
};
}
return {
scope: scope,
displayName: scope,
description: i18next.t("consent:This scope is not defined in the application"),
};
})
.filter(Boolean);
this.setState({
scopeDescriptions: scopeDescriptions,
});
}
handleGrant() {
const {oAuthParams, scopeDescriptions} = this.state;
const application = this.getApplicationObj();
this.setState({granting: true});
const consent = {
owner: application.owner,
application: application.owner + "/" + application.name,
grantedScopes: scopeDescriptions.map(s => s.scope),
};
ConsentBackend.grantConsent(consent, oAuthParams)
.then((res) => {
if (res.status === "ok") {
// res.data contains the authorization code
const code = res.data;
const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?";
const redirectUrl = `${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`;
Setting.goToLink(redirectUrl);
} else {
Setting.showMessage("error", res.msg);
this.setState({granting: false});
}
});
}
handleDeny() {
const {oAuthParams} = this.state;
const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?";
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}error=access_denied&error_description=User denied consent&state=${oAuthParams.state}`);
}
render() {
const application = this.getApplicationObj();
if (application === undefined) {
return null;
}
if (!application) {
return (
<Result
status="error"
title={i18next.t("general:Invalid application")}
/>
);
}
const {scopeDescriptions, granting} = this.state;
const isScopeEmpty = scopeDescriptions.length === 0;
return (
<div className="login-content">
<div className={Setting.isDarkTheme(this.props.themeAlgorithm) ? "login-panel-dark" : "login-panel"}>
<div className="login-form">
<Card
style={{
padding: "32px",
width: 450,
borderRadius: "12px",
boxShadow: "0 10px 25px rgba(0, 0, 0, 0.05)",
border: "1px solid #f0f0f0",
}}
>
<div style={{textAlign: "center", marginBottom: 24}}>
{application.logo && (
<div style={{marginBottom: 16}}>
<img
src={application.logo}
alt={application.displayName || application.name}
style={{height: 56, objectFit: "contain"}}
/>
</div>
)}
<h2 style={{margin: 0, fontWeight: 600, fontSize: "24px"}}>
{i18next.t("consent:Authorization Request")}
</h2>
</div>
<div style={{marginBottom: 32}}>
<p style={{fontSize: 15, color: "#666", textAlign: "center", lineHeight: "1.6"}}>
<span style={{fontWeight: 600, color: "#000"}}>{application.displayName || application.name}</span>
{" "}{i18next.t("consent:wants to access your account")}
</p>
{application.homepageUrl && (
<div style={{textAlign: "center", marginTop: 4}}>
<a href={application.homepageUrl} target="_blank" rel="noopener noreferrer" style={{fontSize: 13, color: "#1890ff"}}>
{application.homepageUrl}
</a>
</div>
)}
</div>
<div style={{marginBottom: 32}}>
<div style={{fontSize: 14, color: "#8c8c8c", marginBottom: 16}}>
<LockOutlined style={{marginRight: 8}} /> {i18next.t("consent:This application is requesting")}
</div>
<div style={{display: "flex", justifyContent: "center"}}>
<List
size="small"
dataSource={scopeDescriptions}
style={{width: "100%"}}
renderItem={item => (
<List.Item style={{borderBottom: "none", width: "100%"}}>
<div style={{display: "inline-grid", gridTemplateColumns: "16px auto", columnGap: 8, alignItems: "start"}}>
<CheckOutlined style={{color: "#52c41a", fontSize: "14px", marginTop: "4px", justifySelf: "center"}} />
<div style={{fontWeight: 500, fontSize: "14px", lineHeight: "22px"}}>{item.displayName || item.scope}</div>
</div>
<div style={{fontSize: "12px", color: "#8c8c8c", marginTop: 2}}>{item.description}</div>
</List.Item>
)}
/>
</div>
</div>
<div style={{textAlign: "center", marginBottom: 24}}>
<Space size={16}>
<Button
type="primary"
size="large"
shape="round"
onClick={() => this.handleGrant()}
loading={granting}
disabled={granting || isScopeEmpty}
style={{minWidth: 120, height: 44, fontWeight: 500}}
>
{i18next.t("consent:Allow")}
</Button>
<Button
size="large"
shape="round"
onClick={() => this.handleDeny()}
disabled={granting || isScopeEmpty}
style={{minWidth: 120, height: 44, fontWeight: 500}}
>
{i18next.t("consent:Deny")}
</Button>
</Space>
</div>
<div style={{padding: "16px", backgroundColor: "#fafafa", borderRadius: "8px", border: "1px solid #f0f0f0"}}>
<p style={{margin: 0, fontSize: 12, color: "#8c8c8c", textAlign: "center", lineHeight: "1.5"}}>
{i18next.t("consent:By clicking Allow, you allow this app to use your information")}
</p>
</div>
</Card>
</div>
</div>
</div>
);
}
}
export default withRouter(ConsentPage);

View File

@@ -216,7 +216,7 @@ class LoginPage extends React.Component {
this.setState({
msg: res.msg,
});
return ;
return;
}
this.onUpdateApplication(res.data);
});
@@ -369,6 +369,13 @@ class LoginPage extends React.Component {
return;
}
// Check if consent is required
if (resp.data?.required === true) {
// Consent required, redirect to consent page
Setting.goToLinkSoft(ths, `/consent/${application.name}?${window.location.search.substring(1)}`);
return;
}
if (Setting.hasPromptPage(application)) {
AuthBackend.getAccount()
.then((res) => {
@@ -1141,9 +1148,11 @@ class LoginPage extends React.Component {
visible={this.state.openCaptchaModal}
noModal={noModal}
onUpdateToken={(captchaType, captchaToken, clientSecret) => {
this.setState({captchaValues: {
captchaType, captchaToken, clientSecret,
}});
this.setState({
captchaValues: {
captchaType, captchaToken, clientSecret,
},
});
}}
onOk={(captchaType, captchaToken, clientSecret) => {
const values = this.state.values;

View File

@@ -44,20 +44,15 @@ function generateCodeChallenge(verifier) {
}
function storeCodeVerifier(state, verifier) {
localStorage.setItem("pkce_verifier", `${state}#${verifier}`);
localStorage.setItem(`pkce_verifier_${state}`, verifier);
}
export function getCodeVerifier(state) {
const verifierStore = localStorage.getItem("pkce_verifier");
const [storedState, verifier] = verifierStore ? verifierStore.split("#") : [null, null];
if (storedState !== state) {
return null;
}
return verifier;
return localStorage.getItem(`pkce_verifier_${state}`);
}
export function clearCodeVerifier(state) {
localStorage.removeItem("pkce_verifier");
localStorage.removeItem(`pkce_verifier_${state}`);
}
const authInfo = {
@@ -407,24 +402,27 @@ export function getProviderUrl(provider) {
}
}
export function getProviderLogoWidget(provider) {
export function getProviderLogoWidget(provider, options = {}) {
if (provider === undefined) {
return null;
}
const url = getProviderUrl(provider);
if (url !== "") {
const disableLink = options.disableLink === true;
const imgEl = <img width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />;
if (url !== "" && !disableLink) {
return (
<Tooltip title={provider.type}>
<a target="_blank" rel="noreferrer" href={getProviderUrl(provider)}>
<img width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
{imgEl}
</a>
</Tooltip>
);
} else {
return (
<Tooltip title={provider.type}>
<img width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
{imgEl}
</Tooltip>
);
}

View File

@@ -293,8 +293,17 @@ class SignupPage extends React.Component {
return;
}
// Check if consent is required
if (oAuthParams && res.data && typeof res.data === "object" && res.data.required === true) {
// Consent required, redirect to consent page
Setting.goToLink(`/consent/${application.name}?${window.location.search.substring(1)}`);
return;
}
// the user's id will be returned by `signup()`, if user signup by phone, the `username` in `values` is undefined.
values.username = res.data.split("/")[1];
if (typeof res.data === "string") {
values.username = res.data.split("/")[1];
}
if (Setting.hasPromptPage(application) && (!values.plan || !values.pricing)) {
AuthBackend.getAccount("")
.then((res) => {

View File

@@ -213,17 +213,19 @@ export async function WechatOfficialAccountModal(application, provider, method)
}
const t1 = setInterval(await getEvent, 1000, application, provider, res.data2, method);
{Modal.info({
title: i18next.t("provider:Please use WeChat to scan the QR code and follow the official account for sign in"),
content: (
<div style={{marginRight: "34px"}}>
<QRCode style={{padding: "20px", margin: "auto"}} bordered={false} value={res.data} size={230} />
</div>
),
onOk() {
window.clearInterval(t1);
},
});}
{
Modal.info({
title: i18next.t("provider:Please use WeChat to scan the QR code and follow the official account for sign in"),
content: (
<div style={{marginRight: "34px"}}>
<QRCode style={{padding: "20px", margin: "auto"}} bordered={false} value={res.data} size={230} />
</div>
),
onOk() {
window.clearInterval(t1);
},
});
}
}
);
}

View File

@@ -79,3 +79,10 @@ export function deleteCert(cert) {
},
}).then(res => res.json());
}
export function refreshDomainExpire(owner, name) {
return fetch(`${Setting.ServerUrl}/api/update-cert-domain-expire?id=${owner}/${encodeURIComponent(name)}`, {
method: "POST",
credentials: "include",
}).then(res => res.json());
}

View File

@@ -0,0 +1,50 @@
// 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 * as Setting from "../Setting";
export function grantConsent(consent, oAuthParams) {
const request = {
...consent,
clientId: oAuthParams.clientId,
provider: "",
signinMethod: "",
responseType: oAuthParams.responseType || "code",
redirectUri: oAuthParams.redirectUri,
scope: oAuthParams.scope,
state: oAuthParams.state,
nonce: oAuthParams.nonce || "",
challenge: oAuthParams.codeChallenge || "",
resource: "",
};
return fetch(`${Setting.ServerUrl}/api/grant-consent`, {
method: "POST",
credentials: "include",
body: JSON.stringify(request),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function revokeConsent(consent) {
return fetch(`${Setting.ServerUrl}/api/revoke-consent`, {
method: "POST",
credentials: "include",
body: JSON.stringify(consent),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}

View File

@@ -158,7 +158,7 @@ const Dashboard = (props) => {
i18next.t("general:Adapters"),
i18next.t("general:Enforcers"),
], top: "10%"},
grid: {left: "3%", right: "4%", bottom: "0", top: "25%", containLabel: true},
grid: {left: "3%", right: "4%", bottom: "0", top: "30%", containLabel: true},
xAxis: {type: "category", boundaryGap: false, data: dateArray},
yAxis: {type: "value"},
series: [

View File

@@ -23,24 +23,24 @@ class RegionSelect extends React.Component {
super(props);
this.state = {
classes: props,
value: "",
};
}
onChange(e) {
this.props.onChange(e);
this.setState({value: e});
}
render() {
const value = this.props.value !== undefined && this.props.value !== "" ? this.props.value : (this.props.defaultValue !== undefined && this.props.defaultValue !== "" ? this.props.defaultValue : undefined);
return (
<Select virtual={false}
size={this.props.size}
showSearch
optionFilterProp="label"
style={{width: "100%"}}
defaultValue={this.props.defaultValue || undefined}
value={value}
placeholder="Please select country/region"
onChange={(value => {this.onChange(value);})}
onChange={(val) => {this.onChange(val);}}
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
filterSort={(optionA, optionB) =>
(optionA?.label ?? "").toLowerCase().localeCompare((optionB?.label ?? "").toLowerCase())

View File

@@ -113,19 +113,12 @@
"Order - Tooltip": "Je kleiner der Wert, desto höher rangiert er auf der Apps-Seite",
"Org choice mode": "Organisationsauswahlmodus",
"Org choice mode - Tooltip": "Organisationsauswahlmodus - Hinweis",
"Other domains": "Other domains",
"Other domains - Tooltip": "Other domains - Tooltip",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Bitte aktivieren Sie zuerst \"Anmeldesitzung\", bevor Sie \"Automatische Anmeldung\" aktivieren.",
"Please input additional domains": "Please input additional domains",
"Please input your application!": "Bitte geben Sie Ihre Anwendung ein!",
"Please input your organization!": "Bitte geben Sie Ihre Organisation ein!",
"Please select a HTML file": "Bitte wählen Sie eine HTML-Datei aus",
"Pop up": "Pop-up",
"Providers": "Anbieter",
"Proxy SSL mode": "Proxy SSL mode",
"Proxy SSL mode - Tooltip": "Proxy SSL mode - Tooltip",
"Proxy domain": "Proxy domain",
"Proxy domain - Tooltip": "Proxy domain - Tooltip",
"Random": "Zufällig",
"Real name": "Echter Name",
"Redirect URL": "Weiterleitungs-URL",
@@ -135,7 +128,6 @@
"Refresh token expire": "Gültigkeitsdauer des Refresh-Tokens",
"Refresh token expire - Tooltip": "Angabe der Gültigkeitsdauer des Refresh Tokens",
"Reset to Empty": "Auf leer zurücksetzen",
"Reverse Proxy": "Reverse Proxy",
"Right": "Rechts",
"Rule": "Regel",
"SAML hash algorithm": "SAML-Hash-Algorithmus",
@@ -143,8 +135,6 @@
"SAML metadata": "SAML-Metadaten",
"SAML metadata - Tooltip": "Die Metadaten des SAML-Protokolls - Hinweis",
"SAML reply URL": "SAML Reply-URL",
"SSL cert": "SSL cert",
"SSL cert - Tooltip": "SSL cert - Tooltip",
"Security": "Sicherheit",
"Select": "Auswählen",
"Side panel HTML": "Sidepanel-HTML",
@@ -175,8 +165,6 @@
"Token signing method": "Token-Signaturmethode",
"Token signing method - Tooltip": "Signaturmethode des JWT-Tokens muss mit dem Zertifikat übereinstimmen",
"UI Customization": "UI-Anpassung",
"Upstream host": "Upstream host",
"Upstream host - Tooltip": "Upstream host - Tooltip",
"Use Email as NameID": "E-Mail als NameID verwenden",
"Use Email as NameID - Tooltip": "E-Mail als NameID verwenden",
"Vertical": "Vertikal",
@@ -198,9 +186,7 @@
"Expire in years - Tooltip": "Gültigkeitsdauer des Zertifikats in Jahren",
"New Cert": "Neues Zertifikat",
"Private key": "Private-Key",
"Private key - Tooltip": "Privater Schlüssel, der zum öffentlichen Schlüsselzertifikat gehört",
"Scope - Tooltip": "Nutzungsszenarien des Zertifikats",
"Type - Tooltip": "Art des Zertifikats"
"Private key - Tooltip": "Privater Schlüssel, der zum öffentlichen Schlüsselzertifikat gehört"
},
"code": {
"Code you received": "Der Code, den Sie erhalten haben",
@@ -289,6 +275,7 @@
"Applications that require authentication": "Anwendungen, die eine Authentifizierung erfordern",
"Apps": "Anwendungen",
"Authorization": "Autorisierung",
"Auto": "Auto",
"Avatar": "Profilbild",
"Avatar - Tooltip": "Öffentliches Avatarbild für den Benutzer",
"Back": "Zurück",
@@ -770,7 +757,6 @@
"Account menu - Tooltip": "Kontomenü - Tooltip",
"Admin navbar items": "Admin-Navigationsleisten-Elemente",
"Admin navbar items - Tooltip": "Admin-Navigationsleisten-Elemente - Tooltip",
"All": "All",
"Balance credit": "Guthaben (Credits)",
"Balance credit - Tooltip": "Guthaben (Credits) - Tooltip",
"Balance currency": "Guthabenwährung",
@@ -935,7 +921,6 @@
"Invalid product": "Invalid product",
"Is recharge": "Ist Aufladung",
"Is recharge - Tooltip": "Ob das Produkt zum Aufladen des Guthabens dient",
"Name": "Name",
"New Product": "Neues Produkt",
"No recharge options available": "No recharge options available",
"Order created successfully": "Bestellung erfolgreich erstellt",
@@ -948,7 +933,6 @@
"Please add at least one recharge option when custom amount is disabled": "Bitte fügen Sie mindestens eine Aufladeoption hinzu, wenn der benutzerdefinierte Betrag deaktiviert ist",
"Please select a currency": "Bitte wählen Sie eine Währung",
"Please select at least one payment provider": "Bitte wählen Sie mindestens einen Zahlungsanbieter aus",
"Price": "Price",
"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",
@@ -957,8 +941,6 @@
"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",
"Select amount": "Betrag auswählen",
"Sold": "Verkauft",
@@ -993,7 +975,6 @@
"Auth Key - Tooltip": "Authentifizierungsschlüssel für den Dienst",
"Auth URL": "Auth-URL",
"Auth URL - Tooltip": "URL für die Authentifizierung",
"Auto": "Auto",
"Base URL": "Basis-URL",
"Base URL - Tooltip": "Basis-URL des Dienstes",
"Bucket": "Eimer",
@@ -1002,8 +983,6 @@
"Can signin": "Kann sich einloggen",
"Can signup": "Kann sich registrieren",
"Can unlink": "Entlinken möglich",
"Category": "Kategorie",
"Category - Tooltip": "Kennung zur Kategorisierung und Gruppierung von Elementen oder Inhalten, erleichtert Filterung und Verwaltung",
"Channel No.": "Kanal Nr.",
"Channel No. - Tooltip": "Eindeutige Nummer zur Identifizierung eines Kommunikations- oder Datenübertragungskanals, verwendet zur Unterscheidung verschiedener Übertragungswege",
"Chat ID": "Chat-ID",
@@ -1020,7 +999,6 @@
"Content - Tooltip": "Spezifische Informationen oder Daten in Nachrichten, Benachrichtigungen oder Dokumenten",
"DB test": "DB-Test",
"DB test - Tooltip": "DB-Test - Tooltip",
"Disable": "Disable",
"Domain": "Domäne",
"Domain - Tooltip": "Benutzerdefinierte Domain für Objektspeicher",
"Edit Provider": "Provider bearbeiten",
@@ -1030,7 +1008,6 @@
"Email regex - Tooltip": "Nur E-Mails, die diesem regulären Ausdruck entsprechen, können sich registrieren oder anmelden",
"Email title": "Email-Titel",
"Email title - Tooltip": "Betreff der E-Mail",
"Enable": "Enable",
"Enable PKCE": "PKCE aktivieren",
"Enable PKCE - Tooltip": "Enable PKCE - Tooltip",
"Enable proxy": "Proxy aktivieren",
@@ -1305,8 +1282,6 @@
"SSH password": "SSH-Passwort",
"SSH port": "SSH-Port",
"SSH user": "SSH-Benutzer",
"SSL mode": "SSL-Modus",
"SSL mode - Tooltip": "SSL-Modus",
"Service account key": "Service-Account-Schlüssel",
"Sync interval": "Synchronisierungsintervall",
"Sync interval - Tooltip": "Einheit in Sekunden",

View File

@@ -113,19 +113,12 @@
"Order - Tooltip": "The smaller the value, the higher it ranks in the Apps page",
"Org choice mode": "Org choice mode",
"Org choice mode - Tooltip": "Method used to select the organization to log in",
"Other domains": "Other domains",
"Other domains - Tooltip": "Additional domains that should also route to this application",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Please enable \"Signin session\" first before enabling \"Auto signin\"",
"Please input additional domains": "Please input additional domains",
"Please input your application!": "Please input your application!",
"Please input your organization!": "Please input your organization!",
"Please select a HTML file": "Please select a HTML file",
"Pop up": "Pop up",
"Providers": "Providers",
"Proxy SSL mode": "Proxy SSL mode",
"Proxy SSL mode - Tooltip": "SSL/TLS mode for the reverse proxy",
"Proxy domain": "Proxy domain",
"Proxy domain - Tooltip": "The public-facing domain for this application (e.g., blog.example.com)",
"Random": "Random",
"Real name": "Real name",
"Redirect URL": "Redirect URL",
@@ -135,7 +128,6 @@
"Refresh token expire": "Refresh token expire",
"Refresh token expire - Tooltip": "Refresh token expiration time",
"Reset to Empty": "Reset to Empty",
"Reverse Proxy": "Reverse Proxy",
"Right": "Right",
"Rule": "Rule",
"SAML hash algorithm": "SAML hash algorithm",
@@ -143,8 +135,6 @@
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "The metadata of SAML protocol",
"SAML reply URL": "SAML reply URL",
"SSL cert": "SSL cert",
"SSL cert - Tooltip": "Certificate to use for TLS termination",
"Security": "Security",
"Select": "Select",
"Side panel HTML": "Side panel HTML",
@@ -175,8 +165,6 @@
"Token signing method": "Token signing method",
"Token signing method - Tooltip": "Signing method of JWT token, needs to be the same algorithm as the certificate",
"UI Customization": "UI Customization",
"Upstream host": "Upstream host",
"Upstream host - Tooltip": "The upstream backend address to forward requests to (e.g., localhost:8080 or 192.168.1.100)",
"Use Email as NameID": "Use Email as NameID",
"Use Email as NameID - Tooltip": "Use Email as NameID",
"Vertical": "Vertical",
@@ -198,9 +186,7 @@
"Expire in years - Tooltip": "Validity period of the certificate, in years",
"New Cert": "New Cert",
"Private key": "Private key",
"Private key - Tooltip": "Private key corresponding to the public key certificate",
"Scope - Tooltip": "Usage scenarios of the certificate",
"Type - Tooltip": "Type of certificate"
"Private key - Tooltip": "Private key corresponding to the public key certificate"
},
"code": {
"Code you received": "Code you received",
@@ -289,6 +275,7 @@
"Applications that require authentication": "Applications that require authentication",
"Apps": "Apps",
"Authorization": "Authorization",
"Auto": "Auto",
"Avatar": "Avatar",
"Avatar - Tooltip": "Public avatar image for the user",
"Back": "Back",
@@ -770,7 +757,6 @@
"Account menu - Tooltip": "Account menu - Tooltip",
"Admin navbar items": "Admin navbar items",
"Admin navbar items - Tooltip": "Admin navbar items - Tooltip",
"All": "All",
"Balance credit": "Balance credit",
"Balance credit - Tooltip": "Balance credit - Tooltip",
"Balance currency": "Balance currency",
@@ -935,7 +921,6 @@
"Invalid product": "Invalid product",
"Is recharge": "Is recharge",
"Is recharge - Tooltip": "Whether the current product is to recharge balance",
"Name": "Name",
"New Product": "New Product",
"No recharge options available": "No recharge options available",
"Order created successfully": "Order created successfully",
@@ -948,7 +933,6 @@
"Please add at least one recharge option when custom amount is disabled": "Please add at least one recharge option when custom amount is disabled",
"Please select a currency": "Please select a currency",
"Please select at least one payment provider": "Please select at least one payment provider",
"Price": "Price",
"Processing payment...": "Processing payment...",
"Product list cannot be empty": "Product list cannot be empty",
"Product not found or invalid": "Product not found or invalid",
@@ -957,8 +941,6 @@
"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",
"Select amount": "Select amount",
"Sold": "Sold",
@@ -993,7 +975,6 @@
"Auth Key - Tooltip": "Authentication key for the service",
"Auth URL": "Auth URL",
"Auth URL - Tooltip": "URL for authentication",
"Auto": "Auto",
"Base URL": "Base URL",
"Base URL - Tooltip": "Base URL of the service",
"Bucket": "Bucket",
@@ -1002,8 +983,6 @@
"Can signin": "Can signin",
"Can signup": "Can signup",
"Can unlink": "Can unlink",
"Category": "Category",
"Category - Tooltip": "Identifier for categorizing and grouping items or content, facilitating filtering and management",
"Channel No.": "Channel No.",
"Channel No. - Tooltip": "Unique number identifying a communication or data transmission channel, used to distinguish different transmission paths",
"Chat ID": "Chat ID",
@@ -1020,7 +999,6 @@
"Content - Tooltip": "Specific information or data contained in messages, notifications, or documents",
"DB test": "DB test",
"DB test - Tooltip": "DB test - Tooltip",
"Disable": "Disable",
"Domain": "Domain",
"Domain - Tooltip": "Custom domain for object storage",
"Edit Provider": "Edit Provider",
@@ -1030,7 +1008,6 @@
"Email regex - Tooltip": "Only emails matching this regular expression can register or sign in",
"Email title": "Email title",
"Email title - Tooltip": "Subject of the email",
"Enable": "Enable",
"Enable PKCE": "Enable PKCE",
"Enable PKCE - Tooltip": "Enable PKCE (Proof Key for Code Exchange) for enhanced OAuth 2.0 security",
"Enable proxy": "Enable proxy",
@@ -1305,8 +1282,6 @@
"SSH password": "SSH password",
"SSH port": "SSH port",
"SSH user": "SSH user",
"SSL mode": "SSL mode",
"SSL mode - Tooltip": "The SSL mode used when connecting to the database",
"Service account key": "Service account key",
"Sync interval": "Sync interval",
"Sync interval - Tooltip": "Unit in seconds",

View File

@@ -113,19 +113,12 @@
"Order - Tooltip": "Cuanto menor sea el valor, más alto se clasificará en la página de Aplicaciones",
"Org choice mode": "Modo de selección de organización",
"Org choice mode - Tooltip": "Método utilizado para seleccionar la organización para iniciar sesión - Sugerencia",
"Other domains": "Other domains",
"Other domains - Tooltip": "Other domains - Tooltip",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Por favor, habilita \"Sesión de inicio de sesión\" primero antes de habilitar \"Inicio de sesión automático\"",
"Please input additional domains": "Please input additional domains",
"Please input your application!": "¡Por favor, ingrese su solicitud!",
"Please input your organization!": "¡Por favor, ingrese su organización!",
"Please select a HTML file": "Por favor, seleccione un archivo HTML",
"Pop up": "Ventana emergente",
"Providers": "Proveedores",
"Proxy SSL mode": "Proxy SSL mode",
"Proxy SSL mode - Tooltip": "Proxy SSL mode - Tooltip",
"Proxy domain": "Proxy domain",
"Proxy domain - Tooltip": "Proxy domain - Tooltip",
"Random": "Aleatorio",
"Real name": "Nombre real",
"Redirect URL": "Redireccionar URL",
@@ -135,7 +128,6 @@
"Refresh token expire": "Token de actualización expirado",
"Refresh token expire - Tooltip": "Tiempo de caducidad del token de actualización",
"Reset to Empty": "Restablecer a vacío",
"Reverse Proxy": "Reverse Proxy",
"Right": "Correcto",
"Rule": "Regla",
"SAML hash algorithm": "Algoritmo hash SAML",
@@ -143,8 +135,6 @@
"SAML metadata": "Metadatos de SAML",
"SAML metadata - Tooltip": "Los metadatos del protocolo SAML - Sugerencia",
"SAML reply URL": "URL de respuesta SAML",
"SSL cert": "SSL cert",
"SSL cert - Tooltip": "SSL cert - Tooltip",
"Security": "Seguridad",
"Select": "Seleccionar",
"Side panel HTML": "Panel lateral HTML",
@@ -175,8 +165,6 @@
"Token signing method": "Método de firma del token",
"Token signing method - Tooltip": "Método de firma del token JWT, debe ser el mismo algoritmo que el certificado",
"UI Customization": "Personalización de la interfaz",
"Upstream host": "Upstream host",
"Upstream host - Tooltip": "Upstream host - Tooltip",
"Use Email as NameID": "Usar correo electrónico como NameID",
"Use Email as NameID - Tooltip": "Usar correo electrónico como NameID - Información adicional",
"Vertical": "Vertical",
@@ -198,9 +186,7 @@
"Expire in years - Tooltip": "Período de validez del certificado, en años",
"New Cert": "Nuevo certificado",
"Private key": "Clave privada",
"Private key - Tooltip": "Clave privada correspondiente al certificado de clave pública",
"Scope - Tooltip": "Escenarios de uso del certificado",
"Type - Tooltip": "Tipo de certificado"
"Private key - Tooltip": "Clave privada correspondiente al certificado de clave pública"
},
"code": {
"Code you received": "Código que recibió",
@@ -289,6 +275,7 @@
"Applications that require authentication": "Aplicaciones que requieren autenticación",
"Apps": "Aplicaciones",
"Authorization": "Autorización",
"Auto": "Auto",
"Avatar": "Avatar",
"Avatar - Tooltip": "Imagen de avatar pública para el usuario",
"Back": "Atrás",
@@ -770,7 +757,6 @@
"Account menu - Tooltip": "Menú de cuenta - Tooltip",
"Admin navbar items": "Elementos de la barra de navegación del administrador",
"Admin navbar items - Tooltip": "Elementos de la barra de navegación del administrador - Tooltip",
"All": "All",
"Balance credit": "Saldo (créditos)",
"Balance credit - Tooltip": "Saldo (créditos) - Tooltip",
"Balance currency": "Moneda del saldo",
@@ -935,7 +921,6 @@
"Invalid product": "Invalid product",
"Is recharge": "Es recarga",
"Is recharge - Tooltip": "Indica si el producto actual es para recargar saldo",
"Name": "Name",
"New Product": "Nuevo producto",
"No recharge options available": "No recharge options available",
"Order created successfully": "Pedido creado con éxito",
@@ -948,7 +933,6 @@
"Please add at least one recharge option when custom amount is disabled": "Por favor, añade al menos una opción de recarga cuando el importe personalizado esté desactivado",
"Please select a currency": "Por favor, selecciona una moneda",
"Please select at least one payment provider": "Por favor, selecciona al menos un proveedor de pago",
"Price": "Price",
"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",
@@ -957,8 +941,6 @@
"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",
"Select amount": "Seleccionar importe",
"Sold": "Vendido",
@@ -993,7 +975,6 @@
"Auth Key - Tooltip": "Clave de autenticación para el servicio",
"Auth URL": "URL de autenticación",
"Auth URL - Tooltip": "URL para autenticación",
"Auto": "Auto",
"Base URL": "URL base",
"Base URL - Tooltip": "URL base del servicio",
"Bucket": "Cubo",
@@ -1002,8 +983,6 @@
"Can signin": "¿Puedes iniciar sesión?",
"Can signup": "Puede registrarse",
"Can unlink": "Desvincular",
"Category": "Categoría",
"Category - Tooltip": "Identificador para categorizar y agrupar elementos o contenido, facilitando el filtrado y la gestión",
"Channel No.": "Canal No.",
"Channel No. - Tooltip": "Número único que identifica un canal de comunicación o transmisión de datos, utilizado para distinguir diferentes rutas de transmisión",
"Chat ID": "ID de chat",
@@ -1020,7 +999,6 @@
"Content - Tooltip": "Contenido - Información adicional",
"DB test": "Prueba de BD",
"DB test - Tooltip": "Prueba de BD - Tooltip",
"Disable": "Disable",
"Domain": "Dominio",
"Domain - Tooltip": "Dominio personalizado para almacenamiento de objetos",
"Edit Provider": "Editar proveedor",
@@ -1030,7 +1008,6 @@
"Email regex - Tooltip": "Expresión regular de correo electrónico - Información adicional",
"Email title": "Título del correo electrónico",
"Email title - Tooltip": "Título del correo electrónico",
"Enable": "Enable",
"Enable PKCE": "Habilitar PKCE",
"Enable PKCE - Tooltip": "Habilitar PKCE - Tooltip",
"Enable proxy": "Habilitar proxy",
@@ -1305,8 +1282,6 @@
"SSH password": "Contraseña SSH",
"SSH port": "Puerto SSH",
"SSH user": "Usuario SSH",
"SSL mode": "Modo SSL",
"SSL mode - Tooltip": "Modo SSL",
"Service account key": "Clave de la cuenta de servicio",
"Sync interval": "Intervalo de sincronización",
"Sync interval - Tooltip": "Unidad en segundos",

View File

@@ -113,19 +113,12 @@
"Order - Tooltip": "Plus la valeur est petite, plus elle est classée haut dans la page Applications",
"Org choice mode": "Mode de choix d'organisation",
"Org choice mode - Tooltip": "Méthode utilisée pour sélectionner l'organisation pour se connecter - Info-bulle",
"Other domains": "Other domains",
"Other domains - Tooltip": "Other domains - Tooltip",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Veuillez activer \"Session de connexion\" avant d'activer \"Connexion automatique\"",
"Please input additional domains": "Please input additional domains",
"Please input your application!": "Veuillez saisir votre application !",
"Please input your organization!": "Veuillez saisir votre organisation !",
"Please select a HTML file": "Veuillez sélectionner un fichier HTML",
"Pop up": "Fenêtre contextuelle",
"Providers": "Fournisseurs",
"Proxy SSL mode": "Proxy SSL mode",
"Proxy SSL mode - Tooltip": "Proxy SSL mode - Tooltip",
"Proxy domain": "Proxy domain",
"Proxy domain - Tooltip": "Proxy domain - Tooltip",
"Random": "Aléatoire",
"Real name": "Nom réel",
"Redirect URL": "URL de redirection",
@@ -135,7 +128,6 @@
"Refresh token expire": "Expiration du jeton de rafraîchissement",
"Refresh token expire - Tooltip": "Durée avant expiration du jeton de rafraîchissement",
"Reset to Empty": "Réinitialiser à vide",
"Reverse Proxy": "Reverse Proxy",
"Right": "Droit",
"Rule": "Règle",
"SAML hash algorithm": "Algorithme de hachage SAML",
@@ -143,8 +135,6 @@
"SAML metadata": "Métadonnées SAML",
"SAML metadata - Tooltip": "Métadonnées du protocole SAML - Info-bulle",
"SAML reply URL": "URL de réponse SAML",
"SSL cert": "SSL cert",
"SSL cert - Tooltip": "SSL cert - Tooltip",
"Security": "Sécurité",
"Select": "Sélectionner",
"Side panel HTML": "HTML du panneau latéral",
@@ -175,8 +165,6 @@
"Token signing method": "Méthode de signature du jeton",
"Token signing method - Tooltip": "Méthode de signature du jeton JWT, doit être le même algorithme que le certificat",
"UI Customization": "Personnalisation de l'interface",
"Upstream host": "Upstream host",
"Upstream host - Tooltip": "Upstream host - Tooltip",
"Use Email as NameID": "Utiliser l'e-mail comme NameID",
"Use Email as NameID - Tooltip": "Utiliser l'e-mail comme NameID - Infobulle",
"Vertical": "Vertical",
@@ -198,9 +186,7 @@
"Expire in years - Tooltip": "Période de validité du certificat, en années",
"New Cert": "Nouveau Certificat",
"Private key": "Clé privée",
"Private key - Tooltip": "Clé privée correspondant au certificat de la clé publique",
"Scope - Tooltip": "Scénarios d'utilisation du certificat",
"Type - Tooltip": "Type de certificat"
"Private key - Tooltip": "Clé privée correspondant au certificat de la clé publique"
},
"code": {
"Code you received": "Le code que vous avez reçu",
@@ -289,6 +275,7 @@
"Applications that require authentication": "Applications qui nécessitent une authentification",
"Apps": "Applications",
"Authorization": "Autorisation",
"Auto": "Auto",
"Avatar": "Avatar",
"Avatar - Tooltip": "Image d'avatar publique pour le compte",
"Back": "Retour",
@@ -770,7 +757,6 @@
"Account menu - Tooltip": "Menu du compte - Infobulle",
"Admin navbar items": "Éléments de la barre de navigation admin",
"Admin navbar items - Tooltip": "Éléments de la barre de navigation admin - Infobulle",
"All": "All",
"Balance credit": "Solde (crédits)",
"Balance credit - Tooltip": "Solde (crédits) - Infobulle",
"Balance currency": "Devise du solde",
@@ -935,7 +921,6 @@
"Invalid product": "Invalid product",
"Is recharge": "Est un rechargement",
"Is recharge - Tooltip": "Indique si le produit actuel permet de recharger le solde",
"Name": "Name",
"New Product": "Nouveau produit",
"No recharge options available": "No recharge options available",
"Order created successfully": "Commande créée avec succès",
@@ -948,7 +933,6 @@
"Please add at least one recharge option when custom amount is disabled": "Veuillez ajouter au moins une option de recharge lorsque le montant personnalisé est désactivé",
"Please select a currency": "Veuillez sélectionner une devise",
"Please select at least one payment provider": "Veuillez sélectionner au moins un fournisseur de paiement",
"Price": "Price",
"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",
@@ -957,8 +941,6 @@
"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",
"Select amount": "Sélectionner un montant",
"Sold": "Vendu",
@@ -993,7 +975,6 @@
"Auth Key - Tooltip": "Clé d'authentification - Infobulle",
"Auth URL": "URL d'authentification",
"Auth URL - Tooltip": "URL d'authentification",
"Auto": "Auto",
"Base URL": "URL de base",
"Base URL - Tooltip": "URL de base - Infobulle",
"Bucket": "seau",
@@ -1002,8 +983,6 @@
"Can signin": "Pouvez-vous vous connecter?",
"Can signup": "Peut s'inscrire",
"Can unlink": "Peut annuler le lien",
"Category": "Catégorie",
"Category - Tooltip": "Sélectionnez une catégorie",
"Channel No.": "chaîne n°",
"Channel No. - Tooltip": "Canal N°",
"Chat ID": "ID de chat",
@@ -1020,7 +999,6 @@
"Content - Tooltip": "Contenu - Infobulle",
"DB test": "Test BD",
"DB test - Tooltip": "Test BD - Infobulle",
"Disable": "Disable",
"Domain": "Domaine",
"Domain - Tooltip": "Domaine personnalisé pour le stockage d'objets",
"Edit Provider": "Modifier le fournisseur",
@@ -1030,7 +1008,6 @@
"Email regex - Tooltip": "Regex e-mail - Infobulle",
"Email title": "Titre de l'email",
"Email title - Tooltip": "Titre de l'email",
"Enable": "Enable",
"Enable PKCE": "Activer PKCE",
"Enable PKCE - Tooltip": "Enable PKCE - Tooltip",
"Enable proxy": "Activer le proxy",
@@ -1305,8 +1282,6 @@
"SSH password": "Mot de passe SSH",
"SSH port": "Port SSH",
"SSH user": "Utilisateur SSH",
"SSL mode": "Mode SSL",
"SSL mode - Tooltip": "Mode SSL",
"Service account key": "Clé du compte de service",
"Sync interval": "Intervalle de synchronisation",
"Sync interval - Tooltip": "Unité en secondes",

View File

@@ -113,19 +113,12 @@
"Order - Tooltip": "値が小さいほど、アプリページで上位にランク付けされます",
"Org choice mode": "組織選択モード",
"Org choice mode - Tooltip": "ログインする組織を選択する方法 - ヒント",
"Other domains": "Other domains",
"Other domains - Tooltip": "Other domains - Tooltip",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "\"自動サインイン\"を有効にする前に、まず\"サインインセッション\"を有効にしてください",
"Please input additional domains": "Please input additional domains",
"Please input your application!": "あなたの申請を入力してください!",
"Please input your organization!": "あなたの組織を入力してください!",
"Please select a HTML file": "HTMLファイルを選択してください",
"Pop up": "ポップアップ",
"Providers": "プロバイダー",
"Proxy SSL mode": "Proxy SSL mode",
"Proxy SSL mode - Tooltip": "Proxy SSL mode - Tooltip",
"Proxy domain": "Proxy domain",
"Proxy domain - Tooltip": "Proxy domain - Tooltip",
"Random": "ランダム",
"Real name": "本名",
"Redirect URL": "リダイレクトURL",
@@ -135,7 +128,6 @@
"Refresh token expire": "リフレッシュトークンの有効期限が切れました",
"Refresh token expire - Tooltip": "リフレッシュトークンの有効期限時間",
"Reset to Empty": "空にリセット",
"Reverse Proxy": "Reverse Proxy",
"Right": "右",
"Rule": "ルール",
"SAML hash algorithm": "SAMLハッシュアルゴリズム",
@@ -143,8 +135,6 @@
"SAML metadata": "SAMLメタデータ",
"SAML metadata - Tooltip": "SAMLプロトコルのメタデータ - ヒント",
"SAML reply URL": "SAMLリプライURL",
"SSL cert": "SSL cert",
"SSL cert - Tooltip": "SSL cert - Tooltip",
"Security": "セキュリティ",
"Select": "選択",
"Side panel HTML": "サイドパネルのHTML",
@@ -175,8 +165,6 @@
"Token signing method": "トークン署名方法",
"Token signing method - Tooltip": "JWTトークンの署名方法。証明書と同じアルゴリズムである必要があります。",
"UI Customization": "UIのカスタマイズ",
"Upstream host": "Upstream host",
"Upstream host - Tooltip": "Upstream host - Tooltip",
"Use Email as NameID": "メールアドレスをNameIDとして使用",
"Use Email as NameID - Tooltip": "メールアドレスをNameIDとして使用 - ツールチップ",
"Vertical": "垂直",
@@ -198,9 +186,7 @@
"Expire in years - Tooltip": "証明書の有効期間、年数で",
"New Cert": "新しい証明書",
"Private key": "プライベートキー",
"Private key - Tooltip": "公開鍵証明書に対応する秘密鍵",
"Scope - Tooltip": "証明書の使用シナリオ",
"Type - Tooltip": "証明書の種類"
"Private key - Tooltip": "公開鍵証明書に対応する秘密鍵"
},
"code": {
"Code you received": "受け取ったコード",
@@ -289,6 +275,7 @@
"Applications that require authentication": "認証が必要なアプリケーション",
"Apps": "アプリ",
"Authorization": "認可",
"Auto": "Auto",
"Avatar": "アバター",
"Avatar - Tooltip": "ユーザーのパブリックアバター画像",
"Back": "戻る",
@@ -770,7 +757,6 @@
"Account menu - Tooltip": "アカウントメニュー - ツールチップ",
"Admin navbar items": "管理者ナビバー項目",
"Admin navbar items - Tooltip": "管理者ナビバー項目 - ツールチップ",
"All": "All",
"Balance credit": "残高クレジット",
"Balance credit - Tooltip": "残高クレジット - ツールチップ",
"Balance currency": "残高通貨",
@@ -935,7 +921,6 @@
"Invalid product": "Invalid product",
"Is recharge": "チャージ用か",
"Is recharge - Tooltip": "現在の製品が残高をチャージするためかどうか",
"Name": "Name",
"New Product": "新製品",
"No recharge options available": "No recharge options available",
"Order created successfully": "注文が正常に作成されました",
@@ -948,7 +933,6 @@
"Please add at least one recharge option when custom amount is disabled": "任意金額が無効な場合は、少なくとも1つのチャージオプションを追加してください",
"Please select a currency": "通貨を選択してください",
"Please select at least one payment provider": "少なくとも1つの支払いプロバイダーを選択してください",
"Price": "Price",
"Processing payment...": "支払い処理中...",
"Product list cannot be empty": "商品リストを空にできません",
"Product not found or invalid": "Product not found or invalid",
@@ -957,8 +941,6 @@
"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",
"Select amount": "金額を選択",
"Sold": "売れました",
@@ -993,7 +975,6 @@
"Auth Key - Tooltip": "認証キー - ツールチップ",
"Auth URL": "認証URL",
"Auth URL - Tooltip": "認証URL",
"Auto": "Auto",
"Base URL": "ベースURL",
"Base URL - Tooltip": "ベースURL - ツールチップ",
"Bucket": "バケツ",
@@ -1002,8 +983,6 @@
"Can signin": "サインインできますか?",
"Can signup": "サインアップできますか?",
"Can unlink": "アンリンクすることができます",
"Category": "カテゴリー",
"Category - Tooltip": "カテゴリーを選択してください",
"Channel No.": "チャンネル番号",
"Channel No. - Tooltip": "チャンネル番号",
"Chat ID": "チャットID",
@@ -1020,7 +999,6 @@
"Content - Tooltip": "コンテンツ - ツールチップ",
"DB test": "DBテスト",
"DB test - Tooltip": "DBテスト - ツールチップ",
"Disable": "Disable",
"Domain": "ドメイン",
"Domain - Tooltip": "オブジェクトストレージのカスタムドメイン",
"Edit Provider": "編集プロバイダー",
@@ -1030,7 +1008,6 @@
"Email regex - Tooltip": "メール正規表現 - ツールチップ",
"Email title": "電子メールのタイトル",
"Email title - Tooltip": "メールのタイトル",
"Enable": "Enable",
"Enable PKCE": "PKCEを有効にする",
"Enable PKCE - Tooltip": "PKCEを有効にする - ツールチップ",
"Enable proxy": "プロキシを有効にする",
@@ -1305,8 +1282,6 @@
"SSH password": "SSHパスワード",
"SSH port": "SSHポート",
"SSH user": "SSHユーザー",
"SSL mode": "SSLモード",
"SSL mode - Tooltip": "SSLモード",
"Service account key": "サービスアカウントキー",
"Sync interval": "同期の間隔",
"Sync interval - Tooltip": "単位は秒です",

View File

@@ -113,19 +113,12 @@
"Order - Tooltip": "Im mniejsza wartość, tym wyżej jest rangowana na stronie Aplikacji",
"Org choice mode": "Tryb wyboru organizacji",
"Org choice mode - Tooltip": "Metoda wyboru organizacji do logowania - Podpowiedź",
"Other domains": "Other domains",
"Other domains - Tooltip": "Other domains - Tooltip",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Najpierw włącz \"sesję logowania\", zanim włączysz \"automatyczne logowanie\"",
"Please input additional domains": "Please input additional domains",
"Please input your application!": "Proszę wprowadzić swoją aplikację!",
"Please input your organization!": "Proszę wprowadzić swoją organizację!",
"Please select a HTML file": "Proszę wybrać plik HTML",
"Pop up": "Wyskakujące okno",
"Providers": "Dostawcy",
"Proxy SSL mode": "Proxy SSL mode",
"Proxy SSL mode - Tooltip": "Proxy SSL mode - Tooltip",
"Proxy domain": "Proxy domain",
"Proxy domain - Tooltip": "Proxy domain - Tooltip",
"Random": "Losowy",
"Real name": "Prawdziwe imię",
"Redirect URL": "URL przekierowania",
@@ -135,7 +128,6 @@
"Refresh token expire": "Czas wygaśnięcia odświeżania tokena",
"Refresh token expire - Tooltip": "Czas wygaśnięcia tokena odświeżania",
"Reset to Empty": "Resetuj do pustego",
"Reverse Proxy": "Reverse Proxy",
"Right": "Prawo",
"Rule": "Reguła",
"SAML hash algorithm": "Algorytm skrótu SAML",
@@ -143,8 +135,6 @@
"SAML metadata": "Metadane SAML",
"SAML metadata - Tooltip": "Metadane protokołu SAML - Podpowiedź",
"SAML reply URL": "URL odpowiedzi SAML",
"SSL cert": "SSL cert",
"SSL cert - Tooltip": "SSL cert - Tooltip",
"Security": "Bezpieczeństwo",
"Select": "Wybierz",
"Side panel HTML": "HTML panelu bocznego",
@@ -175,8 +165,6 @@
"Token signing method": "Metoda podpisywania tokena",
"Token signing method - Tooltip": "Metoda podpisywania tokena JWT, musi być tym samym algorytmem co certyfikat",
"UI Customization": "Dostosowanie interfejsu",
"Upstream host": "Upstream host",
"Upstream host - Tooltip": "Upstream host - Tooltip",
"Use Email as NameID": "Użyj e-maila jako NameID",
"Use Email as NameID - Tooltip": "Użyj e-maila jako NameID - Podpowiedź",
"Vertical": "Pionowy",
@@ -198,9 +186,7 @@
"Expire in years - Tooltip": "Okres ważności certyfikatu, w latach",
"New Cert": "Nowy certyfikat",
"Private key": "Klucz prywatny",
"Private key - Tooltip": "Klucz prywatny odpowiadający certyfikatowi klucza publicznego",
"Scope - Tooltip": "Scenariusze użycia certyfikatu",
"Type - Tooltip": "Typ certyfikatu"
"Private key - Tooltip": "Klucz prywatny odpowiadający certyfikatowi klucza publicznego"
},
"code": {
"Code you received": "Kod, który otrzymałeś",
@@ -289,6 +275,7 @@
"Applications that require authentication": "Aplikacje wymagające uwierzytelniania",
"Apps": "Aplikacje",
"Authorization": "Autoryzacja",
"Auto": "Auto",
"Avatar": "Awatar",
"Avatar - Tooltip": "Publiczny obraz awatara użytkownika",
"Back": "Wstecz",
@@ -770,7 +757,6 @@
"Account menu - Tooltip": "Menu konta - Podpowiedź",
"Admin navbar items": "Elementy paska nawigacji administratora",
"Admin navbar items - Tooltip": "Elementy paska nawigacji administratora - Podpowiedź",
"All": "All",
"Balance credit": "Saldo kredytów",
"Balance credit - Tooltip": "Saldo kredytów - Podpowiedź",
"Balance currency": "Waluta salda",
@@ -935,7 +921,6 @@
"Invalid product": "Invalid product",
"Is recharge": "Jest doładowaniem",
"Is recharge - Tooltip": "Czy bieżący produkt służy do doładowania salda",
"Name": "Name",
"New Product": "Nowy produkt",
"No recharge options available": "No recharge options available",
"Order created successfully": "Zamówienie utworzone pomyślnie",
@@ -948,7 +933,6 @@
"Please add at least one recharge option when custom amount is disabled": "Dodaj co najmniej jedną opcję doładowania, gdy kwota niestandardowa jest wyłączona",
"Please select a currency": "Wybierz walutę",
"Please select at least one payment provider": "Wybierz co najmniej jednego dostawcę płatności",
"Price": "Price",
"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",
@@ -957,8 +941,6 @@
"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",
"Select amount": "Wybierz kwotę",
"Sold": "Sprzedano",
@@ -993,7 +975,6 @@
"Auth Key - Tooltip": "Klucz autoryzacji - Podpowiedź",
"Auth URL": "Adres URL autoryzacji",
"Auth URL - Tooltip": "Adres URL autoryzacji",
"Auto": "Auto",
"Base URL": "Podstawowy adres URL",
"Base URL - Tooltip": "Podstawowy adres URL",
"Bucket": "Wiadro",
@@ -1002,8 +983,6 @@
"Can signin": "Można się zalogować",
"Can signup": "Można się zarejestrować",
"Can unlink": "Można odłączyć",
"Category": "Kategoria",
"Category - Tooltip": "Wybierz kategorię",
"Channel No.": "Channel No.",
"Channel No. - Tooltip": "Channel No. - Tooltip",
"Chat ID": "Chat ID",
@@ -1020,7 +999,6 @@
"Content - Tooltip": "Treść",
"DB test": "Test bazy danych",
"DB test - Tooltip": "Test bazy danych",
"Disable": "Disable",
"Domain": "Domain",
"Domain - Tooltip": "Domain - Tooltip",
"Edit Provider": "Edit Provider",
@@ -1030,7 +1008,6 @@
"Email regex - Tooltip": "Email regex - Tooltip",
"Email title": "Email title",
"Email title - Tooltip": "Email title - Tooltip",
"Enable": "Enable",
"Enable PKCE": "Enable PKCE",
"Enable PKCE - Tooltip": "Enable PKCE - Tooltip",
"Enable proxy": "Enable proxy",
@@ -1305,8 +1282,6 @@
"SSH password": "Hasło SSH",
"SSH port": "Port SSH",
"SSH user": "Użytkownik SSH",
"SSL mode": "Tryb SSL",
"SSL mode - Tooltip": "Tryb SSL - etykietka",
"Service account key": "Klucz konta usługi",
"Sync interval": "Interwał synchronizacji",
"Sync interval - Tooltip": "Jednostka w sekundach",

View File

@@ -113,19 +113,12 @@
"Order - Tooltip": "Quanto menor o valor, maior a classificação na página de Aplicativos",
"Org choice mode": "Modo de escolha da organização",
"Org choice mode - Tooltip": "Método usado para selecionar a organização para fazer login - Dica",
"Other domains": "Other domains",
"Other domains - Tooltip": "Other domains - Tooltip",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Por favor, habilite a \"Sessão de login\" primeiro antes de habilitar o \"Login automático\"",
"Please input additional domains": "Please input additional domains",
"Please input your application!": "Por favor, insira o nome da sua aplicação!",
"Please input your organization!": "Por favor, insira o nome da sua organização!",
"Please select a HTML file": "Por favor, selecione um arquivo HTML",
"Pop up": "Abrir em pop-up",
"Providers": "Provedores",
"Proxy SSL mode": "Proxy SSL mode",
"Proxy SSL mode - Tooltip": "Proxy SSL mode - Tooltip",
"Proxy domain": "Proxy domain",
"Proxy domain - Tooltip": "Proxy domain - Tooltip",
"Random": "Aleatório",
"Real name": "Nome real",
"Redirect URL": "URL de redirecionamento",
@@ -135,7 +128,6 @@
"Refresh token expire": "Expiração do token de atualização",
"Refresh token expire - Tooltip": "Tempo de expiração do token de atualização",
"Reset to Empty": "Redefinir para vazio",
"Reverse Proxy": "Reverse Proxy",
"Right": "Direita",
"Rule": "Regra",
"SAML hash algorithm": "Algoritmo de hash SAML",
@@ -143,8 +135,6 @@
"SAML metadata": "Metadados do SAML",
"SAML metadata - Tooltip": "Os metadados do protocolo SAML - Dica",
"SAML reply URL": "URL de resposta do SAML",
"SSL cert": "SSL cert",
"SSL cert - Tooltip": "SSL cert - Tooltip",
"Security": "Segurança",
"Select": "Selecionar",
"Side panel HTML": "HTML do painel lateral",
@@ -175,8 +165,6 @@
"Token signing method": "Método de assinatura do token",
"Token signing method - Tooltip": "Método de assinatura do token JWT. Deve ser o mesmo algoritmo do certificado.",
"UI Customization": "Personalização da interface",
"Upstream host": "Upstream host",
"Upstream host - Tooltip": "Upstream host - Tooltip",
"Use Email as NameID": "Usar e-mail como NameID",
"Use Email as NameID - Tooltip": "Dica: usar e-mail como NameID",
"Vertical": "Vertical",
@@ -198,9 +186,7 @@
"Expire in years - Tooltip": "Período de validade do certificado, em anos",
"New Cert": "Novo Certificado",
"Private key": "Chave privada",
"Private key - Tooltip": "Chave privada correspondente ao certificado de chave pública",
"Scope - Tooltip": "Cenários de uso do certificado",
"Type - Tooltip": "Tipo de certificado"
"Private key - Tooltip": "Chave privada correspondente ao certificado de chave pública"
},
"code": {
"Code you received": "Código que você recebeu",
@@ -289,6 +275,7 @@
"Applications that require authentication": "Aplicações que requerem autenticação",
"Apps": "Aplicativos",
"Authorization": "Autorização",
"Auto": "Auto",
"Avatar": "Avatar",
"Avatar - Tooltip": "Imagem de avatar pública do usuário",
"Back": "Voltar",
@@ -770,7 +757,6 @@
"Account menu - Tooltip": "Dica: menu da conta",
"Admin navbar items": "Itens da barra de navegação do administrador",
"Admin navbar items - Tooltip": "Dica: itens da barra de navegação do administrador",
"All": "All",
"Balance credit": "Saldo de créditos",
"Balance credit - Tooltip": "Dica: saldo de créditos",
"Balance currency": "Moeda do saldo",
@@ -935,7 +921,6 @@
"Invalid product": "Invalid product",
"Is recharge": "É recarga",
"Is recharge - Tooltip": "Se o produto atual é para recarregar saldo",
"Name": "Name",
"New Product": "Novo Produto",
"No recharge options available": "No recharge options available",
"Order created successfully": "Pedido criado com sucesso",
@@ -948,7 +933,6 @@
"Please add at least one recharge option when custom amount is disabled": "Por favor, adicione pelo menos uma opção de recarga quando o valor personalizado estiver desativado",
"Please select a currency": "Por favor, selecione uma moeda",
"Please select at least one payment provider": "Por favor, selecione pelo menos um provedor de pagamento",
"Price": "Price",
"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",
@@ -957,8 +941,6 @@
"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",
"Select amount": "Selecionar valor",
"Sold": "Vendido",
@@ -993,7 +975,6 @@
"Auth Key - Tooltip": "Dica: chave de autenticação",
"Auth URL": "URL de autenticação",
"Auth URL - Tooltip": "URL de autenticação",
"Auto": "Auto",
"Base URL": "URL base",
"Base URL - Tooltip": "Dica: URL base",
"Bucket": "Bucket",
@@ -1002,8 +983,6 @@
"Can signin": "Pode fazer login",
"Can signup": "Pode se inscrever",
"Can unlink": "Pode desvincular",
"Category": "Categoria",
"Category - Tooltip": "Selecione uma categoria",
"Channel No.": "Número do canal",
"Channel No. - Tooltip": "Número do canal",
"Chat ID": "ID do chat",
@@ -1020,7 +999,6 @@
"Content - Tooltip": "Dica: conteúdo",
"DB test": "Teste do banco de dados",
"DB test - Tooltip": "Dica: teste do banco de dados",
"Disable": "Disable",
"Domain": "Domínio",
"Domain - Tooltip": "Domínio personalizado para armazenamento de objetos",
"Edit Provider": "Editar Provedor",
@@ -1030,7 +1008,6 @@
"Email regex - Tooltip": "Dica: regex de e-mail",
"Email title": "Título do e-mail",
"Email title - Tooltip": "Título do e-mail",
"Enable": "Enable",
"Enable PKCE": "Ativar PKCE",
"Enable PKCE - Tooltip": "Dica: ativar PKCE",
"Enable proxy": "Ativar proxy",
@@ -1305,8 +1282,6 @@
"SSH password": "Senha SSH",
"SSH port": "Porta SSH",
"SSH user": "Usuário SSH",
"SSL mode": "Modo SSL",
"SSL mode - Tooltip": "Dica: modo SSL",
"Service account key": "Chave da conta de serviço",
"Sync interval": "Intervalo de sincronização",
"Sync interval - Tooltip": "Unidade em segundos",

View File

@@ -113,19 +113,12 @@
"Order - Tooltip": "Değer ne kadar küçükse, Uygulamalar sayfasında o kadar yüksek sıralanır",
"Org choice mode": "Organizasyon seçim modu",
"Org choice mode - Tooltip": "Giriş yapmak için organizasyon seçme yöntemi - İpucu",
"Other domains": "Other domains",
"Other domains - Tooltip": "Other domains - Tooltip",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Lütfen \"Oturum açma oturumu\"nu etkinleştirmeden önce \"Otomatik oturum açma\"yı etkinleştirin",
"Please input additional domains": "Please input additional domains",
"Please input your application!": "Lütfen uygulamanızı girin!",
"Please input your organization!": "Lütfen organizasyonunuzu girin!",
"Please select a HTML file": "Lütfen bir HTML dosyası seçin",
"Pop up": "Açılır pencere",
"Providers": "Sağlayıcılar",
"Proxy SSL mode": "Proxy SSL mode",
"Proxy SSL mode - Tooltip": "Proxy SSL mode - Tooltip",
"Proxy domain": "Proxy domain",
"Proxy domain - Tooltip": "Proxy domain - Tooltip",
"Random": "Rastgele",
"Real name": "Gerçek ad",
"Redirect URL": "Yönlendirme URL'si",
@@ -135,7 +128,6 @@
"Refresh token expire": "Yenileme jetonu sona erer",
"Refresh token expire - Tooltip": "Yenileme jetonunun son kullanma süresi",
"Reset to Empty": "Boşalt",
"Reverse Proxy": "Reverse Proxy",
"Right": "Sağ",
"Rule": "Kural",
"SAML hash algorithm": "SAML karma algoritması",
@@ -143,8 +135,6 @@
"SAML metadata": "SAML meta verileri",
"SAML metadata - Tooltip": "SAML protokolünün meta verileri - İpucu",
"SAML reply URL": "SAML yanıt URL'si",
"SSL cert": "SSL cert",
"SSL cert - Tooltip": "SSL cert - Tooltip",
"Security": "Güvenlik",
"Select": "Seç",
"Side panel HTML": "Yan panel HTML",
@@ -175,8 +165,6 @@
"Token signing method": "Token imzalama yöntemi",
"Token signing method - Tooltip": "JWT token'ın imzalama yöntemi, sertifika ile aynı algoritma olmalıdır",
"UI Customization": "Arayüz Özelleştirme",
"Upstream host": "Upstream host",
"Upstream host - Tooltip": "Upstream host - Tooltip",
"Use Email as NameID": "NameID olarak E-posta kullan",
"Use Email as NameID - Tooltip": "NameID olarak E-posta kullanın - Araç ipucu",
"Vertical": "Dikey",
@@ -198,9 +186,7 @@
"Expire in years - Tooltip": "Sertifikanın geçerlilik süresi, yıllarda",
"New Cert": "Yeni Sertifika",
"Private key": "Özel anahtar",
"Private key - Tooltip": "Genel anahtar sertifikasına karşılık gelen özel anahtar",
"Scope - Tooltip": "Sertifikanın kullanım senaryoları",
"Type - Tooltip": "Sertifika türü"
"Private key - Tooltip": "Genel anahtar sertifikasına karşılık gelen özel anahtar"
},
"code": {
"Code you received": "Aldığınız kod",
@@ -289,6 +275,7 @@
"Applications that require authentication": "Kimlik doğrulaması gerektiren uygulamalar",
"Apps": "Uygulamalar",
"Authorization": "Yetkilendirme",
"Auto": "Auto",
"Avatar": "Avatar",
"Avatar - Tooltip": "Kullanıcı için genel avatar resmi",
"Back": "Geri",
@@ -770,7 +757,6 @@
"Account menu - Tooltip": "Hesap menüsü - Araç ipucu",
"Admin navbar items": "Yönetici gezinme çubuğu öğeleri",
"Admin navbar items - Tooltip": "Yönetici gezinme çubuğu öğeleri - Araç ipucu",
"All": "All",
"Balance credit": "Bakiye kredisi",
"Balance credit - Tooltip": "Bakiye kredisi - Araç ipucu",
"Balance currency": "Bakiye para birimi",
@@ -935,7 +921,6 @@
"Invalid product": "Invalid product",
"Is recharge": "Yeniden yükleme mi",
"Is recharge - Tooltip": "Mevcut ürün bakiye yeniden yüklemesi ise",
"Name": "Name",
"New Product": "Yeni Ürün",
"No recharge options available": "No recharge options available",
"Order created successfully": "Sipariş başarıyla oluşturuldu",
@@ -948,7 +933,6 @@
"Please add at least one recharge option when custom amount is disabled": "Özel tutar devre dışıyken en az bir yeniden yükleme seçeneği ekleyin",
"Please select a currency": "Lütfen bir para birimi seçin",
"Please select at least one payment provider": "Lütfen en az bir ödeme sağlayıcısı seçin",
"Price": "Price",
"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",
@@ -957,8 +941,6 @@
"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",
"Select amount": "Tutar seç",
"Sold": "Satılmış",
@@ -993,7 +975,6 @@
"Auth Key - Tooltip": "Kimlik Doğrulama Anahtarı - Araç ipucu",
"Auth URL": "Yetkilendirme URL'si",
"Auth URL - Tooltip": "Yetkilendirme URL'si",
"Auto": "Auto",
"Base URL": "Temel URL",
"Base URL - Tooltip": "Temel URL - Araç ipucu",
"Bucket": "Bucket",
@@ -1002,8 +983,6 @@
"Can signin": "Giriş yapabilir",
"Can signup": "Kayıt yapabilir",
"Can unlink": "Bağlantıyı kesebilir",
"Category": "Kategori",
"Category - Tooltip": "Bir kategori seçin",
"Channel No.": "Kanal Numarası",
"Channel No. - Tooltip": "Kanal Numarası",
"Chat ID": "Sohbet Kimliği",
@@ -1020,7 +999,6 @@
"Content - Tooltip": "İçerik - Araç ipucu",
"DB test": "Veritabanı testi",
"DB test - Tooltip": "Veritabanı testi - Araç ipucu",
"Disable": "Disable",
"Domain": "Alan adı",
"Domain - Tooltip": "Nesne depolama için özel alan adı",
"Edit Provider": "Sağlayıcıyı Düzenle",
@@ -1030,7 +1008,6 @@
"Email regex - Tooltip": "E-posta düzenli ifadesi - Araç ipucu",
"Email title": "E-posta başlığı",
"Email title - Tooltip": "E-posta başlığı",
"Enable": "Enable",
"Enable PKCE": "PKCE'yi etkinleştir",
"Enable PKCE - Tooltip": "PKCE'yi etkinleştir - Araç ipucu",
"Enable proxy": "Proxy'yi etkinleştir",
@@ -1305,8 +1282,6 @@
"SSH password": "SSH şifresi",
"SSH port": "SSH portu",
"SSH user": "SSH kullanıcısı",
"SSL mode": "SSL modu",
"SSL mode - Tooltip": "SSL modu - İpucu",
"Service account key": "Service account key",
"Sync interval": "Senkronizasyon aralığı",
"Sync interval - Tooltip": "Birimi saniye cinsinden",

View File

@@ -113,19 +113,12 @@
"Order - Tooltip": "Чим менше значення, тим вище воно ранжується на сторінці програм",
"Org choice mode": "Режим вибору організації",
"Org choice mode - Tooltip": "Режим вибору організації підказка",
"Other domains": "Other domains",
"Other domains - Tooltip": "Other domains - Tooltip",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Спочатку увімкніть \"Сесію входу\", перш ніж увімкнути \"Автоматичний вхід\"",
"Please input additional domains": "Please input additional domains",
"Please input your application!": "Будь ласка, введіть свою заявку!",
"Please input your organization!": "Будь ласка, введіть вашу організацію!",
"Please select a HTML file": "Виберіть файл HTML",
"Pop up": "Вспливаюче вікно",
"Providers": "Постачальники",
"Proxy SSL mode": "Proxy SSL mode",
"Proxy SSL mode - Tooltip": "Proxy SSL mode - Tooltip",
"Proxy domain": "Proxy domain",
"Proxy domain - Tooltip": "Proxy domain - Tooltip",
"Random": "Випадковий",
"Real name": "Справжнє ім'я",
"Redirect URL": "URL-адреса перенаправлення",
@@ -135,7 +128,6 @@
"Refresh token expire": "Термін дії маркера оновлення закінчився",
"Refresh token expire - Tooltip": "Оновити термін дії маркера",
"Reset to Empty": "Скинути до порожнього",
"Reverse Proxy": "Reverse Proxy",
"Right": "правильно",
"Rule": "правило",
"SAML hash algorithm": "Хеш-алгоритм SAML",
@@ -143,8 +135,6 @@
"SAML metadata": "Метадані SAML",
"SAML metadata - Tooltip": "Метадані протоколу SAML",
"SAML reply URL": "URL-адреса відповіді SAML",
"SSL cert": "SSL cert",
"SSL cert - Tooltip": "SSL cert - Tooltip",
"Security": "Безпека",
"Select": "Виберіть",
"Side panel HTML": "HTML бічної панелі",
@@ -175,8 +165,6 @@
"Token signing method": "Метод підпису токена",
"Token signing method - Tooltip": "Метод підпису JWT-токена, повинен бути тим же алгоритмом, що і сертифікат",
"UI Customization": "Налаштування інтерфейсу",
"Upstream host": "Upstream host",
"Upstream host - Tooltip": "Upstream host - Tooltip",
"Use Email as NameID": "Використовувати Email як NameID",
"Use Email as NameID - Tooltip": "Використовувати Email як NameID - підказка",
"Vertical": "Вертикальний",
@@ -198,9 +186,7 @@
"Expire in years - Tooltip": "Термін дії сертифіката, років",
"New Cert": "Новий сертифікат",
"Private key": "Приватний ключ",
"Private key - Tooltip": "Закритий ключ, що відповідає сертифікату відкритого ключа",
"Scope - Tooltip": "Сценарії використання сертифіката",
"Type - Tooltip": "Тип сертифіката"
"Private key - Tooltip": "Закритий ключ, що відповідає сертифікату відкритого ключа"
},
"code": {
"Code you received": "Код, який ви отримали",
@@ -289,6 +275,7 @@
"Applications that require authentication": "Програми, які потребують автентифікації",
"Apps": "програми",
"Authorization": "Авторизація",
"Auto": "Auto",
"Avatar": "Аватар",
"Avatar - Tooltip": "Публічний аватар користувача",
"Back": "Назад",
@@ -770,7 +757,6 @@
"Account menu - Tooltip": "Account menu - Tooltip",
"Admin navbar items": "Admin navbar items",
"Admin navbar items - Tooltip": "Admin navbar items - Tooltip",
"All": "All",
"Balance credit": "Balance credit",
"Balance credit - Tooltip": "Balance credit - Tooltip",
"Balance currency": "Balance currency",
@@ -935,7 +921,6 @@
"Invalid product": "Invalid product",
"Is recharge": "Чи є поповненням",
"Is recharge - Tooltip": "Чи є поточний продукт для поповнення балансу",
"Name": "Name",
"New Product": "Новий продукт",
"No recharge options available": "No recharge options available",
"Order created successfully": "Order created successfully",
@@ -948,7 +933,6 @@
"Please add at least one recharge option when custom amount is disabled": "Please add at least one recharge option when custom amount is disabled",
"Please select a currency": "Please select a currency",
"Please select at least one payment provider": "Please select at least one payment provider",
"Price": "Price",
"Processing payment...": "Processing payment...",
"Product list cannot be empty": "Product list cannot be empty",
"Product not found or invalid": "Product not found or invalid",
@@ -957,8 +941,6 @@
"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",
"Select amount": "Select amount",
"Sold": "Продано",
@@ -993,7 +975,6 @@
"Auth Key - Tooltip": "Ключ автентифікації для сервісу",
"Auth URL": "URL авторизації",
"Auth URL - Tooltip": "URL для автентифікації",
"Auto": "Auto",
"Base URL": "Базовий URL",
"Base URL - Tooltip": "Базовий URL сервісу",
"Bucket": "Відро",
@@ -1002,8 +983,6 @@
"Can signin": "Можна ввійти",
"Can signup": "Можна записатися",
"Can unlink": "Можна від’єднати",
"Category": "Категорія",
"Category - Tooltip": "Ідентифікатор для категоризації та групування елементів або контенту, що полегшує фільтрацію та управління",
"Channel No.": "Номер каналу",
"Channel No. - Tooltip": "Унікальний номер, що ідентифікує канал зв'язку або передачі даних, використовується для розрізнення різних шляхів передачі",
"Chat ID": "Ідентифікатор чату",
@@ -1020,7 +999,6 @@
"Content - Tooltip": "Вміст підказка",
"DB test": "Тест БД",
"DB test - Tooltip": "Тест бази даних - підказка",
"Disable": "Disable",
"Domain": "Домен",
"Domain - Tooltip": "Спеціальний домен для зберігання об'єктів",
"Edit Provider": "Редагувати постачальника",
@@ -1030,7 +1008,6 @@
"Email regex - Tooltip": "Email регулярний вираз - підказка",
"Email title": "Назва електронної пошти",
"Email title - Tooltip": "Заголовок електронного листа",
"Enable": "Enable",
"Enable PKCE": "Увімкнути PKCE",
"Enable PKCE - Tooltip": "Увімкнути PKCE - підказка",
"Enable proxy": "Увімкнути проксі",
@@ -1305,8 +1282,6 @@
"SSH password": "пароль SSH",
"SSH port": "порт SSH",
"SSH user": "Користувач SSH",
"SSL mode": "Режим SSL",
"SSL mode - Tooltip": "Режим SSL підказка",
"Service account key": "Service account key",
"Sync interval": "Інтервал синхронізації",
"Sync interval - Tooltip": "Одиниця в секундах",

View File

@@ -113,19 +113,12 @@
"Order - Tooltip": "Giá trị càng nhỏ, xếp hạng càng cao trong trang Ứng dụng",
"Org choice mode": "Chế độ chọn tổ chức",
"Org choice mode - Tooltip": "Phương thức chọn tổ chức để đăng nhập - Gợi ý",
"Other domains": "Other domains",
"Other domains - Tooltip": "Other domains - Tooltip",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Vui lòng kích hoạt \"Phiên đăng nhập\" trước khi kích hoạt \"Đăng nhập tự động\"",
"Please input additional domains": "Please input additional domains",
"Please input your application!": "Vui lòng nhập ứng dụng của bạn!",
"Please input your organization!": "Vui lòng nhập tổ chức của bạn!",
"Please select a HTML file": "Vui lòng chọn tệp HTML",
"Pop up": "Bật lên",
"Providers": "Nhà cung cấp",
"Proxy SSL mode": "Proxy SSL mode",
"Proxy SSL mode - Tooltip": "Proxy SSL mode - Tooltip",
"Proxy domain": "Proxy domain",
"Proxy domain - Tooltip": "Proxy domain - Tooltip",
"Random": "Ngẫu nhiên",
"Real name": "Tên thật",
"Redirect URL": "Chuyển hướng URL",
@@ -135,7 +128,6 @@
"Refresh token expire": "Làm mới mã thông báo hết hạn",
"Refresh token expire - Tooltip": "Thời gian hết hạn của mã thông báo làm mới",
"Reset to Empty": "Đặt lại thành trống",
"Reverse Proxy": "Reverse Proxy",
"Right": "Đúng",
"Rule": "Quy tắc",
"SAML hash algorithm": "Thuật toán hash SAML",
@@ -143,8 +135,6 @@
"SAML metadata": "SAML metadata: Siêu dữ liệu SAML",
"SAML metadata - Tooltip": "Metadata của giao thức SAML - Gợi ý",
"SAML reply URL": "URL phản hồi SAML",
"SSL cert": "SSL cert",
"SSL cert - Tooltip": "SSL cert - Tooltip",
"Security": "Bảo mật",
"Select": "Chọn",
"Side panel HTML": "Bảng điều khiển HTML bên lề",
@@ -175,8 +165,6 @@
"Token signing method": "Phương thức ký token",
"Token signing method - Tooltip": "Phương thức ký token JWT, cần cùng thuật toán với chứng chỉ",
"UI Customization": "Tùy chỉnh giao diện",
"Upstream host": "Upstream host",
"Upstream host - Tooltip": "Upstream host - Tooltip",
"Use Email as NameID": "Sử dụng Email làm NameID",
"Use Email as NameID - Tooltip": "Gợi ý sử dụng Email làm NameID",
"Vertical": "Dọc",
@@ -198,9 +186,7 @@
"Expire in years - Tooltip": "Thời hạn hiệu lực của chứng chỉ, tính bằng năm",
"New Cert": "Chứng chỉ mới",
"Private key": "Khóa bí mật",
"Private key - Tooltip": "Khóa riêng tương ứng với chứng thư khóa công khai",
"Scope - Tooltip": "Các kịch bản sử dụng của giấy chứng nhận",
"Type - Tooltip": "Loại chứng chỉ"
"Private key - Tooltip": "Khóa riêng tương ứng với chứng thư khóa công khai"
},
"code": {
"Code you received": "Mã bạn nhận được",
@@ -289,6 +275,7 @@
"Applications that require authentication": "Các ứng dụng yêu cầu xác thực",
"Apps": "Ứng dụng",
"Authorization": "Ủy quyền",
"Auto": "Auto",
"Avatar": "Ảnh đại diện",
"Avatar - Tooltip": "Ảnh đại diện công khai cho người dùng",
"Back": "Quay lại",
@@ -770,7 +757,6 @@
"Account menu - Tooltip": "Menu tài khoản",
"Admin navbar items": "Mục navbar quản trị",
"Admin navbar items - Tooltip": "Mục navbar quản trị",
"All": "All",
"Balance credit": "Số dư tín dụng",
"Balance credit - Tooltip": "Số dư tín dụng",
"Balance currency": "Đơn vị tiền tệ",
@@ -935,7 +921,6 @@
"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ư",
"Name": "Name",
"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",
@@ -948,7 +933,6 @@
"Please add at least one recharge option when custom amount is disabled": "Vui lòng thêm ít nhất một tùy chọn nạp tiền khi số tiền tùy chỉnh bị tắt",
"Please select a currency": "Vui lòng chọn một loại tiền tệ",
"Please select at least one payment provider": "Vui lòng chọn ít nhất một nhà cung cấp thanh toán",
"Price": "Price",
"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",
@@ -957,8 +941,6 @@
"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",
"Select amount": "Chọn số tiền",
"Sold": "Đã bán",
@@ -993,7 +975,6 @@
"Auth Key - Tooltip": "Gợi ý khóa xác thực",
"Auth URL": "URL xác thực",
"Auth URL - Tooltip": "URL chứng thực",
"Auto": "Auto",
"Base URL": "URL cơ sở",
"Base URL - Tooltip": "Gợi ý URL cơ sở",
"Bucket": "Thùng đựng nước",
@@ -1002,8 +983,6 @@
"Can signin": "Đăng nhập được không?",
"Can signup": "Đăng ký có thể được thực hiện",
"Can unlink": "Không liên kết được",
"Category": "Thể loại",
"Category - Tooltip": "Chọn một danh mục",
"Channel No.": "Kênh số.",
"Channel No. - Tooltip": "Kênh Số.",
"Chat ID": "ID trò chuyện",
@@ -1020,7 +999,6 @@
"Content - Tooltip": "Gợi ý nội dung",
"DB test": "DB test",
"DB test - Tooltip": "Kiểm tra DB - Gợi ý",
"Disable": "Disable",
"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",
@@ -1030,7 +1008,6 @@
"Email regex - Tooltip": "Gợi ý biểu thức chính quy Email",
"Email title": "Tiêu đề email",
"Email title - Tooltip": "Tiêu đề của email",
"Enable": "Enable",
"Enable PKCE": "Enable PKCE",
"Enable PKCE - Tooltip": "Kích hoạt PKCE - Gợi ý",
"Enable proxy": "Enable proxy",
@@ -1305,8 +1282,6 @@
"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ụ",
"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)",

View File

@@ -113,19 +113,12 @@
"Order - Tooltip": "数值越小,在应用列表页面中排序越靠前",
"Org choice mode": "组织选择模式",
"Org choice mode - Tooltip": "采用什么方式选择要登录的组织",
"Other domains": "其他域名",
"Other domains - Tooltip": "也应路由到此应用的其他域名",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "开启 \"保持登录会话\" 后才能开启 \"自动登录\"",
"Please input additional domains": "请输入其他域名",
"Please input your application!": "请输入你的应用",
"Please input your organization!": "请输入你的组织",
"Please select a HTML file": "请选择一个HTML文件",
"Pop up": "弹框",
"Providers": "提供商",
"Proxy SSL mode": "代理SSL模式",
"Proxy SSL mode - Tooltip": "反向代理的SSL/TLS模式",
"Proxy domain": "代理域名",
"Proxy domain - Tooltip": "此应用的公开访问域名例如blog.example.com",
"Random": "随机",
"Real name": "真实姓名",
"Redirect URL": "重定向 URL",
@@ -135,7 +128,6 @@
"Refresh token expire": "Refresh Token过期",
"Refresh token expire - Tooltip": "Refresh Token过期时间",
"Reset to Empty": "重置为空",
"Reverse Proxy": "反向代理",
"Right": "居右",
"Rule": "规则",
"SAML hash algorithm": "SAML哈希算法",
@@ -143,8 +135,6 @@
"SAML metadata": "SAML元数据",
"SAML metadata - Tooltip": "SAML协议的元数据Metadata信息",
"SAML reply URL": "SAML回复 URL",
"SSL cert": "SSL证书",
"SSL cert - Tooltip": "用于TLS终止的证书",
"Security": "安全设置",
"Select": "选择",
"Side panel HTML": "侧面板HTML",
@@ -175,8 +165,6 @@
"Token signing method": "Token签名算法",
"Token signing method - Tooltip": "JWT token的签名算法需要与证书算法相匹配",
"UI Customization": "界面定制",
"Upstream host": "上游主机",
"Upstream host - Tooltip": "转发请求的上游后端地址例如localhost:8080 或 192.168.1.100",
"Use Email as NameID": "使用邮箱作为NameID",
"Use Email as NameID - Tooltip": "使用邮箱作为NameID",
"Vertical": "垂直",
@@ -198,9 +186,7 @@
"Expire in years - Tooltip": "公钥证书的有效期,以年为单位",
"New Cert": "添加证书",
"Private key": "私钥",
"Private key - Tooltip": "公钥证书对应的私钥",
"Scope - Tooltip": "公钥证书的使用场景",
"Type - Tooltip": "公钥证书的类型"
"Private key - Tooltip": "公钥证书对应的私钥"
},
"code": {
"Code you received": "验证码",
@@ -282,13 +268,14 @@
"Admin": "管理工具",
"Affiliation URL": "工作单位URL",
"Affiliation URL - Tooltip": "工作单位的官网URL",
"All": "全部允许",
"All": "全部",
"Application": "应用",
"Application - Tooltip": "可以访问的应用",
"Applications": "应用",
"Applications that require authentication": "需要认证和鉴权的应用",
"Apps": "应用列表",
"Authorization": "Casbin权限管理",
"Auto": "Auto",
"Avatar": "头像",
"Avatar - Tooltip": "公开展示的用户头像",
"Back": "返回",
@@ -770,7 +757,6 @@
"Account menu - Tooltip": "账户相关的菜单项",
"Admin navbar items": "管理员导航栏项",
"Admin navbar items - Tooltip": "管理员导航栏中的项目",
"All": "All",
"Balance credit": "余额积分",
"Balance credit - Tooltip": "用户的余额积分信息",
"Balance currency": "余额币种",
@@ -935,7 +921,6 @@
"Invalid product": "Invalid product",
"Is recharge": "充值",
"Is recharge - Tooltip": "当前商品是否为充值商品",
"Name": "Name",
"New Product": "添加商品",
"No recharge options available": "No recharge options available",
"Order created successfully": "订单创建成功",
@@ -948,7 +933,6 @@
"Please add at least one recharge option when custom amount is disabled": "当禁用自定义金额时,请至少添加一个充值选项",
"Please select a currency": "请选择一种货币",
"Please select at least one payment provider": "请至少选择一个支付提供商",
"Price": "Price",
"Processing payment...": "正在处理支付...",
"Product list cannot be empty": "商品列表不能为空",
"Product not found or invalid": "Product not found or invalid",
@@ -957,8 +941,6 @@
"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": "货号",
"Select amount": "选择金额",
"Sold": "售出",
@@ -993,7 +975,6 @@
"Auth Key - Tooltip": "服务的认证密钥",
"Auth URL": "认证URL",
"Auth URL - Tooltip": "用于认证的URL",
"Auto": "Auto",
"Base URL": "基本URL",
"Base URL - Tooltip": "服务的基本URL",
"Bucket": "存储桶",
@@ -1002,8 +983,6 @@
"Can signin": "可用于登录",
"Can signup": "可用于注册",
"Can unlink": "可解绑定",
"Category": "分类",
"Category - Tooltip": "用于对项目或内容进行归类分组的标识",
"Channel No.": "Channel号码",
"Channel No. - Tooltip": "标识通信或数据传输通道的唯一编号",
"Chat ID": "聊天ID",
@@ -1020,7 +999,6 @@
"Content - Tooltip": "消息、通知或文档中包含的具体信息或数据内容",
"DB test": "数据库测试",
"DB test - Tooltip": "测试数据库连接是否正常",
"Disable": "Disable",
"Domain": "域名",
"Domain - Tooltip": "对象存储的自定义域名",
"Edit Provider": "编辑提供商",
@@ -1030,7 +1008,6 @@
"Email regex - Tooltip": "只有符合此正则表达式的Email才能进行注册或登录",
"Email title": "邮件标题",
"Email title - Tooltip": "邮件标题",
"Enable": "Enable",
"Enable PKCE": "启用PKCE",
"Enable PKCE - Tooltip": "是否启用PKCE扩展协议",
"Enable proxy": "启用代理",
@@ -1305,8 +1282,6 @@
"SSH password": "SSH密码",
"SSH port": "SSH端口",
"SSH user": "SSH用户",
"SSL mode": "SSL模式",
"SSL mode - Tooltip": "连接数据库采用哪种SSL模式",
"Service account key": "Service account key",
"Sync interval": "同步间隔",
"Sync interval - Tooltip": "单位为秒",

View File

@@ -0,0 +1,44 @@
// 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 {Col, Row} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
import {CaptchaPreview} from "../common/CaptchaPreview";
export function renderCaptchaProviderFields(provider, providerName) {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
</Col>
<Col span={22} >
<CaptchaPreview
owner={provider.owner}
name={provider.name}
provider={provider}
providerName={providerName}
captchaType={provider.type}
subType={provider.subType}
clientId={provider.clientId}
clientSecret={provider.clientSecret}
clientId2={provider.clientId2}
clientSecret2={provider.clientSecret2}
providerUrl={provider.providerUrl}
/>
</Col>
</Row>
);
}

View File

@@ -0,0 +1,257 @@
// 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 {Button, Col, Input, InputNumber, Row, Select, Switch} from "antd";
import {LinkOutlined} from "@ant-design/icons";
import * as Setting from "../Setting";
import i18next from "i18next";
import * as ProviderEditTestEmail from "../common/TestEmailWidget";
import Editor from "../common/Editor";
import HttpHeaderTable from "../table/HttpHeaderTable";
const {Option} = Select;
export function renderEmailProviderFields(provider, updateProviderField, renderEmailMappingInput, account) {
return (
<React.Fragment>
{
["Custom HTTP Email", "SendGrid"].includes(provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:Region endpoint for Internet"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={provider.endpoint} onChange={e => {
updateProviderField("endpoint", e.target.value);
}} />
</Col>
</Row>) : null
}
{provider.type === "Resend" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={provider.host} onChange={e => {
updateProviderField("host", e.target.value);
}} />
</Col>
</Row>
)}
{["Azure ACS", "SendGrid", "Resend"].includes(provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={provider.port} onChange={value => {
updateProviderField("port", value);
}} />
</Col>
</Row>
)}
{["Azure ACS", "SendGrid", "Resend"].includes(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"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "200px"}} value={provider.sslMode || "Auto"} onChange={value => {
updateProviderField("sslMode", value);
}}>
<Option value="Auto">{i18next.t("general:Auto")}</Option>
<Option value="Enable">{i18next.t("general:Enable")}</Option>
<Option value="Disable">{i18next.t("general:Disable")}</Option>
</Select>
</Col>
</Row>
)}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Enable proxy"), i18next.t("provider:Enable proxy - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={provider.enableProxy} onChange={checked => {
updateProviderField("enableProxy", checked);
}} />
</Col>
</Row>
{
provider.type === "Custom HTTP Email" ? (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={provider.method} onChange={value => {
updateProviderField("method", value);
}}>
{
[
{id: "GET", name: "GET"},
{id: "POST", name: "POST"},
{id: "PUT", name: "PUT"},
{id: "DELETE", name: "DELETE"},
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
}
</Select>
</Col>
</Row>
{
provider.method !== "GET" ? (<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("webhook:Content type"), i18next.t("webhook:Content type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={provider.issuerUrl === "" ? "application/x-www-form-urlencoded" : provider.issuerUrl} onChange={value => {
updateProviderField("issuerUrl", value);
}}>
{
[
{id: "application/json", name: "application/json"},
{id: "application/x-www-form-urlencoded", name: "application/x-www-form-urlencoded"},
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
}
</Select>
</Col>
</Row>) : null
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:HTTP header"), i18next.t("provider:HTTP header - Tooltip"))} :
</Col>
<Col span={22} >
<HttpHeaderTable httpHeaders={provider.httpHeaders} onUpdateTable={(value) => {updateProviderField("httpHeaders", value);}} />
</Col>
</Row>
{provider.method !== "GET" ? <Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:HTTP body mapping"), i18next.t("provider:HTTP body mapping - Tooltip"))} :
</Col>
<Col span={22}>
{renderEmailMappingInput()}
</Col>
</Row> : null}
</React.Fragment>
) : null
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Email title"), i18next.t("provider:Email title - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={provider.title} onChange={e => {
updateProviderField("title", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Email content"), i18next.t("provider:Email content - Tooltip"))} :
</Col>
<Col span={22} >
<Row style={{marginTop: "20px"}} >
<Button style={{marginLeft: "10px", marginBottom: "5px"}} onClick={() => updateProviderField("content", "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes. <reset-link>Or click %link to reset</reset-link>")} >
{i18next.t("general:Reset to Default")} (Text)
</Button>
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" onClick={() => updateProviderField("content", Setting.getDefaultHtmlEmailContent())} >
{i18next.t("general:Reset to Default")} (HTML)
</Button>
</Row>
<Row>
<Col span={Setting.isMobile() ? 22 : 11}>
<div style={{height: "300px", margin: "10px"}}>
<Editor
value={provider.content}
fillHeight
dark
lang="html"
onChange={value => {
updateProviderField("content", value);
}}
/>
</div>
</Col>
<Col span={1} />
<Col span={Setting.isMobile() ? 22 : 11}>
<div style={{margin: "10px"}}>
<div dangerouslySetInnerHTML={{__html: provider.content.replace("%s", "123456").replace("%{user.friendlyName}", Setting.getFriendlyUserName(account))}} />
</div>
</Col>
</Row>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(`${i18next.t("provider:Email content")}-${i18next.t("general:Invitations")}`, i18next.t("provider:Email content - Tooltip"))} :
</Col>
<Col span={22} >
<Row style={{marginTop: "20px"}} >
<Button style={{marginLeft: "10px", marginBottom: "5px"}} onClick={() => updateProviderField("metadata", "You have invited to join Casdoor. Here is your invitation code: %s, please enter in 5 minutes. Or click %link to signup")} >
{i18next.t("general:Reset to Default")} (Text)
</Button>
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" onClick={() => updateProviderField("metadata", Setting.getDefaultInvitationHtmlEmailContent())} >
{i18next.t("general:Reset to Default")} (HTML)
</Button>
</Row>
<Row>
<Col span={Setting.isMobile() ? 22 : 11}>
<div style={{height: "300px", margin: "10px"}}>
<Editor
value={provider.metadata}
fillHeight
dark
lang="html"
onChange={value => {
updateProviderField("metadata", value);
}}
/>
</div>
</Col>
<Col span={1} />
<Col span={Setting.isMobile() ? 22 : 11}>
<div style={{margin: "10px"}}>
<div dangerouslySetInnerHTML={{__html: provider.metadata.replace("%code", "123456").replace("%s", "123456")}} />
</div>
</Col>
</Row>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Test Email"), i18next.t("provider:Test Email - Tooltip"))} :
</Col>
<Col span={4}>
<Input value={provider.receiver} placeholder={i18next.t("user:Input your email")}
onChange={e => {
updateProviderField("receiver", e.target.value);
}} />
</Col>
{["Azure ACS", "SendGrid", "Resend"].includes(provider.type) ? null : (
<Button style={{marginLeft: "10px", marginBottom: "5px"}} onClick={() => ProviderEditTestEmail.connectSmtpServer(provider)} >
{i18next.t("provider:Test SMTP Connection")}
</Button>
)}
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary"
disabled={!Setting.isValidEmail(provider.receiver)}
onClick={() => ProviderEditTestEmail.sendTestEmail(provider, provider.receiver)} >
{i18next.t("provider:Send Testing Email")}
</Button>
</Row>
</React.Fragment>
);
}

View File

@@ -0,0 +1,48 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Col, Input, Row} from "antd";
import {LinkOutlined} from "@ant-design/icons";
import * as Setting from "../Setting";
import i18next from "i18next";
export function renderFaceIdProviderFields(provider, updateProviderField) {
return (
<>
{["Alibaba Cloud Facebody"].includes(provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={provider.intranetEndpoint} onChange={e => {
updateProviderField("intranetEndpoint", e.target.value);
}} />
</Col>
</Row>
)}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:Region endpoint for Internet"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={provider.endpoint} onChange={e => {
updateProviderField("endpoint", e.target.value);
}} />
</Col>
</Row>
</>
);
}

View File

@@ -0,0 +1,34 @@
// 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 {Col, Input, Row} from "antd";
import {LinkOutlined} from "@ant-design/icons";
import * as Setting from "../Setting";
import i18next from "i18next";
export function renderIDVerificationProviderFields(provider, updateProviderField) {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:Region endpoint for Internet"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={provider.endpoint} onChange={e => {
updateProviderField("endpoint", e.target.value);
}} />
</Col>
</Row>
);
}

View File

@@ -0,0 +1,56 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Col, Input, InputNumber, Row} from "antd";
import {LinkOutlined} from "@ant-design/icons";
import * as Setting from "../Setting";
import i18next from "i18next";
export function renderMfaProviderFields(provider, updateProviderField) {
return (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={provider.host} placeholder="10.10.10.10" onChange={e => {
updateProviderField("host", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={provider.port} onChange={value => {
updateProviderField("port", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:RADIUS Shared Secret - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={provider.clientSecret} placeholder="Shared secret" onChange={e => {
updateProviderField("clientSecret", e.target.value);
}} />
</Col>
</Row>
</React.Fragment>
);
}

View File

@@ -0,0 +1,103 @@
// 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 {Button, Col, Input, Row, Select} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
import * as ProviderNotification from "../common/TestNotificationWidget";
const {Option} = Select;
const {TextArea} = Input;
export function renderNotificationProviderFields(provider, updateProviderField, getReceiverRow) {
return (
<React.Fragment>
{["CUCloud"].includes(provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{["Casdoor"].includes(provider.type) ?
Setting.getLabel(i18next.t("general:Application"), i18next.t("general:Application - Tooltip")) :
Setting.getLabel(i18next.t("provider:Region ID"), i18next.t("provider:Region ID - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={provider.regionId} onChange={e => {
updateProviderField("regionId", e.target.value);
}} />
</Col>
</Row>
) : null}
{["Custom HTTP"].includes(provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={provider.method} onChange={value => {
updateProviderField("method", value);
}}>
{
[
{id: "GET", name: "GET"},
{id: "POST", name: "POST"},
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
}
</Select>
</Col>
</Row>
) : null}
{["Custom HTTP", "CUCloud"].includes(provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Parameter"), i18next.t("provider:Parameter - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={provider.title} onChange={e => {
updateProviderField("title", e.target.value);
}} />
</Col>
</Row>
) : null}
{["Google Chat", "CUCloud"].includes(provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Metadata"), i18next.t("provider:Metadata - Tooltip"))} :
</Col>
<Col span={22}>
<TextArea rows={4} value={provider.metadata} onChange={e => {
updateProviderField("metadata", e.target.value);
}} />
</Col>
</Row>
) : null}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Content"), i18next.t("provider:Content - Tooltip"))} :
</Col>
<Col span={22} >
<TextArea autoSize={{minRows: 3, maxRows: 100}} value={provider.content} onChange={e => {
updateProviderField("content", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
{getReceiverRow(provider)}
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary"
onClick={() => ProviderNotification.sendTestNotification(provider)} >
{i18next.t("provider:Send Testing Notification")}
</Button>
</Row>
</React.Fragment>
);
}

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