forked from casdoor/casdoor
Compare commits
5 Commits
v2.332.0
...
copilot/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6ab3a18ea | ||
|
|
b7fa3a6194 | ||
|
|
ac1307e576 | ||
|
|
7f2c238eba | ||
|
|
2cf83d3b0c |
@@ -1,5 +1,5 @@
|
||||
<h1 align="center" style="border-bottom: none;">📦⚡️ Casdoor</h1>
|
||||
<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>
|
||||
<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>
|
||||
<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">
|
||||
|
||||
@@ -30,6 +30,8 @@ ldapsServerPort = 636
|
||||
radiusServerPort = 1812
|
||||
radiusDefaultOrganization = "built-in"
|
||||
radiusSecret = "secret"
|
||||
proxyHttpPort =
|
||||
proxyHttpsPort =
|
||||
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
|
||||
logConfig = {"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
|
||||
initDataNewOnly = false
|
||||
|
||||
@@ -185,14 +185,10 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
} else {
|
||||
scope := c.Ctx.Input.Query("scope")
|
||||
nonce := c.Ctx.Input.Query("nonce")
|
||||
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)
|
||||
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)
|
||||
@@ -743,11 +739,7 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
|
||||
// OAuth
|
||||
idpInfo, err := object.FromProviderToIdpInfo(c.Ctx, provider)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
idpInfo := object.FromProviderToIdpInfo(c.Ctx, provider)
|
||||
idpInfo.CodeVerifier = authForm.CodeVerifier
|
||||
var idProvider idp.IdProvider
|
||||
idProvider, err = idp.GetIdProvider(idpInfo, authForm.RedirectUri)
|
||||
@@ -958,13 +950,11 @@ 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 or application's default group
|
||||
// Set group from invitation code if available, otherwise use provider's signup 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
|
||||
|
||||
@@ -264,31 +264,27 @@ func rsaSignWithRSA256(signContent string, privateKey string) (string, error) {
|
||||
|
||||
// privateKey in database is a string, format it to PEM style
|
||||
func formatPrivateKey(privateKey string) string {
|
||||
// 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")
|
||||
// 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
|
||||
}
|
||||
}
|
||||
privateKey = strings.Trim(preFmtPrivateKey, "\n")
|
||||
|
||||
// add pkcs#8 BEGIN and END
|
||||
return "-----BEGIN PRIVATE KEY-----\n" + builder.String() + "\n-----END PRIVATE KEY-----"
|
||||
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
|
||||
}
|
||||
|
||||
@@ -67,8 +67,6 @@
|
||||
{"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"},
|
||||
@@ -83,7 +81,6 @@
|
||||
{"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"},
|
||||
@@ -104,7 +101,6 @@
|
||||
{"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"},
|
||||
@@ -114,14 +110,9 @@
|
||||
{"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"}
|
||||
{"name": "MFA accounts", "visible": true, "viewRule": "Self", "modifyRule": "Self"}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
2
main.go
2
main.go
@@ -72,6 +72,7 @@ func main() {
|
||||
object.InitFromFile()
|
||||
object.InitCasvisorConfig()
|
||||
object.InitCleanupTokens()
|
||||
object.InitApplicationMap()
|
||||
|
||||
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
|
||||
util.SafeGoroutine(func() { controllers.InitCLIDownloader() })
|
||||
@@ -125,6 +126,7 @@ func main() {
|
||||
go ldap.StartLdapServer()
|
||||
go radius.StartRadiusServer()
|
||||
go object.ClearThroughputPerSecond()
|
||||
go proxy.StartProxyServer()
|
||||
|
||||
web.Run(fmt.Sprintf(":%v", port))
|
||||
}
|
||||
|
||||
@@ -173,6 +173,16 @@ func GetOrganizationApplicationCount(owner, organization, field, value string) (
|
||||
return session.Where("organization = ? or is_shared = ? ", organization, true).Count(&Application{})
|
||||
}
|
||||
|
||||
func GetGlobalApplications() ([]*Application, error) {
|
||||
applications := []*Application{}
|
||||
err := ormer.Engine.Desc("created_time").Find(&applications)
|
||||
if err != nil {
|
||||
return applications, err
|
||||
}
|
||||
|
||||
return applications, nil
|
||||
}
|
||||
|
||||
func GetApplications(owner string) ([]*Application, error) {
|
||||
applications := []*Application{}
|
||||
err := ormer.Engine.Desc("created_time").Find(&applications, &Application{Owner: owner})
|
||||
@@ -758,6 +768,12 @@ func UpdateApplication(id string, application *Application, isGlobalAdmin bool,
|
||||
return false, err
|
||||
}
|
||||
|
||||
if affected != 0 {
|
||||
if err := RefreshApplicationCache(); err != nil {
|
||||
fmt.Printf("Failed to refresh application cache after update: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
@@ -809,6 +825,12 @@ func AddApplication(application *Application) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if affected != 0 {
|
||||
if err := RefreshApplicationCache(); err != nil {
|
||||
fmt.Printf("Failed to refresh application cache after add: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
@@ -818,6 +840,12 @@ func deleteApplication(application *Application) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if affected != 0 {
|
||||
if err := RefreshApplicationCache(); err != nil {
|
||||
fmt.Printf("Failed to refresh application cache after delete: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
|
||||
85
object/application_cache.go
Normal file
85
object/application_cache.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
)
|
||||
|
||||
var (
|
||||
applicationMap = make(map[string]*Application)
|
||||
applicationMapMutex sync.RWMutex
|
||||
)
|
||||
|
||||
func InitApplicationMap() error {
|
||||
// Set up the application lookup function for the proxy package
|
||||
proxy.SetApplicationLookup(func(domain string) *proxy.Application {
|
||||
app := GetApplicationByDomain(domain)
|
||||
if app == nil {
|
||||
return nil
|
||||
}
|
||||
return &proxy.Application{
|
||||
Owner: app.Owner,
|
||||
Name: app.Name,
|
||||
UpstreamHost: app.UpstreamHost,
|
||||
}
|
||||
})
|
||||
|
||||
return refreshApplicationMap()
|
||||
}
|
||||
|
||||
func refreshApplicationMap() error {
|
||||
applications, err := GetGlobalApplications()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get global applications: %w", err)
|
||||
}
|
||||
|
||||
newApplicationMap := make(map[string]*Application)
|
||||
for _, app := range applications {
|
||||
if app.Domain != "" {
|
||||
newApplicationMap[strings.ToLower(app.Domain)] = app
|
||||
}
|
||||
for _, domain := range app.OtherDomains {
|
||||
if domain != "" {
|
||||
newApplicationMap[strings.ToLower(domain)] = app
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applicationMapMutex.Lock()
|
||||
applicationMap = newApplicationMap
|
||||
applicationMapMutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetApplicationByDomain(domain string) *Application {
|
||||
applicationMapMutex.RLock()
|
||||
defer applicationMapMutex.RUnlock()
|
||||
|
||||
domain = strings.ToLower(domain)
|
||||
if app, ok := applicationMap[domain]; ok {
|
||||
return app
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func RefreshApplicationCache() error {
|
||||
return refreshApplicationMap()
|
||||
}
|
||||
@@ -53,8 +53,6 @@ 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"},
|
||||
@@ -69,7 +67,6 @@ 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"},
|
||||
@@ -90,7 +87,6 @@ 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"},
|
||||
@@ -100,14 +96,9 @@ func getBuiltInAccountItems() []*AccountItem {
|
||||
{Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
{Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "MFA items", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Last change password time", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Face ID", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "MFA accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Need update password", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
{Name: "IP whitelist", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,12 +62,6 @@ 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,8 +33,6 @@ 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, error) {
|
||||
func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.ProviderInfo {
|
||||
providerInfo := &idp.ProviderInfo{
|
||||
Type: provider.Type,
|
||||
SubType: provider.SubType,
|
||||
@@ -588,19 +588,9 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) (*idp.Provi
|
||||
}
|
||||
} 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, nil
|
||||
return providerInfo
|
||||
}
|
||||
|
||||
func GetIdvProviderFromProvider(provider *Provider) idv.IdvProvider {
|
||||
|
||||
@@ -154,10 +154,6 @@ 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
|
||||
@@ -490,28 +486,6 @@ 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
|
||||
@@ -741,13 +715,6 @@ 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
|
||||
@@ -829,12 +796,6 @@ 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(),
|
||||
@@ -874,13 +835,6 @@ 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
|
||||
|
||||
@@ -180,7 +180,6 @@ 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"`
|
||||
@@ -861,16 +860,16 @@ 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", "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",
|
||||
"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",
|
||||
"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", "telegram", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo",
|
||||
"yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist", "mfa_remember_deadline",
|
||||
"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",
|
||||
}
|
||||
}
|
||||
|
||||
229
proxy/reverse_proxy.go
Normal file
229
proxy/reverse_proxy.go
Normal file
@@ -0,0 +1,229 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/core/logs"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
)
|
||||
|
||||
// Application represents a simplified application structure for reverse proxy
|
||||
type Application struct {
|
||||
Owner string
|
||||
Name string
|
||||
UpstreamHost string
|
||||
}
|
||||
|
||||
// ApplicationLookupFunc is a function type for looking up applications by domain
|
||||
type ApplicationLookupFunc func(domain string) *Application
|
||||
|
||||
var applicationLookup ApplicationLookupFunc
|
||||
|
||||
// SetApplicationLookup sets the function to use for looking up applications by domain
|
||||
func SetApplicationLookup(lookupFunc ApplicationLookupFunc) {
|
||||
applicationLookup = lookupFunc
|
||||
}
|
||||
|
||||
// getDomainWithoutPort removes the port from a domain string
|
||||
func getDomainWithoutPort(domain string) string {
|
||||
if !strings.Contains(domain, ":") {
|
||||
return domain
|
||||
}
|
||||
|
||||
tokens := strings.SplitN(domain, ":", 2)
|
||||
if len(tokens) > 1 {
|
||||
return tokens[0]
|
||||
}
|
||||
return domain
|
||||
}
|
||||
|
||||
// forwardHandler creates and configures a reverse proxy for the given target URL
|
||||
func forwardHandler(targetUrl string, writer http.ResponseWriter, request *http.Request) {
|
||||
target, err := url.Parse(targetUrl)
|
||||
if err != nil {
|
||||
logs.Error("Failed to parse target URL %s: %v", targetUrl, err)
|
||||
http.Error(writer, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||
|
||||
// Configure the Director to set proper headers
|
||||
proxy.Director = func(r *http.Request) {
|
||||
r.URL.Scheme = target.Scheme
|
||||
r.URL.Host = target.Host
|
||||
r.Host = target.Host
|
||||
|
||||
// Set X-Real-IP and X-Forwarded-For headers
|
||||
if clientIP, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
r.Header.Set("X-Forwarded-For", fmt.Sprintf("%s, %s", xff, clientIP))
|
||||
} else {
|
||||
r.Header.Set("X-Forwarded-For", clientIP)
|
||||
}
|
||||
r.Header.Set("X-Real-IP", clientIP)
|
||||
}
|
||||
|
||||
// Set X-Forwarded-Proto header
|
||||
if r.TLS != nil {
|
||||
r.Header.Set("X-Forwarded-Proto", "https")
|
||||
} else {
|
||||
r.Header.Set("X-Forwarded-Proto", "http")
|
||||
}
|
||||
|
||||
// Set X-Forwarded-Host header
|
||||
r.Header.Set("X-Forwarded-Host", request.Host)
|
||||
}
|
||||
|
||||
// Handle ModifyResponse for security enhancements
|
||||
proxy.ModifyResponse = func(resp *http.Response) error {
|
||||
// Add Secure flag to all Set-Cookie headers in HTTPS responses
|
||||
if request.TLS != nil {
|
||||
// Add HSTS header for HTTPS responses if not already set by backend
|
||||
if resp.Header.Get("Strict-Transport-Security") == "" {
|
||||
resp.Header.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||||
}
|
||||
|
||||
cookies := resp.Header["Set-Cookie"]
|
||||
if len(cookies) > 0 {
|
||||
// Clear existing Set-Cookie headers
|
||||
resp.Header.Del("Set-Cookie")
|
||||
// Add them back with Secure flag if not already present
|
||||
for _, cookie := range cookies {
|
||||
// Check if Secure attribute is already present (case-insensitive)
|
||||
cookieLower := strings.ToLower(cookie)
|
||||
hasSecure := strings.Contains(cookieLower, ";secure;") ||
|
||||
strings.Contains(cookieLower, "; secure;") ||
|
||||
strings.HasSuffix(cookieLower, ";secure") ||
|
||||
strings.HasSuffix(cookieLower, "; secure")
|
||||
if !hasSecure {
|
||||
cookie = cookie + "; Secure"
|
||||
}
|
||||
resp.Header.Add("Set-Cookie", cookie)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(writer, request)
|
||||
}
|
||||
|
||||
// HandleReverseProxy handles incoming requests and forwards them to the appropriate upstream
|
||||
func HandleReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||
domain := getDomainWithoutPort(r.Host)
|
||||
|
||||
if applicationLookup == nil {
|
||||
logs.Error("Application lookup function not set")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Lookup the application by domain
|
||||
app := applicationLookup(domain)
|
||||
if app == nil {
|
||||
logs.Info("No application found for domain: %s", domain)
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the application has an upstream host configured
|
||||
if app.UpstreamHost == "" {
|
||||
logs.Warn("Application %s/%s has no upstream host configured", app.Owner, app.Name)
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Build the target URL - just use the upstream host, the actual path/query will be set by the proxy Director
|
||||
targetUrl := app.UpstreamHost
|
||||
if !strings.HasPrefix(targetUrl, "http://") && !strings.HasPrefix(targetUrl, "https://") {
|
||||
targetUrl = "http://" + targetUrl
|
||||
}
|
||||
|
||||
logs.Debug("Forwarding request from %s%s to %s", r.Host, r.RequestURI, targetUrl)
|
||||
forwardHandler(targetUrl, w, r)
|
||||
}
|
||||
|
||||
// StartProxyServer starts the HTTP and HTTPS proxy servers based on configuration
|
||||
func StartProxyServer() {
|
||||
proxyHttpPort := conf.GetConfigString("proxyHttpPort")
|
||||
proxyHttpsPort := conf.GetConfigString("proxyHttpsPort")
|
||||
|
||||
if proxyHttpPort == "" && proxyHttpsPort == "" {
|
||||
logs.Info("Reverse proxy not enabled (proxyHttpPort and proxyHttpsPort are empty)")
|
||||
return
|
||||
}
|
||||
|
||||
serverMux := http.NewServeMux()
|
||||
serverMux.HandleFunc("/", HandleReverseProxy)
|
||||
|
||||
// Start HTTP proxy if configured
|
||||
if proxyHttpPort != "" {
|
||||
go func() {
|
||||
addr := fmt.Sprintf(":%s", proxyHttpPort)
|
||||
logs.Info("Starting reverse proxy HTTP server on %s", addr)
|
||||
err := http.ListenAndServe(addr, serverMux)
|
||||
if err != nil {
|
||||
logs.Error("Failed to start HTTP proxy server: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Start HTTPS proxy if configured
|
||||
if proxyHttpsPort != "" {
|
||||
go func() {
|
||||
addr := fmt.Sprintf(":%s", proxyHttpsPort)
|
||||
|
||||
// For now, HTTPS will need certificate configuration
|
||||
// This can be enhanced later to use Application's SslCert field
|
||||
logs.Info("HTTPS proxy server on %s requires certificate configuration - not implemented yet", addr)
|
||||
|
||||
// When implemented, use code like:
|
||||
// server := &http.Server{
|
||||
// Handler: serverMux,
|
||||
// Addr: addr,
|
||||
// TLSConfig: &tls.Config{
|
||||
// MinVersion: tls.VersionTLS12,
|
||||
// PreferServerCipherSuites: true,
|
||||
// CipherSuites: []uint16{
|
||||
// tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
// tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
// tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
// tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
// tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
// tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
// },
|
||||
// CurvePreferences: []tls.CurveID{
|
||||
// tls.X25519,
|
||||
// tls.CurveP256,
|
||||
// tls.CurveP384,
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// err := server.ListenAndServeTLS("", "")
|
||||
// if err != nil {
|
||||
// logs.Error("Failed to start HTTPS proxy server: %v", err)
|
||||
// }
|
||||
}()
|
||||
}
|
||||
}
|
||||
210
proxy/reverse_proxy_integration_test.go
Normal file
210
proxy/reverse_proxy_integration_test.go
Normal file
@@ -0,0 +1,210 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestReverseProxyIntegration tests the reverse proxy with a real backend server
|
||||
func TestReverseProxyIntegration(t *testing.T) {
|
||||
// Create a test backend server that echoes the request path
|
||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify headers
|
||||
headers := []string{
|
||||
"X-Forwarded-For",
|
||||
"X-Forwarded-Proto",
|
||||
"X-Real-IP",
|
||||
"X-Forwarded-Host",
|
||||
}
|
||||
|
||||
for _, header := range headers {
|
||||
if r.Header.Get(header) == "" {
|
||||
t.Errorf("Expected header %s to be set", header)
|
||||
}
|
||||
}
|
||||
|
||||
// Echo the path and query
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Path: " + r.URL.Path + "\n"))
|
||||
w.Write([]byte("Query: " + r.URL.RawQuery + "\n"))
|
||||
w.Write([]byte("Host: " + r.Host + "\n"))
|
||||
}))
|
||||
defer backend.Close()
|
||||
|
||||
// Set up the application lookup
|
||||
SetApplicationLookup(func(domain string) *Application {
|
||||
if domain == "myapp.example.com" {
|
||||
return &Application{
|
||||
Owner: "test-owner",
|
||||
Name: "my-app",
|
||||
UpstreamHost: backend.URL,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Test various request paths
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
query string
|
||||
expected string
|
||||
}{
|
||||
{"Simple path", "/", "", "Path: /\n"},
|
||||
{"Path with segments", "/api/v1/users", "", "Path: /api/v1/users\n"},
|
||||
{"Path with query", "/search", "q=test&limit=10", "Query: q=test&limit=10\n"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
url := "http://myapp.example.com" + tt.path
|
||||
if tt.query != "" {
|
||||
url += "?" + tt.query
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", url, nil)
|
||||
req.Host = "myapp.example.com"
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
HandleReverseProxy(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(w.Body)
|
||||
bodyStr := string(body)
|
||||
|
||||
if !strings.Contains(bodyStr, tt.expected) {
|
||||
t.Errorf("Expected response to contain %q, got %q", tt.expected, bodyStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestReverseProxyWebSocket tests that WebSocket upgrade headers are preserved
|
||||
func TestReverseProxyWebSocket(t *testing.T) {
|
||||
// Note: WebSocket upgrade through httptest.ResponseRecorder has limitations
|
||||
// This test verifies that WebSocket headers are passed through, but
|
||||
// full WebSocket functionality would need integration testing with real servers
|
||||
|
||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify WebSocket headers are present
|
||||
if r.Header.Get("Upgrade") == "websocket" &&
|
||||
r.Header.Get("Connection") != "" &&
|
||||
r.Header.Get("Sec-WebSocket-Version") != "" &&
|
||||
r.Header.Get("Sec-WebSocket-Key") != "" {
|
||||
// Headers are present - this is what we're testing
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("WebSocket headers received"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Missing WebSocket headers"))
|
||||
}
|
||||
}))
|
||||
defer backend.Close()
|
||||
|
||||
SetApplicationLookup(func(domain string) *Application {
|
||||
if domain == "ws.example.com" {
|
||||
return &Application{
|
||||
Owner: "test-owner",
|
||||
Name: "ws-app",
|
||||
UpstreamHost: backend.URL,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "http://ws.example.com/ws", nil)
|
||||
req.Host = "ws.example.com"
|
||||
req.Header.Set("Upgrade", "websocket")
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
req.Header.Set("Sec-WebSocket-Version", "13")
|
||||
req.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
HandleReverseProxy(w, req)
|
||||
|
||||
body, _ := io.ReadAll(w.Body)
|
||||
bodyStr := string(body)
|
||||
|
||||
// We expect the headers to be passed through to the backend
|
||||
if !strings.Contains(bodyStr, "WebSocket headers received") {
|
||||
t.Errorf("WebSocket headers were not properly forwarded. Got: %s", bodyStr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReverseProxyUpstreamHostVariations tests different UpstreamHost formats
|
||||
func TestReverseProxyUpstreamHostVariations(t *testing.T) {
|
||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
}))
|
||||
defer backend.Close()
|
||||
|
||||
// Parse backend URL to get host
|
||||
backendURL, err := url.Parse(backend.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse backend URL: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
upstreamHost string
|
||||
shouldWork bool
|
||||
}{
|
||||
{"Full URL", backend.URL, true},
|
||||
{"Host only", backendURL.Host, true},
|
||||
{"Empty", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
SetApplicationLookup(func(domain string) *Application {
|
||||
if domain == "test.example.com" {
|
||||
return &Application{
|
||||
Owner: "test-owner",
|
||||
Name: "test-app",
|
||||
UpstreamHost: tt.upstreamHost,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "http://test.example.com/", nil)
|
||||
req.Host = "test.example.com"
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
HandleReverseProxy(w, req)
|
||||
|
||||
if tt.shouldWork {
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", w.Code)
|
||||
}
|
||||
} else {
|
||||
if w.Code == http.StatusOK {
|
||||
t.Errorf("Expected failure, but got status 200")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
148
proxy/reverse_proxy_test.go
Normal file
148
proxy/reverse_proxy_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetDomainWithoutPort(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"example.com", "example.com"},
|
||||
{"example.com:8080", "example.com"},
|
||||
{"localhost:3000", "localhost"},
|
||||
{"subdomain.example.com:443", "subdomain.example.com"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := getDomainWithoutPort(test.input)
|
||||
if result != test.expected {
|
||||
t.Errorf("getDomainWithoutPort(%s) = %s; want %s", test.input, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleReverseProxy(t *testing.T) {
|
||||
// Create a test backend server
|
||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check that headers are set correctly
|
||||
if r.Header.Get("X-Forwarded-For") == "" {
|
||||
t.Error("X-Forwarded-For header not set")
|
||||
}
|
||||
if r.Header.Get("X-Forwarded-Proto") == "" {
|
||||
t.Error("X-Forwarded-Proto header not set")
|
||||
}
|
||||
if r.Header.Get("X-Real-IP") == "" {
|
||||
t.Error("X-Real-IP header not set")
|
||||
}
|
||||
if r.Header.Get("X-Forwarded-Host") == "" {
|
||||
t.Error("X-Forwarded-Host header not set")
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, "Backend response")
|
||||
}))
|
||||
defer backend.Close()
|
||||
|
||||
// Set up a mock application lookup function
|
||||
SetApplicationLookup(func(domain string) *Application {
|
||||
if domain == "test.example.com" {
|
||||
return &Application{
|
||||
Owner: "test-owner",
|
||||
Name: "test-app",
|
||||
UpstreamHost: backend.URL,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Test successful proxy
|
||||
req := httptest.NewRequest("GET", "http://test.example.com/path", nil)
|
||||
req.Host = "test.example.com"
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
HandleReverseProxy(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
// Test domain not found
|
||||
req = httptest.NewRequest("GET", "http://unknown.example.com/path", nil)
|
||||
req.Host = "unknown.example.com"
|
||||
w = httptest.NewRecorder()
|
||||
|
||||
HandleReverseProxy(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("Expected status 404 for unknown domain, got %d", w.Code)
|
||||
}
|
||||
|
||||
// Test application without upstream host
|
||||
SetApplicationLookup(func(domain string) *Application {
|
||||
if domain == "no-upstream.example.com" {
|
||||
return &Application{
|
||||
Owner: "test-owner",
|
||||
Name: "test-app-no-upstream",
|
||||
UpstreamHost: "",
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
req = httptest.NewRequest("GET", "http://no-upstream.example.com/path", nil)
|
||||
req.Host = "no-upstream.example.com"
|
||||
w = httptest.NewRecorder()
|
||||
|
||||
HandleReverseProxy(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("Expected status 404 for app without upstream, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationLookup(t *testing.T) {
|
||||
// Test setting and using the application lookup function
|
||||
called := false
|
||||
SetApplicationLookup(func(domain string) *Application {
|
||||
called = true
|
||||
return &Application{
|
||||
Owner: "test",
|
||||
Name: "app",
|
||||
UpstreamHost: "http://localhost:8080",
|
||||
}
|
||||
})
|
||||
|
||||
if applicationLookup == nil {
|
||||
t.Error("applicationLookup should not be nil after SetApplicationLookup")
|
||||
}
|
||||
|
||||
app := applicationLookup("test.com")
|
||||
if !called {
|
||||
t.Error("applicationLookup function was not called")
|
||||
}
|
||||
if app == nil {
|
||||
t.Error("applicationLookup should return non-nil application")
|
||||
}
|
||||
if app.Owner != "test" {
|
||||
t.Errorf("Expected owner 'test', got '%s'", app.Owner)
|
||||
}
|
||||
}
|
||||
@@ -505,160 +505,6 @@ class ApplicationEditPage extends React.Component {
|
||||
</React.Fragment>
|
||||
)}
|
||||
{this.state.activeMenuKey === "authentication" && (
|
||||
<React.Fragment>
|
||||
<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"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<InputNumber style={{width: "150px"}} value={this.state.application.cookieExpireInHours || 720} min={1} step={1} precision={0} addonAfter="Hours" onChange={value => {
|
||||
this.updateApplicationField("cookieExpireInHours", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21}>
|
||||
<PaginateSelect
|
||||
virtual
|
||||
style={{width: "100%"}}
|
||||
allowClear
|
||||
placeholder={i18next.t("general:Default")}
|
||||
value={this.state.application.defaultGroup || undefined}
|
||||
fetchPage={GroupBackend.getGroups}
|
||||
buildFetchArgs={({page, pageSize, searchText}) => {
|
||||
const field = searchText ? "name" : "";
|
||||
return [this.state.owner, false, page, pageSize, field, searchText, "", ""];
|
||||
}}
|
||||
reloadKey={this.state.owner}
|
||||
optionMapper={(group) => Setting.getOption(
|
||||
<Space>
|
||||
{group.type === "Physical" ? <UsergroupAddOutlined /> : <HolderOutlined />}
|
||||
{group.displayName}
|
||||
</Space>,
|
||||
`${group.owner}/${group.name}`
|
||||
)}
|
||||
filterOption={false}
|
||||
onChange={(value) => {
|
||||
this.updateApplicationField("defaultGroup", value || "");
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Enable signup"), i18next.t("application:Enable signup - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.application.enableSignUp} onChange={checked => {
|
||||
this.updateApplicationField("enableSignUp", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Disable signin"), i18next.t("application:Disable signin - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.application.disableSignin} onChange={checked => {
|
||||
this.updateApplicationField("disableSignin", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Enable exclusive signin"), i18next.t("application:Enable exclusive signin - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.application.enableExclusiveSignin} onChange={checked => {
|
||||
this.updateApplicationField("enableExclusiveSignin", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Signin session"), i18next.t("application:Enable signin session - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.application.enableSigninSession} onChange={checked => {
|
||||
if (!checked) {
|
||||
this.updateApplicationField("enableAutoSignin", false);
|
||||
}
|
||||
|
||||
this.updateApplicationField("enableSigninSession", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Auto signin"), i18next.t("application:Auto signin - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.application.enableAutoSignin} onChange={checked => {
|
||||
if (!this.state.application.enableSigninSession && checked) {
|
||||
Setting.showMessage("error", i18next.t("application:Please enable \"Signin session\" first before enabling \"Auto signin\""));
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateApplicationField("enableAutoSignin", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Enable Email linking"), i18next.t("application:Enable Email linking - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.application.enableLinkWithEmail} onChange={checked => {
|
||||
this.updateApplicationField("enableLinkWithEmail", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("general:Signup URL"), i18next.t("general:Signup URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.application.signupUrl} onChange={e => {
|
||||
this.updateApplicationField("signupUrl", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("general:Signin URL"), i18next.t("general:Signin URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.application.signinUrl} onChange={e => {
|
||||
this.updateApplicationField("signinUrl", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("general:Forget URL"), i18next.t("general:Forget URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.application.forgetUrl} onChange={e => {
|
||||
this.updateApplicationField("forgetUrl", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("general:Affiliation URL"), i18next.t("general:Affiliation URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.application.affiliationUrl} onChange={e => {
|
||||
this.updateApplicationField("affiliationUrl", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{this.state.activeMenuKey === "oidc-oauth" && (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
@@ -811,11 +657,157 @@ class ApplicationEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{this.state.activeMenuKey === "saml" && (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<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"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<InputNumber style={{width: "150px"}} value={this.state.application.cookieExpireInHours || 720} min={1} step={1} precision={0} addonAfter="Hours" onChange={value => {
|
||||
this.updateApplicationField("cookieExpireInHours", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21}>
|
||||
<PaginateSelect
|
||||
virtual
|
||||
style={{width: "100%"}}
|
||||
allowClear
|
||||
placeholder={i18next.t("general:Default")}
|
||||
value={this.state.application.defaultGroup || undefined}
|
||||
fetchPage={GroupBackend.getGroups}
|
||||
buildFetchArgs={({page, pageSize, searchText}) => {
|
||||
const field = searchText ? "name" : "";
|
||||
return [this.state.owner, false, page, pageSize, field, searchText, "", ""];
|
||||
}}
|
||||
reloadKey={this.state.owner}
|
||||
optionMapper={(group) => Setting.getOption(
|
||||
<Space>
|
||||
{group.type === "Physical" ? <UsergroupAddOutlined /> : <HolderOutlined />}
|
||||
{group.displayName}
|
||||
</Space>,
|
||||
`${group.owner}/${group.name}`
|
||||
)}
|
||||
filterOption={false}
|
||||
onChange={(value) => {
|
||||
this.updateApplicationField("defaultGroup", value || "");
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Enable signup"), i18next.t("application:Enable signup - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.application.enableSignUp} onChange={checked => {
|
||||
this.updateApplicationField("enableSignUp", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Disable signin"), i18next.t("application:Disable signin - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.application.disableSignin} onChange={checked => {
|
||||
this.updateApplicationField("disableSignin", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Enable exclusive signin"), i18next.t("application:Enable exclusive signin - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.application.enableExclusiveSignin} onChange={checked => {
|
||||
this.updateApplicationField("enableExclusiveSignin", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Signin session"), i18next.t("application:Enable signin session - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.application.enableSigninSession} onChange={checked => {
|
||||
if (!checked) {
|
||||
this.updateApplicationField("enableAutoSignin", false);
|
||||
}
|
||||
|
||||
this.updateApplicationField("enableSigninSession", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Auto signin"), i18next.t("application:Auto signin - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.application.enableAutoSignin} onChange={checked => {
|
||||
if (!this.state.application.enableSigninSession && checked) {
|
||||
Setting.showMessage("error", i18next.t("application:Please enable \"Signin session\" first before enabling \"Auto signin\""));
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateApplicationField("enableAutoSignin", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Enable Email linking"), i18next.t("application:Enable Email linking - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.application.enableLinkWithEmail} onChange={checked => {
|
||||
this.updateApplicationField("enableLinkWithEmail", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("general:Signup URL"), i18next.t("general:Signup URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.application.signupUrl} onChange={e => {
|
||||
this.updateApplicationField("signupUrl", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("general:Signin URL"), i18next.t("general:Signin URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.application.signinUrl} onChange={e => {
|
||||
this.updateApplicationField("signinUrl", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("general:Forget URL"), i18next.t("general:Forget URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.application.forgetUrl} onChange={e => {
|
||||
this.updateApplicationField("forgetUrl", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("general:Affiliation URL"), i18next.t("general:Affiliation URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.application.affiliationUrl} onChange={e => {
|
||||
this.updateApplicationField("affiliationUrl", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<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>
|
||||
@@ -1460,7 +1452,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, height: 38, minHeight: 38}}>
|
||||
<Header style={{background: "inherit", padding: "0px", position: "sticky", top: 0}}>
|
||||
<div className="demo-logo" />
|
||||
<Tabs
|
||||
onChange={(key) => {
|
||||
@@ -1469,12 +1461,9 @@ 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"},
|
||||
@@ -1499,8 +1488,6 @@ 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>
|
||||
|
||||
@@ -57,8 +57,6 @@ 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"},
|
||||
@@ -68,7 +66,6 @@ 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"},
|
||||
@@ -89,8 +86,6 @@ 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"},
|
||||
@@ -104,15 +99,10 @@ class OrganizationListPage extends BaseListPage {
|
||||
{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"},
|
||||
{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"},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ 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;
|
||||
|
||||
@@ -29,6 +30,7 @@ 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",
|
||||
@@ -37,6 +39,7 @@ class PaymentEditPage extends React.Component {
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getPayment();
|
||||
this.getProducts();
|
||||
}
|
||||
|
||||
getPayment() {
|
||||
@@ -55,6 +58,19 @@ 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) {
|
||||
@@ -224,6 +240,29 @@ 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, Col, List, Row, Table, Tooltip} from "antd";
|
||||
import {Button, List, Table, Tooltip} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as PaymentBackend from "./backend/PaymentBackend";
|
||||
@@ -195,31 +195,21 @@ class PaymentListPage extends BaseListPage {
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
renderItem={(productInfo, i) => {
|
||||
const price = productInfo.price || 0;
|
||||
const number = productInfo.quantity || 1;
|
||||
const price = productInfo.price * (productInfo.quantity || 1);
|
||||
const currency = record.currency || "USD";
|
||||
const productName = productInfo.displayName || productInfo.name;
|
||||
return (
|
||||
<List.Item>
|
||||
<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>
|
||||
<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>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
|
||||
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: "140px",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("statusCode"),
|
||||
},
|
||||
|
||||
@@ -457,8 +457,8 @@ export const UserFields = ["owner", "name", "password", "display_name", "id", "t
|
||||
"is_admin", "homepage", "birthday", "gender", "password_type", "password_salt", "external_id", "avatar", "first_name", "last_name",
|
||||
"avatar_type", "permanent_avatar", "email_verified", "region", "location", "address",
|
||||
"affiliation", "title", "id_card_type", "id_card", "real_name", "is_verified", "bio", "tag", "language",
|
||||
"education", "score", "karma", "ranking", "balance", "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",
|
||||
"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",
|
||||
"created_ip", "last_signin_time", "last_signin_ip", "github", "google", "qq", "wechat", "facebook", "dingtalk",
|
||||
"weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs", "baidu", "alipay", "casdoor", "infoflow", "apple",
|
||||
"azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "kwai", "line", "amazon", "auth0",
|
||||
@@ -469,7 +469,7 @@ export const UserFields = ["owner", "name", "password", "display_name", "id", "t
|
||||
"wepay", "xero", "yahoo", "yammer", "yandex", "zoom", "metamask", "web3onboard", "custom", "webauthnCredentials",
|
||||
"preferred_mfa_type", "recovery_codes", "totp_secret", "mfa_phone_enabled", "mfa_email_enabled", "invitation",
|
||||
"invitation_code", "face_ids", "ldap", "properties", "roles", "permissions", "groups", "last_change_password_time",
|
||||
"last_signin_wrong_time", "signin_wrong_times", "managedAccounts", "mfaAccounts", "mfaItems", "need_update_password",
|
||||
"last_signin_wrong_time", "signin_wrong_times", "managedAccounts", "mfaAccounts", "need_update_password",
|
||||
"created_time", "updated_time", "deleted_time",
|
||||
"ip_whitelist"];
|
||||
|
||||
@@ -500,7 +500,6 @@ 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")},
|
||||
@@ -524,8 +523,6 @@ 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")},
|
||||
@@ -540,7 +537,6 @@ 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")},
|
||||
@@ -558,8 +554,6 @@ 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") {
|
||||
|
||||
@@ -609,20 +609,13 @@ class UserEditPage extends React.Component {
|
||||
);
|
||||
} else if (accountItem.name === "Addresses") {
|
||||
return (
|
||||
<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>
|
||||
<AddressTable
|
||||
title={i18next.t("user:Addresses")}
|
||||
table={this.state.user.addresses}
|
||||
onUpdateTable={(value) => {
|
||||
this.updateUserField("addresses", value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (accountItem.name === "Affiliation") {
|
||||
return (
|
||||
@@ -887,7 +880,7 @@ class UserEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("general:Transactions"), i18next.t("general:Transactions"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<TransactionTable title={i18next.t("general:Transactions")} transactions={this.state.transactions} hideTag={true} />
|
||||
<TransactionTable transactions={this.state.transactions} hideTag={true} />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
@@ -1137,21 +1130,15 @@ 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={
|
||||
<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>
|
||||
}>
|
||||
<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
|
||||
}>
|
||||
<List
|
||||
size="small"
|
||||
rowKey="mfaType"
|
||||
itemLayout="horizontal"
|
||||
dataSource={this.state.multiFactorAuths}
|
||||
|
||||
@@ -44,15 +44,20 @@ function generateCodeChallenge(verifier) {
|
||||
}
|
||||
|
||||
function storeCodeVerifier(state, verifier) {
|
||||
localStorage.setItem(`pkce_verifier_${state}`, verifier);
|
||||
localStorage.setItem("pkce_verifier", `${state}#${verifier}`);
|
||||
}
|
||||
|
||||
export function getCodeVerifier(state) {
|
||||
return localStorage.getItem(`pkce_verifier_${state}`);
|
||||
const verifierStore = localStorage.getItem("pkce_verifier");
|
||||
const [storedState, verifier] = verifierStore ? verifierStore.split("#") : [null, null];
|
||||
if (storedState !== state) {
|
||||
return null;
|
||||
}
|
||||
return verifier;
|
||||
}
|
||||
|
||||
export function clearCodeVerifier(state) {
|
||||
localStorage.removeItem(`pkce_verifier_${state}`);
|
||||
localStorage.removeItem("pkce_verifier");
|
||||
}
|
||||
|
||||
const authInfo = {
|
||||
@@ -402,27 +407,24 @@ export function getProviderUrl(provider) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getProviderLogoWidget(provider, options = {}) {
|
||||
export function getProviderLogoWidget(provider) {
|
||||
if (provider === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = getProviderUrl(provider);
|
||||
const disableLink = options.disableLink === true;
|
||||
const imgEl = <img width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />;
|
||||
|
||||
if (url !== "" && !disableLink) {
|
||||
if (url !== "") {
|
||||
return (
|
||||
<Tooltip title={provider.type}>
|
||||
<a target="_blank" rel="noreferrer" href={getProviderUrl(provider)}>
|
||||
{imgEl}
|
||||
<img width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tooltip title={provider.type}>
|
||||
{imgEl}
|
||||
<img width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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%"}}
|
||||
value={value}
|
||||
defaultValue={this.props.defaultValue || undefined}
|
||||
placeholder="Please select country/region"
|
||||
onChange={(val) => {this.onChange(val);}}
|
||||
onChange={(value => {this.onChange(value);})}
|
||||
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
|
||||
filterSort={(optionA, optionB) =>
|
||||
(optionA?.label ?? "").toLowerCase().localeCompare((optionB?.label ?? "").toLowerCase())
|
||||
|
||||
@@ -268,7 +268,7 @@
|
||||
"Admin": "管理工具",
|
||||
"Affiliation URL": "工作单位URL",
|
||||
"Affiliation URL - Tooltip": "工作单位的官网URL",
|
||||
"All": "全部",
|
||||
"All": "全部允许",
|
||||
"Application": "应用",
|
||||
"Application - Tooltip": "可以访问的应用",
|
||||
"Applications": "应用",
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {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>
|
||||
);
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {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
|
||||
}
|
||||
<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"].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"].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"].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>
|
||||
);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {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>
|
||||
);
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {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>
|
||||
);
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {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>
|
||||
);
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {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>
|
||||
);
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {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>
|
||||
);
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {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>
|
||||
);
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {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>
|
||||
);
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {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>
|
||||
);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Checkbox, Col, Row} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
import * as Web3Auth from "../auth/Web3Auth";
|
||||
|
||||
export function renderWeb3ProviderFields(provider, updateProviderField) {
|
||||
const getWalletValue = () => {
|
||||
try {
|
||||
return JSON.parse(provider.metadata);
|
||||
} catch {
|
||||
return ["injected"];
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
provider.type === "Web3Onboard" ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Wallets"), i18next.t("provider:Wallets - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Checkbox.Group
|
||||
options={Web3Auth.getWeb3OnboardWalletsOptions()}
|
||||
value={getWalletValue()}
|
||||
onChange={options => {
|
||||
updateProviderField("metadata", JSON.stringify(options));
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
);
|
||||
}
|
||||
@@ -14,16 +14,12 @@
|
||||
|
||||
import React from "react";
|
||||
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {AutoComplete, Button, Col, Input, Row, Table, Tooltip} from "antd";
|
||||
import {Button, Col, Input, Row, Select, Table, Tooltip} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
import RegionSelect from "../common/select/RegionSelect";
|
||||
|
||||
const TAG_OPTIONS = [
|
||||
{value: "Home", label: "Home"},
|
||||
{value: "Work", label: "Work"},
|
||||
{value: "Other", label: "Other"},
|
||||
];
|
||||
const {Option} = Select;
|
||||
|
||||
class AddressTable extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -90,20 +86,16 @@ class AddressTable extends React.Component {
|
||||
key: "tag",
|
||||
width: "100px",
|
||||
render: (text, record, index) => {
|
||||
const tagOptions = TAG_OPTIONS.map(opt => ({...opt, label: opt.value === "Home" ? i18next.t("general:Home") : opt.value === "Work" ? i18next.t("user:Work") : i18next.t("user:Other")}));
|
||||
return (
|
||||
<AutoComplete
|
||||
size="small"
|
||||
style={{width: "100%"}}
|
||||
value={text || ""}
|
||||
options={tagOptions}
|
||||
<Select virtual={false} style={{width: "100%"}}
|
||||
value={text}
|
||||
onChange={value => {
|
||||
this.updateField(table, index, "tag", value);
|
||||
}}
|
||||
onSelect={value => {
|
||||
this.updateField(table, index, "tag", value);
|
||||
}}
|
||||
/>
|
||||
}} >
|
||||
<Option value="Home">{i18next.t("general:Home")}</Option>
|
||||
<Option value="Work">{i18next.t("user:Work")}</Option>
|
||||
<Option value="Other">{i18next.t("user:Other")}</Option>
|
||||
</Select>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -114,7 +106,7 @@ class AddressTable extends React.Component {
|
||||
width: "150px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input size="small" value={text} onChange={e => {
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "line1", e.target.value);
|
||||
}} />
|
||||
);
|
||||
@@ -127,7 +119,7 @@ class AddressTable extends React.Component {
|
||||
width: "150px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input size="small" value={text} onChange={e => {
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "line2", e.target.value);
|
||||
}} />
|
||||
);
|
||||
@@ -140,7 +132,7 @@ class AddressTable extends React.Component {
|
||||
width: "120px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input size="small" value={text} onChange={e => {
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "city", e.target.value);
|
||||
}} />
|
||||
);
|
||||
@@ -153,7 +145,7 @@ class AddressTable extends React.Component {
|
||||
width: "100px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input size="small" value={text} onChange={e => {
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "state", e.target.value);
|
||||
}} />
|
||||
);
|
||||
@@ -166,7 +158,7 @@ class AddressTable extends React.Component {
|
||||
width: "100px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input size="small" value={text} onChange={e => {
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "zipCode", e.target.value);
|
||||
}} />
|
||||
);
|
||||
@@ -180,7 +172,6 @@ class AddressTable extends React.Component {
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<RegionSelect
|
||||
size="small"
|
||||
value={text}
|
||||
onChange={value => {
|
||||
this.updateField(table, index, "region", value);
|
||||
|
||||
@@ -86,7 +86,7 @@ class ManagedAccountTable extends React.Component {
|
||||
render: (text, record, index) => {
|
||||
const items = this.props.applications;
|
||||
return (
|
||||
<Select virtual={false} size="small" style={{width: "100%"}}
|
||||
<Select virtual={false} style={{width: "100%"}}
|
||||
value={text}
|
||||
onChange={value => {
|
||||
this.updateField(table, index, "application", value);
|
||||
@@ -105,7 +105,7 @@ class ManagedAccountTable extends React.Component {
|
||||
// width: "420px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input size="small" prefix={<LinkOutlined />} value={text} onChange={e => {
|
||||
<Input prefix={<LinkOutlined />} value={text} onChange={e => {
|
||||
this.updateField(table, index, "signinUrl", e.target.value);
|
||||
}} />
|
||||
);
|
||||
@@ -118,7 +118,7 @@ class ManagedAccountTable extends React.Component {
|
||||
width: "200px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input size="small" value={text} onChange={e => {
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "username", e.target.value);
|
||||
}} />
|
||||
);
|
||||
@@ -131,7 +131,7 @@ class ManagedAccountTable extends React.Component {
|
||||
width: "200px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input.Password size="small" value={text} onChange={e => {
|
||||
<Input.Password value={text} onChange={e => {
|
||||
this.updateField(table, index, "password", e.target.value);
|
||||
}} />
|
||||
);
|
||||
|
||||
@@ -86,7 +86,7 @@ class MfaAccountTable extends React.Component {
|
||||
width: "400px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input size="small" value={text} onChange={e => {
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "accountName", e.target.value);
|
||||
}} />
|
||||
);
|
||||
@@ -99,7 +99,7 @@ class MfaAccountTable extends React.Component {
|
||||
width: "300px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input size="small" value={text} onChange={e => {
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "issuer", e.target.value);
|
||||
}} />
|
||||
);
|
||||
@@ -111,7 +111,7 @@ class MfaAccountTable extends React.Component {
|
||||
key: "origin",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input size="small" value={text} onChange={e => {
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "origin", e.target.value);
|
||||
}} />
|
||||
);
|
||||
@@ -123,7 +123,7 @@ class MfaAccountTable extends React.Component {
|
||||
key: "secretKey",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input.Password size="small" value={text} onChange={e => {
|
||||
<Input.Password value={text} onChange={e => {
|
||||
this.updateField(table, index, "secretKey", e.target.value);
|
||||
}} />
|
||||
);
|
||||
|
||||
@@ -84,7 +84,7 @@ class MfaTable extends React.Component {
|
||||
key: "name",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Select virtual={false} size="small" style={{width: "100%"}}
|
||||
<Select virtual={false} style={{width: "100%"}}
|
||||
value={text}
|
||||
onChange={value => {
|
||||
this.updateField(table, index, "name", value);
|
||||
@@ -103,7 +103,7 @@ class MfaTable extends React.Component {
|
||||
width: "100px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Select virtual={false} size="small" style={{width: "100%"}}
|
||||
<Select virtual={false} style={{width: "100%"}}
|
||||
value={text}
|
||||
defaultValue="Optional"
|
||||
options={RuleItems.map((item) =>
|
||||
|
||||
@@ -110,17 +110,7 @@ class ProviderTable extends React.Component {
|
||||
width: "100px",
|
||||
render: (text, record, index) => {
|
||||
const provider = Setting.getArrayItem(this.props.providers, "name", record.name);
|
||||
const owner = provider?.owner || this.getUserOrganization()?.name;
|
||||
const editUrl = provider && owner && provider.name ? `/providers/${owner}/${provider.name}` : null;
|
||||
const categoryText = provider?.category;
|
||||
if (editUrl && categoryText) {
|
||||
return (
|
||||
<a href={editUrl} target="_blank" rel="noopener noreferrer">
|
||||
{categoryText}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return categoryText;
|
||||
return provider?.category;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -130,17 +120,7 @@ class ProviderTable extends React.Component {
|
||||
width: "80px",
|
||||
render: (text, record, index) => {
|
||||
const provider = Setting.getArrayItem(this.props.providers, "name", record.name);
|
||||
const owner = provider?.owner || this.getUserOrganization()?.name;
|
||||
const editUrl = provider && owner && provider.name ? `/providers/${owner}/${provider.name}` : null;
|
||||
const typeWidget = Provider.getProviderLogoWidget(provider, {disableLink: !!editUrl});
|
||||
if (editUrl && typeWidget) {
|
||||
return (
|
||||
<a href={editUrl} target="_blank" rel="noopener noreferrer">
|
||||
{typeWidget}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return typeWidget;
|
||||
return Provider.getProviderLogoWidget(provider);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -131,18 +131,13 @@ class TransactionTable extends React.Component {
|
||||
columns={columns}
|
||||
dataSource={this.props.transactions}
|
||||
rowKey={(record) => `${record.owner}/${record.name}`}
|
||||
size="middle"
|
||||
size="small"
|
||||
bordered
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ["10", "20", "50", "100"],
|
||||
}}
|
||||
title={this.props.title ? () => (
|
||||
<div>
|
||||
{this.props.title}
|
||||
</div>
|
||||
) : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user