forked from casdoor/casdoor
Compare commits
36 Commits
copilot/ad
...
v2.340.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
167d24fb1f | ||
|
|
dc58ac0503 | ||
|
|
038d021797 | ||
|
|
7ba660fd7f | ||
|
|
b1c31a4a9d | ||
|
|
90d7add503 | ||
|
|
c961e75ad3 | ||
|
|
547189a034 | ||
|
|
be725eda74 | ||
|
|
0765b352c9 | ||
|
|
a2a8b582d9 | ||
|
|
0973652be4 | ||
|
|
fef75715bf | ||
|
|
4f78d56e31 | ||
|
|
712bc756bc | ||
|
|
1c9952e3d9 | ||
|
|
bbaa28133f | ||
|
|
baef7680ea | ||
|
|
d15b66177c | ||
|
|
5ce6bac529 | ||
|
|
0621f35665 | ||
|
|
1ac2490419 | ||
|
|
8c50ada494 | ||
|
|
22da90576e | ||
|
|
b00404cb3a | ||
|
|
2ed27f4f0a | ||
|
|
bf538d5260 | ||
|
|
13ee5fd150 | ||
|
|
04cdd5a012 | ||
|
|
7b4873734b | ||
|
|
8d2290944a | ||
|
|
6a2bba1627 | ||
|
|
07554bbbe5 | ||
|
|
a050403ee5 | ||
|
|
118eb0af80 | ||
|
|
c16aebe642 |
@@ -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">
|
||||
|
||||
@@ -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
107
certificate/account.go
Normal 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
|
||||
}
|
||||
47
certificate/account_test.go
Normal file
47
certificate/account_test.go
Normal 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
20
certificate/conf.go
Normal 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
151
certificate/dns.go
Normal 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
55
certificate/ecc.go
Normal 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
34
certificate/ecc_test.go
Normal 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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
226
controllers/consent.go
Normal 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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
48
email/resend.go
Normal 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
12
go.mod
@@ -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
30
go.sum
@@ -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=
|
||||
|
||||
@@ -106,8 +106,8 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Gruppen importieren fehlgeschlagen",
|
||||
"Failed to import users": "Fehler beim Importieren von Benutzern",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Unzureichendes Guthaben: neues Guthaben %v wäre unter dem Kreditlimit %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Unzureichendes Guthaben: neues Organisationsguthaben %v wäre unter dem Kreditlimit %v",
|
||||
"Insufficient balance: new balance %v would exceed the credit limit %v": "Unzureichendes Guthaben: neues Guthaben %v würde das Kreditlimit %v überschreiten",
|
||||
"Insufficient balance: new organization balance %v would exceed the credit limit %v": "Unzureichendes Guthaben: neues Organisationsguthaben %v würde das Kreditlimit %v überschreiten",
|
||||
"Missing parameter": "Fehlender Parameter",
|
||||
"Only admin user can specify user": "Nur Administrator kann Benutzer angeben",
|
||||
"Please login first": "Bitte zuerst einloggen",
|
||||
|
||||
@@ -106,8 +106,8 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Failed to import groups",
|
||||
"Failed to import users": "Failed to import users",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Insufficient balance: new balance %v would be below credit limit %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Insufficient balance: new organization balance %v would be below credit limit %v",
|
||||
"Insufficient balance: new balance %v would exceed the credit limit %v": "Insufficient balance: new balance %v would exceed the credit limit %v",
|
||||
"Insufficient balance: new organization balance %v would exceed the credit limit %v": "Insufficient balance: new organization balance %v would exceed the credit limit %v",
|
||||
"Missing parameter": "Missing parameter",
|
||||
"Only admin user can specify user": "Only admin user can specify user",
|
||||
"Please login first": "Please login first",
|
||||
|
||||
@@ -106,8 +106,8 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Error al importar grupos",
|
||||
"Failed to import users": "Error al importar usuarios",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Saldo insuficiente: el nuevo saldo %v estaría por debajo del límite de crédito %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Saldo insuficiente: el nuevo saldo de la organización %v estaría por debajo del límite de crédito %v",
|
||||
"Insufficient balance: new balance %v would exceed the credit limit %v": "Saldo insuficiente: el nuevo saldo %v excedería el límite de crédito %v",
|
||||
"Insufficient balance: new organization balance %v would exceed the credit limit %v": "Saldo insuficiente: el nuevo saldo de la organización %v excedería el límite de crédito %v",
|
||||
"Missing parameter": "Parámetro faltante",
|
||||
"Only admin user can specify user": "Solo el usuario administrador puede especificar usuario",
|
||||
"Please login first": "Por favor, inicia sesión primero",
|
||||
|
||||
@@ -106,8 +106,8 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Échec de l'importation des groupes",
|
||||
"Failed to import users": "Échec de l'importation des utilisateurs",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Solde insuffisant : le nouveau solde %v serait inférieur à la limite de crédit %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Solde insuffisant : le nouveau solde de l'organisation %v serait inférieur à la limite de crédit %v",
|
||||
"Insufficient balance: new balance %v would exceed the credit limit %v": "Solde insuffisant : le nouveau solde %v dépasserait la limite de crédit %v",
|
||||
"Insufficient balance: new organization balance %v would exceed the credit limit %v": "Solde insuffisant : le nouveau solde de l'organisation %v dépasserait la limite de crédit %v",
|
||||
"Missing parameter": "Paramètre manquant",
|
||||
"Only admin user can specify user": "Seul un administrateur peut désigner un utilisateur",
|
||||
"Please login first": "Veuillez d'abord vous connecter",
|
||||
|
||||
@@ -106,8 +106,8 @@
|
||||
"general": {
|
||||
"Failed to import groups": "グループのインポートに失敗しました",
|
||||
"Failed to import users": "ユーザーのインポートに失敗しました",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "残高不足:新しい残高 %v がクレジット制限 %v を下回ります",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "残高不足:新しい組織残高 %v がクレジット制限 %v を下回ります",
|
||||
"Insufficient balance: new balance %v would exceed the credit limit %v": "残高不足:新しい残高 %v がクレジット制限 %v を超過します",
|
||||
"Insufficient balance: new organization balance %v would exceed the credit limit %v": "残高不足:新しい組織残高 %v がクレジット制限 %v を超過します",
|
||||
"Missing parameter": "不足しているパラメーター",
|
||||
"Only admin user can specify user": "管理者ユーザーのみがユーザーを指定できます",
|
||||
"Please login first": "最初にログインしてください",
|
||||
|
||||
@@ -106,8 +106,8 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Nie udało się zaimportować grup",
|
||||
"Failed to import users": "Nie udało się zaimportować użytkowników",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Niewystarczające saldo: nowe saldo %v byłoby poniżej limitu kredytowego %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Niewystarczające saldo: nowe saldo organizacji %v byłoby poniżej limitu kredytowego %v",
|
||||
"Insufficient balance: new balance %v would exceed the credit limit %v": "Niewystarczające saldo: nowe saldo %v przekroczyłoby limit kredytowy %v",
|
||||
"Insufficient balance: new organization balance %v would exceed the credit limit %v": "Niewystarczające saldo: nowe saldo organizacji %v przekroczyłoby limit kredytowy %v",
|
||||
"Missing parameter": "Brakujący parametr",
|
||||
"Only admin user can specify user": "Tylko administrator może wskazać użytkownika",
|
||||
"Please login first": "Najpierw się zaloguj",
|
||||
|
||||
@@ -106,8 +106,8 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Falha ao importar grupos",
|
||||
"Failed to import users": "Falha ao importar usuários",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Saldo insuficiente: o novo saldo %v estaria abaixo do limite de crédito %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Saldo insuficiente: o novo saldo da organização %v estaria abaixo do limite de crédito %v",
|
||||
"Insufficient balance: new balance %v would exceed the credit limit %v": "Saldo insuficiente: o novo saldo %v excederia o limite de crédito %v",
|
||||
"Insufficient balance: new organization balance %v would exceed the credit limit %v": "Saldo insuficiente: o novo saldo da organização %v excederia o limite de crédito %v",
|
||||
"Missing parameter": "Parâmetro ausente",
|
||||
"Only admin user can specify user": "Apenas um administrador pode especificar um usuário",
|
||||
"Please login first": "Por favor, faça login primeiro",
|
||||
|
||||
@@ -106,8 +106,8 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Gruplar içe aktarılamadı",
|
||||
"Failed to import users": "Kullanıcılar içe aktarılamadı",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Yetersiz bakiye: yeni bakiye %v kredi limitinin altında olacak %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Yetersiz bakiye: yeni organizasyon bakiyesi %v kredi limitinin altında olacak %v",
|
||||
"Insufficient balance: new balance %v would exceed the credit limit %v": "Yetersiz bakiye: yeni bakiye %v kredi limitini aşacak %v",
|
||||
"Insufficient balance: new organization balance %v would exceed the credit limit %v": "Yetersiz bakiye: yeni organizasyon bakiyesi %v kredi limitini aşacak %v",
|
||||
"Missing parameter": "Eksik parametre",
|
||||
"Only admin user can specify user": "Yalnızca yönetici kullanıcı kullanıcı belirleyebilir",
|
||||
"Please login first": "Lütfen önce giriş yapın",
|
||||
|
||||
@@ -106,8 +106,8 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Не вдалося імпортувати групи",
|
||||
"Failed to import users": "Не вдалося імпортувати користувачів",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Недостатній баланс: новий баланс %v буде нижче кредитного ліміту %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Недостатній баланс: новий баланс організації %v буде нижче кредитного ліміту %v",
|
||||
"Insufficient balance: new balance %v would exceed the credit limit %v": "Недостатній баланс: новий баланс %v перевищить кредитний ліміт %v",
|
||||
"Insufficient balance: new organization balance %v would exceed the credit limit %v": "Недостатній баланс: новий баланс організації %v перевищить кредитний ліміт %v",
|
||||
"Missing parameter": "Відсутній параметр",
|
||||
"Only admin user can specify user": "Лише адміністратор може вказати користувача",
|
||||
"Please login first": "Спочатку увійдіть",
|
||||
|
||||
@@ -106,8 +106,8 @@
|
||||
"general": {
|
||||
"Failed to import groups": "Không thể nhập nhóm",
|
||||
"Failed to import users": "Không thể nhập người dùng",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "Số dư không đủ: số dư mới %v sẽ thấp hơn giới hạn tín dụng %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "Số dư không đủ: số dư tổ chức mới %v sẽ thấp hơn giới hạn tín dụng %v",
|
||||
"Insufficient balance: new balance %v would exceed the credit limit %v": "Số dư không đủ: số dư mới %v sẽ vượt quá giới hạn tín dụng %v",
|
||||
"Insufficient balance: new organization balance %v would exceed the credit limit %v": "Số dư không đủ: số dư tổ chức mới %v sẽ vượt quá giới hạn tín dụng %v",
|
||||
"Missing parameter": "Thiếu tham số",
|
||||
"Only admin user can specify user": "Chỉ người dùng quản trị mới có thể chỉ định người dùng",
|
||||
"Please login first": "Vui lòng đăng nhập trước",
|
||||
|
||||
@@ -106,8 +106,8 @@
|
||||
"general": {
|
||||
"Failed to import groups": "导入群组失败",
|
||||
"Failed to import users": "导入用户失败",
|
||||
"Insufficient balance: new balance %v would be below credit limit %v": "余额不足:新余额 %v 将低于信用限额 %v",
|
||||
"Insufficient balance: new organization balance %v would be below credit limit %v": "余额不足:新组织余额 %v 将低于信用限额 %v",
|
||||
"Insufficient balance: new balance %v would exceed the credit limit %v": "余额不足:新余额 %v 将超过信用限额 %v",
|
||||
"Insufficient balance: new organization balance %v would exceed the credit limit %v": "余额不足:新组织余额 %v 将超过信用限额 %v",
|
||||
"Missing parameter": "缺少参数",
|
||||
"Only admin user can specify user": "仅管理员用户可以指定用户",
|
||||
"Please login first": "请先登录",
|
||||
|
||||
19
i18n/util.go
19
i18n/util.go
@@ -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]]
|
||||
|
||||
@@ -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-----"
|
||||
}
|
||||
|
||||
@@ -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"}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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,6 +156,8 @@ type Application struct {
|
||||
FailedSigninFrozenTime int `json:"failedSigninFrozenTime"`
|
||||
CodeResendTimeout int `json:"codeResendTimeout"`
|
||||
|
||||
CustomScopes []*ScopeDescription `xorm:"mediumtext" json:"customScopes"`
|
||||
|
||||
// Reverse proxy fields
|
||||
Domain string `xorm:"varchar(100)" json:"domain"`
|
||||
OtherDomains []string `xorm:"varchar(1000)" json:"otherDomains"`
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
75
object/cert_whois.go
Normal 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)
|
||||
}
|
||||
@@ -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"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -614,9 +614,9 @@ func UpdateOrganizationBalance(owner string, name string, balance float64, curre
|
||||
var newBalance float64
|
||||
if isOrgBalance {
|
||||
newBalance = AddPrices(organization.OrgBalance, convertedBalance)
|
||||
// Check organization balance credit limit
|
||||
if newBalance < organization.BalanceCredit {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:Insufficient balance: new organization balance %v would be below credit limit %v"), newBalance, organization.BalanceCredit)
|
||||
// Check organization balance credit limit (BalanceCredit is an overdraft limit: balance can go down to -BalanceCredit)
|
||||
if newBalance < -organization.BalanceCredit {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:Insufficient balance: new organization balance %v would exceed the credit limit %v"), newBalance, organization.BalanceCredit)
|
||||
}
|
||||
organization.OrgBalance = newBalance
|
||||
columns = []string{"org_balance"}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
103
object/site_cert_account.go
Normal 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
59
object/site_cert_ecc.go
Normal 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
|
||||
}
|
||||
@@ -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...)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -71,6 +71,19 @@ func (syncer *Syncer) updateUserForOriginalFields(user *User, key string) (bool,
|
||||
columns := syncer.getCasdoorColumns()
|
||||
columns = append(columns, "affiliation", "hash", "pre_hash")
|
||||
|
||||
// Skip password-related columns when the incoming user has no password data.
|
||||
// API-based syncers (DingTalk, WeCom, Lark, etc.) do not provide passwords,
|
||||
// so updating these columns would wipe out locally set passwords.
|
||||
if user.Password == "" {
|
||||
filtered := make([]string, 0, len(columns))
|
||||
for _, col := range columns {
|
||||
if col != "password" && col != "password_salt" && col != "password_type" {
|
||||
filtered = append(filtered, col)
|
||||
}
|
||||
}
|
||||
columns = filtered
|
||||
}
|
||||
|
||||
// Add provider-specific field for API-based syncers to enable login binding
|
||||
// This allows synced users to login via their provider accounts
|
||||
switch syncer.Type {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -62,9 +62,9 @@ func validateOrganizationBalance(owner string, name string, balance float64, cur
|
||||
var newBalance float64
|
||||
if isOrgBalance {
|
||||
newBalance = AddPrices(organization.OrgBalance, convertedBalance)
|
||||
// Check organization balance credit limit
|
||||
if newBalance < organization.BalanceCredit {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:Insufficient balance: new organization balance %v would be below credit limit %v"), newBalance, organization.BalanceCredit)
|
||||
// Check organization balance credit limit (BalanceCredit is an overdraft limit: balance can go down to -BalanceCredit)
|
||||
if newBalance < -organization.BalanceCredit {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:Insufficient balance: new organization balance %v would exceed the credit limit %v"), newBalance, organization.BalanceCredit)
|
||||
}
|
||||
} else {
|
||||
// User balance is just a sum of all users' balances, no credit limit check here
|
||||
@@ -120,9 +120,9 @@ func validateUserBalance(owner string, name string, balance float64, currency st
|
||||
}
|
||||
}
|
||||
|
||||
// Validate new balance against credit limit
|
||||
if newBalance < balanceCredit {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:Insufficient balance: new balance %v would be below credit limit %v"), newBalance, balanceCredit)
|
||||
// Validate new balance against credit limit (BalanceCredit is an overdraft limit: balance can go down to -BalanceCredit)
|
||||
if newBalance < -balanceCredit {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:Insufficient balance: new balance %v would exceed the credit limit %v"), newBalance, balanceCredit)
|
||||
}
|
||||
|
||||
// In validation mode, we don't actually update the balance
|
||||
|
||||
@@ -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
|
||||
@@ -1563,9 +1572,9 @@ func UpdateUserBalance(owner string, name string, balance float64, currency stri
|
||||
}
|
||||
}
|
||||
|
||||
// Validate new balance against credit limit
|
||||
if newBalance < balanceCredit {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:Insufficient balance: new balance %v would be below credit limit %v"), newBalance, balanceCredit)
|
||||
// Validate new balance against credit limit (BalanceCredit is an overdraft limit: balance can go down to -BalanceCredit)
|
||||
if newBalance < -balanceCredit {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:Insufficient balance: new balance %v would exceed the credit limit %v"), newBalance, balanceCredit)
|
||||
}
|
||||
|
||||
user.Balance = newBalance
|
||||
|
||||
119
object/user_scope.go
Normal file
119
object/user_scope.go
Normal 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
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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
50
util/cert.go
Normal 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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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});
|
||||
}}
|
||||
|
||||
@@ -248,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;
|
||||
@@ -506,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"))} :
|
||||
@@ -807,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>
|
||||
@@ -1308,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);})}>
|
||||
@@ -1318,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"))} :
|
||||
@@ -1353,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>
|
||||
@@ -1452,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) => {
|
||||
@@ -1461,9 +1509,12 @@ 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"},
|
||||
@@ -1488,6 +1539,8 @@ 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>
|
||||
@@ -1547,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>
|
||||
)
|
||||
}
|
||||
@@ -1577,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>
|
||||
@@ -1621,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) => {
|
||||
|
||||
@@ -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,
|
||||
@@ -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"))} :
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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} />)} />
|
||||
|
||||
@@ -193,12 +193,12 @@ function ManagementPage(props) {
|
||||
{
|
||||
renderAvatar()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
{Setting.isMobile() ? null : Setting.getShortText(Setting.getNameAtLeast(props.account.displayName), 30)} <DownOutlined />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</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,
|
||||
|
||||
@@ -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"},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -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"))} :
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -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
@@ -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"),
|
||||
},
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")}
|
||||
{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
260
web/src/auth/ConsentPage.js
Normal 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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -130,7 +130,7 @@ export function getOAuthGetParameters(params) {
|
||||
}
|
||||
|
||||
let state = getRefinedValue(queries.get("state"));
|
||||
if (state.startsWith("/auth/oauth2/login.php?wantsurl=")) {
|
||||
if (state.startsWith("/auth/oauth2/login.php?wantsurl")) {
|
||||
// state contains URL param encoding for Moodle, URLSearchParams automatically decoded it, so here encode it again
|
||||
state = encodeURIComponent(state);
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
50
web/src/backend/ConsentBackend.js
Normal file
50
web/src/backend/ConsentBackend.js
Normal 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());
|
||||
}
|
||||
@@ -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: [
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -941,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",
|
||||
|
||||
@@ -941,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",
|
||||
|
||||
@@ -941,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",
|
||||
|
||||
@@ -941,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",
|
||||
|
||||
@@ -941,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": "売れました",
|
||||
|
||||
@@ -941,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",
|
||||
|
||||
@@ -941,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",
|
||||
|
||||
@@ -941,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ış",
|
||||
|
||||
@@ -941,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": "Продано",
|
||||
|
||||
@@ -941,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",
|
||||
|
||||
@@ -268,7 +268,7 @@
|
||||
"Admin": "管理工具",
|
||||
"Affiliation URL": "工作单位URL",
|
||||
"Affiliation URL - Tooltip": "工作单位的官网URL",
|
||||
"All": "全部允许",
|
||||
"All": "全部",
|
||||
"Application": "应用",
|
||||
"Application - Tooltip": "可以访问的应用",
|
||||
"Applications": "应用",
|
||||
@@ -941,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": "售出",
|
||||
|
||||
44
web/src/provider/CaptchaProviderFields.js
Normal file
44
web/src/provider/CaptchaProviderFields.js
Normal 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>
|
||||
);
|
||||
}
|
||||
257
web/src/provider/EmailProviderFields.js
Normal file
257
web/src/provider/EmailProviderFields.js
Normal 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>
|
||||
);
|
||||
}
|
||||
48
web/src/provider/FaceIDProviderFields.js
Normal file
48
web/src/provider/FaceIDProviderFields.js
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
34
web/src/provider/IDVerificationProviderFields.js
Normal file
34
web/src/provider/IDVerificationProviderFields.js
Normal 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>
|
||||
);
|
||||
}
|
||||
56
web/src/provider/MfaProviderFields.js
Normal file
56
web/src/provider/MfaProviderFields.js
Normal 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>
|
||||
);
|
||||
}
|
||||
103
web/src/provider/NotificationProviderFields.js
Normal file
103
web/src/provider/NotificationProviderFields.js
Normal 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>
|
||||
);
|
||||
}
|
||||
218
web/src/provider/OAuthProviderFields.js
Normal file
218
web/src/provider/OAuthProviderFields.js
Normal file
@@ -0,0 +1,218 @@
|
||||
// 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, Radio, Row, Switch} from "antd";
|
||||
import {LinkOutlined} from "@ant-design/icons";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
const {TextArea} = Input;
|
||||
|
||||
export function renderOAuthProviderFields(provider, updateProviderField, renderUserMappingInput) {
|
||||
const getDomainLabel = provider => {
|
||||
switch (provider.category) {
|
||||
case "OAuth":
|
||||
if (provider.type === "AzureAD" || provider.type === "AzureADB2C") {
|
||||
return Setting.getLabel(i18next.t("provider:Tenant ID"), i18next.t("provider:Tenant ID - Tooltip"));
|
||||
} else {
|
||||
return Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"));
|
||||
}
|
||||
default:
|
||||
return Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Email regex"), i18next.t("provider:Email regex - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<TextArea rows={4} value={provider.emailRegex} onChange={e => {
|
||||
updateProviderField("emailRegex", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
provider.type !== "WeChat" ? null : (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Use WeChat Media Platform in PC"), i18next.t("provider:Use WeChat Media Platform in PC - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch disabled={!provider.clientId} checked={provider.disableSsl} onChange={checked => {
|
||||
updateProviderField("disableSsl", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("token:Access token"), i18next.t("token:Access token - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={provider.content} disabled={!provider.disableSsl || !provider.clientId2} onChange={e => {
|
||||
updateProviderField("content", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Follow-up action"), i18next.t("provider:Follow-up action - Tooltip"))} :
|
||||
</Col>
|
||||
<Col>
|
||||
<Radio.Group value={provider.signName}
|
||||
disabled={!provider.disableSsl || !provider.clientId || !provider.clientId2}
|
||||
buttonStyle="solid"
|
||||
onChange={e => {
|
||||
updateProviderField("signName", e.target.value);
|
||||
}}>
|
||||
<Radio.Button value="open">{i18next.t("provider:Use WeChat Open Platform to login")}</Radio.Button>
|
||||
<Radio.Button value="media">{i18next.t("provider:Use WeChat Media Platform to login")}</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
{
|
||||
provider.type !== "ADFS" && provider.type !== "AzureAD"
|
||||
&& provider.type !== "AzureADB2C" && (provider.type !== "Casdoor" && provider.category !== "Storage")
|
||||
&& provider.type !== "Okta" && provider.type !== "Nextcloud" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{getDomainLabel(provider)} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input prefix={<LinkOutlined />} value={provider.domain} onChange={e => {
|
||||
updateProviderField("domain", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
{
|
||||
provider.type !== "Google" && provider.type !== "Lark" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{provider.type === "Google" ?
|
||||
Setting.getLabel(i18next.t("provider:Get phone number"), i18next.t("provider:Get phone number - Tooltip"))
|
||||
: Setting.getLabel(i18next.t("provider:Use global endpoint"), i18next.t("provider:Use global endpoint - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch disabled={!provider.clientId} checked={provider.disableSsl} onChange={checked => {
|
||||
updateProviderField("disableSsl", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
{
|
||||
provider.type.startsWith("Custom") ? (
|
||||
<React.Fragment>
|
||||
<Col>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Auth URL"), i18next.t("provider:Auth URL - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={provider.customAuthUrl} onChange={e => {
|
||||
updateProviderField("customAuthUrl", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Token URL"), i18next.t("provider:Token URL - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={provider.customTokenUrl} onChange={e => {
|
||||
updateProviderField("customTokenUrl", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={provider.scopes} onChange={e => {
|
||||
updateProviderField("scopes", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:UserInfo URL"), i18next.t("provider:UserInfo URL - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={provider.customUserInfoUrl} onChange={e => {
|
||||
updateProviderField("customUserInfoUrl", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Enable PKCE"), i18next.t("provider:Enable PKCE - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Switch checked={provider.enablePkce} onChange={checked => {
|
||||
updateProviderField("enablePkce", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:User mapping"), i18next.t("provider:User mapping - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
{renderUserMappingInput()}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Favicon"), i18next.t("general:Favicon - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
|
||||
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={23} >
|
||||
<Input prefix={<LinkOutlined />} value={provider.customLogo} onChange={e => {
|
||||
updateProviderField("customLogo", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
|
||||
{i18next.t("general:Preview")}:
|
||||
</Col>
|
||||
<Col span={23} >
|
||||
<a target="_blank" rel="noreferrer" href={provider.customLogo}>
|
||||
<img src={provider.customLogo} alt={provider.customLogo} height={90} style={{marginBottom: "20px"}} />
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
) : null
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
72
web/src/provider/PaymentProviderFields.js
Normal file
72
web/src/provider/PaymentProviderFields.js
Normal file
@@ -0,0 +1,72 @@
|
||||
// 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, Select} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
import {LinkOutlined} from "@ant-design/icons";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
export function renderPaymentProviderFields(provider, updateProviderField, certs) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{
|
||||
(provider.type === "Alipay" || provider.type === "WeChat Pay" || provider.type === "Casdoor") ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Cert"), i18next.t("general:Cert - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={provider.cert} onChange={(value => {updateProviderField("cert", value);})}>
|
||||
{
|
||||
certs.map((cert, index) => <Option key={index} value={cert.name}>{cert.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
(provider.type === "Alipay") ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Root cert"), i18next.t("general:Root cert - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={provider.metadata} onChange={(value => {updateProviderField("metadata", value);})}>
|
||||
{
|
||||
certs.map((cert, index) => <Option key={index} value={cert.name}>{cert.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
{(provider.type === "GC" || provider.type === "FastSpring") ? (
|
||||
<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>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
133
web/src/provider/SamlProviderFields.js
Normal file
133
web/src/provider/SamlProviderFields.js
Normal file
@@ -0,0 +1,133 @@
|
||||
// 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, Switch} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
import {authConfig} from "../auth/Auth";
|
||||
import copy from "copy-to-clipboard";
|
||||
|
||||
const {TextArea} = Input;
|
||||
|
||||
export function renderSamlProviderFields(provider, updateProviderField, metadataConfig) {
|
||||
const {requestUrl, setRequestUrl, metadataLoading, fetchSamlMetadata, parseSamlMetadata} = metadataConfig;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Sign request"), i18next.t("provider:Sign request - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Switch checked={provider.enableSignAuthnRequest} onChange={checked => {
|
||||
updateProviderField("enableSignAuthnRequest", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Metadata url"), i18next.t("provider:Metadata url - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={6} >
|
||||
<Input value={requestUrl} onChange={e => {
|
||||
setRequestUrl(e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
<Col span={16} >
|
||||
<Button style={{marginLeft: "10px"}} type="primary" loading={metadataLoading} onClick={() => {fetchSamlMetadata();}}>{i18next.t("general:Request")}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<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>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{marginTop: "5px"}} span={2} />
|
||||
<Col span={2}>
|
||||
<Button type="primary" onClick={() => {parseSamlMetadata();}}>
|
||||
{i18next.t("provider:Parse")}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:SAML 2.0 Endpoint (HTTP)"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={provider.endpoint} onChange={e => {
|
||||
updateProviderField("endpoint", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:IdP"), i18next.t("provider:IdP certificate"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={provider.idP} onChange={e => {
|
||||
updateProviderField("idP", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Issuer URL"), i18next.t("provider:Issuer URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={provider.issuerUrl} onChange={e => {
|
||||
updateProviderField("issuerUrl", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:SP ACS URL"), i18next.t("provider:SP ACS URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Input value={`${authConfig.serverUrl}/api/acs`} readOnly="readonly" />
|
||||
</Col>
|
||||
<Col span={1}>
|
||||
<Button type="primary" onClick={() => {
|
||||
copy(`${authConfig.serverUrl}/api/acs`);
|
||||
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||
}}>
|
||||
{i18next.t("general:Copy")}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:SP Entity ID"), i18next.t("provider:SP Entity ID - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Input value={`${authConfig.serverUrl}/api/acs`} readOnly="readonly" />
|
||||
</Col>
|
||||
<Col span={1}>
|
||||
<Button type="primary" onClick={() => {
|
||||
copy(`${authConfig.serverUrl}/api/acs`);
|
||||
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||
}}>
|
||||
{i18next.t("general:Copy")}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
182
web/src/provider/SmsProviderFields.js
Normal file
182
web/src/provider/SmsProviderFields.js
Normal file
@@ -0,0 +1,182 @@
|
||||
// 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, Switch} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
import * as ProviderEditTestSms from "../common/TestSmsWidget";
|
||||
import {CountryCodeSelect} from "../common/select/CountryCodeSelect";
|
||||
import HttpHeaderTable from "../table/HttpHeaderTable";
|
||||
import {LinkOutlined} from "@ant-design/icons";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
const SMS_PROVIDERS_WITHOUT_SIGN_NAME = ["Custom HTTP SMS", "Twilio SMS", "Amazon SNS", "Msg91 SMS", "Infobip SMS"];
|
||||
const SMS_PROVIDERS_WITHOUT_TEMPLATE_CODE = ["Infobip SMS", "Custom HTTP SMS"];
|
||||
|
||||
export function renderSmsProviderFields(provider, updateProviderField, renderSmsMappingInput, account) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{SMS_PROVIDERS_WITHOUT_SIGN_NAME.includes(provider.type) ?
|
||||
null :
|
||||
(<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Sign Name"), i18next.t("provider:Sign Name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={provider.signName} onChange={e => {
|
||||
updateProviderField("signName", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
{SMS_PROVIDERS_WITHOUT_TEMPLATE_CODE.includes(provider.type) ?
|
||||
null :
|
||||
(<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Template code"), i18next.t("provider:Template code - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={provider.templateCode} onChange={e => {
|
||||
updateProviderField("templateCode", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
{
|
||||
provider.type === "Custom HTTP SMS" ? (
|
||||
<React.Fragment>
|
||||
<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>
|
||||
<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}>
|
||||
{renderSmsMappingInput()}
|
||||
</Col>
|
||||
</Row> : null}
|
||||
<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>
|
||||
</React.Fragment>
|
||||
) : null
|
||||
}
|
||||
<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>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:SMS Test"), i18next.t("provider:SMS Test - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={4} >
|
||||
<Input.Group compact>
|
||||
<CountryCodeSelect
|
||||
style={{width: "90px"}}
|
||||
initValue={provider.content}
|
||||
onChange={(value) => {
|
||||
updateProviderField("content", value);
|
||||
}}
|
||||
countryCodes={account.organization.countryCodes}
|
||||
/>
|
||||
<Input value={provider.receiver}
|
||||
style={{width: "150px"}}
|
||||
placeholder = {i18next.t("user:Input your phone number")}
|
||||
onChange={e => {
|
||||
updateProviderField("receiver", e.target.value);
|
||||
}} />
|
||||
</Input.Group>
|
||||
</Col>
|
||||
<Col span={2} >
|
||||
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary"
|
||||
disabled={!Setting.isValidPhone(provider.receiver) || (provider.type === "Custom HTTP SMS" && provider.endpoint === "")}
|
||||
onClick={() => ProviderEditTestSms.sendTestSms(provider, "+" + Setting.getCountryCode(provider.content) + provider.receiver)} >
|
||||
{i18next.t("provider:Send Testing SMS")}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
112
web/src/provider/StorageProviderFields.js
Normal file
112
web/src/provider/StorageProviderFields.js
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
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 renderStorageProviderFields(provider, updateProviderField) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{["Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor"].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>
|
||||
)}
|
||||
{["Local File System"].includes(provider.type) ? null : (
|
||||
<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>
|
||||
)}
|
||||
{["Local File System"].includes(provider.type) ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{["Casdoor"].includes(provider.type) ?
|
||||
Setting.getLabel(i18next.t("general:Provider"), i18next.t("general:Provider - Tooltip"))
|
||||
: Setting.getLabel(i18next.t("provider:Bucket"), i18next.t("provider:Bucket - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={provider.bucket} onChange={e => {
|
||||
updateProviderField("bucket", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={provider.pathPrefix} onChange={e => {
|
||||
updateProviderField("pathPrefix", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
{["Synology", "Casdoor"].includes(provider.type) ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input prefix={<LinkOutlined />} value={provider.domain} disabled={provider.type === "Local File System"} onChange={e => {
|
||||
updateProviderField("domain", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
{["Casdoor"].includes(provider.type) ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={provider.content} onChange={e => {
|
||||
updateProviderField("content", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
) : null}
|
||||
{["AWS S3", "Tencent Cloud COS", "Qiniu Cloud Kodo", "Casdoor", "CUCloud OSS", "MinIO"].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}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user