Compare commits

...

20 Commits

Author SHA1 Message Date
Yang Luo
bbaa28133f feat: apply application.DefaultGroup for OAuth signups (#5157) 2026-02-22 01:06:18 +08:00
Yang Luo
baef7680ea feat: validate OAuth scopes against Application config; return invalid_scope per RFC 6749 (#5153) 2026-02-21 17:44:26 +08:00
Yang Luo
d15b66177c feat: add missing Telegram field to User struct (#5151) 2026-02-21 17:21:31 +08:00
Yang Luo
5ce6bac529 fix: improve provider table links 2026-02-21 01:36:00 +08:00
Yang Luo
0621f35665 fix: improve tabs height UI in app edit page 2026-02-21 01:16:36 +08:00
Yang Luo
1ac2490419 fix: add OIDC and SAML tabs in application edit page 2026-02-21 01:13:54 +08:00
DacongDA
8c50ada494 feat: refactor provider edit page into different JS files (#5141) 2026-02-21 00:57:38 +08:00
Yang Luo
22da90576e feat: can free input in "Tag" in Addresses table 2026-02-20 16:49:50 +08:00
Yang Luo
b00404cb3a fix: fix RegionSelect cannot save value bug in Addresses table 2026-02-20 16:45:43 +08:00
Yang Luo
2ed27f4f0a fix: improve tables UI in my account page 2026-02-20 16:35:29 +08:00
Yang Luo
bf538d5260 fix: update UpdateUser() columns for missing User fields 2026-02-20 11:02:52 +08:00
Yang Luo
13ee5fd150 feat: sync newOrganization() accountItems with getBuiltInAccountItems() (#5146) 2026-02-20 10:47:02 +08:00
Yang Luo
04cdd5a012 feat: add missing user fields to GetTranslatedUserItems, getBuiltInAccountItems, init_data template, and UserFields (#5144) 2026-02-20 10:37:51 +08:00
Yang Luo
7b4873734b feat: fix "--config" flag to actually load specified configuration file (#5139) 2026-02-19 02:13:29 +08:00
Yang Luo
8d2290944a fix: add back Payment.ProductName and ProductDisplayName fields for backward compatibility 2026-02-18 19:28:14 +08:00
Yang Luo
6a2bba1627 feat: fix field visibility logic for provider types in ProviderEditPage (#5134) 2026-02-18 15:22:28 +08:00
Yang Luo
07554bbbe5 feat: fix Alipay OAuth provider by loading private key from cert object (#5119) 2026-02-17 14:42:21 +08:00
karatekaneen
a050403ee5 feat: fix bug that PKCE fails when multiple custom OAuth providers are configured (#5117) 2026-02-16 23:32:07 +08:00
IsAurora6
118eb0af80 feat: Optimize the display of payment products. (#5115) 2026-02-16 16:32:02 +08:00
Yang Luo
c16aebe642 fix: update README slogan 2026-02-16 02:33:45 +08:00
39 changed files with 1822 additions and 1286 deletions

View File

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

View File

@@ -185,10 +185,14 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
} else {
scope := c.Ctx.Input.Query("scope")
nonce := c.Ctx.Input.Query("nonce")
token, _ := object.GetTokenByUser(application, user, scope, nonce, c.Ctx.Request.Host)
resp = tokenToResponse(token)
if !object.IsScopeValid(scope, application) {
resp = &Response{Status: "error", Msg: "error: invalid_scope", Data: ""}
} else {
token, _ := object.GetTokenByUser(application, user, scope, nonce, c.Ctx.Request.Host)
resp = tokenToResponse(token)
resp.Data3 = user.NeedUpdatePassword
resp.Data3 = user.NeedUpdatePassword
}
}
} else if form.Type == ResponseTypeDevice {
authCache, ok := object.DeviceAuthMap.LoadAndDelete(form.UserCode)
@@ -739,7 +743,11 @@ func (c *ApiController) Login() {
}
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
// OAuth
idpInfo := object.FromProviderToIdpInfo(c.Ctx, provider)
idpInfo, err := object.FromProviderToIdpInfo(c.Ctx, provider)
if err != nil {
c.ResponseError(err.Error())
return
}
idpInfo.CodeVerifier = authForm.CodeVerifier
var idProvider idp.IdProvider
idProvider, err = idp.GetIdProvider(idpInfo, authForm.RedirectUri)
@@ -950,11 +958,13 @@ func (c *ApiController) Login() {
RegisterSource: fmt.Sprintf("%s/%s", application.Organization, application.Name),
}
// Set group from invitation code if available, otherwise use provider's signup group
// Set group from invitation code if available, otherwise use provider's signup group or application's default group
if invitation != nil && invitation.SignupGroup != "" {
user.Groups = []string{invitation.SignupGroup}
} else if providerItem.SignupGroup != "" {
user.Groups = []string{providerItem.SignupGroup}
} else if application.DefaultGroup != "" {
user.Groups = []string{application.DefaultGroup}
}
var affected bool

View File

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

View File

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

View File

@@ -53,6 +53,8 @@ func getBuiltInAccountItems() []*AccountItem {
{Name: "ID", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Name", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Display name", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "First name", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Last name", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Avatar", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "User type", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Password", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
@@ -67,6 +69,7 @@ func getBuiltInAccountItems() []*AccountItem {
{Name: "Title", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "ID card type", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "ID card", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "ID card info", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Real name", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "ID verification", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Homepage", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
@@ -87,6 +90,7 @@ 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"},
@@ -96,9 +100,14 @@ func getBuiltInAccountItems() []*AccountItem {
{Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "MFA items", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Last change password time", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Face ID", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "MFA accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Need update password", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "IP whitelist", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
}
}

View File

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

View File

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

View File

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

View File

@@ -154,6 +154,10 @@ func CheckOAuthLogin(clientId string, responseType string, redirectUri string, s
return fmt.Sprintf(i18n.Translate(lang, "token:Redirect URI: %s doesn't exist in the allowed Redirect URI list"), redirectUri), application, nil
}
if !IsScopeValid(scope, application) {
return i18n.Translate(lang, "token:Invalid scope"), application, nil
}
// Mask application for /api/get-app-login
application.ClientSecret = ""
return "", application, nil
@@ -486,6 +490,28 @@ func IsGrantTypeValid(method string, grantTypes []string) bool {
return false
}
// IsScopeValid checks whether all space-separated scopes in the scope string
// are defined in the application's Scopes list.
// If the application has no defined scopes, every scope is considered valid
// (backward-compatible behaviour).
func IsScopeValid(scope string, application *Application) bool {
if len(application.Scopes) == 0 || scope == "" {
return true
}
allowed := make(map[string]bool, len(application.Scopes))
for _, s := range application.Scopes {
allowed[s.Name] = true
}
for _, s := range strings.Fields(scope) {
if !allowed[s] {
return false
}
}
return true
}
// createGuestUserToken creates a new guest user and returns a token for them
func createGuestUserToken(application *Application, clientSecret string, verifier string) (*Token, *TokenError, error) {
// Verify client secret if provided
@@ -715,6 +741,13 @@ func GetAuthorizationCodeToken(application *Application, clientSecret string, co
// GetPasswordToken
// Resource Owner Password Credentials flow
func GetPasswordToken(application *Application, username string, password string, scope string, host string) (*Token, *TokenError, error) {
if !IsScopeValid(scope, application) {
return nil, &TokenError{
Error: InvalidScope,
ErrorDescription: "the requested scope is invalid or not defined in the application",
}, nil
}
user, err := GetUserByFields(application.Organization, username)
if err != nil {
return nil, nil, err
@@ -796,6 +829,12 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
ErrorDescription: "client_secret is invalid",
}, nil
}
if !IsScopeValid(scope, application) {
return nil, &TokenError{
Error: InvalidScope,
ErrorDescription: "the requested scope is invalid or not defined in the application",
}, nil
}
nullUser := &User{
Owner: application.Owner,
Id: application.GetId(),
@@ -835,6 +874,13 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
// GetImplicitToken
// Implicit flow
func GetImplicitToken(application *Application, username string, scope string, nonce string, host string) (*Token, *TokenError, error) {
if !IsScopeValid(scope, application) {
return nil, &TokenError{
Error: InvalidScope,
ErrorDescription: "the requested scope is invalid or not defined in the application",
}, nil
}
user, err := GetUserByFields(application.Organization, username)
if err != nil {
return nil, nil, err

View File

@@ -180,6 +180,7 @@ type User struct {
Spotify string `xorm:"spotify varchar(100)" json:"spotify"`
Strava string `xorm:"strava varchar(100)" json:"strava"`
Stripe string `xorm:"stripe varchar(100)" json:"stripe"`
Telegram string `xorm:"telegram varchar(100)" json:"telegram"`
TikTok string `xorm:"tiktok varchar(100)" json:"tiktok"`
Tumblr string `xorm:"tumblr varchar(100)" json:"tumblr"`
Twitch string `xorm:"twitch varchar(100)" json:"twitch"`
@@ -860,16 +861,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", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
"signin_wrong_times", "last_change_password_time", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled", "email_verified",
"location", "address", "addresses", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application", "register_type", "register_source",
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "mfa_items", "last_change_password_time", "managedAccounts", "face_ids", "mfaAccounts",
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled", "email_verified",
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "kwai", "line", "amazon",
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
"spotify", "strava", "stripe", "type", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo",
"yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist", "mfa_items", "mfa_remember_deadline",
"spotify", "strava", "stripe", "type", "telegram", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo",
"yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist", "mfa_remember_deadline",
"cart",
}
}

View File

@@ -505,6 +505,160 @@ 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}>
@@ -657,157 +811,11 @@ class ApplicationEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Cookie expire"), i18next.t("application:Cookie expire - Tooltip"))} :
</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"}} >
</React.Fragment>
)}
{this.state.activeMenuKey === "saml" && (
<React.Fragment>
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:SAML reply URL"), i18next.t("application:Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip"))} :
</Col>
@@ -1452,7 +1460,7 @@ class ApplicationEditPage extends React.Component {
<Layout style={{background: "inherit", height: "100%", overflow: "auto"}}>
{
this.state.menuMode === "horizontal" || !this.state.menuMode ? (
<Header style={{background: "inherit", padding: "0px", position: "sticky", top: 0}}>
<Header style={{background: "inherit", padding: "0px", position: "sticky", top: 0, height: 38, minHeight: 38}}>
<div className="demo-logo" />
<Tabs
onChange={(key) => {
@@ -1461,9 +1469,12 @@ class ApplicationEditPage extends React.Component {
}}
type="card"
activeKey={this.state.activeMenuKey}
tabBarStyle={{marginBottom: 0}}
items={[
{label: i18next.t("application:Basic"), key: "basic"},
{label: i18next.t("application:Authentication"), key: "authentication"},
{label: "OIDC/OAuth", key: "oidc-oauth"},
{label: "SAML", key: "saml"},
{label: i18next.t("application:Providers"), key: "providers"},
{label: i18next.t("application:UI Customization"), key: "ui-customization"},
{label: i18next.t("application:Security"), key: "security"},
@@ -1488,6 +1499,8 @@ class ApplicationEditPage extends React.Component {
>
<Menu.Item key="basic">{i18next.t("application:Basic")}</Menu.Item>
<Menu.Item key="authentication">{i18next.t("application:Authentication")}</Menu.Item>
<Menu.Item key="oidc-oauth">OIDC/OAuth</Menu.Item>
<Menu.Item key="saml">SAML</Menu.Item>
<Menu.Item key="providers">{i18next.t("application:Providers")}</Menu.Item>
<Menu.Item key="ui-customization">{i18next.t("application:UI Customization")}</Menu.Item>
<Menu.Item key="security">{i18next.t("application:Security")}</Menu.Item>

View File

@@ -57,6 +57,8 @@ class OrganizationListPage extends BaseListPage {
{name: "ID", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "Name", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Display name", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "First name", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Last name", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Avatar", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "User type", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Password", visible: true, viewRule: "Self", modifyRule: "Self"},
@@ -66,6 +68,7 @@ class OrganizationListPage extends BaseListPage {
{name: "Country/Region", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Location", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Address", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Addresses", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Affiliation", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Title", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "ID card type", visible: true, viewRule: "Public", modifyRule: "Self"},
@@ -86,6 +89,8 @@ class OrganizationListPage extends BaseListPage {
{name: "Balance", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Balance credit", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Balance currency", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Cart", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "Transactions", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Register type", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Register source", visible: true, viewRule: "Public", modifyRule: "Admin"},
@@ -99,10 +104,15 @@ 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: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "MFA accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{name: "Multi-factor authentication", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "MFA items", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "WebAuthn credentials", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "Last change password time", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Managed accounts", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "Face ID", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "MFA accounts", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "Need update password", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "IP whitelist", visible: true, viewRule: "Admin", modifyRule: "Admin"},
],
};
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -609,13 +609,20 @@ class UserEditPage extends React.Component {
);
} else if (accountItem.name === "Addresses") {
return (
<AddressTable
title={i18next.t("user:Addresses")}
table={this.state.user.addresses}
onUpdateTable={(value) => {
this.updateUserField("addresses", value);
}}
/>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Addresses"), i18next.t("user:Addresses"))} :
</Col>
<Col span={22} >
<AddressTable
title={i18next.t("user:Addresses")}
table={this.state.user.addresses}
onUpdateTable={(value) => {
this.updateUserField("addresses", value);
}}
/>
</Col>
</Row>
);
} else if (accountItem.name === "Affiliation") {
return (
@@ -880,7 +887,7 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Transactions"), i18next.t("general:Transactions"))} :
</Col>
<Col span={22}>
<TransactionTable transactions={this.state.transactions} hideTag={true} />
<TransactionTable title={i18next.t("general:Transactions")} transactions={this.state.transactions} hideTag={true} />
</Col>
</Row>
);
@@ -1130,15 +1137,21 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("mfa:Multi-factor authentication"), i18next.t("mfa:Multi-factor authentication - Tooltip "))} :
</Col>
<Col span={22} >
<Card size="small" title={i18next.t("mfa:Multi-factor methods")}
extra={this.state.multiFactorAuths?.some(mfaProps => mfaProps.enabled) ?
<PopconfirmModal
text={i18next.t("general:Disable")}
title={i18next.t("general:Sure to disable") + "?"}
onConfirm={() => this.deleteMfa()}
/> : null
}>
<Card size="small" title={
<div>
{i18next.t("mfa:Multi-factor methods")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.multiFactorAuths?.some(mfaProps => mfaProps.enabled) ?
<PopconfirmModal
text={i18next.t("general:Disable")}
title={i18next.t("general:Sure to disable") + "?"}
onConfirm={() => this.deleteMfa()}
size="small"
/> : null
}
</div>
}>
<List
size="small"
rowKey="mfaType"
itemLayout="horizontal"
dataSource={this.state.multiFactorAuths}

View File

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

View File

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

View File

@@ -268,7 +268,7 @@
"Admin": "管理工具",
"Affiliation URL": "工作单位URL",
"Affiliation URL - Tooltip": "工作单位的官网URL",
"All": "全部允许",
"All": "全部",
"Application": "应用",
"Application - Tooltip": "可以访问的应用",
"Applications": "应用",

View File

@@ -0,0 +1,44 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Col, Row} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
import {CaptchaPreview} from "../common/CaptchaPreview";
export function renderCaptchaProviderFields(provider, providerName) {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
</Col>
<Col span={22} >
<CaptchaPreview
owner={provider.owner}
name={provider.name}
provider={provider}
providerName={providerName}
captchaType={provider.type}
subType={provider.subType}
clientId={provider.clientId}
clientSecret={provider.clientSecret}
clientId2={provider.clientId2}
clientSecret2={provider.clientSecret2}
providerUrl={provider.providerUrl}
/>
</Col>
</Row>
);
}

View File

@@ -0,0 +1,255 @@
// 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>
);
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,103 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Col, Input, Row, Select} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
import * as ProviderNotification from "../common/TestNotificationWidget";
const {Option} = Select;
const {TextArea} = Input;
export function renderNotificationProviderFields(provider, updateProviderField, getReceiverRow) {
return (
<React.Fragment>
{["CUCloud"].includes(provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{["Casdoor"].includes(provider.type) ?
Setting.getLabel(i18next.t("general:Application"), i18next.t("general:Application - Tooltip")) :
Setting.getLabel(i18next.t("provider:Region ID"), i18next.t("provider:Region ID - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={provider.regionId} onChange={e => {
updateProviderField("regionId", e.target.value);
}} />
</Col>
</Row>
) : null}
{["Custom HTTP"].includes(provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={provider.method} onChange={value => {
updateProviderField("method", value);
}}>
{
[
{id: "GET", name: "GET"},
{id: "POST", name: "POST"},
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
}
</Select>
</Col>
</Row>
) : null}
{["Custom HTTP", "CUCloud"].includes(provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Parameter"), i18next.t("provider:Parameter - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={provider.title} onChange={e => {
updateProviderField("title", e.target.value);
}} />
</Col>
</Row>
) : null}
{["Google Chat", "CUCloud"].includes(provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Metadata"), i18next.t("provider:Metadata - Tooltip"))} :
</Col>
<Col span={22}>
<TextArea rows={4} value={provider.metadata} onChange={e => {
updateProviderField("metadata", e.target.value);
}} />
</Col>
</Row>
) : null}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Content"), i18next.t("provider:Content - Tooltip"))} :
</Col>
<Col span={22} >
<TextArea autoSize={{minRows: 3, maxRows: 100}} value={provider.content} onChange={e => {
updateProviderField("content", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
{getReceiverRow(provider)}
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary"
onClick={() => ProviderNotification.sendTestNotification(provider)} >
{i18next.t("provider:Send Testing Notification")}
</Button>
</Row>
</React.Fragment>
);
}

View File

@@ -0,0 +1,218 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Col, Input, Radio, Row, Switch} from "antd";
import {LinkOutlined} from "@ant-design/icons";
import * as Setting from "../Setting";
import i18next from "i18next";
const {TextArea} = Input;
export function renderOAuthProviderFields(provider, updateProviderField, renderUserMappingInput) {
const getDomainLabel = provider => {
switch (provider.category) {
case "OAuth":
if (provider.type === "AzureAD" || provider.type === "AzureADB2C") {
return Setting.getLabel(i18next.t("provider:Tenant ID"), i18next.t("provider:Tenant ID - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"));
}
default:
return Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"));
}
};
return (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Email regex"), i18next.t("provider:Email regex - Tooltip"))} :
</Col>
<Col span={22}>
<TextArea rows={4} value={provider.emailRegex} onChange={e => {
updateProviderField("emailRegex", e.target.value);
}} />
</Col>
</Row>
{
provider.type !== "WeChat" ? null : (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Use WeChat Media Platform in PC"), i18next.t("provider:Use WeChat Media Platform in PC - Tooltip"))} :
</Col>
<Col span={1} >
<Switch disabled={!provider.clientId} checked={provider.disableSsl} onChange={checked => {
updateProviderField("disableSsl", checked);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("token:Access token"), i18next.t("token:Access token - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={provider.content} disabled={!provider.disableSsl || !provider.clientId2} onChange={e => {
updateProviderField("content", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Follow-up action"), i18next.t("provider:Follow-up action - Tooltip"))} :
</Col>
<Col>
<Radio.Group value={provider.signName}
disabled={!provider.disableSsl || !provider.clientId || !provider.clientId2}
buttonStyle="solid"
onChange={e => {
updateProviderField("signName", e.target.value);
}}>
<Radio.Button value="open">{i18next.t("provider:Use WeChat Open Platform to login")}</Radio.Button>
<Radio.Button value="media">{i18next.t("provider:Use WeChat Media Platform to login")}</Radio.Button>
</Radio.Group>
</Col>
</Row>
</React.Fragment>
)
}
{
provider.type !== "ADFS" && provider.type !== "AzureAD"
&& provider.type !== "AzureADB2C" && (provider.type !== "Casdoor" && provider.category !== "Storage")
&& provider.type !== "Okta" && provider.type !== "Nextcloud" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{getDomainLabel(provider)} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={provider.domain} onChange={e => {
updateProviderField("domain", e.target.value);
}} />
</Col>
</Row>
)
}
{
provider.type !== "Google" && provider.type !== "Lark" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{provider.type === "Google" ?
Setting.getLabel(i18next.t("provider:Get phone number"), i18next.t("provider:Get phone number - Tooltip"))
: Setting.getLabel(i18next.t("provider:Use global endpoint"), i18next.t("provider:Use global endpoint - Tooltip"))} :
</Col>
<Col span={1} >
<Switch disabled={!provider.clientId} checked={provider.disableSsl} onChange={checked => {
updateProviderField("disableSsl", checked);
}} />
</Col>
</Row>
)
}
{
provider.type.startsWith("Custom") ? (
<React.Fragment>
<Col>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Auth URL"), i18next.t("provider:Auth URL - Tooltip"))}
</Col>
<Col span={22} >
<Input value={provider.customAuthUrl} onChange={e => {
updateProviderField("customAuthUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Token URL"), i18next.t("provider:Token URL - Tooltip"))}
</Col>
<Col span={22} >
<Input value={provider.customTokenUrl} onChange={e => {
updateProviderField("customTokenUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))}
</Col>
<Col span={22} >
<Input value={provider.scopes} onChange={e => {
updateProviderField("scopes", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:UserInfo URL"), i18next.t("provider:UserInfo URL - Tooltip"))}
</Col>
<Col span={22} >
<Input value={provider.customUserInfoUrl} onChange={e => {
updateProviderField("customUserInfoUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Enable PKCE"), i18next.t("provider:Enable PKCE - Tooltip"))} :
</Col>
<Col span={22} >
<Switch checked={provider.enablePkce} onChange={checked => {
updateProviderField("enablePkce", checked);
}} />
</Col>
</Row>
</Col>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:User mapping"), i18next.t("provider:User mapping - Tooltip"))} :
</Col>
<Col span={22} >
{renderUserMappingInput()}
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Favicon"), i18next.t("general:Favicon - Tooltip"))} :
</Col>
<Col span={22} >
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col>
<Col span={23} >
<Input prefix={<LinkOutlined />} value={provider.customLogo} onChange={e => {
updateProviderField("customLogo", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
{i18next.t("general:Preview")}:
</Col>
<Col span={23} >
<a target="_blank" rel="noreferrer" href={provider.customLogo}>
<img src={provider.customLogo} alt={provider.customLogo} height={90} style={{marginBottom: "20px"}} />
</a>
</Col>
</Row>
</Col>
</Row>
</React.Fragment>
) : null
}
</React.Fragment>
);
}

View File

@@ -0,0 +1,72 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Col, Input, Row, Select} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
import {LinkOutlined} from "@ant-design/icons";
const {Option} = Select;
export function renderPaymentProviderFields(provider, updateProviderField, certs) {
return (
<React.Fragment>
{
(provider.type === "Alipay" || provider.type === "WeChat Pay" || provider.type === "Casdoor") ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Cert"), i18next.t("general:Cert - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={provider.cert} onChange={(value => {updateProviderField("cert", value);})}>
{
certs.map((cert, index) => <Option key={index} value={cert.name}>{cert.name}</Option>)
}
</Select>
</Col>
</Row>
) : null
}
{
(provider.type === "Alipay") ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Root cert"), i18next.t("general:Root cert - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={provider.metadata} onChange={(value => {updateProviderField("metadata", value);})}>
{
certs.map((cert, index) => <Option key={index} value={cert.name}>{cert.name}</Option>)
}
</Select>
</Col>
</Row>
) : null
}
{(provider.type === "GC" || provider.type === "FastSpring") ? (
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
</Col>
<Col span={22}>
<Input prefix={<LinkOutlined />} value={provider.host} onChange={e => {
updateProviderField("host", e.target.value);
}} />
</Col>
</Row>
) : null}
</React.Fragment>
);
}

View File

@@ -0,0 +1,133 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Col, Input, Row, Switch} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
import {authConfig} from "../auth/Auth";
import copy from "copy-to-clipboard";
const {TextArea} = Input;
export function renderSamlProviderFields(provider, updateProviderField, metadataConfig) {
const {requestUrl, setRequestUrl, metadataLoading, fetchSamlMetadata, parseSamlMetadata} = metadataConfig;
return (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Sign request"), i18next.t("provider:Sign request - Tooltip"))} :
</Col>
<Col span={22} >
<Switch checked={provider.enableSignAuthnRequest} onChange={checked => {
updateProviderField("enableSignAuthnRequest", checked);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Metadata url"), i18next.t("provider:Metadata url - Tooltip"))} :
</Col>
<Col span={6} >
<Input value={requestUrl} onChange={e => {
setRequestUrl(e.target.value);
}} />
</Col>
<Col span={16} >
<Button style={{marginLeft: "10px"}} type="primary" loading={metadataLoading} onClick={() => {fetchSamlMetadata();}}>{i18next.t("general:Request")}</Button>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Metadata"), i18next.t("provider:Metadata - Tooltip"))} :
</Col>
<Col span={22}>
<TextArea rows={4} value={provider.metadata} onChange={e => {
updateProviderField("metadata", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={2} />
<Col span={2}>
<Button type="primary" onClick={() => {parseSamlMetadata();}}>
{i18next.t("provider:Parse")}
</Button>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:SAML 2.0 Endpoint (HTTP)"))} :
</Col>
<Col span={22} >
<Input value={provider.endpoint} onChange={e => {
updateProviderField("endpoint", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:IdP"), i18next.t("provider:IdP certificate"))} :
</Col>
<Col span={22} >
<Input value={provider.idP} onChange={e => {
updateProviderField("idP", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Issuer URL"), i18next.t("provider:Issuer URL - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={provider.issuerUrl} onChange={e => {
updateProviderField("issuerUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:SP ACS URL"), i18next.t("provider:SP ACS URL - Tooltip"))} :
</Col>
<Col span={21} >
<Input value={`${authConfig.serverUrl}/api/acs`} readOnly="readonly" />
</Col>
<Col span={1}>
<Button type="primary" onClick={() => {
copy(`${authConfig.serverUrl}/api/acs`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}>
{i18next.t("general:Copy")}
</Button>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:SP Entity ID"), i18next.t("provider:SP Entity ID - Tooltip"))} :
</Col>
<Col span={21} >
<Input value={`${authConfig.serverUrl}/api/acs`} readOnly="readonly" />
</Col>
<Col span={1}>
<Button type="primary" onClick={() => {
copy(`${authConfig.serverUrl}/api/acs`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}>
{i18next.t("general:Copy")}
</Button>
</Col>
</Row>
</React.Fragment>
);
}

View File

@@ -0,0 +1,182 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Col, Input, Row, Select, Switch} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
import * as ProviderEditTestSms from "../common/TestSmsWidget";
import {CountryCodeSelect} from "../common/select/CountryCodeSelect";
import HttpHeaderTable from "../table/HttpHeaderTable";
import {LinkOutlined} from "@ant-design/icons";
const {Option} = Select;
const SMS_PROVIDERS_WITHOUT_SIGN_NAME = ["Custom HTTP SMS", "Twilio SMS", "Amazon SNS", "Msg91 SMS", "Infobip SMS"];
const SMS_PROVIDERS_WITHOUT_TEMPLATE_CODE = ["Infobip SMS", "Custom HTTP SMS"];
export function renderSmsProviderFields(provider, updateProviderField, renderSmsMappingInput, account) {
return (
<React.Fragment>
{SMS_PROVIDERS_WITHOUT_SIGN_NAME.includes(provider.type) ?
null :
(<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Sign Name"), i18next.t("provider:Sign Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={provider.signName} onChange={e => {
updateProviderField("signName", e.target.value);
}} />
</Col>
</Row>
)
}
{SMS_PROVIDERS_WITHOUT_TEMPLATE_CODE.includes(provider.type) ?
null :
(<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Template code"), i18next.t("provider:Template code - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={provider.templateCode} onChange={e => {
updateProviderField("templateCode", e.target.value);
}} />
</Col>
</Row>
)
}
{
provider.type === "Custom HTTP SMS" ? (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:Region endpoint for Internet"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={provider.endpoint} onChange={e => {
updateProviderField("endpoint", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={provider.method} onChange={value => {
updateProviderField("method", value);
}}>
{
[
{id: "GET", name: "GET"},
{id: "POST", name: "POST"},
{id: "PUT", name: "PUT"},
{id: "DELETE", name: "DELETE"},
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
}
</Select>
</Col>
</Row>
{
provider.method !== "GET" ? (<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("webhook:Content type"), i18next.t("webhook:Content type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={provider.issuerUrl === "" ? "application/x-www-form-urlencoded" : provider.issuerUrl} onChange={value => {
updateProviderField("issuerUrl", value);
}}>
{
[
{id: "application/json", name: "application/json"},
{id: "application/x-www-form-urlencoded", name: "application/x-www-form-urlencoded"},
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
}
</Select>
</Col>
</Row>) : null
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:HTTP header"), i18next.t("provider:HTTP header - Tooltip"))} :
</Col>
<Col span={22} >
<HttpHeaderTable httpHeaders={provider.httpHeaders} onUpdateTable={(value) => {updateProviderField("httpHeaders", value);}} />
</Col>
</Row>
{provider.method !== "GET" ? <Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:HTTP body mapping"), i18next.t("provider:HTTP body mapping - Tooltip"))} :
</Col>
<Col span={22}>
{renderSmsMappingInput()}
</Col>
</Row> : null}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Parameter"), i18next.t("provider:Parameter - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={provider.title} onChange={e => {
updateProviderField("title", e.target.value);
}} />
</Col>
</Row>
</React.Fragment>
) : null
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Enable proxy"), i18next.t("provider:Enable proxy - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={provider.enableProxy} onChange={checked => {
updateProviderField("enableProxy", checked);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:SMS Test"), i18next.t("provider:SMS Test - Tooltip"))} :
</Col>
<Col span={4} >
<Input.Group compact>
<CountryCodeSelect
style={{width: "90px"}}
initValue={provider.content}
onChange={(value) => {
updateProviderField("content", value);
}}
countryCodes={account.organization.countryCodes}
/>
<Input value={provider.receiver}
style={{width: "150px"}}
placeholder = {i18next.t("user:Input your phone number")}
onChange={e => {
updateProviderField("receiver", e.target.value);
}} />
</Input.Group>
</Col>
<Col span={2} >
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary"
disabled={!Setting.isValidPhone(provider.receiver) || (provider.type === "Custom HTTP SMS" && provider.endpoint === "")}
onClick={() => ProviderEditTestSms.sendTestSms(provider, "+" + Setting.getCountryCode(provider.content) + provider.receiver)} >
{i18next.t("provider:Send Testing SMS")}
</Button>
</Col>
</Row>
</React.Fragment>
);
}

View File

@@ -0,0 +1,112 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Col, Input, Row} from "antd";
import {LinkOutlined} from "@ant-design/icons";
import * as Setting from "../Setting";
import i18next from "i18next";
export function renderStorageProviderFields(provider, updateProviderField) {
return (
<React.Fragment>
{["Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor"].includes(provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={provider.intranetEndpoint} onChange={e => {
updateProviderField("intranetEndpoint", e.target.value);
}} />
</Col>
</Row>
)}
{["Local File System"].includes(provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:Region endpoint for Internet"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={provider.endpoint} onChange={e => {
updateProviderField("endpoint", e.target.value);
}} />
</Col>
</Row>
)}
{["Local File System"].includes(provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{["Casdoor"].includes(provider.type) ?
Setting.getLabel(i18next.t("general:Provider"), i18next.t("general:Provider - Tooltip"))
: Setting.getLabel(i18next.t("provider:Bucket"), i18next.t("provider:Bucket - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={provider.bucket} onChange={e => {
updateProviderField("bucket", e.target.value);
}} />
</Col>
</Row>
)}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={provider.pathPrefix} onChange={e => {
updateProviderField("pathPrefix", e.target.value);
}} />
</Col>
</Row>
{["Synology", "Casdoor"].includes(provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={provider.domain} disabled={provider.type === "Local File System"} onChange={e => {
updateProviderField("domain", e.target.value);
}} />
</Col>
</Row>
)}
{["Casdoor"].includes(provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={provider.content} onChange={e => {
updateProviderField("content", e.target.value);
}} />
</Col>
</Row>
) : null}
{["AWS S3", "Tencent Cloud COS", "Qiniu Cloud Kodo", "Casdoor", "CUCloud OSS", "MinIO"].includes(provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{["Casdoor"].includes(provider.type) ?
Setting.getLabel(i18next.t("general:Application"), i18next.t("general:Application - Tooltip")) :
Setting.getLabel(i18next.t("provider:Region ID"), i18next.t("provider:Region ID - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={provider.regionId} onChange={e => {
updateProviderField("regionId", e.target.value);
}} />
</Col>
</Row>
) : null}
</React.Fragment>
);
}

View File

@@ -0,0 +1,48 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {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
);
}

View File

@@ -14,12 +14,16 @@
import React from "react";
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Input, Row, Select, Table, Tooltip} from "antd";
import {AutoComplete, Button, Col, Input, Row, Table, Tooltip} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
import RegionSelect from "../common/select/RegionSelect";
const {Option} = Select;
const TAG_OPTIONS = [
{value: "Home", label: "Home"},
{value: "Work", label: "Work"},
{value: "Other", label: "Other"},
];
class AddressTable extends React.Component {
constructor(props) {
@@ -86,16 +90,20 @@ 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 (
<Select virtual={false} style={{width: "100%"}}
value={text}
<AutoComplete
size="small"
style={{width: "100%"}}
value={text || ""}
options={tagOptions}
onChange={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>
}}
onSelect={value => {
this.updateField(table, index, "tag", value);
}}
/>
);
},
},
@@ -106,7 +114,7 @@ class AddressTable extends React.Component {
width: "150px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
<Input size="small" value={text} onChange={e => {
this.updateField(table, index, "line1", e.target.value);
}} />
);
@@ -119,7 +127,7 @@ class AddressTable extends React.Component {
width: "150px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
<Input size="small" value={text} onChange={e => {
this.updateField(table, index, "line2", e.target.value);
}} />
);
@@ -132,7 +140,7 @@ class AddressTable extends React.Component {
width: "120px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
<Input size="small" value={text} onChange={e => {
this.updateField(table, index, "city", e.target.value);
}} />
);
@@ -145,7 +153,7 @@ class AddressTable extends React.Component {
width: "100px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
<Input size="small" value={text} onChange={e => {
this.updateField(table, index, "state", e.target.value);
}} />
);
@@ -158,7 +166,7 @@ class AddressTable extends React.Component {
width: "100px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
<Input size="small" value={text} onChange={e => {
this.updateField(table, index, "zipCode", e.target.value);
}} />
);
@@ -172,6 +180,7 @@ class AddressTable extends React.Component {
render: (text, record, index) => {
return (
<RegionSelect
size="small"
value={text}
onChange={value => {
this.updateField(table, index, "region", value);

View File

@@ -86,7 +86,7 @@ class ManagedAccountTable extends React.Component {
render: (text, record, index) => {
const items = this.props.applications;
return (
<Select virtual={false} style={{width: "100%"}}
<Select virtual={false} size="small" 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 prefix={<LinkOutlined />} value={text} onChange={e => {
<Input size="small" 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 value={text} onChange={e => {
<Input size="small" 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 value={text} onChange={e => {
<Input.Password size="small" value={text} onChange={e => {
this.updateField(table, index, "password", e.target.value);
}} />
);

View File

@@ -86,7 +86,7 @@ class MfaAccountTable extends React.Component {
width: "400px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
<Input size="small" 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 value={text} onChange={e => {
<Input size="small" 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 value={text} onChange={e => {
<Input size="small" 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 value={text} onChange={e => {
<Input.Password size="small" value={text} onChange={e => {
this.updateField(table, index, "secretKey", e.target.value);
}} />
);

View File

@@ -84,7 +84,7 @@ class MfaTable extends React.Component {
key: "name",
render: (text, record, index) => {
return (
<Select virtual={false} style={{width: "100%"}}
<Select virtual={false} size="small" 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} style={{width: "100%"}}
<Select virtual={false} size="small" style={{width: "100%"}}
value={text}
defaultValue="Optional"
options={RuleItems.map((item) =>

View File

@@ -110,7 +110,17 @@ class ProviderTable extends React.Component {
width: "100px",
render: (text, record, index) => {
const provider = Setting.getArrayItem(this.props.providers, "name", record.name);
return provider?.category;
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;
},
},
{
@@ -120,7 +130,17 @@ class ProviderTable extends React.Component {
width: "80px",
render: (text, record, index) => {
const provider = Setting.getArrayItem(this.props.providers, "name", record.name);
return Provider.getProviderLogoWidget(provider);
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;
},
},
{

View File

@@ -131,13 +131,18 @@ class TransactionTable extends React.Component {
columns={columns}
dataSource={this.props.transactions}
rowKey={(record) => `${record.owner}/${record.name}`}
size="small"
size="middle"
bordered
pagination={{
pageSize: 10,
showSizeChanger: true,
pageSizeOptions: ["10", "20", "50", "100"],
}}
title={this.props.title ? () => (
<div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
</div>
) : undefined}
/>
);
}