Compare commits

...

27 Commits

Author SHA1 Message Date
Yang Luo
80b4c0b1a7 feat: remove special handling for Dummy payment provider (#5068) 2026-02-13 10:06:14 +08:00
Yang Luo
eb5a422026 feat: replace DisableSsl boolean with SslMode enum for Email providers (#5063) 2026-02-13 02:15:20 +08:00
DacongDA
f7bd70e0a3 feat: improve tab height UI in application edit page (#5055) 2026-02-12 21:57:57 +08:00
Copilot
5e7dbe4b56 feat: fix CAPTCHA rule enforcement in verification code flow (#5009) 2026-02-12 21:22:47 +08:00
Yang Luo
bd1fca2f32 feat: Add LDAP group/OU hierarchy syncing with automatic user membership (#5052) 2026-02-12 17:11:20 +08:00
IsAurora6
3d4cc42f1f feat: mark cart items as invalid when product is removed, renamed, or currency is changed. (#5050) 2026-02-12 00:46:54 +08:00
Yang Luo
1836cab44d feat: fix icons for 5 payment providers 2026-02-11 01:42:37 +08:00
Yang Luo
75b18635f7 feat: fix issue that Webhook records for set-password API were missing user context (#5008) 2026-02-11 01:32:11 +08:00
Yang Luo
47cd44c7ce feat: support "snsapi_privateinfo" scope in WeCom OAuth provider to support fetching Emails (#5034) 2026-02-11 01:21:29 +08:00
Yang Luo
090ca97dcd feat: bind provider IDs in WeCom/DingTalk/Lark syncers (#5033) 2026-02-11 01:04:26 +08:00
Yang Luo
bed01b31f1 feat: add AWS IAM syncer (#5043) 2026-02-11 01:00:41 +08:00
Yang Luo
c8f8f88d85 feat: add "Existing Field" category for token attributes table in application edit page (#5041) 2026-02-11 00:58:50 +08:00
IsAurora6
7acb303995 feat: Fixed cart anomalies when updating product information. (#5039) 2026-02-10 20:58:18 +08:00
IsAurora6
2607f8d3e5 feat: fix DingTalk syncer to fetch nested departments recursively (#5036) 2026-02-10 18:11:03 +08:00
IsAurora6
481db33e58 feat: Optimize the display of rechargeable product content on the ProductStorePage.js. (#5028) 2026-02-09 20:28:18 +08:00
DacongDA
f556c7e11f feat: add PaginateSelect widget to fix non-pagination fetch API issue (#5023) 2026-02-09 20:07:41 +08:00
IsAurora6
f590992f28 feat: update i18n translations (#5021) 2026-02-09 00:05:08 +08:00
Yang Luo
80f9db0fa2 feat: move captcha provider validation from frontend filter to backend check (#5019) 2026-02-08 02:16:47 +08:00
Yang Luo
0748661d2a feat: store OAuth tokens per provider instead of single originalToken field (#5016) 2026-02-08 01:22:24 +08:00
Yang Luo
83552ed143 feat: fix renderRightDropdown() scrollbar UI bug 2026-02-08 00:45:46 +08:00
Yang Luo
8cb8541f96 feat: add Plan.IsExclusive field for single subscription enforcement (#5004) 2026-02-07 01:23:22 +08:00
Yang Luo
5b646a726c fix: fix format issue in DuplicateInfo 2026-02-07 00:51:11 +08:00
Yang Luo
19b9586670 fix: fix broken links for role/plan/user/payment columns (#4999) 2026-02-07 00:46:36 +08:00
Yang Luo
73f8d19c5f fix: de-duplicate i18n translation keys in frontend and backend (#4997) 2026-02-07 00:35:46 +08:00
Yang Luo
04da531df3 fix: sync all i18n strings 2026-02-07 00:18:07 +08:00
Yang Luo
d97558051d fix: add duplicate key detection tests for i18n JSON files (#4994) 2026-02-07 00:17:53 +08:00
Yang Luo
ac55355290 fix: deduplicate the i18n strings 2026-02-06 21:42:10 +08:00
118 changed files with 4651 additions and 2029 deletions

View File

@@ -688,6 +688,51 @@ func (c *ApiController) GetCaptcha() {
applicationId := c.Ctx.Input.Query("applicationId")
isCurrentProvider := c.Ctx.Input.Query("isCurrentProvider")
// When isCurrentProvider == "true", the frontend passes a provider ID instead of an application ID.
// In that case, skip application lookup and rule evaluation, and just return the provider config.
shouldSkipCaptcha := false
if isCurrentProvider != "true" {
application, err := object.GetApplication(applicationId)
if err != nil {
c.ResponseError(err.Error())
return
}
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), applicationId))
return
}
// Check the CAPTCHA rule to determine if CAPTCHA should be shown
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
// For Internet-Only rule, we can determine on the backend if CAPTCHA should be shown
// For other rules (Dynamic, Always), we need to return the CAPTCHA config
for _, providerItem := range application.Providers {
if providerItem.Provider == nil || providerItem.Provider.Category != "Captcha" {
continue
}
// For "None" rule, skip CAPTCHA
if providerItem.Rule == "None" || providerItem.Rule == "" {
shouldSkipCaptcha = true
} else if providerItem.Rule == "Internet-Only" {
// For Internet-Only rule, check if the client is from intranet
if !util.IsInternetIp(clientIp) {
// Client is from intranet, skip CAPTCHA
shouldSkipCaptcha = true
}
}
break // Only check the first CAPTCHA provider
}
if shouldSkipCaptcha {
c.ResponseOk(Captcha{Type: "none"})
return
}
}
captchaProvider, err := object.GetCaptchaProviderByApplication(applicationId, isCurrentProvider, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())

View File

@@ -103,7 +103,7 @@ func (c *ApiController) GetInvitationCodeInfo() {
return
}
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The application: %s does not exist"), applicationId))
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), applicationId))
return
}
@@ -230,7 +230,7 @@ func (c *ApiController) SendInvitation() {
return
}
if organization == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The organization: %s does not exist"), invitation.Owner))
c.ResponseError(fmt.Sprintf(c.T("auth:The organization: %s does not exist"), invitation.Owner))
return
}

View File

@@ -16,6 +16,7 @@ package controllers
import (
"encoding/json"
"fmt"
"github.com/beego/beego/v2/core/utils/pagination"
"github.com/casdoor/casdoor/object"
@@ -150,6 +151,26 @@ func (c *ApiController) AddSubscription() {
return
}
// Check if plan restricts user to one subscription
if subscription.Plan != "" {
plan, err := object.GetPlan(util.GetId(subscription.Owner, subscription.Plan))
if err != nil {
c.ResponseError(err.Error())
return
}
if plan != nil && plan.IsExclusive {
hasSubscription, err := object.HasActiveSubscriptionForPlan(subscription.Owner, subscription.User, subscription.Plan)
if err != nil {
c.ResponseError(err.Error())
return
}
if hasSubscription {
c.ResponseError(fmt.Sprintf("User already has an active subscription for plan: %s", subscription.Plan))
return
}
}
}
c.Data["json"] = wrapActionResponse(object.AddSubscription(&subscription))
c.ServeJSON()
}

View File

@@ -942,7 +942,7 @@ func (c *ApiController) VerifyIdentification() {
}
if provider == nil {
c.ResponseError(fmt.Sprintf(c.T("provider:The provider: %s does not exist"), providerName))
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s does not exist"), providerName))
return
}

View File

@@ -151,39 +151,14 @@ func (c *ApiController) SendVerificationCode() {
return
}
provider, err := object.GetCaptchaProviderByApplication(vform.ApplicationId, "false", c.GetAcceptLanguage())
application, err := object.GetApplication(vform.ApplicationId)
if err != nil {
c.ResponseError(err.Error())
return
}
if provider != nil {
if vform.CaptchaType != provider.Type {
c.ResponseError(c.T("verification:Turing test failed."))
return
}
if provider.Type != "Default" {
vform.ClientSecret = provider.ClientSecret
}
if vform.CaptchaType != "none" {
if captchaProvider := captcha.GetCaptchaProvider(vform.CaptchaType); captchaProvider == nil {
c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType)
return
} else if isHuman, err := captchaProvider.VerifyCaptcha(vform.CaptchaToken, provider.ClientId, vform.ClientSecret, provider.ClientId2); err != nil {
c.ResponseError(err.Error())
return
} else if !isHuman {
c.ResponseError(c.T("verification:Turing test failed."))
return
}
}
}
application, err := object.GetApplication(vform.ApplicationId)
if err != nil {
c.ResponseError(err.Error())
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), vform.ApplicationId))
return
}
@@ -214,6 +189,7 @@ func (c *ApiController) SendVerificationCode() {
}
var user *object.User
// Try to resolve user for CAPTCHA rule checking
// checkUser != "", means method is ForgetVerification
if vform.CheckUser != "" {
owner := application.Organization
@@ -231,18 +207,86 @@ func (c *ApiController) SendVerificationCode() {
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
return
}
}
// mfaUserSession != "", means method is MfaAuthVerification
if mfaUserSession := c.getMfaUserSession(); mfaUserSession != "" {
} else if mfaUserSession := c.getMfaUserSession(); mfaUserSession != "" {
// mfaUserSession != "", means method is MfaAuthVerification
user, err = object.GetUser(mfaUserSession)
if err != nil {
c.ResponseError(err.Error())
return
}
} else if vform.Method == ResetVerification {
// For reset verification, get the current logged-in user
user = c.getCurrentUser()
} else if vform.Method == LoginVerification {
// For login verification, try to find user by email/phone for CAPTCHA check
// This is a preliminary lookup; the actual validation happens later in the switch statement
if vform.Type == object.VerifyTypeEmail && util.IsEmailValid(vform.Dest) {
user, err = object.GetUserByEmail(organization.Name, vform.Dest)
if err != nil {
c.ResponseError(err.Error())
return
}
} else if vform.Type == object.VerifyTypePhone {
// Prefer resolving the user directly by phone, consistent with the later login switch,
// so that Dynamic CAPTCHA is not skipped due to missing/invalid country code.
user, err = object.GetUserByPhone(organization.Name, vform.Dest)
if err != nil {
c.ResponseError(err.Error())
return
}
}
}
// Determine username for CAPTCHA check
username := ""
if user != nil {
username = user.Name
} else if vform.CheckUser != "" {
username = vform.CheckUser
}
// Check if CAPTCHA should be enabled based on the rule (Dynamic/Always/Internet-Only)
enableCaptcha, err := object.CheckToEnableCaptcha(application, organization.Name, username, clientIp)
if err != nil {
c.ResponseError(err.Error())
return
}
// Only verify CAPTCHA if it should be enabled
if enableCaptcha {
captchaProvider, err := object.GetCaptchaProviderByApplication(vform.ApplicationId, "false", c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
if captchaProvider != nil {
if vform.CaptchaType != captchaProvider.Type {
c.ResponseError(c.T("verification:Turing test failed."))
return
}
if captchaProvider.Type != "Default" {
vform.ClientSecret = captchaProvider.ClientSecret
}
if vform.CaptchaType != "none" {
if captchaService := captcha.GetCaptchaProvider(vform.CaptchaType); captchaService == nil {
c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType)
return
} else if isHuman, err := captchaService.VerifyCaptcha(vform.CaptchaToken, captchaProvider.ClientId, vform.ClientSecret, captchaProvider.ClientId2); err != nil {
c.ResponseError(err.Error())
return
} else if !isHuman {
c.ResponseError(c.T("verification:Turing test failed."))
return
}
}
}
}
sendResp := errors.New("invalid dest type")
var provider *object.Provider
switch vform.Type {
case object.VerifyTypeEmail:

View File

@@ -18,7 +18,7 @@ type EmailProvider interface {
Send(fromAddress string, fromName string, toAddress []string, subject string, content string) error
}
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool, endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string, enableProxy bool) EmailProvider {
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, sslMode string, endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string, enableProxy bool) EmailProvider {
if typ == "Azure ACS" {
return NewAzureACSEmailProvider(clientSecret, host)
} else if typ == "Custom HTTP Email" {
@@ -26,6 +26,6 @@ func GetEmailProvider(typ string, clientId string, clientSecret string, host str
} else if typ == "SendGrid" {
return NewSendgridEmailProvider(clientSecret, host, endpoint)
} else {
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl, enableProxy)
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, sslMode, enableProxy)
}
}

View File

@@ -25,13 +25,20 @@ type SmtpEmailProvider struct {
Dialer *gomail.Dialer
}
func NewSmtpEmailProvider(userName string, password string, host string, port int, typ string, disableSsl bool, enableProxy bool) *SmtpEmailProvider {
func NewSmtpEmailProvider(userName string, password string, host string, port int, typ string, sslMode string, enableProxy bool) *SmtpEmailProvider {
dialer := gomail.NewDialer(host, port, userName, password)
if typ == "SUBMAIL" {
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
}
dialer.SSL = !disableSsl
// Handle SSL mode: "Auto" (or empty) means don't override gomail's default behavior
// "Enable" means force SSL on, "Disable" means force SSL off
if sslMode == "Enable" {
dialer.SSL = true
} else if sslMode == "Disable" {
dialer.SSL = false
}
// If sslMode is "Auto" or empty, don't set dialer.SSL - let gomail decide based on port
if enableProxy {
socks5Proxy := conf.GetConfigString("socks5Proxy")

2
go.mod
View File

@@ -17,6 +17,7 @@ require (
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible
github.com/aliyun/credentials-go v1.3.10
github.com/aws/aws-sdk-go v1.45.5
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0
github.com/beego/beego/v2 v2.3.8
github.com/beevik/etree v1.1.0
@@ -114,7 +115,6 @@ require (
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/apistd/uni-go-sdk v0.0.2 // indirect
github.com/atc0005/go-teams-notify/v2 v2.13.0 // indirect
github.com/aws/aws-sdk-go v1.45.5 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/baidubce/bce-sdk-go v0.9.156 // indirect
github.com/beorn7/perks v1.0.1 // indirect

140
i18n/deduplicate_test.go Normal file
View File

@@ -0,0 +1,140 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package i18n
import (
"bytes"
"encoding/json"
"fmt"
"os"
"testing"
)
// DuplicateInfo represents information about a duplicate key
type DuplicateInfo struct {
Key string
OldPrefix string
NewPrefix string
OldPrefixKey string // e.g., "general:Submitter"
NewPrefixKey string // e.g., "permission:Submitter"
}
// findDuplicateKeysInJSON finds duplicate keys across the entire JSON file
// Returns a list of duplicate information showing old and new prefix:key pairs
// The order is determined by the order keys appear in the JSON file (git history)
func findDuplicateKeysInJSON(filePath string) ([]DuplicateInfo, error) {
// Read the JSON file
fileContent, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %w", filePath, err)
}
// Track the first occurrence of each key (prefix where it was first seen)
keyFirstPrefix := make(map[string]string)
var duplicates []DuplicateInfo
// To preserve order, we need to parse the JSON with order preservation
// We'll use a decoder to read through the top-level object
decoder := json.NewDecoder(bytes.NewReader(fileContent))
// Read the opening brace of the top-level object
token, err := decoder.Token()
if err != nil {
return nil, fmt.Errorf("failed to read token: %w", err)
}
if delim, ok := token.(json.Delim); !ok || delim != '{' {
return nil, fmt.Errorf("expected object start, got %v", token)
}
// Read all namespaces in order
for decoder.More() {
// Read the namespace (prefix) name
token, err := decoder.Token()
if err != nil {
return nil, fmt.Errorf("failed to read namespace: %w", err)
}
prefix, ok := token.(string)
if !ok {
return nil, fmt.Errorf("expected string namespace, got %v", token)
}
// Read the namespace object as raw message
var namespaceData map[string]string
if err := decoder.Decode(&namespaceData); err != nil {
return nil, fmt.Errorf("failed to decode namespace %s: %w", prefix, err)
}
// Now check each key in this namespace
for key := range namespaceData {
// Check if this key was already seen in a different prefix
if firstPrefix, exists := keyFirstPrefix[key]; exists {
// This is a duplicate - the key exists in another prefix
duplicates = append(duplicates, DuplicateInfo{
Key: key,
OldPrefix: firstPrefix,
NewPrefix: prefix,
OldPrefixKey: fmt.Sprintf("%s:%s", firstPrefix, key),
NewPrefixKey: fmt.Sprintf("%s:%s", prefix, key),
})
} else {
// First time seeing this key, record the prefix
keyFirstPrefix[key] = prefix
}
}
}
return duplicates, nil
}
// TestDeduplicateFrontendI18n checks for duplicate i18n keys in the frontend en.json file
func TestDeduplicateFrontendI18n(t *testing.T) {
filePath := "../web/src/locales/en/data.json"
// Find duplicate keys
duplicates, err := findDuplicateKeysInJSON(filePath)
if err != nil {
t.Fatalf("Failed to check for duplicates in frontend i18n file: %v", err)
}
// Print all duplicates and fail the test if any are found
if len(duplicates) > 0 {
t.Errorf("Found duplicate i18n keys in frontend file (%s):", filePath)
for _, dup := range duplicates {
t.Errorf(" i18next.t(\"%s\") duplicates with i18next.t(\"%s\")", dup.NewPrefixKey, dup.OldPrefixKey)
}
t.Fail()
}
}
// TestDeduplicateBackendI18n checks for duplicate i18n keys in the backend en.json file
func TestDeduplicateBackendI18n(t *testing.T) {
filePath := "../i18n/locales/en/data.json"
// Find duplicate keys
duplicates, err := findDuplicateKeysInJSON(filePath)
if err != nil {
t.Fatalf("Failed to check for duplicates in backend i18n file: %v", err)
}
// Print all duplicates and fail the test if any are found
if len(duplicates) > 0 {
t.Errorf("Found duplicate i18n keys in backend file (%s):", filePath)
for _, dup := range duplicates {
t.Errorf(" i18n.Translate(\"%s\") duplicates with i18n.Translate(\"%s\")", dup.NewPrefixKey, dup.OldPrefixKey)
}
t.Fail()
}
}

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "Konnte den Benutzer nicht hinzufügen",
"Get init score failed, error: %w": "Init-Score konnte nicht abgerufen werden, Fehler: %w",
"Please sign out first": "Bitte melden Sie sich zuerst ab",
"The application does not allow to sign up new account": "Die Anwendung erlaubt es nicht, sich für ein neues Konto anzumelden"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "Die Anmeldemethode: Anmeldung per E-Mail ist für die Anwendung nicht aktiviert",
"The login method: login with face is not enabled for the application": "Die Anmeldemethode: Anmeldung per Gesicht ist für die Anwendung nicht aktiviert",
"The login method: login with password is not enabled for the application": "Die Anmeldeart \"Anmeldung mit Passwort\" ist für die Anwendung nicht aktiviert",
"The order: %s does not exist": "Die Bestellung: %s existiert nicht",
"The organization: %s does not exist": "Die Organisation: %s existiert nicht",
"The organization: %s has disabled users to signin": "Die Organisation: %s hat die Anmeldung von Benutzern deaktiviert",
"The plan: %s does not exist": "Der Plan: %s existiert nicht",
@@ -48,7 +48,7 @@
"CIDR for IP: %s should not be empty": "CIDR für IP: %s darf nicht leer sein",
"Default code does not match the code's matching rules": "Standardcode entspricht nicht den Übereinstimmungsregeln des Codes",
"DisplayName cannot be blank": "Anzeigename kann nicht leer sein",
"DisplayName is not valid real name": "DisplayName ist kein gültiger Vorname",
"DisplayName is not valid real name": "Der Anzeigename ist kein gültiger echter Name",
"Email already exists": "E-Mail existiert bereits",
"Email cannot be empty": "E-Mail darf nicht leer sein",
"Email is invalid": "E-Mail ist ungültig",
@@ -57,11 +57,11 @@
"Face data mismatch": "Gesichtsdaten stimmen nicht überein",
"Failed to parse client IP: %s": "Fehler beim Parsen der Client-IP: %s",
"FirstName cannot be blank": "Vorname darf nicht leer sein",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Gastbenutzer müssen ihr Konto aktualisieren, indem sie einen Benutzernamen und ein Passwort festlegen, bevor sie sich direkt anmelden können",
"Invitation code cannot be blank": "Einladungscode darf nicht leer sein",
"Invitation code exhausted": "Einladungscode aufgebraucht",
"Invitation code is invalid": "Einladungscode ist ungültig",
"Invitation code suspended": "Einladungscode ausgesetzt",
"LDAP user name or password incorrect": "Ldap Benutzername oder Passwort falsch",
"LastName cannot be blank": "Nachname darf nicht leer sein",
"Multiple accounts with same uid, please check your ldap server": "Mehrere Konten mit derselben uid, bitte überprüfen Sie Ihren LDAP-Server",
"Organization does not exist": "Organisation existiert nicht",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Gruppen importieren fehlgeschlagen",
"Failed to import users": "Fehler beim Importieren von Benutzern",
"Insufficient balance: new balance %v would be below credit limit %v": "Unzureichendes Guthaben: neues Guthaben %v wäre unter dem Kreditlimit %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Unzureichendes Guthaben: neues Organisationsguthaben %v wäre unter dem Kreditlimit %v",
"Missing parameter": "Fehlender Parameter",
"Only admin user can specify user": "Nur Administrator kann Benutzer angeben",
"Please login first": "Bitte zuerst einloggen",
"The LDAP: %s does not exist": "Das LDAP: %s existiert nicht",
"The organization: %s should have one application at least": "Die Organisation: %s sollte mindestens eine Anwendung haben",
"The syncer: %s does not exist": "Der Synchronizer: %s existiert nicht",
"The user: %s doesn't exist": "Der Benutzer %s existiert nicht",
"The user: %s is not found": "Der Benutzer: %s wurde nicht gefunden",
"User is required for User category transaction": "Benutzer ist für Benutzer-Kategorie-Transaktionen erforderlich",
"Wrong userId": "Falsche Benutzer-ID",
"don't support captchaProvider: ": "Unterstütze captchaProvider nicht:",
"this operation is not allowed in demo mode": "Dieser Vorgang ist im Demo-Modus nicht erlaubt",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "Die Berechtigung: \"%s\" existiert nicht"
},
"product": {
"Product list cannot be empty": "Produktliste darf nicht leer sein"
},
"provider": {
"Failed to initialize ID Verification provider": "ID-Verifizierungsanbieter konnte nicht initialisiert werden",
"Invalid application id": "Ungültige Anwendungs-ID",
"No ID Verification provider configured": "Kein ID-Verifizierungsanbieter konfiguriert",
"Provider is not an ID Verification provider": "Anbieter ist kein ID-Verifizierungsanbieter",
"the provider: %s does not exist": "Der Anbieter %s existiert nicht"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Ungültige E-Mail-Empfänger: %s",
"Invalid phone receivers: %s": "Ungültige Telefonempfänger: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "Sitzungs-ID %s ist die aktuelle Sitzung und kann nicht gelöscht werden"
},
"storage": {
"The objectKey: %s is not allowed": "Der Objektschlüssel %s ist nicht erlaubt",
"The provider type: %s is not supported": "Der Anbieter-Typ %s wird nicht unterstützt"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Fehler"
},
"ticket": {
"Ticket not found": "Ticket nicht gefunden"
},
"token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s wird von dieser Anwendung nicht unterstützt",
"Invalid application or wrong clientSecret": "Ungültige Anwendung oder falsches clientSecret",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "Anzeigename darf nicht leer sein",
"ID card information and real name are required": "Personalausweisinformationen und vollständiger Name sind erforderlich",
"Identity verification failed": "Identitätsprüfung fehlgeschlagen",
"MFA email is enabled but email is empty": "MFA-E-Mail ist aktiviert, aber E-Mail ist leer",
"MFA phone is enabled but phone number is empty": "MFA-Telefon ist aktiviert, aber Telefonnummer ist leer",
"New password cannot contain blank space.": "Das neue Passwort darf keine Leerzeichen enthalten.",
"No application found for user": "Keine Anwendung für Benutzer gefunden",
"The new password must be different from your current password": "Das neue Passwort muss sich von Ihrem aktuellen Passwort unterscheiden",
"User is already verified": "Benutzer ist bereits verifiziert",
"the user's owner and name should not be empty": "Eigentümer und Name des Benutzers dürfen nicht leer sein"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "Ungültiger Captcha-Anbieter.",
"Phone number is invalid in your region %s": "Die Telefonnummer ist in Ihrer Region %s ungültig",
"The forgot password feature is disabled": "Die Funktion \"Passwort vergessen\" ist deaktiviert",
"The verification code has already been used!": "Der Verifizierungscode wurde bereits verwendet!",
"The verification code has not been sent yet!": "Der Verifizierungscode wurde noch nicht gesendet!",
"Turing test failed.": "Turing-Test fehlgeschlagen.",

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "Failed to add user",
"Get init score failed, error: %w": "Get init score failed, error: %w",
"Please sign out first": "Please sign out first",
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "The login method: login with email is not enabled for the application",
"The login method: login with face is not enabled for the application": "The login method: login with face is not enabled for the application",
"The login method: login with password is not enabled for the application": "The login method: login with password is not enabled for the application",
"The order: %s does not exist": "The order: %s does not exist",
"The organization: %s does not exist": "The organization: %s does not exist",
"The organization: %s has disabled users to signin": "The organization: %s has disabled users to signin",
"The plan: %s does not exist": "The plan: %s does not exist",
@@ -57,11 +57,11 @@
"Face data mismatch": "Face data mismatch",
"Failed to parse client IP: %s": "Failed to parse client IP: %s",
"FirstName cannot be blank": "FirstName cannot be blank",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Guest users must upgrade their account by setting a username and password before they can sign in directly",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code exhausted": "Invitation code exhausted",
"Invitation code is invalid": "Invitation code is invalid",
"Invitation code suspended": "Invitation code suspended",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Insufficient balance: new balance %v would be below credit limit %v": "Insufficient balance: new balance %v would be below credit limit %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Insufficient balance: new organization balance %v would be below credit limit %v",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
"The LDAP: %s does not exist": "The LDAP: %s does not exist",
"The organization: %s should have one application at least": "The organization: %s should have one application at least",
"The syncer: %s does not exist": "The syncer: %s does not exist",
"The user: %s doesn't exist": "The user: %s doesn't exist",
"The user: %s is not found": "The user: %s is not found",
"User is required for User category transaction": "User is required for User category transaction",
"Wrong userId": "Wrong userId",
"don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "The permission: \"%s\" doesn't exist"
},
"product": {
"Product list cannot be empty": "Product list cannot be empty"
},
"provider": {
"Failed to initialize ID Verification provider": "Failed to initialize ID Verification provider",
"Invalid application id": "Invalid application id",
"No ID Verification provider configured": "No ID Verification provider configured",
"Provider is not an ID Verification provider": "Provider is not an ID Verification provider",
"the provider: %s does not exist": "the provider: %s does not exist"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Invalid Email receivers: %s",
"Invalid phone receivers: %s": "Invalid phone receivers: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
},
"storage": {
"The objectKey: %s is not allowed": "The objectKey: %s is not allowed",
"The provider type: %s is not supported": "The provider type: %s is not supported"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Error"
},
"ticket": {
"Ticket not found": "Ticket not found"
},
"token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s is not supported in this application",
"Invalid application or wrong clientSecret": "Invalid application or wrong clientSecret",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "Display name cannot be empty",
"ID card information and real name are required": "ID card information and real name are required",
"Identity verification failed": "Identity verification failed",
"MFA email is enabled but email is empty": "MFA email is enabled but email is empty",
"MFA phone is enabled but phone number is empty": "MFA phone is enabled but phone number is empty",
"New password cannot contain blank space.": "New password cannot contain blank space.",
"No application found for user": "No application found for user",
"The new password must be different from your current password": "The new password must be different from your current password",
"User is already verified": "User is already verified",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"util": {

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "No se pudo agregar el usuario",
"Get init score failed, error: %w": "Error al obtener el puntaje de inicio, error: %w",
"Please sign out first": "Por favor, cierra sesión primero",
"The application does not allow to sign up new account": "La aplicación no permite registrarse con una cuenta nueva"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "El método de inicio de sesión: inicio de sesión con correo electrónico no está habilitado para la aplicación",
"The login method: login with face is not enabled for the application": "El método de inicio de sesión: inicio de sesión con reconocimiento facial no está habilitado para la aplicación",
"The login method: login with password is not enabled for the application": "El método de inicio de sesión: inicio de sesión con contraseña no está habilitado para la aplicación",
"The order: %s does not exist": "El pedido: %s no existe",
"The organization: %s does not exist": "La organización: %s no existe",
"The organization: %s has disabled users to signin": "La organización: %s ha desactivado el inicio de sesión de usuarios",
"The plan: %s does not exist": "El plan: %s no existe",
@@ -35,7 +35,7 @@
"User's tag: %s is not listed in the application's tags": "La etiqueta del usuario: %s no está incluida en las etiquetas de la aplicación",
"UserCode Expired": "Código de usuario expirado",
"UserCode Invalid": "Código de usuario inválido",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "El usuario de pago %s no tiene una suscripción activa o pendiente y la aplicación: %s no tiene precio predeterminado",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "El usuario de pago %s no tiene una suscripción activa o pendiente y la aplicación %s no tiene precios predeterminados",
"the application for user %s is not found": "no se encontró la aplicación para el usuario %s",
"the organization: %s is not found": "no se encontró la organización: %s"
},
@@ -44,9 +44,9 @@
},
"check": {
"%s does not meet the CIDR format requirements: %s": "%s no cumple con los requisitos del formato CIDR: %s",
"Affiliation cannot be blank": "Afiliación no puede estar en blanco",
"Affiliation cannot be blank": "La afiliación no puede estar vacía",
"CIDR for IP: %s should not be empty": "El CIDR para la IP: %s no debe estar vacío",
"Default code does not match the code's matching rules": "El código predeterminado no coincide con las reglas de coincidencia de códigos",
"Default code does not match the code's matching rules": "El código predeterminado no cumple con las reglas de validación del código",
"DisplayName cannot be blank": "El nombre de visualización no puede estar en blanco",
"DisplayName is not valid real name": "El nombre de pantalla no es un nombre real válido",
"Email already exists": "El correo electrónico ya existe",
@@ -57,11 +57,11 @@
"Face data mismatch": "Los datos faciales no coinciden",
"Failed to parse client IP: %s": "Error al analizar la IP del cliente: %s",
"FirstName cannot be blank": "El nombre no puede estar en blanco",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Los usuarios invitados deben actualizar su cuenta configurando un nombre de usuario y una contraseña antes de poder iniciar sesión directamente",
"Invitation code cannot be blank": "El código de invitación no puede estar vacío",
"Invitation code exhausted": "Código de invitación agotado",
"Invitation code is invalid": "Código de invitación inválido",
"Invitation code suspended": "Código de invitación suspendido",
"LDAP user name or password incorrect": "Nombre de usuario o contraseña de Ldap incorrectos",
"LastName cannot be blank": "El apellido no puede estar en blanco",
"Multiple accounts with same uid, please check your ldap server": "Cuentas múltiples con el mismo uid, por favor revise su servidor ldap",
"Organization does not exist": "La organización no existe",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Error al importar grupos",
"Failed to import users": "Error al importar usuarios",
"Insufficient balance: new balance %v would be below credit limit %v": "Saldo insuficiente: el nuevo saldo %v estaría por debajo del límite de crédito %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Saldo insuficiente: el nuevo saldo de la organización %v estaría por debajo del límite de crédito %v",
"Missing parameter": "Parámetro faltante",
"Only admin user can specify user": "Solo el usuario administrador puede especificar usuario",
"Please login first": "Por favor, inicia sesión primero",
"The LDAP: %s does not exist": "El LDAP: %s no existe",
"The organization: %s should have one application at least": "La organización: %s debe tener al menos una aplicación",
"The syncer: %s does not exist": "El sincronizador: %s no existe",
"The user: %s doesn't exist": "El usuario: %s no existe",
"The user: %s is not found": "El usuario: %s no encontrado",
"User is required for User category transaction": "El usuario es obligatorio para la transacción de la categoría Usuario",
"Wrong userId": "ID de usuario incorrecto",
"don't support captchaProvider: ": "No apoyo a captchaProvider",
"this operation is not allowed in demo mode": "esta operación no está permitida en modo de demostración",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "El permiso: \"%s\" no existe"
},
"product": {
"Product list cannot be empty": "La lista de productos no puede estar vacía"
},
"provider": {
"Failed to initialize ID Verification provider": "Error al inicializar el proveedor de verificación de ID",
"Invalid application id": "Identificación de aplicación no válida",
"No ID Verification provider configured": "No hay proveedor de verificación de ID configurado",
"Provider is not an ID Verification provider": "El proveedor no es un proveedor de verificación de ID",
"the provider: %s does not exist": "El proveedor: %s no existe"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Receptores de correo electrónico no válidos: %s",
"Invalid phone receivers: %s": "Receptores de teléfono no válidos: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
},
"storage": {
"The objectKey: %s is not allowed": "El objectKey: %s no está permitido",
"The provider type: %s is not supported": "El tipo de proveedor: %s no es compatible"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Error"
},
"ticket": {
"Ticket not found": "Ticket no encontrado"
},
"token": {
"Grant_type: %s is not supported in this application": "El tipo de subvención: %s no es compatible con esta aplicación",
"Invalid application or wrong clientSecret": "Solicitud inválida o clientSecret incorrecto",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "El nombre de pantalla no puede estar vacío",
"ID card information and real name are required": "Se requiere información de la tarjeta de identificación y el nombre real",
"Identity verification failed": "Falló la verificación de identidad",
"MFA email is enabled but email is empty": "El correo electrónico MFA está habilitado pero el correo está vacío",
"MFA phone is enabled but phone number is empty": "El teléfono MFA está habilitado pero el número de teléfono está vacío",
"New password cannot contain blank space.": "La nueva contraseña no puede contener espacios en blanco.",
"No application found for user": "No se encontró aplicación para el usuario",
"The new password must be different from your current password": "La nueva contraseña debe ser diferente de su contraseña actual",
"User is already verified": "El usuario ya está verificado",
"the user's owner and name should not be empty": "el propietario y el nombre del usuario no deben estar vacíos"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "Proveedor de captcha no válido.",
"Phone number is invalid in your region %s": "El número de teléfono es inválido en tu región %s",
"The forgot password feature is disabled": "La función de contraseña olvidada está deshabilitada",
"The verification code has already been used!": "¡El código de verificación ya ha sido utilizado!",
"The verification code has not been sent yet!": "¡El código de verificación aún no ha sido enviado!",
"Turing test failed.": "El test de Turing falló.",

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "Échec d'ajout d'utilisateur",
"Get init score failed, error: %w": "Obtention du score initiale échouée, erreur : %w",
"Please sign out first": "Veuillez vous déconnecter en premier",
"The application does not allow to sign up new account": "L'application ne permet pas de créer un nouveau compte"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "La méthode de connexion : connexion par e-mail n'est pas activée pour l'application",
"The login method: login with face is not enabled for the application": "La méthode de connexion : connexion par visage n'est pas activée pour l'application",
"The login method: login with password is not enabled for the application": "La méthode de connexion : connexion avec mot de passe n'est pas activée pour l'application",
"The order: %s does not exist": "La commande : %s n'existe pas",
"The organization: %s does not exist": "L'organisation : %s n'existe pas",
"The organization: %s has disabled users to signin": "L'organisation: %s a désactivé la connexion des utilisateurs",
"The plan: %s does not exist": "Le plan : %s n'existe pas",
@@ -35,7 +35,7 @@
"User's tag: %s is not listed in the application's tags": "Le tag de l'utilisateur : %s n'est pas répertorié dans les tags de l'application",
"UserCode Expired": "Code utilisateur expiré",
"UserCode Invalid": "Code utilisateur invalide",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "L'utilisateur payant %s n'a pas d'abonnement actif ou en attente et l'application : %s n'a pas de tarification par défaut",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "L'utilisateur payant %s n'a pas d'abonnement actif ou en attente et l'application %s n'a pas de tarification par défaut",
"the application for user %s is not found": "L'application pour l'utilisateur %s est introuvable",
"the organization: %s is not found": "L'organisation : %s est introuvable"
},
@@ -44,9 +44,9 @@
},
"check": {
"%s does not meet the CIDR format requirements: %s": "%s ne respecte pas les exigences du format CIDR : %s",
"Affiliation cannot be blank": "Affiliation ne peut pas être vide",
"Affiliation cannot be blank": "L'affiliation ne peut pas être vide",
"CIDR for IP: %s should not be empty": "Le CIDR pour l'IP : %s ne doit pas être vide",
"Default code does not match the code's matching rules": "Le code par défaut ne correspond pas aux règles de correspondance du code",
"Default code does not match the code's matching rules": "Le code par défaut ne respecte pas les règles de validation du code",
"DisplayName cannot be blank": "Le nom d'affichage ne peut pas être vide",
"DisplayName is not valid real name": "DisplayName n'est pas un nom réel valide",
"Email already exists": "E-mail déjà existant",
@@ -57,11 +57,11 @@
"Face data mismatch": "Données faciales incorrectes",
"Failed to parse client IP: %s": "Échec de l'analyse de l'IP client : %s",
"FirstName cannot be blank": "Le prénom ne peut pas être laissé vide",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Les utilisateurs invités doivent mettre à niveau leur compte en définissant un nom d'utilisateur et un mot de passe avant de pouvoir se connecter directement",
"Invitation code cannot be blank": "Le code d'invitation ne peut pas être vide",
"Invitation code exhausted": "Code d'invitation épuisé",
"Invitation code is invalid": "Code d'invitation invalide",
"Invitation code suspended": "Code d'invitation suspendu",
"LDAP user name or password incorrect": "Nom d'utilisateur ou mot de passe LDAP incorrect",
"LastName cannot be blank": "Le nom de famille ne peut pas être vide",
"Multiple accounts with same uid, please check your ldap server": "Plusieurs comptes avec le même identifiant d'utilisateur, veuillez vérifier votre serveur LDAP",
"Organization does not exist": "L'organisation n'existe pas",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Échec de l'importation des groupes",
"Failed to import users": "Échec de l'importation des utilisateurs",
"Insufficient balance: new balance %v would be below credit limit %v": "Solde insuffisant : le nouveau solde %v serait inférieur à la limite de crédit %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Solde insuffisant : le nouveau solde de l'organisation %v serait inférieur à la limite de crédit %v",
"Missing parameter": "Paramètre manquant",
"Only admin user can specify user": "Seul un administrateur peut désigner un utilisateur",
"Please login first": "Veuillez d'abord vous connecter",
"The LDAP: %s does not exist": "Le LDAP : %s n'existe pas",
"The organization: %s should have one application at least": "L'organisation : %s doit avoir au moins une application",
"The syncer: %s does not exist": "Le synchroniseur : %s n'existe pas",
"The user: %s doesn't exist": "L'utilisateur : %s n'existe pas",
"The user: %s is not found": "L'utilisateur : %s est introuvable",
"User is required for User category transaction": "L'utilisateur est requis pour la transaction de catégorie Utilisateur",
"Wrong userId": "ID utilisateur incorrect",
"don't support captchaProvider: ": "ne prend pas en charge captchaProvider: ",
"this operation is not allowed in demo mode": "cette opération n'est pas autorisée en mode démo",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "La permission : \"%s\" n'existe pas"
},
"product": {
"Product list cannot be empty": "La liste des produits ne peut pas être vide"
},
"provider": {
"Failed to initialize ID Verification provider": "Échec de l'initialisation du fournisseur de vérification d'identité",
"Invalid application id": "Identifiant d'application invalide",
"No ID Verification provider configured": "Aucun fournisseur de vérification d'identité configuré",
"Provider is not an ID Verification provider": "Le fournisseur n'est pas un fournisseur de vérification d'identité",
"the provider: %s does not exist": "Le fournisseur : %s n'existe pas"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Destinataires d'e-mail invalides : %s",
"Invalid phone receivers: %s": "Destinataires de téléphone invalide : %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "session id %s is the current session and cannot be deleted"
},
"storage": {
"The objectKey: %s is not allowed": "La clé d'objet : %s n'est pas autorisée",
"The provider type: %s is not supported": "Le type de fournisseur : %s n'est pas pris en charge"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Erreur"
},
"ticket": {
"Ticket not found": "Ticket introuvable"
},
"token": {
"Grant_type: %s is not supported in this application": "Type_de_subvention : %s n'est pas pris en charge dans cette application",
"Invalid application or wrong clientSecret": "Application invalide ou clientSecret incorrect",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "Le nom d'affichage ne peut pas être vide",
"ID card information and real name are required": "Les informations de la carte d'identité et le nom réel sont requis",
"Identity verification failed": "Échec de la vérification d'identité",
"MFA email is enabled but email is empty": "L'authentification MFA par e-mail est activée mais l'e-mail est vide",
"MFA phone is enabled but phone number is empty": "L'authentification MFA par téléphone est activée mais le numéro de téléphone est vide",
"New password cannot contain blank space.": "Le nouveau mot de passe ne peut pas contenir d'espace.",
"No application found for user": "Aucune application trouvée pour l'utilisateur",
"The new password must be different from your current password": "Le nouveau mot de passe doit être différent de votre mot de passe actuel",
"User is already verified": "L'utilisateur est déjà vérifié",
"the user's owner and name should not be empty": "le propriétaire et le nom de l'utilisateur ne doivent pas être vides"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "Fournisseur de captcha invalide.",
"Phone number is invalid in your region %s": "Le numéro de téléphone n'est pas valide dans votre région %s",
"The forgot password feature is disabled": "La fonction de mot de passe oublié est désactivée",
"The verification code has already been used!": "Le code de vérification a déjà été utilisé !",
"The verification code has not been sent yet!": "Le code de vérification n'a pas encore été envoyé !",
"Turing test failed.": "Le test de Turing a échoué.",

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "ユーザーの追加に失敗しました",
"Get init score failed, error: %w": "イニットスコアの取得に失敗しました。エラー:%w",
"Please sign out first": "最初にサインアウトしてください",
"The application does not allow to sign up new account": "アプリケーションは新しいアカウントの登録を許可しません"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "このアプリケーションではメールログインは有効になっていません",
"The login method: login with face is not enabled for the application": "このアプリケーションでは顔認証ログインは有効になっていません",
"The login method: login with password is not enabled for the application": "ログイン方法:パスワードでのログインはアプリケーションで有効になっていません",
"The order: %s does not exist": "注文:%s は存在しません",
"The organization: %s does not exist": "組織「%s」は存在しません",
"The organization: %s has disabled users to signin": "組織: %s はユーザーのサインインを無効にしました",
"The plan: %s does not exist": "プラン: %sは存在しません",
@@ -57,11 +57,11 @@
"Face data mismatch": "顔認証データが一致しません",
"Failed to parse client IP: %s": "クライアント IP「%s」の解析に失敗しました",
"FirstName cannot be blank": "ファーストネームは空白にできません",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "ゲストユーザーは直接サインインする前に、ユーザー名とパスワードを設定してアカウントをアップグレードする必要があります",
"Invitation code cannot be blank": "招待コードは空にできません",
"Invitation code exhausted": "招待コードの使用回数が上限に達しました",
"Invitation code is invalid": "招待コードが無効です",
"Invitation code suspended": "招待コードは一時的に無効化されています",
"LDAP user name or password incorrect": "Ldapのユーザー名またはパスワードが間違っています",
"LastName cannot be blank": "姓は空白にできません",
"Multiple accounts with same uid, please check your ldap server": "同じuidを持つ複数のアカウントがあります。あなたのLDAPサーバーを確認してください",
"Organization does not exist": "組織は存在しません",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "グループのインポートに失敗しました",
"Failed to import users": "ユーザーのインポートに失敗しました",
"Insufficient balance: new balance %v would be below credit limit %v": "残高不足:新しい残高 %v がクレジット制限 %v を下回ります",
"Insufficient balance: new organization balance %v would be below credit limit %v": "残高不足:新しい組織残高 %v がクレジット制限 %v を下回ります",
"Missing parameter": "不足しているパラメーター",
"Only admin user can specify user": "管理者ユーザーのみがユーザーを指定できます",
"Please login first": "最初にログインしてください",
"The LDAP: %s does not exist": "LDAP%s は存在しません",
"The organization: %s should have one application at least": "組織「%s」は少なくとも1つのアプリケーションを持っている必要があります",
"The syncer: %s does not exist": "同期装置:%s は存在しません",
"The user: %s doesn't exist": "そのユーザー:%sは存在しません",
"The user: %s is not found": "ユーザー:%s が見つかりません",
"User is required for User category transaction": "ユーザーカテゴリトランザクションにはユーザーが必要です",
"Wrong userId": "無効なユーザーIDです",
"don't support captchaProvider: ": "captchaProviderをサポートしないでください",
"this operation is not allowed in demo mode": "この操作はデモモードでは許可されていません",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "権限「%s」は存在しません"
},
"product": {
"Product list cannot be empty": "商品リストは空にできません"
},
"provider": {
"Failed to initialize ID Verification provider": "ID認証プロバイダーの初期化に失敗しました",
"Invalid application id": "アプリケーションIDが無効です",
"No ID Verification provider configured": "ID認証プロバイダーが設定されていません",
"Provider is not an ID Verification provider": "プロバイダーはID認証プロバイダーではありません",
"the provider: %s does not exist": "プロバイダー%sは存在しません"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "無効な電子メール受信者:%s",
"Invalid phone receivers: %s": "電話受信者が無効です:%s"
},
"session": {
"session id %s is the current session and cannot be deleted": "セッションID %s は現在のセッションであり、削除できません"
},
"storage": {
"The objectKey: %s is not allowed": "オブジェクトキー %s は許可されていません",
"The provider type: %s is not supported": "プロバイダータイプ:%sはサポートされていません"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "エラー"
},
"ticket": {
"Ticket not found": "チケットが見つかりません"
},
"token": {
"Grant_type: %s is not supported in this application": "grant_type%sはこのアプリケーションでサポートされていません",
"Invalid application or wrong clientSecret": "無効なアプリケーションまたは誤ったクライアントシークレットです",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "表示名は空にできません",
"ID card information and real name are required": "身分証明書の情報と実名が必要です",
"Identity verification failed": "身元確認に失敗しました",
"MFA email is enabled but email is empty": "MFA メールが有効になっていますが、メールアドレスが空です",
"MFA phone is enabled but phone number is empty": "MFA 電話番号が有効になっていますが、電話番号が空です",
"New password cannot contain blank space.": "新しいパスワードにはスペースを含めることはできません。",
"No application found for user": "ユーザーのアプリケーションが見つかりません",
"The new password must be different from your current password": "新しいパスワードは現在のパスワードと異なる必要があります",
"User is already verified": "ユーザーは既に認証済みです",
"the user's owner and name should not be empty": "ユーザーのオーナーと名前は空にできません"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "無効なCAPTCHAプロバイダー。",
"Phone number is invalid in your region %s": "電話番号はあなたの地域で無効です %s",
"The forgot password feature is disabled": "パスワードを忘れた機能は無効になっています",
"The verification code has already been used!": "この検証コードは既に使用されています!",
"The verification code has not been sent yet!": "検証コードはまだ送信されていません!",
"Turing test failed.": "チューリングテストは失敗しました。",

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "Nie udało się dodać użytkownika",
"Get init score failed, error: %w": "Pobranie początkowego wyniku nie powiodło się, błąd: %w",
"Please sign out first": "Najpierw się wyloguj",
"The application does not allow to sign up new account": "Aplikacja nie pozwala na rejestrację nowego konta"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "Metoda logowania: logowanie przez email nie jest włączona dla aplikacji",
"The login method: login with face is not enabled for the application": "Metoda logowania: logowanie przez twarz nie jest włączona dla aplikacji",
"The login method: login with password is not enabled for the application": "Metoda logowania: logowanie przez hasło nie jest włączone dla aplikacji",
"The order: %s does not exist": "Zamówienie: %s nie istnieje",
"The organization: %s does not exist": "Organizacja: %s nie istnieje",
"The organization: %s has disabled users to signin": "Organizacja: %s wyłączyła logowanie użytkowników",
"The plan: %s does not exist": "Plan: %s nie istnieje",
@@ -57,11 +57,11 @@
"Face data mismatch": "Niezgodność danych twarzy",
"Failed to parse client IP: %s": "Nie udało się przeanalizować IP klienta: %s",
"FirstName cannot be blank": "Imię nie może być puste",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Użytkownicy-goście muszą uaktualnić swoje konto, ustawiając nazwę użytkownika i hasło, zanim będą mogli się zalogować bezpośrednio",
"Invitation code cannot be blank": "Kod zaproszenia nie może być pusty",
"Invitation code exhausted": "Kod zaproszenia został wykorzystany",
"Invitation code is invalid": "Kod zaproszenia jest nieprawidłowy",
"Invitation code suspended": "Kod zaproszenia został zawieszony",
"LDAP user name or password incorrect": "Nazwa użytkownika LDAP lub hasło jest nieprawidłowe",
"LastName cannot be blank": "Nazwisko nie może być puste",
"Multiple accounts with same uid, please check your ldap server": "Wiele kont z tym samym uid, sprawdź swój serwer ldap",
"Organization does not exist": "Organizacja nie istnieje",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Nie udało się zaimportować grup",
"Failed to import users": "Nie udało się zaimportować użytkowników",
"Insufficient balance: new balance %v would be below credit limit %v": "Niewystarczające saldo: nowe saldo %v byłoby poniżej limitu kredytowego %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Niewystarczające saldo: nowe saldo organizacji %v byłoby poniżej limitu kredytowego %v",
"Missing parameter": "Brakujący parametr",
"Only admin user can specify user": "Tylko administrator może wskazać użytkownika",
"Please login first": "Najpierw się zaloguj",
"The LDAP: %s does not exist": "LDAP: %s nie istnieje",
"The organization: %s should have one application at least": "Organizacja: %s powinna mieć co najmniej jedną aplikację",
"The syncer: %s does not exist": "Synchronizer: %s nie istnieje",
"The user: %s doesn't exist": "Użytkownik: %s nie istnieje",
"The user: %s is not found": "Użytkownik: %s nie został znaleziony",
"User is required for User category transaction": "Użytkownik jest wymagany do transakcji kategorii użytkownika",
"Wrong userId": "Nieprawidłowy userId",
"don't support captchaProvider: ": "nie obsługuje captchaProvider: ",
"this operation is not allowed in demo mode": "ta operacja nie jest dozwolona w trybie demo",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "Uprawnienie: \"%s\" nie istnieje"
},
"product": {
"Product list cannot be empty": "Lista produktów nie może być pusta"
},
"provider": {
"Failed to initialize ID Verification provider": "Nie udało się zainicjować dostawcy weryfikacji ID",
"Invalid application id": "Nieprawidłowe id aplikacji",
"No ID Verification provider configured": "Brak skonfigurowanego dostawcy weryfikacji ID",
"Provider is not an ID Verification provider": "Dostawca nie jest dostawcą weryfikacji ID",
"the provider: %s does not exist": "dostawca: %s nie istnieje"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Nieprawidłowi odbiorcy email: %s",
"Invalid phone receivers: %s": "Nieprawidłowi odbiorcy telefonu: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "identyfikator sesji %s jest bieżącą sesją i nie może być usunięty"
},
"storage": {
"The objectKey: %s is not allowed": "Klucz obiektu: %s jest niedozwolony",
"The provider type: %s is not supported": "Typ dostawcy: %s nie jest obsługiwany"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Błąd"
},
"ticket": {
"Ticket not found": "Nie znaleziono biletu"
},
"token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s nie jest obsługiwany w tej aplikacji",
"Invalid application or wrong clientSecret": "Nieprawidłowa aplikacja lub błędny clientSecret",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "Nazwa wyświetlana nie może być pusta",
"ID card information and real name are required": "Wymagane są informacje z dowodu osobistego i prawdziwe nazwisko",
"Identity verification failed": "Weryfikacja tożsamości nie powiodła się",
"MFA email is enabled but email is empty": "MFA email jest włączone, ale email jest pusty",
"MFA phone is enabled but phone number is empty": "MFA telefon jest włączony, ale numer telefonu jest pusty",
"New password cannot contain blank space.": "Nowe hasło nie może zawierać spacji.",
"No application found for user": "Nie znaleziono aplikacji dla użytkownika",
"The new password must be different from your current password": "Nowe hasło musi różnić się od obecnego hasła",
"User is already verified": "Użytkownik jest już zweryfikowany",
"the user's owner and name should not be empty": "właściciel i nazwa użytkownika nie powinny być puste"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "Nieprawidłowy dostawca captcha.",
"Phone number is invalid in your region %s": "Numer telefonu jest nieprawidłowy w twoim regionie %s",
"The forgot password feature is disabled": "Funkcja \"Zapomniałem hasła\" jest wyłączona",
"The verification code has already been used!": "Kod weryfikacyjny został już wykorzystany!",
"The verification code has not been sent yet!": "Kod weryfikacyjny nie został jeszcze wysłany!",
"Turing test failed.": "Test Turinga nie powiódł się.",

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "Falha ao adicionar usuário",
"Get init score failed, error: %w": "Falha ao obter pontuação inicial, erro: %w",
"Please sign out first": "Por favor, saia primeiro",
"The application does not allow to sign up new account": "O aplicativo não permite a criação de novas contas"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "O método de login com e-mail não está habilitado para o aplicativo",
"The login method: login with face is not enabled for the application": "O método de login com reconhecimento facial não está habilitado para o aplicativo",
"The login method: login with password is not enabled for the application": "O método de login com senha não está habilitado para o aplicativo",
"The order: %s does not exist": "O pedido: %s não existe",
"The organization: %s does not exist": "A organização: %s não existe",
"The organization: %s has disabled users to signin": "A organização: %s desativou o login de usuários",
"The plan: %s does not exist": "O plano: %s não existe",
@@ -57,11 +57,11 @@
"Face data mismatch": "Dados faciais não correspondem",
"Failed to parse client IP: %s": "Falha ao analisar o IP do cliente: %s",
"FirstName cannot be blank": "O primeiro nome não pode estar em branco",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Usuários convidados devem atualizar suas contas definindo um nome de usuário e senha antes de poderem entrar diretamente",
"Invitation code cannot be blank": "O código de convite não pode estar em branco",
"Invitation code exhausted": "O código de convite foi esgotado",
"Invitation code is invalid": "Código de convite inválido",
"Invitation code suspended": "Código de convite suspenso",
"LDAP user name or password incorrect": "Nome de usuário ou senha LDAP incorretos",
"LastName cannot be blank": "O sobrenome não pode estar em branco",
"Multiple accounts with same uid, please check your ldap server": "Múltiplas contas com o mesmo uid, verifique seu servidor LDAP",
"Organization does not exist": "A organização não existe",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Falha ao importar grupos",
"Failed to import users": "Falha ao importar usuários",
"Insufficient balance: new balance %v would be below credit limit %v": "Saldo insuficiente: o novo saldo %v estaria abaixo do limite de crédito %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Saldo insuficiente: o novo saldo da organização %v estaria abaixo do limite de crédito %v",
"Missing parameter": "Parâmetro ausente",
"Only admin user can specify user": "Apenas um administrador pode especificar um usuário",
"Please login first": "Por favor, faça login primeiro",
"The LDAP: %s does not exist": "O LDAP: %s não existe",
"The organization: %s should have one application at least": "A organização: %s deve ter pelo menos um aplicativo",
"The syncer: %s does not exist": "O sincronizador: %s não existe",
"The user: %s doesn't exist": "O usuário: %s não existe",
"The user: %s is not found": "O usuário: %s não foi encontrado",
"User is required for User category transaction": "Usuário é obrigatório para transação de categoria de usuário",
"Wrong userId": "ID de usuário incorreto",
"don't support captchaProvider: ": "captchaProvider não suportado: ",
"this operation is not allowed in demo mode": "esta operação não é permitida no modo de demonstração",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "A permissão: \"%s\" não existe"
},
"product": {
"Product list cannot be empty": "A lista de produtos não pode estar vazia"
},
"provider": {
"Failed to initialize ID Verification provider": "Falha ao inicializar provedor de verificação de ID",
"Invalid application id": "ID de aplicativo inválido",
"No ID Verification provider configured": "Nenhum provedor de verificação de ID configurado",
"Provider is not an ID Verification provider": "Provedor não é um provedor de verificação de ID",
"the provider: %s does not exist": "O provedor: %s não existe"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Destinatários de e-mail inválidos: %s",
"Invalid phone receivers: %s": "Destinatários de telefone inválidos: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "ID da sessão %s é a sessão atual e não pode ser excluída"
},
"storage": {
"The objectKey: %s is not allowed": "A chave de objeto: %s não é permitida",
"The provider type: %s is not supported": "O tipo de provedor: %s não é suportado"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Erro"
},
"ticket": {
"Ticket not found": "Ticket não encontrado"
},
"token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s não é suportado neste aplicativo",
"Invalid application or wrong clientSecret": "Aplicativo inválido ou clientSecret incorreto",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "O nome de exibição não pode estar vazio",
"ID card information and real name are required": "Informações do documento de identidade e nome verdadeiro são obrigatórios",
"Identity verification failed": "Falha na verificação de identidade",
"MFA email is enabled but email is empty": "MFA por e-mail está habilitado, mas o e-mail está vazio",
"MFA phone is enabled but phone number is empty": "MFA por telefone está habilitado, mas o número de telefone está vazio",
"New password cannot contain blank space.": "A nova senha não pode conter espaços em branco.",
"No application found for user": "Nenhum aplicativo encontrado para o usuário",
"The new password must be different from your current password": "A nova senha deve ser diferente da senha atual",
"User is already verified": "Usuário já está verificado",
"the user's owner and name should not be empty": "O proprietário e o nome do usuário não devem estar vazios"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "Provedor de captcha inválido.",
"Phone number is invalid in your region %s": "Número de telefone inválido na sua região %s",
"The forgot password feature is disabled": "A funcionalidade de esqueci a senha está desabilitada",
"The verification code has already been used!": "O código de verificação já foi utilizado!",
"The verification code has not been sent yet!": "O código de verificação ainda não foi enviado!",
"Turing test failed.": "O teste de Turing falhou.",

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "Kullanıcı eklenemedi",
"Get init score failed, error: %w": "Başlangıç puanı alınamadı, hata: %w",
"Please sign out first": "Lütfen önce çıkış yapın",
"The application does not allow to sign up new account": "Uygulama yeni hesap kaydına izin vermiyor"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "Uygulama için e-posta ile giriş yöntemi etkin değil",
"The login method: login with face is not enabled for the application": "Uygulama için yüz ile giriş yöntemi etkin değil",
"The login method: login with password is not enabled for the application": "Şifre ile giriş yöntemi bu uygulama için etkin değil",
"The order: %s does not exist": "Sipariş: %s mevcut değil",
"The organization: %s does not exist": "Organizasyon: %s mevcut değil",
"The organization: %s has disabled users to signin": "Organizasyon: %s kullanıcıların oturum açmasını devre dışı bıraktı",
"The plan: %s does not exist": "Plan: %s mevcut değil",
@@ -57,11 +57,11 @@
"Face data mismatch": "Yüz verisi uyuşmazlığı",
"Failed to parse client IP: %s": "İstemci IP'si ayrıştırılamadı: %s",
"FirstName cannot be blank": "Ad boş olamaz",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Misafir kullanıcılar doğrudan giriş yapabilmek için kullanıcı adı ve şifre belirleyerek hesaplarını yükseltmelidir",
"Invitation code cannot be blank": "Davet kodu boş olamaz",
"Invitation code exhausted": "Davet kodu kullanım dışı",
"Invitation code is invalid": "Davet kodu geçersiz",
"Invitation code suspended": "Davet kodu askıya alındı",
"LDAP user name or password incorrect": "LDAP kullanıcı adı veya şifre yanlış",
"LastName cannot be blank": "Soyad boş olamaz",
"Multiple accounts with same uid, please check your ldap server": "Aynı uid'ye sahip birden fazla hesap, lütfen ldap sunucunuzu kontrol edin",
"Organization does not exist": "Organizasyon bulunamadı",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Gruplar içe aktarılamadı",
"Failed to import users": "Kullanıcılar içe aktarılamadı",
"Insufficient balance: new balance %v would be below credit limit %v": "Yetersiz bakiye: yeni bakiye %v kredi limitinin altında olacak %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Yetersiz bakiye: yeni organizasyon bakiyesi %v kredi limitinin altında olacak %v",
"Missing parameter": "Eksik parametre",
"Only admin user can specify user": "Yalnızca yönetici kullanıcı kullanıcı belirleyebilir",
"Please login first": "Lütfen önce giriş yapın",
"The LDAP: %s does not exist": "LDAP: %s mevcut değil",
"The organization: %s should have one application at least": "Organizasyon: %s en az bir uygulamaya sahip olmalı",
"The syncer: %s does not exist": "Senkronizasyon: %s mevcut değil",
"The user: %s doesn't exist": "Kullanıcı: %s bulunamadı",
"The user: %s is not found": "Kullanıcı: %s bulunamadı",
"User is required for User category transaction": "Kullanıcı kategorisi işlemi için kullanıcı gerekli",
"Wrong userId": "Yanlış kullanıcı kimliği",
"don't support captchaProvider: ": "captchaProvider desteklenmiyor: ",
"this operation is not allowed in demo mode": "bu işlem demo modunda izin verilmiyor",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "İzin: \"%s\" mevcut değil"
},
"product": {
"Product list cannot be empty": "Ürün listesi boş olamaz"
},
"provider": {
"Failed to initialize ID Verification provider": "Kimlik Doğrulama sağlayıcısı başlatılamadı",
"Invalid application id": "Geçersiz uygulama id",
"No ID Verification provider configured": "Kimlik Doğrulama sağlayıcısı yapılandırılmamış",
"Provider is not an ID Verification provider": "Sağlayıcı bir Kimlik Doğrulama sağlayıcısı değil",
"the provider: %s does not exist": "provider: %s bulunamadı"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Geçersiz e-posta alıcıları: %s",
"Invalid phone receivers: %s": "Geçersiz telefon alıcıları: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "oturum kimliği %s geçerli oturumdur ve silinemez"
},
"storage": {
"The objectKey: %s is not allowed": "objectKey: %s izin verilmiyor",
"The provider type: %s is not supported": "provider türü: %s desteklenmiyor"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Hata"
},
"ticket": {
"Ticket not found": "Bilet bulunamadı"
},
"token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s bu uygulamada desteklenmiyor",
"Invalid application or wrong clientSecret": "Geçersiz uygulama veya yanlış clientSecret",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "Görünen ad boş olamaz",
"ID card information and real name are required": "Kimlik kartı bilgileri ve gerçek adı gereklidir",
"Identity verification failed": "Kimlik doğrulama başarısız",
"MFA email is enabled but email is empty": "MFA e-postası etkin ancak e-posta boş",
"MFA phone is enabled but phone number is empty": "MFA telefonu etkin ancak telefon numarası boş",
"New password cannot contain blank space.": "Yeni şifre boşluk içeremez.",
"No application found for user": "Kullanıcı için uygulama bulunamadı",
"The new password must be different from your current password": "Yeni şifre mevcut şifrenizden farklı olmalıdır",
"User is already verified": "Kullanıcı zaten doğrulanmış",
"the user's owner and name should not be empty": "kullanıcının sahibi ve adı boş olmamalıdır"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "Geçersiz captcha sağlayıcı.",
"Phone number is invalid in your region %s": "Telefon numaranız bölgenizde geçersiz %s",
"The forgot password feature is disabled": "Şifremi unuttum özelliği devre dışı",
"The verification code has already been used!": "Doğrulama kodu zaten kullanılmış!",
"The verification code has not been sent yet!": "Doğrulama kodu henüz gönderilmedi!",
"Turing test failed.": "Turing testi başarısız.",

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "Не вдалося додати користувача",
"Get init score failed, error: %w": "Не вдалося отримати початковий бал, помилка: %w",
"Please sign out first": "Спочатку вийдіть із системи",
"The application does not allow to sign up new account": "Додаток не дозволяє реєструвати нові облікові записи"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "Метод входу через email не увімкнено для цього додатка",
"The login method: login with face is not enabled for the application": "Метод входу через обличчя не увімкнено для цього додатка",
"The login method: login with password is not enabled for the application": "Метод входу через пароль не увімкнено для цього додатка",
"The order: %s does not exist": "Замовлення: %s не існує",
"The organization: %s does not exist": "Організація: %s не існує",
"The organization: %s has disabled users to signin": "Організація: %s вимкнула вхід користувачів",
"The plan: %s does not exist": "План: %s не існує",
@@ -57,11 +57,11 @@
"Face data mismatch": "Невідповідність даних обличчя",
"Failed to parse client IP: %s": "Не вдалося розібрати IP клієнта: %s",
"FirstName cannot be blank": "Ім’я не може бути порожнім",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Гостьові користувачі повинні оновити свій обліковий запис, встановивши ім'я користувача та пароль, перш ніж вони зможуть увійти безпосередньо",
"Invitation code cannot be blank": "Код запрошення не може бути порожнім",
"Invitation code exhausted": "Код запрошення вичерпано",
"Invitation code is invalid": "Код запрошення недійсний",
"Invitation code suspended": "Код запрошення призупинено",
"LDAP user name or password incorrect": "Ім’я користувача або пароль LDAP неправильні",
"LastName cannot be blank": "Прізвище не може бути порожнім",
"Multiple accounts with same uid, please check your ldap server": "Кілька облікових записів з однаковим uid, перевірте ваш ldap-сервер",
"Organization does not exist": "Організація не існує",
@@ -83,8 +83,8 @@
"The user is forbidden to sign in, please contact the administrator": "Користувачу заборонено вхід, зверніться до адміністратора",
"The user: %s doesn't exist in LDAP server": "Користувач: %s не існує на сервері LDAP",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Ім’я користувача може містити лише буквено-цифрові символи, підкреслення або дефіси, не може мати послідовні дефіси або підкреслення та не може починатися або закінчуватися дефісом або підкресленням.",
"The value \"%s\" for account field \"%s\" doesn't match the account item regex": "Värdet \"%s\" för kontofältet \"%s\" matchar inte kontots regex",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "Värdet \"%s\" för registreringsfältet \"%s\" matchar inte registreringsfältets regex för applikationen \"%s\"",
"The value \"%s\" for account field \"%s\" doesn't match the account item regex": "Значення \"%s\" для поля облікового запису \"%s\" не відповідає регулярному виразу облікового запису",
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "Значення \"%s\" для поля реєстрації \"%s\" не відповідає регулярному виразу поля реєстрації додатка \"%s\"",
"Username already exists": "Ім’я користувача вже існує",
"Username cannot be an email address": "Ім’я користувача не може бути email-адресою",
"Username cannot contain white spaces": "Ім’я користувача не може містити пробіли",
@@ -94,7 +94,7 @@
"Username supports email format. Also The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline. Also pay attention to the email format.": "Ім’я користувача підтримує формат email. Також може містити лише буквено-цифрові символи, підкреслення або дефіси, не може мати послідовні дефіси або підкреслення та не може починатися або закінчуватися дефісом або підкресленням. Зверніть увагу на формат email.",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Ви ввели неправильний пароль або код забагато разів, зачекайте %d хвилин і спробуйте знову",
"Your IP address: %s has been banned according to the configuration of: ": "Ваша IP-адреса: %s заблокована відповідно до конфігурації: ",
"Your password has expired. Please reset your password by clicking \"Forgot password\"": "Ditt lösenord har gått ut. Återställ det genom att klicka på \"Glömt lösenord\"",
"Your password has expired. Please reset your password by clicking \"Forgot password\"": "Ваш пароль застарів. Будь ласка, скиньте пароль, натиснувши \"Забув пароль\"",
"Your region is not allow to signup by phone": "У вашому регіоні реєстрація за телефоном недоступна",
"password or code is incorrect": "пароль або код неправильний",
"password or code is incorrect, you have %s remaining chances": "пароль або код неправильний, у вас залишилось %s спроб",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Не вдалося імпортувати групи",
"Failed to import users": "Не вдалося імпортувати користувачів",
"Insufficient balance: new balance %v would be below credit limit %v": "Недостатній баланс: новий баланс %v буде нижче кредитного ліміту %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Недостатній баланс: новий баланс організації %v буде нижче кредитного ліміту %v",
"Missing parameter": "Відсутній параметр",
"Only admin user can specify user": "Лише адміністратор може вказати користувача",
"Please login first": "Спочатку увійдіть",
"The LDAP: %s does not exist": "LDAP: %s не існує",
"The organization: %s should have one application at least": "Організація: %s має мати щонайменше один додаток",
"The syncer: %s does not exist": "Синхронізатор: %s не існує",
"The user: %s doesn't exist": "Користувач: %s не існує",
"The user: %s is not found": "Користувач: %s не знайдено",
"User is required for User category transaction": "Користувач обов'язковий для транзакції категорії користувача",
"Wrong userId": "Неправильний userId",
"don't support captchaProvider: ": "не підтримується captchaProvider: ",
"this operation is not allowed in demo mode": "ця операція недоступна в демо-режимі",
@@ -137,10 +143,16 @@
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "Додавання нового користувача до організації «built-in» (вбудованої) на даний момент вимкнено. Зауважте: усі користувачі в організації «built-in» є глобальними адміністраторами в Casdoor. Дивіться документацію: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. Якщо ви все ще хочете створити користувача для організації «built-in», перейдіть на сторінку налаштувань організації та увімкніть опцію «Має згоду на привілеї»."
},
"permission": {
"The permission: \"%s\" doesn't exist": "Behörigheten: \"%s\" finns inte"
"The permission: \"%s\" doesn't exist": "Дозвіл: \"%s\" не існує"
},
"product": {
"Product list cannot be empty": "Список товарів не може бути порожнім"
},
"provider": {
"Failed to initialize ID Verification provider": "Не вдалося ініціалізувати провайдера верифікації ID",
"Invalid application id": "Недійсний id додатка",
"No ID Verification provider configured": "Провайдер верифікації ID не налаштований",
"Provider is not an ID Verification provider": "Провайдер не є провайдером верифікації ID",
"the provider: %s does not exist": "провайдер: %s не існує"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Недійсні отримувачі Email: %s",
"Invalid phone receivers: %s": "Недійсні отримувачі телефону: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "ідентифікатор сесії %s є поточною сесією і не може бути видалений"
},
"storage": {
"The objectKey: %s is not allowed": "objectKey: %s не дозволено",
"The provider type: %s is not supported": "Тип провайдера: %s не підтримується"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Помилка"
},
"ticket": {
"Ticket not found": "Квиток не знайдено"
},
"token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s не підтримується в цьому додатку",
"Invalid application or wrong clientSecret": "Недійсний додаток або неправильний clientSecret",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "Відображуване ім’я не може бути порожнім",
"ID card information and real name are required": "Інформація про посвідчення особи та справжнє ім'я обов'язкові",
"Identity verification failed": "Верифікація особи не вдалася",
"MFA email is enabled but email is empty": "MFA email увімкнено, але email порожній",
"MFA phone is enabled but phone number is empty": "MFA телефон увімкнено, але номер телефону порожній",
"New password cannot contain blank space.": "Новий пароль не може містити пробіли.",
"No application found for user": "Не знайдено додаток для користувача",
"The new password must be different from your current password": "Новий пароль повинен відрізнятися від поточного пароля",
"User is already verified": "Користувач уже верифікований",
"the user's owner and name should not be empty": "власник ім’я користувача не повинні бути порожніми"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "Недійсний провайдер captcha.",
"Phone number is invalid in your region %s": "Номер телефону недійсний у вашому регіоні %s",
"The forgot password feature is disabled": "Функція відновлення пароля вимкнена",
"The verification code has already been used!": "Код підтвердження вже використано!",
"The verification code has not been sent yet!": "Код підтвердження ще не надіслано!",
"Turing test failed.": "Тест Тюрінга не пройдено.",
@@ -196,8 +219,8 @@
"Unknown type": "Невідомий тип",
"Wrong verification code!": "Неправильний код підтвердження!",
"You should verify your code in %d min!": "Ви маєте підтвердити код за %d хв!",
"please add a SMS provider to the \"Providers\" list for the application: %s": "lägg till en SMS-leverantör i listan \"Leverantörer\" för applikationen: %s",
"please add an Email provider to the \"Providers\" list for the application: %s": "lägg till en e-postleverantör i listan \"Leverantörer\" för applikationen: %s",
"please add a SMS provider to the \"Providers\" list for the application: %s": "будь ласка, додайте SMS-провайдера до списку \"Провайдери\" для додатка: %s",
"please add an Email provider to the \"Providers\" list for the application: %s": "будь ласка, додайте Email-провайдера до списку \"Провайдери\" для додатка: %s",
"the user does not exist, please sign up first": "користувача не існує, спочатку зареєструйтесь"
},
"webauthn": {

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "Không thể thêm người dùng",
"Get init score failed, error: %w": "Lấy điểm khởi đầu thất bại, lỗi: %w",
"Please sign out first": "Vui lòng đăng xuất trước",
"The application does not allow to sign up new account": "Ứng dụng không cho phép đăng ký tài khoản mới"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "Phương thức đăng nhập bằng email chưa được bật cho ứng dụng",
"The login method: login with face is not enabled for the application": "Phương thức đăng nhập bằng khuôn mặt chưa được bật cho ứng dụng",
"The login method: login with password is not enabled for the application": "Phương thức đăng nhập: đăng nhập bằng mật khẩu không được kích hoạt cho ứng dụng",
"The order: %s does not exist": "Đơn hàng: %s không tồn tại",
"The organization: %s does not exist": "Tổ chức: %s không tồn tại",
"The organization: %s has disabled users to signin": "Tổ chức: %s đã vô hiệu hóa đăng nhập của người dùng",
"The plan: %s does not exist": "Kế hoạch: %s không tồn tại",
@@ -57,11 +57,11 @@
"Face data mismatch": "Dữ liệu khuôn mặt không khớp",
"Failed to parse client IP: %s": "Không thể phân tích IP khách: %s",
"FirstName cannot be blank": "Tên không được để trống",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "Người dùng khách phải nâng cấp tài khoản bằng cách đặt tên người dùng và mật khẩu trước khi có thể đăng nhập trực tiếp",
"Invitation code cannot be blank": "Mã mời không được để trống",
"Invitation code exhausted": "Mã mời đã hết",
"Invitation code is invalid": "Mã mời không hợp lệ",
"Invitation code suspended": "Mã mời đã bị tạm ngưng",
"LDAP user name or password incorrect": "Tên người dùng hoặc mật khẩu Ldap không chính xác",
"LastName cannot be blank": "Họ không thể để trống",
"Multiple accounts with same uid, please check your ldap server": "Nhiều tài khoản với cùng một uid, vui lòng kiểm tra máy chủ ldap của bạn",
"Organization does not exist": "Tổ chức không tồn tại",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "Không thể nhập nhóm",
"Failed to import users": "Không thể nhập người dùng",
"Insufficient balance: new balance %v would be below credit limit %v": "Số dư không đủ: số dư mới %v sẽ thấp hơn giới hạn tín dụng %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "Số dư không đủ: số dư tổ chức mới %v sẽ thấp hơn giới hạn tín dụng %v",
"Missing parameter": "Thiếu tham số",
"Only admin user can specify user": "Chỉ người dùng quản trị mới có thể chỉ định người dùng",
"Please login first": "Vui lòng đăng nhập trước",
"The LDAP: %s does not exist": "LDAP: %s không tồn tại",
"The organization: %s should have one application at least": "Tổ chức: %s cần có ít nhất một ứng dụng",
"The syncer: %s does not exist": "Bộ đồng bộ: %s không tồn tại",
"The user: %s doesn't exist": "Người dùng: %s không tồn tại",
"The user: %s is not found": "Người dùng: %s không được tìm thấy",
"User is required for User category transaction": "Người dùng được yêu cầu cho giao dịch danh mục Người dùng",
"Wrong userId": "ID người dùng sai",
"don't support captchaProvider: ": "không hỗ trợ captchaProvider: ",
"this operation is not allowed in demo mode": "thao tác này không được phép trong chế độ demo",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "Quyền: \"%s\" không tồn tại"
},
"product": {
"Product list cannot be empty": "Danh sách sản phẩm không thể trống"
},
"provider": {
"Failed to initialize ID Verification provider": "Không thể khởi tạo nhà cung cấp Xác minh ID",
"Invalid application id": "Sai ID ứng dụng",
"No ID Verification provider configured": "Không có nhà cung cấp Xác minh ID được cấu hình",
"Provider is not an ID Verification provider": "Nhà cung cấp không phải là nhà cung cấp Xác minh ID",
"the provider: %s does not exist": "Nhà cung cấp: %s không tồn tại"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "Người nhận Email không hợp lệ: %s",
"Invalid phone receivers: %s": "Người nhận điện thoại không hợp lệ: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "id phiên %s là phiên hiện tại và không thể bị xóa"
},
"storage": {
"The objectKey: %s is not allowed": "Khóa đối tượng: %s không được phép",
"The provider type: %s is not supported": "Loại nhà cung cấp: %s không được hỗ trợ"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "Lỗi"
},
"ticket": {
"Ticket not found": "Không tìm thấy vé"
},
"token": {
"Grant_type: %s is not supported in this application": "Loại cấp phép: %s không được hỗ trợ trong ứng dụng này",
"Invalid application or wrong clientSecret": "Đơn đăng ký không hợp lệ hoặc sai clientSecret",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "Tên hiển thị không thể trống",
"ID card information and real name are required": "Thông tin chứng minh nhân dân và tên thật là bắt buộc",
"Identity verification failed": "Xác minh danh tính thất bại",
"MFA email is enabled but email is empty": "MFA email đã bật nhưng email trống",
"MFA phone is enabled but phone number is empty": "MFA điện thoại đã bật nhưng số điện thoại trống",
"New password cannot contain blank space.": "Mật khẩu mới không thể chứa dấu trắng.",
"No application found for user": "Không tìm thấy ứng dụng cho người dùng",
"The new password must be different from your current password": "Mật khẩu mới phải khác với mật khẩu hiện tại của bạn",
"User is already verified": "Người dùng đã được xác minh",
"the user's owner and name should not be empty": "chủ sở hữu và tên người dùng không được để trống"
},
"util": {
@@ -188,6 +210,7 @@
"verification": {
"Invalid captcha provider.": "Nhà cung cấp captcha không hợp lệ.",
"Phone number is invalid in your region %s": "Số điện thoại không hợp lệ trong vùng của bạn %s",
"The forgot password feature is disabled": "Tính năng quên mật khẩu đã bị tắt",
"The verification code has already been used!": "Mã xác thực đã được sử dụng!",
"The verification code has not been sent yet!": "Mã xác thực chưa được gửi!",
"Turing test failed.": "Kiểm định Turing thất bại.",

View File

@@ -2,7 +2,6 @@
"account": {
"Failed to add user": "添加用户失败",
"Get init score failed, error: %w": "初始化分数失败: %w",
"Please sign out first": "请先退出登录",
"The application does not allow to sign up new account": "该应用不允许注册新用户"
},
"auth": {
@@ -23,6 +22,7 @@
"The login method: login with email is not enabled for the application": "该应用禁止采用邮箱登录方式",
"The login method: login with face is not enabled for the application": "该应用禁止采用人脸登录",
"The login method: login with password is not enabled for the application": "该应用禁止采用密码登录方式",
"The order: %s does not exist": "订单: %s 不存在",
"The organization: %s does not exist": "组织: %s 不存在",
"The organization: %s has disabled users to signin": "组织: %s 禁止用户登录",
"The plan: %s does not exist": "计划: %s不存在",
@@ -35,7 +35,7 @@
"User's tag: %s is not listed in the application's tags": "用户的标签: %s不在该应用的标签列表中",
"UserCode Expired": "用户代码已过期",
"UserCode Invalid": "用户代码无效",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s 没有激活或正在等待订阅且应用: %s 没有默认",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "付费用户 %s 没有激活或待处理的订阅且应用 %s 没有默认定价",
"the application for user %s is not found": "未找到用户 %s 的应用程序",
"the organization: %s is not found": "组织: %s 不存在"
},
@@ -57,11 +57,11 @@
"Face data mismatch": "人脸不匹配",
"Failed to parse client IP: %s": "无法解析客户端 IP 地址: %s",
"FirstName cannot be blank": "名不可以为空",
"Guest users must upgrade their account by setting a username and password before they can sign in directly": "访客用户必须通过设置用户名和密码来升级账户,然后才能直接登录",
"Invitation code cannot be blank": "邀请码不能为空",
"Invitation code exhausted": "邀请码使用次数已耗尽",
"Invitation code is invalid": "邀请码无效",
"Invitation code suspended": "邀请码已被禁止使用",
"LDAP user name or password incorrect": "LDAP密码错误",
"LastName cannot be blank": "姓不可以为空",
"Multiple accounts with same uid, please check your ldap server": "多个帐户具有相同的uid请检查您的 LDAP 服务器",
"Organization does not exist": "组织不存在",
@@ -106,11 +106,17 @@
"general": {
"Failed to import groups": "导入群组失败",
"Failed to import users": "导入用户失败",
"Insufficient balance: new balance %v would be below credit limit %v": "余额不足:新余额 %v 将低于信用限额 %v",
"Insufficient balance: new organization balance %v would be below credit limit %v": "余额不足:新组织余额 %v 将低于信用限额 %v",
"Missing parameter": "缺少参数",
"Only admin user can specify user": "仅管理员用户可以指定用户",
"Please login first": "请先登录",
"The LDAP: %s does not exist": "LDAP: %s 不存在",
"The organization: %s should have one application at least": "组织: %s 应该拥有至少一个应用",
"The syncer: %s does not exist": "同步器: %s 不存在",
"The user: %s doesn't exist": "用户: %s不存在",
"The user: %s is not found": "用户: %s 未找到",
"User is required for User category transaction": "用户类别交易需要用户",
"Wrong userId": "错误的 userId",
"don't support captchaProvider: ": "不支持验证码提供商: ",
"this operation is not allowed in demo mode": "demo模式下不允许该操作",
@@ -139,8 +145,14 @@
"permission": {
"The permission: \"%s\" doesn't exist": "权限: \"%s\" 不存在"
},
"product": {
"Product list cannot be empty": "产品列表不能为空"
},
"provider": {
"Failed to initialize ID Verification provider": "初始化身份验证提供商失败",
"Invalid application id": "无效的应用ID",
"No ID Verification provider configured": "未配置身份验证提供商",
"Provider is not an ID Verification provider": "提供商不是身份验证提供商",
"the provider: %s does not exist": "提供商: %s不存在"
},
"resource": {
@@ -158,6 +170,9 @@
"Invalid Email receivers: %s": "无效的邮箱收件人: %s",
"Invalid phone receivers: %s": "无效的手机短信收信人: %s"
},
"session": {
"session id %s is the current session and cannot be deleted": "会话ID %s 是当前会话,无法删除"
},
"storage": {
"The objectKey: %s is not allowed": "objectKey: %s被禁止",
"The provider type: %s is not supported": "不支持的提供商类型: %s"
@@ -165,6 +180,9 @@
"subscription": {
"Error": "错误"
},
"ticket": {
"Ticket not found": "工单未找到"
},
"token": {
"Grant_type: %s is not supported in this application": "该应用不支持Grant_type: %s",
"Invalid application or wrong clientSecret": "无效应用或错误的clientSecret",
@@ -174,10 +192,14 @@
},
"user": {
"Display name cannot be empty": "显示名称不可为空",
"ID card information and real name are required": "需要身份证信息和真实姓名",
"Identity verification failed": "身份验证失败",
"MFA email is enabled but email is empty": "MFA 电子邮件已启用,但电子邮件为空",
"MFA phone is enabled but phone number is empty": "MFA 电话已启用,但电话号码为空",
"New password cannot contain blank space.": "新密码不可以包含空格",
"No application found for user": "未找到用户的应用程序",
"The new password must be different from your current password": "新密码必须与您当前的密码不同",
"User is already verified": "用户已验证",
"the user's owner and name should not be empty": "用户的组织和名称不能为空"
},
"util": {

View File

@@ -61,9 +61,10 @@ type SamlItem struct {
}
type JwtItem struct {
Name string `json:"name"`
Value string `json:"value"`
Type string `json:"type"`
Name string `json:"name"`
Category string `json:"category"`
Value string `json:"value"`
Type string `json:"type"`
}
type Application struct {
@@ -669,6 +670,21 @@ func GetAllowedApplications(applications []*Application, userId string, lang str
return res, nil
}
func checkMultipleCaptchaProviders(application *Application, lang string) error {
var captchaProviders []string
for _, providerItem := range application.Providers {
if providerItem.Provider != nil && providerItem.Provider.Category == "Captcha" {
captchaProviders = append(captchaProviders, providerItem.Name)
}
}
if len(captchaProviders) > 1 {
return fmt.Errorf(i18n.Translate(lang, "general:Multiple captcha providers are not allowed in the same application: %s"), strings.Join(captchaProviders, ", "))
}
return nil
}
func UpdateApplication(id string, application *Application, isGlobalAdmin bool, lang string) (bool, error) {
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
if err != nil {
@@ -707,6 +723,11 @@ func UpdateApplication(id string, application *Application, isGlobalAdmin bool,
return false, fmt.Errorf("only applications belonging to built-in organization can be shared")
}
err = checkMultipleCaptchaProviders(application, lang)
if err != nil {
return false, err
}
for _, providerItem := range application.Providers {
providerItem.Provider = nil
}

View File

@@ -20,7 +20,8 @@ import "github.com/casdoor/casdoor/email"
// TestSmtpServer Test the SMTP server
func TestSmtpServer(provider *Provider) error {
smtpEmailProvider := email.NewSmtpEmailProvider(provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.Type, provider.DisableSsl, provider.EnableProxy)
sslMode := getSslMode(provider)
smtpEmailProvider := email.NewSmtpEmailProvider(provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.Type, sslMode, provider.EnableProxy)
sender, err := smtpEmailProvider.Dialer.Dial()
if err != nil {
return err
@@ -31,7 +32,8 @@ func TestSmtpServer(provider *Provider) error {
}
func SendEmail(provider *Provider, title string, content string, dest []string, sender string) error {
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl, provider.Endpoint, provider.Method, provider.HttpHeaders, provider.UserMapping, provider.IssuerUrl, provider.EnableProxy)
sslMode := getSslMode(provider)
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, sslMode, provider.Endpoint, provider.Method, provider.HttpHeaders, provider.UserMapping, provider.IssuerUrl, provider.EnableProxy)
fromAddress := provider.ClientId2
if fromAddress == "" {
@@ -45,3 +47,19 @@ func SendEmail(provider *Provider, title string, content string, dest []string,
return emailProvider.Send(fromAddress, fromName, dest, title, content)
}
// getSslMode returns the SSL mode for the provider, with backward compatibility for DisableSsl
func getSslMode(provider *Provider) string {
// If SslMode is set, use it
if provider.SslMode != "" {
return provider.SslMode
}
// Backward compatibility: convert DisableSsl to SslMode
if provider.DisableSsl {
return "Disable"
}
// Default to "Auto" for new configurations or when DisableSsl is false
return "Auto"
}

View File

@@ -37,8 +37,9 @@ type Ldap struct {
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
CustomAttributes map[string]string `json:"customAttributes"`
AutoSync int `json:"autoSync"`
LastSync string `xorm:"varchar(100)" json:"lastSync"`
AutoSync int `json:"autoSync"`
LastSync string `xorm:"varchar(100)" json:"lastSync"`
EnableGroups bool `xorm:"bool" json:"enableGroups"`
}
func AddLdap(ldap *Ldap) (bool, error) {
@@ -152,7 +153,7 @@ func UpdateLdap(ldap *Ldap) (bool, error) {
}
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type", "allow_self_signed_cert", "custom_attributes").Update(ldap)
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type", "allow_self_signed_cert", "custom_attributes", "enable_groups").Update(ldap)
if err != nil {
return false, nil
}

View File

@@ -91,13 +91,28 @@ func (l *LdapAutoSynchronizer) syncRoutine(ldap *Ldap, stopChan chan struct{}) e
return err
}
// fetch all users
// fetch all users and groups
conn, err := ldap.GetLdapConn()
if err != nil {
logs.Warning(fmt.Sprintf("autoSync failed for %s, error %s", ldap.Id, err))
continue
}
// Sync groups first if enabled (so they exist before assigning users)
if ldap.EnableGroups {
groups, err := conn.GetLdapGroups(ldap)
if err != nil {
logs.Warning(fmt.Sprintf("autoSync failed to fetch groups for %s, error %s", ldap.Id, err))
} else {
newGroups, updatedGroups, err := SyncLdapGroups(ldap.Owner, groups, ldap.Id)
if err != nil {
logs.Warning(fmt.Sprintf("autoSync failed to sync groups for %s, error %s", ldap.Id, err))
} else {
logs.Info(fmt.Sprintf("ldap group sync success for %s, %d new groups, %d updated groups", ldap.Id, newGroups, updatedGroups))
}
}
}
users, err := conn.GetLdapUsers(ldap)
if err != nil {
conn.Close()

View File

@@ -87,10 +87,19 @@ type LdapUser struct {
GroupId string `json:"groupId"`
Address string `json:"address"`
MemberOf string `json:"memberOf"`
MemberOf []string `json:"memberOf"`
Attributes map[string]string `json:"attributes"`
}
type LdapGroup struct {
Dn string `json:"dn"`
Cn string `json:"cn"`
Name string `json:"name"`
Description string `json:"description"`
Member []string `json:"member"`
ParentDn string `json:"parentDn"`
}
func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) {
var conn *goldap.Conn
tlsConfig := tls.Config{
@@ -179,7 +188,7 @@ func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]LdapUser, error) {
SearchAttributes := []string{
"uidNumber", "cn", "sn", "gidNumber", "entryUUID", "displayName", "mail", "email",
"emailAddress", "telephoneNumber", "mobile", "mobileTelephoneNumber", "registeredAddress", "postalAddress",
"c", "co",
"c", "co", "memberOf",
}
if l.IsAD {
SearchAttributes = append(SearchAttributes, "sAMAccountName")
@@ -247,7 +256,7 @@ func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]LdapUser, error) {
case "co":
user.CountryName = attribute.Values[0]
case "memberOf":
user.MemberOf = attribute.Values[0]
user.MemberOf = attribute.Values
default:
if propName, ok := ldapServer.CustomAttributes[attribute.Name]; ok {
if user.Attributes == nil {
@@ -263,42 +272,135 @@ func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]LdapUser, error) {
return ldapUsers, nil
}
// FIXME: The Base DN does not necessarily contain the Group
//
// func (l *ldapConn) GetLdapGroups(baseDn string) (map[string]ldapGroup, error) {
// SearchFilter := "(objectClass=posixGroup)"
// SearchAttributes := []string{"cn", "gidNumber"}
// groupMap := make(map[string]ldapGroup)
//
// searchReq := goldap.NewSearchRequest(baseDn,
// goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
// SearchFilter, SearchAttributes, nil)
// searchResult, err := l.Conn.Search(searchReq)
// if err != nil {
// return nil, err
// }
//
// if len(searchResult.Entries) == 0 {
// return nil, errors.New("no result")
// }
//
// for _, entry := range searchResult.Entries {
// var ldapGroupItem ldapGroup
// for _, attribute := range entry.Attributes {
// switch attribute.Name {
// case "gidNumber":
// ldapGroupItem.GidNumber = attribute.Values[0]
// break
// case "cn":
// ldapGroupItem.Cn = attribute.Values[0]
// break
// }
// }
// groupMap[ldapGroupItem.GidNumber] = ldapGroupItem
// }
//
// return groupMap, nil
// }
// GetLdapGroups fetches LDAP groups and organizational units
func (l *LdapConn) GetLdapGroups(ldapServer *Ldap) ([]LdapGroup, error) {
var allGroups []LdapGroup
// Search for LDAP groups (groupOfNames, groupOfUniqueNames, posixGroup)
groupFilters := []string{
"(objectClass=groupOfNames)",
"(objectClass=groupOfUniqueNames)",
"(objectClass=posixGroup)",
}
// Add Active Directory group filter
if l.IsAD {
groupFilters = append(groupFilters, "(objectClass=group)")
}
// Build combined filter
var filterBuilder strings.Builder
filterBuilder.WriteString("(|")
for _, filter := range groupFilters {
filterBuilder.WriteString(filter)
}
filterBuilder.WriteString(")")
SearchAttributes := []string{"cn", "name", "description", "member", "uniqueMember", "memberUid"}
searchReq := goldap.NewSearchRequest(ldapServer.BaseDn,
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
filterBuilder.String(), SearchAttributes, nil)
searchResult, err := l.Conn.SearchWithPaging(searchReq, 100)
if err != nil {
// Groups might not exist, which is okay
return allGroups, nil
}
for _, entry := range searchResult.Entries {
group := LdapGroup{
Dn: entry.DN,
}
for _, attribute := range entry.Attributes {
switch attribute.Name {
case "cn":
group.Cn = attribute.Values[0]
case "name":
group.Name = attribute.Values[0]
case "description":
if len(attribute.Values) > 0 {
group.Description = attribute.Values[0]
}
case "member", "uniqueMember", "memberUid":
group.Member = append(group.Member, attribute.Values...)
}
}
// Use cn as name if name is not set
if group.Name == "" {
group.Name = group.Cn
}
// Parse parent DN from the entry DN
group.ParentDn = getParentDn(entry.DN)
allGroups = append(allGroups, group)
}
// Also fetch organizational units as groups
ouFilter := "(objectClass=organizationalUnit)"
ouSearchReq := goldap.NewSearchRequest(ldapServer.BaseDn,
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
ouFilter, []string{"ou", "description"}, nil)
ouSearchResult, err := l.Conn.SearchWithPaging(ouSearchReq, 100)
if err == nil {
for _, entry := range ouSearchResult.Entries {
ou := LdapGroup{
Dn: entry.DN,
}
for _, attribute := range entry.Attributes {
switch attribute.Name {
case "ou":
ou.Name = attribute.Values[0]
ou.Cn = attribute.Values[0]
case "description":
if len(attribute.Values) > 0 {
ou.Description = attribute.Values[0]
}
}
}
// Parse parent DN from the entry DN
ou.ParentDn = getParentDn(entry.DN)
allGroups = append(allGroups, ou)
}
}
return allGroups, nil
}
// getParentDn extracts the parent DN from a full DN
func getParentDn(dn string) string {
// Split DN by comma
parts := strings.Split(dn, ",")
if len(parts) <= 1 {
return ""
}
// Remove the first component (the current node) and rejoin
return strings.Join(parts[1:], ",")
}
// parseDnToGroupName converts a DN to a group name
func parseDnToGroupName(dn string) string {
// Extract the CN or OU from the DN
parts := strings.Split(dn, ",")
if len(parts) == 0 {
return ""
}
firstPart := parts[0]
// Extract value after = sign
if idx := strings.Index(firstPart, "="); idx != -1 {
return firstPart[idx+1:]
}
return firstPart
}
func AutoAdjustLdapUser(users []LdapUser) []LdapUser {
res := make([]LdapUser, len(users))
@@ -315,6 +417,7 @@ func AutoAdjustLdapUser(users []LdapUser) []LdapUser {
Address: util.ReturnAnyNotEmpty(user.Address, user.PostalAddress, user.RegisteredAddress),
Country: util.ReturnAnyNotEmpty(user.Country, user.CountryName),
CountryName: user.CountryName,
MemberOf: user.MemberOf,
Attributes: user.Attributes,
}
}
@@ -398,8 +501,22 @@ func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUser
}
formatUserPhone(newUser)
// Assign user to groups based on memberOf attribute
userGroups := []string{}
if ldap.DefaultGroup != "" {
newUser.Groups = []string{ldap.DefaultGroup}
userGroups = append(userGroups, ldap.DefaultGroup)
}
// Extract group names from memberOf DNs
for _, memberDn := range syncUser.MemberOf {
groupName := dnToGroupName(owner, memberDn)
if groupName != "" {
userGroups = append(userGroups, groupName)
}
}
if len(userGroups) > 0 {
newUser.Groups = userGroups
}
affected, err := AddUser(newUser, "en")
@@ -420,6 +537,179 @@ func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUser
return existUsers, failedUsers, err
}
// SyncLdapGroups syncs LDAP groups/OUs to Casdoor groups with hierarchy
func SyncLdapGroups(owner string, ldapGroups []LdapGroup, ldapId string) (newGroups int, updatedGroups int, err error) {
if len(ldapGroups) == 0 {
return 0, 0, nil
}
// Create a map of DN to group for quick lookup
dnToGroup := make(map[string]*LdapGroup)
for i := range ldapGroups {
dnToGroup[ldapGroups[i].Dn] = &ldapGroups[i]
}
// Get existing groups for this organization
existingGroups, err := GetGroups(owner)
if err != nil {
return 0, 0, err
}
existingGroupMap := make(map[string]*Group)
for _, group := range existingGroups {
existingGroupMap[group.Name] = group
}
ldap, err := GetLdap(ldapId)
if err != nil {
return 0, 0, err
}
// Process groups in hierarchical order (parents before children)
processedGroups := make(map[string]bool)
var processGroup func(ldapGroup *LdapGroup) error
processGroup = func(ldapGroup *LdapGroup) error {
if processedGroups[ldapGroup.Dn] {
return nil
}
// Generate group name from DN
groupName := dnToGroupName(owner, ldapGroup.Dn)
if groupName == "" {
return nil
}
// Determine parent
var parentId string
var isTopGroup bool
if ldapGroup.ParentDn == "" || ldapGroup.ParentDn == ldap.BaseDn {
isTopGroup = true
parentId = ""
} else {
// Process parent first
if parentGroup, exists := dnToGroup[ldapGroup.ParentDn]; exists {
err := processGroup(parentGroup)
if err != nil {
return err
}
parentId = dnToGroupName(owner, ldapGroup.ParentDn)
} else {
isTopGroup = true
}
}
// Check if group already exists
if existingGroup, exists := existingGroupMap[groupName]; exists {
// Update existing group
existingGroup.DisplayName = ldapGroup.Name
existingGroup.ParentId = parentId
existingGroup.IsTopGroup = isTopGroup
existingGroup.Type = "ldap-synced"
existingGroup.UpdatedTime = util.GetCurrentTime()
_, err := UpdateGroup(existingGroup.GetId(), existingGroup)
if err == nil {
updatedGroups++
}
} else {
// Create new group
newGroup := &Group{
Owner: owner,
Name: groupName,
CreatedTime: util.GetCurrentTime(),
UpdatedTime: util.GetCurrentTime(),
DisplayName: ldapGroup.Name,
ParentId: parentId,
IsTopGroup: isTopGroup,
Type: "ldap-synced",
IsEnabled: true,
}
_, err := AddGroup(newGroup)
if err == nil {
newGroups++
existingGroupMap[groupName] = newGroup
}
}
processedGroups[ldapGroup.Dn] = true
return nil
}
// Process all groups
for i := range ldapGroups {
err := processGroup(&ldapGroups[i])
if err != nil {
// Log error but continue processing other groups
continue
}
}
return newGroups, updatedGroups, nil
}
// dnToGroupName converts an LDAP DN to a Casdoor group name
func dnToGroupName(owner, dn string) string {
if dn == "" {
return ""
}
// Parse DN to extract meaningful components
parts := strings.Split(dn, ",")
// Build a hierarchical name from DN components (excluding DC parts)
var nameComponents []string
for _, part := range parts {
part = strings.TrimSpace(part)
lowerPart := strings.ToLower(part)
// Skip DC (domain component) parts
if strings.HasPrefix(lowerPart, "dc=") {
continue
}
// Extract value after = sign
if idx := strings.Index(part, "="); idx != -1 {
value := part[idx+1:]
nameComponents = append(nameComponents, value)
}
}
if len(nameComponents) == 0 {
return ""
}
// Reverse to get top-down hierarchy
for i, j := 0, len(nameComponents)-1; i < j; i, j = i+1, j-1 {
nameComponents[i], nameComponents[j] = nameComponents[j], nameComponents[i]
}
// Join with underscore to create a unique group name
groupName := strings.Join(nameComponents, "_")
// Sanitize group name - replace invalid characters with underscores
// Keep only alphanumeric characters, underscores, and hyphens
var sanitized strings.Builder
for _, r := range groupName {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' {
sanitized.WriteRune(r)
} else {
sanitized.WriteRune('_')
}
}
groupName = sanitized.String()
// Remove consecutive underscores and trim
for strings.Contains(groupName, "__") {
groupName = strings.ReplaceAll(groupName, "__", "_")
}
groupName = strings.Trim(groupName, "_")
return groupName
}
func GetExistUuids(owner string, uuids []string) ([]string, error) {
var existUuids []string

View File

@@ -179,6 +179,17 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
return nil, nil, fmt.Errorf("the plan: %s does not exist", productInfo.PlanName)
}
// Check if plan restricts user to one subscription
if plan.IsExclusive {
hasSubscription, err := HasActiveSubscriptionForPlan(owner, user.Name, plan.Name)
if err != nil {
return nil, nil, err
}
if hasSubscription {
return nil, nil, fmt.Errorf("user already has an active subscription for plan: %s", plan.Name)
}
}
sub, err := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
if err != nil {
return nil, nil, err
@@ -265,7 +276,7 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
OutOrderId: payResp.OrderId,
}
if provider.Type == "Dummy" || provider.Type == "Balance" {
if provider.Type == "Balance" {
payment.State = pp.PaymentStatePaid
}
@@ -340,7 +351,7 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
}
order.Payment = payment.Name
if provider.Type == "Dummy" || provider.Type == "Balance" {
if provider.Type == "Balance" {
order.State = "Paid"
order.Message = "Payment successful"
order.UpdateTime = util.GetCurrentTime()
@@ -353,7 +364,7 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
}
// Update product stock after order state is persisted (for instant payment methods)
if provider.Type == "Dummy" || provider.Type == "Balance" {
if provider.Type == "Balance" {
err = UpdateProductStock(orderProductInfos)
if err != nil {
return nil, nil, err

View File

@@ -35,6 +35,7 @@ type Plan struct {
Product string `xorm:"varchar(100)" json:"product"`
PaymentProviders []string `xorm:"varchar(100)" json:"paymentProviders"` // payment providers for related product
IsEnabled bool `json:"isEnabled"`
IsExclusive bool `json:"isExclusive"` // if true, a user can only have at most one subscription of this plan
Role string `xorm:"varchar(100)" json:"role"`
Options []string `xorm:"-" json:"options"`

View File

@@ -53,7 +53,8 @@ type Provider struct {
Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"`
DisableSsl bool `json:"disableSsl"` // If the provider type is WeChat, DisableSsl means EnableQRCode, if type is Google, it means sync phone number
DisableSsl bool `json:"disableSsl"` // Deprecated: Use SslMode instead. If the provider type is WeChat, DisableSsl means EnableQRCode, if type is Google, it means sync phone number
SslMode string `xorm:"varchar(100)" json:"sslMode"` // "Auto" (empty means Auto), "Enable", "Disable"
Title string `xorm:"varchar(100)" json:"title"`
Content string `xorm:"varchar(2000)" json:"content"` // If provider type is WeChat, Content means QRCode string by Base64 encoding
Receiver string `xorm:"varchar(100)" json:"receiver"`

View File

@@ -217,6 +217,26 @@ func GetSubscription(id string) (*Subscription, error) {
return getSubscription(owner, name)
}
func HasActiveSubscriptionForPlan(owner, userName, planName string) (bool, error) {
subscriptions := []*Subscription{}
err := ormer.Engine.Find(&subscriptions, &Subscription{Owner: owner, User: userName, Plan: planName})
if err != nil {
return false, err
}
for _, sub := range subscriptions {
err = sub.UpdateState()
if err != nil {
return false, err
}
// Check if subscription is active, upcoming, or pending (not expired, error, or suspended)
if sub.State == SubStateActive || sub.State == SubStateUpcoming || sub.State == SubStatePending {
return true, nil
}
}
return false, nil
}
func UpdateSubscription(id string, subscription *Subscription) (bool, error) {
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
if err != nil {

327
object/syncer_awsiam.go Normal file
View File

@@ -0,0 +1,327 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"context"
"fmt"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/casdoor/casdoor/util"
)
// AwsIamSyncerProvider implements SyncerProvider for AWS IAM API-based syncers
type AwsIamSyncerProvider struct {
Syncer *Syncer
iamClient *iam.IAM
}
// InitAdapter initializes the AWS IAM syncer
func (p *AwsIamSyncerProvider) InitAdapter() error {
// syncer.Host should be the AWS region (e.g., "us-east-1")
// syncer.User should be the AWS Access Key ID
// syncer.Password should be the AWS Secret Access Key
region := p.Syncer.Host
if region == "" {
return fmt.Errorf("AWS region (host field) is required for AWS IAM syncer")
}
accessKeyId := p.Syncer.User
if accessKeyId == "" {
return fmt.Errorf("AWS Access Key ID (user field) is required for AWS IAM syncer")
}
secretAccessKey := p.Syncer.Password
if secretAccessKey == "" {
return fmt.Errorf("AWS Secret Access Key (password field) is required for AWS IAM syncer")
}
// Create AWS session
sess, err := session.NewSession(&aws.Config{
Region: aws.String(region),
Credentials: credentials.NewStaticCredentials(accessKeyId, secretAccessKey, ""),
})
if err != nil {
return fmt.Errorf("failed to create AWS session: %w", err)
}
// Create IAM client
p.iamClient = iam.New(sess)
return nil
}
// GetOriginalUsers retrieves all users from AWS IAM API
func (p *AwsIamSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
if p.iamClient == nil {
if err := p.InitAdapter(); err != nil {
return nil, err
}
}
return p.getAwsIamUsers()
}
// AddUser adds a new user to AWS IAM (not supported for read-only API)
func (p *AwsIamSyncerProvider) AddUser(user *OriginalUser) (bool, error) {
// AWS IAM syncer is typically read-only
return false, fmt.Errorf("adding users to AWS IAM is not supported")
}
// UpdateUser updates an existing user in AWS IAM (not supported for read-only API)
func (p *AwsIamSyncerProvider) UpdateUser(user *OriginalUser) (bool, error) {
// AWS IAM syncer is typically read-only
return false, fmt.Errorf("updating users in AWS IAM is not supported")
}
// TestConnection tests the AWS IAM API connection
func (p *AwsIamSyncerProvider) TestConnection() error {
if p.iamClient == nil {
if err := p.InitAdapter(); err != nil {
return err
}
}
// Try to list users with a limit of 1 to test the connection
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
input := &iam.ListUsersInput{
MaxItems: aws.Int64(1),
}
_, err := p.iamClient.ListUsersWithContext(ctx, input)
return err
}
// Close closes any open connections
func (p *AwsIamSyncerProvider) Close() error {
// AWS IAM client doesn't require explicit cleanup
p.iamClient = nil
return nil
}
// getAwsIamUsers gets all users from AWS IAM API
func (p *AwsIamSyncerProvider) getAwsIamUsers() ([]*OriginalUser, error) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
allUsers := []*iam.User{}
var marker *string
// Paginate through all users
for {
input := &iam.ListUsersInput{
Marker: marker,
}
result, err := p.iamClient.ListUsersWithContext(ctx, input)
if err != nil {
return nil, fmt.Errorf("failed to list IAM users: %w", err)
}
allUsers = append(allUsers, result.Users...)
if result.IsTruncated == nil || !*result.IsTruncated {
break
}
marker = result.Marker
}
// Convert AWS IAM users to Casdoor OriginalUser
originalUsers := []*OriginalUser{}
for _, iamUser := range allUsers {
originalUser, err := p.awsIamUserToOriginalUser(iamUser)
if err != nil {
// Log error but continue processing other users
userName := "unknown"
if iamUser.UserName != nil {
userName = *iamUser.UserName
}
fmt.Printf("Warning: Failed to convert IAM user %s: %v\n", userName, err)
continue
}
originalUsers = append(originalUsers, originalUser)
}
return originalUsers, nil
}
// awsIamUserToOriginalUser converts AWS IAM user to Casdoor OriginalUser
func (p *AwsIamSyncerProvider) awsIamUserToOriginalUser(iamUser *iam.User) (*OriginalUser, error) {
if iamUser == nil {
return nil, fmt.Errorf("IAM user is nil")
}
user := &OriginalUser{
Address: []string{},
Properties: map[string]string{},
Groups: []string{},
}
// Set ID from UserId (unique identifier)
if iamUser.UserId != nil {
user.Id = *iamUser.UserId
}
// Set Name from UserName
if iamUser.UserName != nil {
user.Name = *iamUser.UserName
}
// Set DisplayName (use UserName if not available separately)
if iamUser.UserName != nil {
user.DisplayName = *iamUser.UserName
}
// Set CreatedTime from CreateDate
if iamUser.CreateDate != nil {
user.CreatedTime = iamUser.CreateDate.Format(time.RFC3339)
} else {
user.CreatedTime = util.GetCurrentTime()
}
// Get user tags which might contain additional information
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
tagsInput := &iam.ListUserTagsInput{
UserName: iamUser.UserName,
}
tagsResult, err := p.iamClient.ListUserTagsWithContext(ctx, tagsInput)
if err == nil && tagsResult != nil {
// Process tags to extract additional user information
for _, tag := range tagsResult.Tags {
if tag.Key != nil && tag.Value != nil {
key := *tag.Key
value := *tag.Value
switch key {
case "Email", "email":
user.Email = value
case "Phone", "phone":
user.Phone = value
case "DisplayName", "displayName":
user.DisplayName = value
case "FirstName", "firstName":
user.FirstName = value
case "LastName", "lastName":
user.LastName = value
case "Title", "title":
user.Title = value
case "Department", "department":
user.Affiliation = value
default:
// Store other tags in Properties
user.Properties[key] = value
}
}
}
}
// AWS IAM users are active by default unless specified in tags
// Check if there's a "Status" or "Active" tag
if status, ok := user.Properties["Status"]; ok {
if status == "Inactive" || status == "Disabled" {
user.IsForbidden = true
}
}
if active, ok := user.Properties["Active"]; ok {
if active == "false" || active == "False" || active == "0" {
user.IsForbidden = true
}
}
return user, nil
}
// GetOriginalGroups retrieves all groups from AWS IAM
func (p *AwsIamSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
if p.iamClient == nil {
if err := p.InitAdapter(); err != nil {
return nil, err
}
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
allGroups := []*iam.Group{}
var marker *string
// Paginate through all groups
for {
input := &iam.ListGroupsInput{
Marker: marker,
}
result, err := p.iamClient.ListGroupsWithContext(ctx, input)
if err != nil {
return nil, fmt.Errorf("failed to list IAM groups: %w", err)
}
allGroups = append(allGroups, result.Groups...)
if result.IsTruncated == nil || !*result.IsTruncated {
break
}
marker = result.Marker
}
// Convert AWS IAM groups to Casdoor OriginalGroup
originalGroups := []*OriginalGroup{}
for _, iamGroup := range allGroups {
if iamGroup.GroupId != nil && iamGroup.GroupName != nil {
group := &OriginalGroup{
Id: *iamGroup.GroupId,
Name: *iamGroup.GroupName,
}
if iamGroup.GroupName != nil {
group.DisplayName = *iamGroup.GroupName
}
originalGroups = append(originalGroups, group)
}
}
return originalGroups, nil
}
// GetOriginalUserGroups retrieves the group IDs that a user belongs to
func (p *AwsIamSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
if p.iamClient == nil {
if err := p.InitAdapter(); err != nil {
return nil, err
}
}
// Note: AWS IAM API requires UserName to query groups, but this interface provides UserId.
// This is a known limitation. To properly implement this, we would need to:
// 1. Maintain a mapping cache from UserId to UserName, or
// 2. Modify the interface to accept both UserId and UserName
// For now, returning empty groups to maintain interface compatibility.
// TODO: Implement user group synchronization by maintaining a UserId->UserName mapping
return []string{}, nil
}

View File

@@ -175,14 +175,18 @@ func (p *DingtalkSyncerProvider) getDingtalkAccessToken() (string, error) {
return tokenResp.AccessToken, nil
}
// getDingtalkDepartments gets all department IDs from DingTalk API
// getDingtalkDepartments gets all department IDs from DingTalk API recursively
func (p *DingtalkSyncerProvider) getDingtalkDepartments(accessToken string) ([]int64, error) {
return p.getDingtalkDepartmentsRecursive(accessToken, 1)
}
// getDingtalkDepartmentsRecursive recursively fetches all departments starting from parentDeptId
func (p *DingtalkSyncerProvider) getDingtalkDepartmentsRecursive(accessToken string, parentDeptId int64) ([]int64, error) {
apiUrl := fmt.Sprintf("https://oapi.dingtalk.com/topapi/v2/department/listsub?access_token=%s",
url.QueryEscape(accessToken))
// Get root department (dept_id=1)
postData := map[string]interface{}{
"dept_id": 1,
"dept_id": parentDeptId,
}
data, err := p.postJSON(apiUrl, postData)
@@ -201,9 +205,16 @@ func (p *DingtalkSyncerProvider) getDingtalkDepartments(accessToken string) ([]i
deptResp.Errcode, deptResp.Errmsg)
}
deptIds := []int64{1} // Include root department
// Start with the parent department itself
deptIds := []int64{parentDeptId}
// Recursively fetch all child departments
for _, dept := range deptResp.Result {
deptIds = append(deptIds, dept.DeptId)
childDeptIds, err := p.getDingtalkDepartmentsRecursive(accessToken, dept.DeptId)
if err != nil {
return nil, err
}
deptIds = append(deptIds, childDeptIds...)
}
return deptIds, nil
@@ -415,6 +426,7 @@ func (p *DingtalkSyncerProvider) dingtalkUserToOriginalUser(dingtalkUser *Dingta
Address: []string{},
Properties: map[string]string{},
Groups: []string{},
DingTalk: dingtalkUser.UserId, // Link DingTalk provider account
}
// Add department IDs to Groups field

View File

@@ -72,6 +72,8 @@ func GetSyncerProvider(syncer *Syncer) SyncerProvider {
return &OktaSyncerProvider{Syncer: syncer}
case "SCIM":
return &SCIMSyncerProvider{Syncer: syncer}
case "AWS IAM":
return &AwsIamSyncerProvider{Syncer: syncer}
case "Keycloak":
return &KeycloakSyncerProvider{
DatabaseSyncerProvider: DatabaseSyncerProvider{Syncer: syncer},

View File

@@ -376,6 +376,7 @@ func (p *LarkSyncerProvider) larkUserToOriginalUser(larkUser *LarkUser) *Origina
Address: []string{},
Properties: map[string]string{},
Groups: []string{},
Lark: larkUser.UserId, // Link Lark provider account
}
// Set avatar if available

View File

@@ -70,6 +70,18 @@ func (syncer *Syncer) updateUserForOriginalFields(user *User, key string) (bool,
columns := syncer.getCasdoorColumns()
columns = append(columns, "affiliation", "hash", "pre_hash")
// Add provider-specific field for API-based syncers to enable login binding
// This allows synced users to login via their provider accounts
switch syncer.Type {
case "WeCom":
columns = append(columns, "wecom")
case "DingTalk":
columns = append(columns, "dingtalk")
case "Lark":
columns = append(columns, "lark")
}
affected, err := ormer.Engine.Where(key+" = ? and owner = ?", syncer.getUserValue(&oldUser, key), oldUser.Owner).Cols(columns...).Update(user)
if err != nil {
return false, err

View File

@@ -275,6 +275,7 @@ func (p *WecomSyncerProvider) wecomUserToOriginalUser(wecomUser *WecomUser) *Ori
Address: []string{},
Properties: map[string]string{},
Groups: []string{},
Wecom: wecomUser.UserId, // Link WeCom provider account
}
// Set gender

View File

@@ -338,6 +338,50 @@ func getClaimsWithoutThirdIdp(claims Claims) ClaimsWithoutThirdIdp {
return res
}
// getUserFieldValue gets the value of a user field by name, handling special cases like Roles and Permissions
func getUserFieldValue(user *User, fieldName string) (interface{}, bool) {
if user == nil {
return nil, false
}
// Handle special fields that need conversion
switch fieldName {
case "Roles":
return getUserRoleNames(user), true
case "Permissions":
return getUserPermissionNames(user), true
case "permissionNames":
permissionNames := []string{}
for _, val := range user.Permissions {
permissionNames = append(permissionNames, val.Name)
}
return permissionNames, true
}
// Handle Properties fields (e.g., Properties.my_field)
if strings.HasPrefix(fieldName, "Properties.") {
parts := strings.Split(fieldName, ".")
if len(parts) == 2 {
propName := parts[1]
if user.Properties != nil {
if value, exists := user.Properties[propName]; exists {
return value, true
}
}
}
return nil, false
}
// Use reflection to get the field value
userValue := reflect.ValueOf(user).Elem()
userField := userValue.FieldByName(fieldName)
if userField.IsValid() {
return userField.Interface(), true
}
return nil, false
}
func getClaimsCustom(claims Claims, tokenField []string, tokenAttributes []*JwtItem) jwt.MapClaims {
res := make(jwt.MapClaims)
@@ -414,16 +458,30 @@ func getClaimsCustom(claims Claims, tokenField []string, tokenAttributes []*JwtI
}
for _, item := range tokenAttributes {
valueList := replaceAttributeValue(claims.User, item.Value)
if len(valueList) == 0 {
continue
var value interface{}
// If Category is "Existing Field", get the actual field value from the user
if item.Category == "Existing Field" {
fieldValue, found := getUserFieldValue(claims.User, item.Value)
if !found {
continue
}
value = fieldValue
} else {
// Default behavior: use replaceAttributeValue for "Static Value" or empty category
valueList := replaceAttributeValue(claims.User, item.Value)
if len(valueList) == 0 {
continue
}
if item.Type == "String" {
value = valueList[0]
} else {
value = valueList
}
}
if item.Type == "String" {
res[item.Name] = valueList[0]
} else {
res[item.Name] = valueList
}
res[item.Name] = value
}
return res

View File

@@ -689,6 +689,15 @@ func GetMaskedUser(user *User, isAdminOrSelf bool, errs ...error) (*User, error)
if user.OriginalRefreshToken != "" {
user.OriginalRefreshToken = "***"
}
// Mask per-provider OAuth tokens in Properties
if user.Properties != nil {
for key := range user.Properties {
// More specific pattern matching to avoid masking unrelated properties
if strings.HasPrefix(key, "oauth_") && (strings.HasSuffix(key, "_accessToken") || strings.HasSuffix(key, "_refreshToken")) {
user.Properties[key] = "***"
}
}
}
}
if user.ManagedAccounts != nil {

View File

@@ -184,9 +184,32 @@ func getUserExtraProperty(user *User, providerType, key string) (string, error)
return extra[key], nil
}
// getOAuthTokenPropertyKey returns the property key for storing OAuth tokens
func getOAuthTokenPropertyKey(providerType string, tokenType string) string {
return fmt.Sprintf("oauth_%s_%s", providerType, tokenType)
}
// GetUserOAuthAccessToken retrieves the OAuth access token for a specific provider
func GetUserOAuthAccessToken(user *User, providerType string) string {
return getUserProperty(user, getOAuthTokenPropertyKey(providerType, "accessToken"))
}
// GetUserOAuthRefreshToken retrieves the OAuth refresh token for a specific provider
func GetUserOAuthRefreshToken(user *User, providerType string) string {
return getUserProperty(user, getOAuthTokenPropertyKey(providerType, "refreshToken"))
}
func SetUserOAuthProperties(organization *Organization, user *User, providerType string, userInfo *idp.UserInfo, token *oauth2.Token, userMapping ...map[string]string) (bool, error) {
// Store the original OAuth provider token if available
if token != nil && token.AccessToken != "" {
// Store tokens per provider in Properties map
setUserProperty(user, getOAuthTokenPropertyKey(providerType, "accessToken"), token.AccessToken)
if token.RefreshToken != "" {
setUserProperty(user, getOAuthTokenPropertyKey(providerType, "refreshToken"), token.RefreshToken)
}
// Also update the legacy fields for backward compatibility
user.OriginalToken = token.AccessToken
user.OriginalRefreshToken = token.RefreshToken
}

View File

@@ -65,6 +65,22 @@ func RecordMessage(ctx *context.Context) {
userId := getUser(ctx)
// Special handling for set-password endpoint to capture target user
if ctx.Request.URL.Path == "/api/set-password" {
// Parse form if not already parsed
if err := ctx.Request.ParseForm(); err != nil {
fmt.Printf("RecordMessage() error parsing form: %s\n", err.Error())
} else {
userOwner := ctx.Request.Form.Get("userOwner")
userName := ctx.Request.Form.Get("userName")
if userOwner != "" && userName != "" {
targetUserId := util.GetId(userOwner, userName)
ctx.Input.SetParam("recordTargetUserId", targetUserId)
}
}
}
ctx.Input.SetParam("recordUserId", userId)
}
@@ -76,7 +92,20 @@ func AfterRecordMessage(ctx *context.Context) {
}
userId := ctx.Input.Params()["recordUserId"]
if userId != "" {
targetUserId := ctx.Input.Params()["recordTargetUserId"]
// For set-password endpoint, use target user if available
// We use defensive error handling here (log instead of panic) because target user
// parsing is a new feature. If it fails, we gracefully fall back to the regular
// userId flow or empty user/org fields, maintaining backward compatibility.
if record.Action == "set-password" && targetUserId != "" {
owner, user, err := util.GetOwnerAndNameFromIdWithError(targetUserId)
if err != nil {
fmt.Printf("AfterRecordMessage() error parsing target user %s: %s\n", targetUserId, err.Error())
} else {
record.Organization, record.User = owner, user
}
} else if userId != "" {
owner, user, err := util.GetOwnerAndNameFromIdWithError(userId)
if err != nil {
panic(err)

View File

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

View File

@@ -134,7 +134,7 @@ class AdapterListPage extends BaseListPage {
},
},
{
title: i18next.t("provider:Type"),
title: i18next.t("general:Type"),
dataIndex: "type",
key: "type",
width: "100px",

View File

@@ -58,6 +58,7 @@ import * as GroupBackend from "./backend/GroupBackend";
import TokenAttributeTable from "./table/TokenAttributeTable";
import {Content, Header} from "antd/es/layout/layout";
import Sider from "antd/es/layout/Sider";
import PaginateSelect from "./common/PaginateSelect";
const {Option} = Select;
@@ -149,7 +150,6 @@ class ApplicationEditPage extends React.Component {
UNSAFE_componentWillMount() {
this.getApplication();
this.getOrganizations();
this.getGroups();
}
getApplication() {
@@ -201,17 +201,6 @@ class ApplicationEditPage extends React.Component {
});
}
getGroups() {
GroupBackend.getGroups(this.state.owner)
.then((res) => {
if (res.status === "ok") {
this.setState({
groups: res.data,
});
}
});
}
getCerts(application) {
let owner = application.organization;
if (application.isShared) {
@@ -611,24 +600,30 @@ class ApplicationEditPage extends React.Component {
{Setting.getLabel(i18next.t("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} :
</Col>
<Col span={21}>
<Select virtual={false} style={{width: "100%"}} value={this.state.application.defaultGroup ?? []} onChange={(value => {
this.updateApplicationField("defaultGroup", value);
})}
>
<Option key={""} value={""}>
<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>
{i18next.t("general:Default")}
</Space>
</Option>
{
this.state.groups?.map((group) => <Option key={group.name} value={`${group.owner}/${group.name}`}>
<Space>
{group.type === "Physical" ? <UsergroupAddOutlined /> : <HolderOutlined />}
{group.displayName}
</Space>
</Option>)
}
</Select>
{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"}} >
@@ -870,11 +865,11 @@ class ApplicationEditPage extends React.Component {
<React.Fragment>
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("general:Providers"), i18next.t("general:Providers - Tooltip"))} :
{Setting.getLabel(i18next.t("application:Providers"), i18next.t("general:Providers - Tooltip"))} :
</Col>
<Col span={21} >
<ProviderTable
title={i18next.t("general:Providers")}
title={i18next.t("application:Providers")}
table={this.state.application.providers}
providers={this.state.providers}
application={this.state.application}
@@ -1318,11 +1313,12 @@ class ApplicationEditPage extends React.Component {
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitApplicationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteApplication()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
<Layout style={{background: "inherit"}}>
} style={{margin: (Setting.isMobile()) ? "5px" : {}, height: "calc(100vh - 145px - 48px)", overflow: "hidden"}}
styles={{body: {height: "100%"}}} type="inner">
<Layout style={{background: "inherit", height: "100%", overflow: "auto"}}>
{
this.state.menuMode === "horizontal" || !this.state.menuMode ? (
<Header style={{background: "inherit", padding: "0px"}}>
<Header style={{background: "inherit", padding: "0px", position: "sticky", top: 0}}>
<div className="demo-logo" />
<Tabs
onChange={(key) => {
@@ -1342,7 +1338,7 @@ class ApplicationEditPage extends React.Component {
</Header>
) : null
}
<Layout style={{background: "inherit", maxHeight: "calc(70vh - 70px)", overflow: "auto"}}>
<Layout style={{background: "inherit", overflow: "auto"}}>
{
this.state.menuMode === "vertical" ? (
<Sider width={200} style={{background: "inherit", position: "sticky", top: 0}}>

View File

@@ -208,7 +208,7 @@ class ApplicationListPage extends BaseListPage {
},
},
{
title: i18next.t("general:Providers"),
title: i18next.t("application:Providers"),
dataIndex: "providers",
key: "providers",
...this.getColumnSearchProps("providers"),

View File

@@ -132,7 +132,7 @@ class BaseListPage extends React.Component {
{i18next.t("general:Search")}
</Button>
<Button onClick={() => this.handleReset(clearFilters)} size="small" style={{width: 90}}>
{i18next.t("general:Reset")}
{i18next.t("forget:Reset")}
</Button>
<Button
type="link"

View File

@@ -75,6 +75,11 @@ class CartListPage extends BaseListPage {
const owner = this.state.user?.owner || this.props.account.owner;
const carts = this.state.data || [];
const invalidCarts = carts.filter(item => item.isInvalid);
if (invalidCarts.length > 0) {
Setting.showMessage("error", i18next.t("product:Cart contains invalid products, please delete them before placing an order"));
return;
}
if (carts.length === 0) {
Setting.showMessage("error", i18next.t("product:Product list cannot be empty"));
return;
@@ -117,7 +122,11 @@ class CartListPage extends BaseListPage {
return;
}
const index = user.cart.findIndex(item => item.name === record.name && item.price === record.price && (item.pricingName || "") === (record.pricingName || "") && (item.planName || "") === (record.planName || ""));
const index = user.cart.findIndex(item =>
item.name === record.name &&
(record.price !== null ? item.price === record.price : true) &&
(item.pricingName || "") === (record.pricingName || "") &&
(item.planName || "") === (record.planName || ""));
if (index === -1) {
Setting.showMessage("error", i18next.t("general:Failed to delete"));
return;
@@ -144,7 +153,7 @@ class CartListPage extends BaseListPage {
return;
}
const itemKey = `${record.name}-${record.price}-${record.pricingName || ""}-${record.planName || ""}`;
const itemKey = `${record.name}-${record.price !== null ? record.price : "null"}-${record.pricingName || ""}-${record.planName || ""}`;
if (this.updatingCartItemsRef?.[itemKey]) {
return;
}
@@ -152,64 +161,71 @@ class CartListPage extends BaseListPage {
this.updatingCartItemsRef[itemKey] = true;
const user = Setting.deepCopy(this.state.user);
const index = user.cart.findIndex(item => item.name === record.name && item.price === record.price && (item.pricingName || "") === (record.pricingName || "") && (item.planName || "") === (record.planName || ""));
const index = user.cart.findIndex(item =>
item.name === record.name &&
(record.isRecharge ? item.price === record.price : true) &&
(item.pricingName || "") === (record.pricingName || "") &&
(item.planName || "") === (record.planName || ""));
if (index === -1) {
delete this.updatingCartItemsRef[itemKey];
return;
}
if (index !== -1) {
user.cart[index].quantity = newQuantity;
const newData = [...this.state.data];
const dataIndex = newData.findIndex(item => item.name === record.name && item.price === record.price && (item.pricingName || "") === (record.pricingName || "") && (item.planName || "") === (record.planName || ""));
if (dataIndex !== -1) {
newData[dataIndex].quantity = newQuantity;
this.setState({data: newData});
}
this.setState(prevState => ({
updatingCartItems: {
...(prevState.updatingCartItems || {}),
[itemKey]: true,
},
}));
UserBackend.updateUser(user.owner, user.name, user)
.then((res) => {
if (res.status === "ok") {
this.setState({user: user});
} else {
Setting.showMessage("error", res.msg);
this.fetch();
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
this.fetch();
})
.finally(() => {
delete this.updatingCartItemsRef[itemKey];
this.setState(prevState => {
const updatingCartItems = {...(prevState.updatingCartItems || {})};
delete updatingCartItems[itemKey];
return {updatingCartItems};
});
});
user.cart[index].quantity = newQuantity;
const newData = [...this.state.data];
const dataIndex = newData.findIndex(item =>
item.name === record.name &&
(record.price !== null ? item.price === record.price : true) &&
(item.pricingName || "") === (record.pricingName || "") &&
(item.planName || "") === (record.planName || ""));
if (dataIndex !== -1) {
newData[dataIndex].quantity = newQuantity;
this.setState({data: newData});
}
this.setState(prevState => ({
updatingCartItems: {
...(prevState.updatingCartItems || {}),
[itemKey]: true,
},
}));
UserBackend.updateUser(user.owner, user.name, user)
.then((res) => {
if (res.status === "ok") {
this.setState({user: user});
} else {
Setting.showMessage("error", res.msg);
this.fetch();
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
this.fetch();
})
.finally(() => {
delete this.updatingCartItemsRef[itemKey];
this.setState(prevState => {
const updatingCartItems = {...(prevState.updatingCartItems || {})};
delete updatingCartItems[itemKey];
return {updatingCartItems};
});
});
}
renderTable(carts) {
const isEmpty = carts === undefined || carts === null || carts.length === 0;
const hasInvalidItems = carts && carts.some(item => item.isInvalid);
const owner = this.state.user?.owner || this.props.account.owner;
let total = 0;
let currency = "";
if (carts && carts.length > 0) {
carts.forEach(item => {
const validCarts = carts.filter(item => !item.isInvalid);
validCarts.forEach(item => {
total += item.price * item.quantity;
});
currency = carts[0].currency;
currency = validCarts.length > 0 ? validCarts[0].currency : (carts[0].currency || "USD");
}
const columns = [
@@ -222,6 +238,9 @@ class CartListPage extends BaseListPage {
sorter: true,
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
if (record.isInvalid) {
return <span style={{color: "red"}}>{text}</span>;
}
return (
<Link to={`/products/${owner}/${text}`}>
{text}
@@ -235,6 +254,12 @@ class CartListPage extends BaseListPage {
key: "displayName",
width: "170px",
sorter: true,
render: (text, record) => {
if (record.isInvalid) {
return <span style={{color: "red"}}>{i18next.t("product:Invalid product")}</span>;
}
return text;
},
},
{
title: i18next.t("product:Image"),
@@ -250,7 +275,7 @@ class CartListPage extends BaseListPage {
},
},
{
title: i18next.t("product:Price"),
title: i18next.t("order:Price"),
dataIndex: "price",
key: "price",
width: "160px",
@@ -268,6 +293,9 @@ class CartListPage extends BaseListPage {
sorter: true,
render: (text, record) => {
if (!text) {return null;}
if (record.isInvalid) {
return <span style={{color: "red"}}>{text}</span>;
}
return (
<Link to={`/pricings/${owner}/${text}`}>
{text}
@@ -283,6 +311,9 @@ class CartListPage extends BaseListPage {
sorter: true,
render: (text, record) => {
if (!text) {return null;}
if (record.isInvalid) {
return <span style={{color: "red"}}>{text}</span>;
}
return (
<Link to={`/plans/${owner}/${text}`}>
{text}
@@ -297,7 +328,7 @@ class CartListPage extends BaseListPage {
width: "100px",
sorter: true,
render: (text, record) => {
const itemKey = `${record.name}-${record.price}-${record.pricingName || ""}-${record.planName || ""}`;
const itemKey = `${record.name}-${record.price !== null ? record.price : "null"}-${record.pricingName || ""}-${record.planName || ""}`;
const isUpdating = this.state.updatingCartItems?.[itemKey] === true;
return (
<QuantityStepper
@@ -306,7 +337,7 @@ class CartListPage extends BaseListPage {
onIncrease={() => this.updateCartItemQuantity(record, text + 1)}
onDecrease={() => this.updateCartItemQuantity(record, text - 1)}
onChange={null}
disabled={isUpdating}
disabled={isUpdating || record.isInvalid}
/>
);
},
@@ -320,8 +351,12 @@ class CartListPage extends BaseListPage {
render: (text, record, index) => {
return (
<div style={{display: "flex", flexWrap: "wrap", gap: "8px"}}>
<Button type="primary" onClick={() => this.props.history.push(`/products/${owner}/${record.name}/buy`)}>
{i18next.t("product:Detail")}
<Button
type="primary"
onClick={() => this.props.history.push(`/products/${owner}/${record.name}/buy`)}
disabled={record.isInvalid}
>
{i18next.t("general:Detail")}
</Button>
<PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
@@ -358,7 +393,7 @@ class CartListPage extends BaseListPage {
onConfirm={() => this.clearCart()}
disabled={isEmpty}
/>
<Button type="primary" size="small" onClick={() => this.placeOrder()} disabled={isEmpty || this.state.isPlacingOrder} loading={this.state.isPlacingOrder}>{i18next.t("general:Place Order")}</Button>
<Button type="primary" size="small" onClick={() => this.placeOrder()} disabled={isEmpty || hasInvalidItems || this.state.isPlacingOrder} loading={this.state.isPlacingOrder}>{i18next.t("general:Place Order")}</Button>
</div>
);
}}
@@ -379,7 +414,7 @@ class CartListPage extends BaseListPage {
size="large"
style={{height: "50px", fontSize: "20px", padding: "0 40px", borderRadius: "5px"}}
onClick={() => this.placeOrder()}
disabled={this.state.isPlacingOrder}
disabled={hasInvalidItems || this.state.isPlacingOrder}
loading={this.state.isPlacingOrder}
>
{i18next.t("general:Place Order")}
@@ -404,17 +439,32 @@ class CartListPage extends BaseListPage {
ProductBackend.getProduct(organizationName, item.name)
.then(pRes => {
if (pRes.status === "ok" && pRes.data) {
const isCurrencyChanged = item.currency && pRes.data.currency && item.currency !== pRes.data.currency;
if (isCurrencyChanged) {
Setting.showMessage("warning", i18next.t("product:Product not found or invalid") + `: ${item.name}`);
}
return {
...pRes.data,
pricingName: item.pricingName,
planName: item.planName,
quantity: item.quantity,
price: pRes.data.isRecharge ? item.price : pRes.data.price,
isInvalid: isCurrencyChanged,
};
}
return item;
Setting.showMessage("warning", i18next.t("product:Product not found or invalid") + `: ${item.name}`);
return {
...item,
isInvalid: true,
};
})
.catch(() => {
Setting.showMessage("warning", i18next.t("product:Product not found or invalid") + `: ${item.name}`);
return {
...item,
isInvalid: true,
};
})
.catch(() => item)
);
const fullCartData = await Promise.all(productPromises);
@@ -445,6 +495,11 @@ class CartListPage extends BaseListPage {
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
const invalidProducts = sortedData.filter(item => item.isInvalid);
invalidProducts.forEach(item => {
Setting.showMessage("error", i18next.t("product:Product not found or invalid") + `: ${item.name}`);
});
} else {
this.setState({loading: false});
Setting.showMessage("error", res.msg);

View File

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

View File

@@ -147,7 +147,7 @@ class CertListPage extends BaseListPage {
sorter: true,
},
{
title: i18next.t("provider:Type"),
title: i18next.t("general:Type"),
dataIndex: "type",
key: "type",
filterMultiple: false,

View File

@@ -93,7 +93,7 @@ class FormEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={Setting.isMobile() ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
</Col>
<Col span={22}>
<Select
@@ -115,7 +115,7 @@ class FormEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={Setting.isMobile() ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("user:Tag - Tooltip"))} :
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("product:Tag - Tooltip"))} :
</Col>
<Col span={22}>
<Input value={this.state.form.tag} onChange={e => {

View File

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

View File

@@ -92,7 +92,7 @@ class GroupListPage extends BaseListPage {
uploadFile(info) {
const {status, msg} = info;
if (status === "ok") {
Setting.showMessage("success", "Groups uploaded successfully, refreshing the page");
Setting.showMessage("success", i18next.t("general:Successfully saved"));
const {pagination} = this.state;
this.fetch({pagination});
} else if (status === "error") {

View File

@@ -218,7 +218,7 @@ class LdapEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Admin"), i18next.t("ldap:Admin - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Admin"), i18next.t("ldap:Admin - Tooltip"))} :
</Col>
<Col span={21}>
<Input value={this.state.ldap.username} onChange={e => {

View File

@@ -188,7 +188,7 @@ function ManagementPage(props) {
};
return (
<Dropdown key="/rightDropDown" menu={{items, onClick}} >
<Dropdown key="/rightDropDown" menu={{items, onClick}} placement="bottomRight" >
<div className="rightDropDown">
{
renderAvatar()
@@ -320,7 +320,7 @@ function ManagementPage(props) {
res.push(Setting.getItem(<Link style={{color: textColor}} to="/applications">{i18next.t("general:Identity")}</Link>, "/identity", <LockTwoTone twoToneColor={twoToneColor} />, [
Setting.getItem(<Link to="/applications">{i18next.t("general:Applications")}</Link>, "/applications"),
Setting.getItem(<Link to="/providers">{i18next.t("general:Providers")}</Link>, "/providers"),
Setting.getItem(<Link to="/providers">{i18next.t("application:Providers")}</Link>, "/providers"),
Setting.getItem(<Link to="/resources">{i18next.t("general:Resources")}</Link>, "/resources"),
Setting.getItem(<Link to="/certs">{i18next.t("general:Certs")}</Link>, "/certs"),
]));

View File

@@ -14,6 +14,7 @@
import React from "react";
import {Button, Card, Col, Input, Row, Select} from "antd";
import PaginateSelect from "./common/PaginateSelect";
import * as OrderBackend from "./backend/OrderBackend";
import * as ProductBackend from "./backend/ProductBackend";
import * as UserBackend from "./backend/UserBackend";
@@ -41,7 +42,6 @@ class OrderEditPage extends React.Component {
UNSAFE_componentWillMount() {
this.getOrder();
this.getProducts();
this.getUsers();
this.getPayments();
}
@@ -72,19 +72,6 @@ class OrderEditPage extends React.Component {
});
}
getUsers() {
UserBackend.getUsers(this.state.organizationName)
.then((res) => {
if (res.status === "ok") {
this.setState({
users: res.data,
});
} else {
Setting.showMessage("error", `Failed to get users: ${res.msg}`);
}
});
}
getPayments() {
PaymentBackend.getPayments(this.state.organizationName)
.then((res) => {
@@ -158,7 +145,7 @@ class OrderEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("order:Products")}:
{i18next.t("general:Products")}:
</Col>
<Col span={22} >
<Select
@@ -184,18 +171,29 @@ class OrderEditPage extends React.Component {
{i18next.t("general:User")}:
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.order.user} disabled={isViewMode} onChange={(value) => {
this.updateOrderField("user", value);
}}>
{
this.state.users?.map((user, index) => <Option key={index} value={user.name}>{user.name}</Option>)
}
</Select>
<PaginateSelect
virtual
style={{width: "100%"}}
value={this.state.order.user}
disabled={isViewMode}
allowClear
fetchPage={UserBackend.getUsers}
buildFetchArgs={({page, pageSize, searchText}) => {
const field = searchText ? "name" : "";
return [this.state.organizationName, page, pageSize, field, searchText];
}}
reloadKey={this.state.organizationName}
optionMapper={(user) => Setting.getOption(user.name, user.name)}
filterOption={false}
onChange={(value) => {
this.updateOrderField("user", value || "");
}}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("order:Payment")}:
{i18next.t("general:Payment")}:
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.order.payment} disabled={isViewMode} onChange={(value) => {
@@ -231,7 +229,7 @@ class OrderEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("general:Message")}:
{i18next.t("payment:Message")}:
</Col>
<Col span={22} >
<Input value={this.state.order.message} onChange={e => {

View File

@@ -137,7 +137,7 @@ class OrderListPage extends BaseListPage {
},
},
{
title: i18next.t("order:Products"),
title: i18next.t("general:Products"),
dataIndex: "products",
key: "products",
...this.getColumnSearchProps("products"),

View File

@@ -233,7 +233,7 @@ class OrderPayPage extends React.Component {
<img src={product?.image} alt={Setting.getLanguageText(product?.displayName)} height={90} style={{objectFit: "contain"}} />
</Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Price")} span={1}>
<Descriptions.Item label={i18next.t("order:Price")} span={1}>
<span style={{fontSize: 18, fontWeight: "bold"}}>
{this.getProductPrice(product)}
</span>
@@ -245,7 +245,7 @@ class OrderPayPage extends React.Component {
</Descriptions.Item>
{product?.detail && (
<Descriptions.Item label={i18next.t("product:Detail")} span={2}>
<Descriptions.Item label={i18next.t("general:Detail")} span={2}>
<span style={{fontSize: 16}}>{Setting.getLanguageText(product?.detail)}</span>
</Descriptions.Item>
)}
@@ -286,7 +286,7 @@ class OrderPayPage extends React.Component {
<div className="login-content">
<Spin spinning={this.state.isProcessingPayment} size="large" tip={i18next.t("product:Processing payment...")} style={{paddingTop: "10%"}} >
<div style={{marginBottom: "20px"}}>
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("general:Order")}</span>} bordered column={3}>
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("application:Order")}</span>} bordered column={3}>
<Descriptions.Item label={i18next.t("general:ID")} span={3}>
<span style={{fontSize: 16}}>
{order.name}
@@ -325,14 +325,14 @@ class OrderPayPage extends React.Component {
</div>
<div>
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("order:Payment")}</span>} bordered column={3}>
<Descriptions.Item label={i18next.t("product:Price")} span={3}>
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("general:Payment")}</span>} bordered column={3}>
<Descriptions.Item label={i18next.t("order:Price")} span={3}>
<span style={{fontSize: 28, color: "red", fontWeight: "bold"}}>
{this.getPrice(order)}
</span>
</Descriptions.Item>
{!this.state.isViewMode && (
<Descriptions.Item label={i18next.t("product:Pay")} span={3}>
<Descriptions.Item label={i18next.t("order:Pay")} span={3}>
{this.renderPaymentMethods()}
</Descriptions.Item>
)}

View File

@@ -407,7 +407,7 @@ class OrganizationEditPage extends React.Component {
}}
filterOption={(input, option) => (option?.text ?? "").toLowerCase().includes(input.toLowerCase())}
>
{Setting.getCountryCodeOption({name: i18next.t("organization:All"), code: "All", phone: 0})}
{Setting.getCountryCodeOption({name: i18next.t("general:All"), code: "All", phone: 0})}
{
Setting.getCountryCodeData().map((country) => Setting.getCountryCodeOption(country))
}
@@ -481,7 +481,7 @@ class OrganizationEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Tags"), i18next.t("organization:Tags - Tooltip"))} :
{Setting.getLabel(i18next.t("organization:Tags"), i18next.t("application:Tags - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.organization.tags} onChange={(value => {this.updateOrganizationField("tags", value);})}>
@@ -684,7 +684,7 @@ class OrganizationEditPage extends React.Component {
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.accountMenu || "Horizontal"} onChange={(value => {this.updateOrganizationField("accountMenu", value);})}
options={[{value: "Horizontal", label: i18next.t("general:Horizontal")}, {value: "Vertical", label: i18next.t("general:Vertical")}].map(item => Setting.getOption(item.label, item.value))}
options={[{value: "Horizontal", label: i18next.t("application:Horizontal")}, {value: "Vertical", label: i18next.t("application:Vertical")}].map(item => Setting.getOption(item.label, item.value))}
/>
</Col>
</Row>
@@ -827,7 +827,7 @@ class OrganizationEditPage extends React.Component {
this.state.organization !== null ? this.renderOrganization() : null
}
{this.state.mode !== "add" && this.state.transactions.length > 0 ? (
<Card size="small" title={i18next.t("transaction:Transactions")} style={{marginTop: "20px"}} type="inner">
<Card size="small" title={i18next.t("general:Transactions")} style={{marginTop: "20px"}} type="inner">
<TransactionTable transactions={this.state.transactions} includeUser={true} />
</Card>
) : null}

View File

@@ -232,7 +232,7 @@ class PaymentEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("payment:Type - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Input disabled={true} value={this.state.payment.type} onChange={e => {
@@ -242,7 +242,7 @@ class PaymentEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("payment:Products"), i18next.t("payment:Products - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Products"), i18next.t("payment:Products - Tooltip"))} :
</Col>
<Col span={22} >
<Select
@@ -265,7 +265,7 @@ class PaymentEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
{Setting.getLabel(i18next.t("order:Price"), i18next.t("plan:Price - Tooltip"))} :
</Col>
<Col span={22} >
<Input disabled={true} value={this.state.payment.price} onChange={e => {
@@ -456,7 +456,7 @@ class PaymentEditPage extends React.Component {
}
if (!Setting.isValidEmail(this.state.payment.personEmail)) {
return i18next.t("signup:The input is not valid Email!");
return i18next.t("login:The input is not valid Email!");
}
if (!Setting.isValidPhone(this.state.payment.personPhone)) {

View File

@@ -161,7 +161,7 @@ class PaymentListPage extends BaseListPage {
},
},
{
title: i18next.t("provider:Type"),
title: i18next.t("general:Type"),
dataIndex: "type",
key: "type",
width: "140px",
@@ -175,7 +175,7 @@ class PaymentListPage extends BaseListPage {
},
},
{
title: i18next.t("order:Products"),
title: i18next.t("general:Products"),
dataIndex: "products",
key: "products",
...this.getColumnSearchProps("products"),
@@ -219,7 +219,7 @@ class PaymentListPage extends BaseListPage {
},
},
{
title: i18next.t("product:Price"),
title: i18next.t("order:Price"),
dataIndex: "price",
key: "price",
width: "160px",

View File

@@ -14,6 +14,7 @@
import React from "react";
import {Button, Card, Col, Input, Row, Select, Switch} from "antd";
import PaginateSelect from "./common/PaginateSelect";
import * as PermissionBackend from "./backend/PermissionBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as UserBackend from "./backend/UserBackend";
@@ -68,9 +69,6 @@ class PermissionEditPage extends React.Component {
permission: permission,
});
this.getUsers(permission.owner);
this.getGroups(permission.owner);
this.getRoles(permission.owner);
this.getModels(permission.owner);
this.getResources(permission.owner);
this.getModel(permission.model);
@@ -86,48 +84,6 @@ class PermissionEditPage extends React.Component {
});
}
getUsers(organizationName) {
UserBackend.getUsers(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
users: res.data,
});
});
}
getGroups(organizationName) {
GroupBackend.getGroups(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
groups: res.data,
});
});
}
getRoles(organizationName) {
RoleBackend.getRoles(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
roles: res.data,
});
});
}
getModels(organizationName) {
ModelBackend.getModels(organizationName)
.then((res) => {
@@ -211,9 +167,6 @@ class PermissionEditPage extends React.Component {
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.permission.owner} onChange={(owner => {
this.updatePermissionField("owner", owner);
this.getUsers(owner);
this.getGroups(owner);
this.getRoles(owner);
this.getModels(owner);
this.getResources(owner);
})}
@@ -268,12 +221,35 @@ class PermissionEditPage extends React.Component {
{Setting.getLabel(i18next.t("role:Sub users"), i18next.t("role:Sub users - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.users}
<PaginateSelect
virtual
mode="multiple"
style={{width: "100%"}}
value={this.state.permission.users}
allowClear
fetchPage={async(...args) => {
const res = await UserBackend.getUsers(...args);
if (res.status !== "ok") {
return res;
}
const data = res.data.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`));
if (args?.[1] === 1 && Array.isArray(res?.data)) {
res.data = [
Setting.getOption(i18next.t("general:All"), "*"),
...data,
];
} else {
res.data = data;
}
return res;
}}
buildFetchArgs={({page, pageSize, searchText}) => {
const field = searchText ? "name" : "";
return [this.state.permission.owner, page, pageSize, field, searchText];
}}
reloadKey={this.state.permission?.owner}
filterOption={false}
onChange={(value => {this.updatePermissionField("users", value);})}
options={[
Setting.getOption(i18next.t("organization:All"), "*"),
...this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`)),
]}
/>
</Col>
</Row>
@@ -282,12 +258,35 @@ class PermissionEditPage extends React.Component {
{Setting.getLabel(i18next.t("role:Sub groups"), i18next.t("role:Sub groups - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.groups}
<PaginateSelect
virtual
mode="multiple"
style={{width: "100%"}}
value={this.state.permission.groups}
allowClear
fetchPage={async(...args) => {
const res = await GroupBackend.getGroups(...args);
if (res.status !== "ok") {
return res;
}
const data = res.data.map((group) => Setting.getOption(`${group.owner}/${group.name}`, `${group.owner}/${group.name}`));
if (args?.[2] === 1 && Array.isArray(res?.data)) {
res.data = [
Setting.getOption(i18next.t("general:All"), "*"),
...data,
];
} else {
res.data = data;
}
return res;
}}
buildFetchArgs={({page, pageSize, searchText}) => {
const field = searchText ? "name" : "";
return [this.state.permission.owner, false, page, pageSize, field, searchText, "", ""];
}}
reloadKey={this.state.permission?.owner}
filterOption={false}
onChange={(value => {this.updatePermissionField("groups", value);})}
options={[
Setting.getOption(i18next.t("organization:All"), "*"),
...this.state.groups.map((group) => Setting.getOption(`${group.owner}/${group.name}`, `${group.owner}/${group.name}`)),
]}
/>
</Col>
</Row>
@@ -296,12 +295,37 @@ class PermissionEditPage extends React.Component {
{Setting.getLabel(i18next.t("role:Sub roles"), i18next.t("role:Sub roles - Tooltip"))} :
</Col>
<Col span={22} >
<Select disabled={!this.hasRoleDefinition(this.state.model)} placeholder={this.hasRoleDefinition(this.state.model) ? "" : "This field is disabled because the model is empty or it doesn't support RBAC (in another word, doesn't contain [role_definition])"} virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.roles}
<PaginateSelect
virtual
mode="multiple"
style={{width: "100%"}}
value={this.state.permission.roles}
disabled={!this.hasRoleDefinition(this.state.model)}
allowClear
fetchPage={async(...args) => {
const res = await RoleBackend.getRoles(...args);
if (res.status !== "ok") {
return res;
}
const data = res.data.map((role) => Setting.getOption(`${role.owner}/${role.name}`, `${role.owner}/${role.name}`));
if (args?.[1] === 1 && Array.isArray(res?.data)) {
// res.data = [{owner: i18next.t("organization:All"), name: "*"}, ...res.data];
res.data = [
Setting.getOption(i18next.t("general:All"), "*"),
...data,
];
} else {
res.data = data;
}
return res;
}}
buildFetchArgs={({page, pageSize, searchText}) => {
const field = searchText ? "name" : "";
return [this.state.permission.owner, page, pageSize, field, searchText, "", ""];
}}
reloadKey={this.state.permission?.owner}
filterOption={false}
onChange={(value => {this.updatePermissionField("roles", value);})}
options={[
Setting.getOption(i18next.t("organization:All"), "*"),
...this.state.roles.filter(roles => (roles.owner !== this.state.roles.owner || roles.name !== this.state.roles.name)).map((permission) => Setting.getOption(`${permission.owner}/${permission.name}`, `${permission.owner}/${permission.name}`)),
]}
/>
</Col>
</Row>
@@ -315,7 +339,7 @@ class PermissionEditPage extends React.Component {
this.updatePermissionField("domains", value);
})}
options={[
Setting.getOption(i18next.t("organization:All"), "*"),
Setting.getOption(i18next.t("general:All"), "*"),
...this.state.permission.domains.filter(domain => domain !== "*").map((domain) => Setting.getOption(domain, domain)),
]}
/>
@@ -349,7 +373,7 @@ class PermissionEditPage extends React.Component {
options={this.state.permission.resourceType === "API" ? Setting.getApiPaths().map((option, index) => {
return Setting.getOption(option, option);
}) : [
Setting.getOption(i18next.t("organization:All"), "*"),
Setting.getOption(i18next.t("general:All"), "*"),
...this.state.resources.map((resource) => Setting.getOption(`${resource.name}`, `${resource.name}`)),
]}
/>
@@ -369,7 +393,7 @@ class PermissionEditPage extends React.Component {
] : [
{value: "Read", name: i18next.t("permission:Read")},
{value: "Write", name: i18next.t("permission:Write")},
{value: "Admin", name: i18next.t("permission:Admin")},
{value: "Admin", name: i18next.t("general:Admin")},
].map((item) => Setting.getOption(item.name, item.value))}
/>
</Col>

View File

@@ -337,7 +337,7 @@ class PermissionListPage extends BaseListPage {
case "Write":
return i18next.t("permission:Write");
case "Admin":
return i18next.t("permission:Admin");
return i18next.t("general:Admin");
default:
return null;
}

View File

@@ -14,10 +14,10 @@
import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select, Switch} from "antd";
import PaginateSelect from "./common/PaginateSelect";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as RoleBackend from "./backend/RoleBackend";
import * as PlanBackend from "./backend/PlanBackend";
import * as UserBackend from "./backend/UserBackend";
import * as ProviderBackend from "./backend/ProviderBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
@@ -57,40 +57,10 @@ class PlanEditPage extends React.Component {
plan: res.data,
});
this.getUsers(this.state.organizationName);
this.getRoles(this.state.organizationName);
this.getPaymentProviders(this.state.organizationName);
});
}
getRoles(organizationName) {
RoleBackend.getRoles(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
roles: res.data,
});
});
}
getUsers(organizationName) {
UserBackend.getUsers(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
users: res.data,
});
});
}
getPaymentProviders(organizationName) {
ProviderBackend.getProviders(organizationName)
.then((res) => {
@@ -151,8 +121,6 @@ class PlanEditPage extends React.Component {
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.plan.owner} disabled={isViewMode} onChange={(owner => {
this.updatePlanField("owner", owner);
this.getUsers(owner);
this.getRoles(owner);
this.getPaymentProviders(owner);
})}
options={this.state.organizations.map((organization) => Setting.getOption(organization.name, organization.name))
@@ -184,9 +152,22 @@ class PlanEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Role"), i18next.t("general:Role - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.plan.role} disabled={isViewMode} onChange={(value => {this.updatePlanField("role", value);})}
options={this.state.roles.map((role) => Setting.getOption(role.name, role.name))
} />
<PaginateSelect
virtual
style={{width: "100%"}}
value={this.state.plan.role}
disabled={isViewMode}
allowClear
fetchPage={RoleBackend.getRoles}
buildFetchArgs={({page, pageSize, searchText}) => {
const field = searchText ? "name" : "";
return [this.state.plan.owner, page, pageSize, field, searchText, "", ""];
}}
reloadKey={this.state.plan.owner}
optionMapper={(role) => Setting.getOption(role.name, role.name)}
filterOption={false}
onChange={(value => {this.updatePlanField("role", value || "");})}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
@@ -201,7 +182,7 @@ class PlanEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("plan:Price"), i18next.t("plan:Price - Tooltip"))} :
{Setting.getLabel(i18next.t("order:Price"), i18next.t("plan:Price - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.plan.price} disabled={isViewMode} onChange={value => {
@@ -260,6 +241,16 @@ class PlanEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("plan:Is exclusive"), i18next.t("plan:Is exclusive - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.plan.isExclusive} disabled={isViewMode} onChange={checked => {
this.updatePlanField("isExclusive", checked);
}} />
</Col>
</Row>
</Card>
);
}

View File

@@ -130,7 +130,7 @@ class PlanListPage extends BaseListPage {
...this.getColumnSearchProps("displayName"),
},
{
title: i18next.t("plan:Price"),
title: i18next.t("order:Price"),
dataIndex: "price",
key: "price",
width: "160px",
@@ -154,7 +154,7 @@ class PlanListPage extends BaseListPage {
...this.getColumnSearchProps("role"),
render: (text, record, index) => {
return (
<Link to={`/roles/${record.owner}/${text}`}>
<Link to={`/roles/${text}`}>
{text}
</Link>
);

View File

@@ -193,7 +193,13 @@ class ProductBuyPage extends React.Component {
}
}
const existingItemIndex = cart.findIndex(item => item.name === product.name && item.price === actualPrice && (item.pricingName || "") === pricingName && (item.planName || "") === planName);
const cartPrice = product.isRecharge ? actualPrice : null;
const existingItemIndex = cart.findIndex(item =>
item.name === product.name &&
(product.isRecharge ? item.price === actualPrice : true) &&
(item.pricingName || "") === pricingName &&
(item.planName || "") === planName
);
const quantityToAdd = this.state.buyQuantity;
if (existingItemIndex !== -1) {
@@ -201,7 +207,7 @@ class ProductBuyPage extends React.Component {
} else {
const newProductInfo = {
name: product.name,
price: actualPrice,
price: cartPrice,
currency: product.currency,
pricingName: pricingName,
planName: planName,
@@ -389,7 +395,7 @@ class ProductBuyPage extends React.Component {
disabled={this.state.isPlacingOrder || isRechargeUnpurchasable || isAmountZero}
loading={this.state.isPlacingOrder}
>
{i18next.t("order:Place Order")}
{i18next.t("general:Place Order")}
</Button>
</div>
);
@@ -416,7 +422,7 @@ class ProductBuyPage extends React.Component {
{Setting.getLanguageText(product?.displayName)}
</span>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Detail")}><span style={{fontSize: 16}}>{Setting.getLanguageText(product?.detail)}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Detail")}><span style={{fontSize: 16}}>{Setting.getLanguageText(product?.detail)}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("user:Tag")}><span style={{fontSize: 16}}>{product?.tag}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:SKU")}><span style={{fontSize: 16}}>{product?.name}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Image")} span={3}>
@@ -424,12 +430,12 @@ class ProductBuyPage extends React.Component {
</Descriptions.Item>
{
product.isRecharge ? (
<Descriptions.Item span={3} label={i18next.t("product:Price")}>
<Descriptions.Item span={3} label={i18next.t("order:Price")}>
{this.renderRechargeInput(product)}
</Descriptions.Item>
) : (
<React.Fragment>
<Descriptions.Item label={i18next.t("product:Price")}>
<Descriptions.Item label={i18next.t("order:Price")}>
<span style={{fontSize: 28, color: "red", fontWeight: "bold"}}>
{
this.getPrice(product)
@@ -441,7 +447,7 @@ class ProductBuyPage extends React.Component {
</React.Fragment>
)
}
<Descriptions.Item label={i18next.t("order:Place Order")} span={3}>
<Descriptions.Item label={i18next.t("general:Place Order")} span={3}>
<div style={{display: "flex", justifyContent: "center", alignItems: "center", minHeight: "80px"}}>
{placeOrderButton}
</div>

View File

@@ -182,7 +182,7 @@ class ProductEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Detail"), i18next.t("product:Detail - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Detail"), i18next.t("product:Detail - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.product.detail} disabled={isViewMode} onChange={e => {
@@ -266,7 +266,7 @@ class ProductEditPage extends React.Component {
) : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
{Setting.getLabel(i18next.t("order:Price"), i18next.t("plan:Price - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.product.price} disabled={isViewMode || isCreatedByPlan} onChange={value => {

View File

@@ -153,7 +153,7 @@ class ProductListPage extends BaseListPage {
...this.getColumnSearchProps("tag"),
},
{
title: i18next.t("product:Price"),
title: i18next.t("order:Price"),
dataIndex: "price",
key: "price",
width: "160px",

View File

@@ -22,8 +22,6 @@ import {FloatingCartButton, QuantityStepper} from "./common/product/CartControls
const {Text, Title} = Typography;
const MAX_DISPLAYED_RECHARGE_OPTIONS = 3;
class ProductStorePage extends React.Component {
constructor(props) {
super(props);
@@ -128,7 +126,13 @@ class ProductStorePage extends React.Component {
}
}
const existingItemIndex = cart.findIndex(item => item.name === product.name && item.price === product.price);
if (product.isRecharge) {
Setting.showMessage("error", i18next.t("product:Recharge products need to go to the product detail page to set custom amount"));
this.setState(prevState => ({addingToCartProducts: prevState.addingToCartProducts.filter(name => name !== product.name)}));
return;
}
const existingItemIndex = cart.findIndex(item => item.name === product.name);
const quantityToAdd = this.state.productQuantities[product.name] || 1;
if (existingItemIndex !== -1) {
@@ -136,7 +140,6 @@ class ProductStorePage extends React.Component {
} else {
const newCartProductInfo = {
name: product.name,
price: product.price,
currency: product.currency,
pricingName: "",
planName: "",
@@ -275,17 +278,15 @@ class ProductStorePage extends React.Component {
<Text type="secondary" style={{fontSize: "13px", display: "block", marginBottom: 4}}>
{i18next.t("product:Recharge options")}:
</Text>
<div style={{display: "flex", flexWrap: "wrap", gap: "4px"}}>
{product.rechargeOptions.slice(0, MAX_DISPLAYED_RECHARGE_OPTIONS).map((amount, index) => (
<Tag key={index} color="blue" style={{fontSize: "14px", fontWeight: 600, margin: 0}}>
<div style={{display: "flex", flexWrap: "wrap", gap: "4px", alignItems: "center"}}>
{product.rechargeOptions.map((amount, index) => (
<Tag key={amount} color="blue" style={{fontSize: "14px", fontWeight: 600, margin: 0}}>
{Setting.getCurrencySymbol(product.currency)}{amount}
</Tag>
))}
{product.rechargeOptions.length > MAX_DISPLAYED_RECHARGE_OPTIONS && (
<Tag color="blue" style={{fontSize: "14px", fontWeight: 600, margin: 0}}>
+{product.rechargeOptions.length - MAX_DISPLAYED_RECHARGE_OPTIONS}
</Tag>
)}
<Text type="secondary" style={{fontSize: "13px", marginLeft: 8}}>
{Setting.getCurrencyWithFlag(product.currency)}
</Text>
</div>
</div>
)}
@@ -294,13 +295,23 @@ class ProductStorePage extends React.Component {
<Text strong style={{fontSize: "16px", color: "#1890ff"}}>
{i18next.t("product:Custom amount available")}
</Text>
{(!product.rechargeOptions || product.rechargeOptions.length === 0) && (
<Text type="secondary" style={{fontSize: "13px", marginLeft: 8}}>
{Setting.getCurrencyWithFlag(product.currency)}
</Text>
)}
</div>
)}
{(!product.rechargeOptions || product.rechargeOptions.length === 0) && product.disableCustomRecharge === true && (
<div style={{marginBottom: 8}}>
<Text type="secondary" style={{fontSize: "13px", display: "block", marginBottom: 4}}>
{i18next.t("product:No recharge options available")}
</Text>
<Text type="secondary" style={{fontSize: "13px"}}>
{Setting.getCurrencyWithFlag(product.currency)}
</Text>
</div>
)}
<div>
<Text type="secondary" style={{fontSize: "13px"}}>
{Setting.getCurrencyWithFlag(product.currency)}
</Text>
</div>
</>
) : (
<>

View File

@@ -257,7 +257,7 @@ class ProviderEditPage extends React.Component {
<Input value={this.state.provider.userMapping.affiliation} onChange={e => {
this.updateUserMappingField("affiliation", e.target.value);
}} />
{Setting.getLabel(i18next.t("user:Title"), i18next.t("user:Title - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Title"), i18next.t("general:Title - Tooltip"))} :
<Input value={this.state.provider.userMapping.title} onChange={e => {
this.updateUserMappingField("title", e.target.value);
}} />
@@ -319,7 +319,7 @@ class ProviderEditPage extends React.Component {
return Setting.getLabel(i18next.t("signup:Username"), i18next.t("signup:Username - Tooltip"));
case "SMS":
if (provider.type === "Volc Engine SMS" || provider.type === "Amazon SNS" || provider.type === "Baidu Cloud SMS") {
return Setting.getLabel(i18next.t("provider:Access key"), i18next.t("provider:Access key - Tooltip"));
return Setting.getLabel(i18next.t("general:Access key"), i18next.t("general:Access key - Tooltip"));
} else if (provider.type === "Huawei Cloud SMS") {
return Setting.getLabel(i18next.t("provider:App key"), i18next.t("provider:App key - Tooltip"));
} else if (provider.type === "UCloud SMS") {
@@ -331,19 +331,19 @@ class ProviderEditPage extends React.Component {
}
case "Captcha":
if (provider.type === "Aliyun Captcha") {
return Setting.getLabel(i18next.t("provider:Access key"), i18next.t("provider:Access key - Tooltip"));
return Setting.getLabel(i18next.t("general:Access key"), i18next.t("general:Access key - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Site key"), i18next.t("provider:Site key - Tooltip"));
}
case "Notification":
if (provider.type === "DingTalk") {
return Setting.getLabel(i18next.t("provider:Access key"), i18next.t("provider:Access key - Tooltip"));
return Setting.getLabel(i18next.t("general:Access key"), i18next.t("general:Access key - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"));
}
case "ID Verification":
if (provider.type === "Alibaba Cloud") {
return Setting.getLabel(i18next.t("provider:Access key"), i18next.t("provider:Access key - Tooltip"));
return Setting.getLabel(i18next.t("general:Access key"), i18next.t("general:Access key - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"));
}
@@ -698,7 +698,7 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("type", "Default");
this.updateProviderField("host", "smtp.example.com");
this.updateProviderField("port", 465);
this.updateProviderField("disableSsl", false);
this.updateProviderField("sslMode", "Auto");
this.updateProviderField("title", "Casdoor Verification Code");
this.updateProviderField("content", Setting.getDefaultHtmlEmailContent());
this.updateProviderField("metadata", Setting.getDefaultInvitationHtmlEmailContent());
@@ -751,7 +751,7 @@ class ProviderEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("provider:Type - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} showSearch value={this.state.provider.type} onChange={(value => {
@@ -816,13 +816,26 @@ class ProviderEditPage extends React.Component {
}}>
{
[
{id: "Normal", name: i18next.t("provider:Normal")},
{id: "Normal", name: i18next.t("application:Normal")},
{id: "Silent", name: i18next.t("provider:Silent")},
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.scopes} onChange={value => {
this.updateProviderField("scopes", value);
}}>
<Option key="snsapi_userinfo" value="snsapi_userinfo">snsapi_userinfo</Option>
<Option key="snsapi_privateinfo" value="snsapi_privateinfo">snsapi_privateinfo</Option>
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Use id as name"), i18next.t("provider:Use id as name - Tooltip"))} :
@@ -880,7 +893,7 @@ class ProviderEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))}
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("cert:Scope - Tooltip"))}
</Col>
<Col span={22} >
<Input value={this.state.provider.scopes} onChange={e => {
@@ -1127,7 +1140,7 @@ class ProviderEditPage extends React.Component {
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{["Casdoor"].includes(this.state.provider.type) ?
Setting.getLabel(i18next.t("general:Provider"), i18next.t("provider:Provider - Tooltip"))
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} >
@@ -1281,12 +1294,16 @@ class ProviderEditPage extends React.Component {
{["Azure ACS", "SendGrid"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Disable SSL"), i18next.t("provider:Disable SSL - Tooltip"))} :
{Setting.getLabel(i18next.t("provider:SSL mode"), i18next.t("provider:SSL mode - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.provider.disableSsl} onChange={checked => {
this.updateProviderField("disableSsl", checked);
}} />
<Col span={22} >
<Select virtual={false} style={{width: "200px"}} value={this.state.provider.sslMode || "Auto"} onChange={value => {
this.updateProviderField("sslMode", value);
}}>
<Option value="Auto">{i18next.t("provider:Auto")}</Option>
<Option value="Enable">{i18next.t("provider:Enable")}</Option>
<Option value="Disable">{i18next.t("provider:Disable")}</Option>
</Select>
</Col>
</Row>
)}
@@ -1758,7 +1775,7 @@ class ProviderEditPage extends React.Component {
copy(`${authConfig.serverUrl}/api/acs`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}>
{i18next.t("provider:Copy")}
{i18next.t("general:Copy")}
</Button>
</Col>
</Row>
@@ -1774,7 +1791,7 @@ class ProviderEditPage extends React.Component {
copy(`${authConfig.serverUrl}/api/acs`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}>
{i18next.t("provider:Copy")}
{i18next.t("general:Copy")}
</Button>
</Col>
</Row>

View File

@@ -158,7 +158,7 @@ class ProviderListPage extends BaseListPage {
sorter: true,
},
{
title: i18next.t("provider:Type"),
title: i18next.t("general:Type"),
dataIndex: "type",
key: "type",
width: "110px",
@@ -243,7 +243,7 @@ class ProviderListPage extends BaseListPage {
<Table scroll={{x: "max-content"}} columns={filteredColumns} dataSource={providers} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Providers")}&nbsp;&nbsp;&nbsp;&nbsp;
{i18next.t("application:Providers")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button id="add-button" type="primary" size="small" onClick={this.addProvider.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}

View File

@@ -189,7 +189,7 @@ class ResourceListPage extends BaseListPage {
// sorter: (a, b) => a.fileName.localeCompare(b.fileName),
// },
{
title: i18next.t("provider:Type"),
title: i18next.t("general:Type"),
dataIndex: "fileType",
key: "fileType",
width: "80px",

View File

@@ -20,6 +20,7 @@ import * as GroupBackend from "./backend/GroupBackend";
import * as RoleBackend from "./backend/RoleBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import PaginateSelect from "./common/PaginateSelect";
class RoleEditPage extends React.Component {
constructor(props) {
@@ -30,9 +31,6 @@ class RoleEditPage extends React.Component {
roleName: decodeURIComponent(props.match.params.roleName),
role: null,
organizations: [],
users: [],
groups: [],
roles: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@@ -57,10 +55,6 @@ class RoleEditPage extends React.Component {
this.setState({
role: res.data,
});
this.getUsers(this.state.organizationName);
this.getGroups(this.state.organizationName);
this.getRoles(this.state.organizationName);
});
}
@@ -73,48 +67,6 @@ class RoleEditPage extends React.Component {
});
}
getUsers(organizationName) {
UserBackend.getUsers(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
users: res.data,
});
});
}
getGroups(organizationName) {
GroupBackend.getGroups(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
groups: res.data,
});
});
}
getRoles(organizationName) {
RoleBackend.getRoles(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
roles: res.data,
});
});
}
parseRoleField(key, value) {
if ([""].includes(key)) {
value = Setting.myParseInt(value);
@@ -187,9 +139,20 @@ class RoleEditPage extends React.Component {
{Setting.getLabel(i18next.t("role:Sub users"), i18next.t("role:Sub users - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={true} mode="multiple" style={{width: "100%"}} value={this.state.role.users}
<PaginateSelect
virtual
mode="multiple"
style={{width: "100%"}}
value={this.state.role.users}
fetchPage={UserBackend.getUsers}
buildFetchArgs={({page, pageSize, searchText}) => {
const field = searchText ? "name" : "";
return [this.state.role.owner, page, pageSize, field, searchText];
}}
reloadKey={this.state.role.owner}
optionMapper={(user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`)}
filterOption={false}
onChange={(value => {this.updateRoleField("users", value);})}
options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))}
/>
</Col>
</Row>
@@ -198,9 +161,19 @@ class RoleEditPage extends React.Component {
{Setting.getLabel(i18next.t("role:Sub groups"), i18next.t("role:Sub groups - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.role.groups}
<PaginateSelect
mode="multiple"
style={{width: "100%"}}
value={this.state.role.groups}
fetchPage={GroupBackend.getGroups}
buildFetchArgs={({page, pageSize, searchText}) => {
const field = searchText ? "name" : "";
return [this.state.role.owner, false, page, pageSize, field, searchText, "", ""];
}}
reloadKey={this.state.role.owner}
optionMapper={(group) => Setting.getOption(`${group.owner}/${group.name}`, `${group.owner}/${group.name}`)}
filterOption={false}
onChange={(value => {this.updateRoleField("groups", value);})}
options={this.state.groups.map((group) => Setting.getOption(`${group.owner}/${group.name}`, `${group.owner}/${group.name}`))}
/>
</Col>
</Row>
@@ -209,9 +182,25 @@ class RoleEditPage extends React.Component {
{Setting.getLabel(i18next.t("role:Sub roles"), i18next.t("role:Sub roles - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.role.roles} onChange={(value => {this.updateRoleField("roles", value);})}
options={this.state.roles.filter(role => (role.owner !== this.state.role.owner || role.name !== this.state.role.name)).map((role) => Setting.getOption(`${role.owner}/${role.name}`, `${role.owner}/${role.name}`))
} />
<PaginateSelect
mode="multiple"
style={{width: "100%"}}
value={this.state.role.roles}
fetchPage={RoleBackend.getRoles}
buildFetchArgs={({page, pageSize, searchText}) => {
const field = searchText ? "name" : "";
return [this.state.role.owner, page, pageSize, field, searchText, "", ""];
}}
reloadKey={`${this.state.role.owner}/${this.state.role.name}`}
optionMapper={(role) => {
if (role.owner === this.state.role.owner && role.name === this.state.role.name) {
return null;
}
return Setting.getOption(`${role.owner}/${role.name}`, `${role.owner}/${role.name}`);
}}
filterOption={false}
onChange={(value => {this.updateRoleField("roles", value);})}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >

View File

@@ -34,6 +34,9 @@ export const ServerUrl = "";
export const StaticBaseUrl = "https://cdn.casbin.org";
export const MAX_PAGE_SIZE = 25;
export const SEARCH_DEBOUNCE_MS = 300;
export const Countries = [
{label: "English", key: "en", country: "US", alt: "English"},
{label: "Español", key: "es", country: "ES", alt: "Español"},
@@ -286,11 +289,11 @@ export const OtherProviderInfo = {
url: "https://fastspring.com/",
},
"Lemon Squeezy": {
logo: `${StaticBaseUrl}/img/payment_lemonsqueezy.png`,
logo: `${StaticBaseUrl}/img/payment_lemonsqueezy.jpg`,
url: "https://www.lemonsqueezy.com/",
},
"Adyen": {
logo: `${StaticBaseUrl}/img/payment_adyen.png`,
logo: `${StaticBaseUrl}/img/payment_adyen.svg`,
url: "https://www.adyen.com/",
},
},
@@ -498,11 +501,11 @@ export const GetTranslatedUserItems = () => {
{name: "Location", label: i18next.t("user:Location")},
{name: "Address", label: i18next.t("user:Address")},
{name: "Affiliation", label: i18next.t("user:Affiliation")},
{name: "Title", label: i18next.t("user:Title")},
{name: "Title", label: i18next.t("general:Title")},
{name: "ID card type", label: i18next.t("user:ID card type")},
{name: "ID card", label: i18next.t("user:ID card")},
{name: "ID card info", label: i18next.t("user:ID card info")},
{name: "Real name", label: i18next.t("user:Real name")},
{name: "Real name", label: i18next.t("application:Real name")},
{name: "ID verification", label: i18next.t("user:ID verification")},
{name: "Homepage", label: i18next.t("user:Homepage")},
{name: "Bio", label: i18next.t("user:Bio")},
@@ -514,7 +517,7 @@ export const GetTranslatedUserItems = () => {
{name: "Balance", label: i18next.t("user:Balance")},
{name: "Balance currency", label: i18next.t("organization:Balance currency")},
{name: "Balance credit", label: i18next.t("organization:Balance credit")},
{name: "Transactions", label: i18next.t("transaction:Transactions")},
{name: "Transactions", label: i18next.t("general:Transactions")},
{name: "Score", label: i18next.t("user:Score")},
{name: "Karma", label: i18next.t("user:Karma")},
{name: "Ranking", label: i18next.t("user:Ranking")},
@@ -531,10 +534,10 @@ export const GetTranslatedUserItems = () => {
{name: "Is deleted", label: i18next.t("user:Is deleted")},
{name: "Need update password", label: i18next.t("user:Need update password")},
{name: "IP whitelist", label: i18next.t("general:IP whitelist")},
{name: "Multi-factor authentication", label: i18next.t("user:Multi-factor authentication")},
{name: "Multi-factor authentication", label: i18next.t("mfa:Multi-factor authentication")},
{name: "WebAuthn credentials", label: i18next.t("user:WebAuthn credentials")},
{name: "Managed accounts", label: i18next.t("user:Managed accounts")},
{name: "Face ID", label: i18next.t("user:Face ID")},
{name: "Face ID", label: i18next.t("login:Face ID")},
{name: "MFA accounts", label: i18next.t("user:MFA accounts")},
{name: "MFA items", label: i18next.t("general:MFA items")},
];
@@ -2232,7 +2235,7 @@ export function createFormAndSubmit(url, params) {
export function getFormTypeOptions() {
return [
{id: "users", name: "general:Users"},
{id: "providers", name: "general:Providers"},
{id: "providers", name: "application:Providers"},
{id: "applications", name: "general:Applications"},
{id: "organizations", name: "general:Organizations"},
];
@@ -2264,7 +2267,7 @@ export function getFormTypeItems(formType) {
{name: "createdTime", label: "general:Created time", visible: true, width: "180"},
{name: "displayName", label: "general:Display name", visible: true, width: "150"},
{name: "category", label: "provider:Category", visible: true, width: "110"},
{name: "type", label: "provider:Type", visible: true, width: "110"},
{name: "type", label: "general:Type", visible: true, width: "110"},
{name: "clientId", label: "provider:Client ID", visible: true, width: "100"},
{name: "providerUrl", label: "provider:Provider URL", visible: true, width: "150"},
];
@@ -2275,7 +2278,7 @@ export function getFormTypeItems(formType) {
{name: "displayName", label: "general:Display name", visible: true, width: "150"},
{name: "logo", label: "Logo", visible: true, width: "200"},
{name: "organization", label: "general:Organization", visible: true, width: "150"},
{name: "providers", label: "general:Providers", visible: true, width: "500"},
{name: "providers", label: "application:Providers", visible: true, width: "500"},
];
} else if (formType === "organizations") {
return [

View File

@@ -15,6 +15,7 @@
import moment from "moment";
import React from "react";
import {Button, Card, Col, DatePicker, Input, Row, Select} from "antd";
import PaginateSelect from "./common/PaginateSelect";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as PricingBackend from "./backend/PricingBackend";
import * as PlanBackend from "./backend/PlanBackend";
@@ -63,7 +64,6 @@ class SubscriptionEditPage extends React.Component {
subscription: res.data,
});
this.getUsers(this.state.organizationName);
this.getPricings(this.state.organizationName);
this.getPlans(this.state.organizationName);
});
@@ -87,20 +87,6 @@ class SubscriptionEditPage extends React.Component {
});
}
getUsers(organizationName) {
UserBackend.getUsers(organizationName)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({
users: res.data,
});
});
}
getOrganizations() {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
@@ -147,7 +133,6 @@ class SubscriptionEditPage extends React.Component {
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.subscription.owner} disabled={isViewMode} onChange={(owner => {
this.updateSubscriptionField("owner", owner);
this.getUsers(owner);
this.getPlans(owner);
})}
options={this.state.organizations.map((organization) => Setting.getOption(organization.name, organization.name))
@@ -217,10 +202,21 @@ class SubscriptionEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} :
</Col>
<Col span={22} >
<Select style={{width: "100%"}} value={this.state.subscription.user}
<PaginateSelect
virtual
style={{width: "100%"}}
value={this.state.subscription.user}
disabled={isViewMode}
onChange={(value => {this.updateSubscriptionField("user", value);})}
options={this.state.users.map((user) => Setting.getOption(user.name, user.name))}
allowClear
fetchPage={UserBackend.getUsers}
buildFetchArgs={({page, pageSize, searchText}) => {
const field = searchText ? "name" : "";
return [this.state.subscription.owner, page, pageSize, field, searchText];
}}
reloadKey={this.state.subscription?.owner}
optionMapper={(user) => Setting.getOption(user.name, user.name)}
filterOption={false}
onChange={(value => {this.updateSubscriptionField("user", value || "");})}
/>
</Col>
</Row>
@@ -287,7 +283,7 @@ class SubscriptionEditPage extends React.Component {
this.updateSubscriptionField("state", value);
})}
options={[
{value: "Pending", name: i18next.t("subscription:Pending")},
{value: "Pending", name: i18next.t("permission:Pending")},
{value: "Active", name: i18next.t("subscription:Active")},
{value: "Upcoming", name: i18next.t("subscription:Upcoming")},
{value: "Expired", name: i18next.t("subscription:Expired")},

View File

@@ -131,14 +131,14 @@ class SubscriptionListPage extends BaseListPage {
...this.getColumnSearchProps("displayName"),
},
{
title: i18next.t("subscription:Period"),
title: i18next.t("plan:Period"),
dataIndex: "period",
key: "period",
width: "140px",
...this.getColumnSearchProps("period"),
},
{
title: i18next.t("general:Start time"),
title: i18next.t("subscription:Start time"),
dataIndex: "startTime",
key: "startTime",
width: "140px",
@@ -148,7 +148,7 @@ class SubscriptionListPage extends BaseListPage {
},
},
{
title: i18next.t("general:End time"),
title: i18next.t("subscription:End time"),
dataIndex: "endTime",
key: "endTime",
width: "140px",
@@ -165,7 +165,7 @@ class SubscriptionListPage extends BaseListPage {
...this.getColumnSearchProps("plan"),
render: (text, record, index) => {
return (
<Link to={`/plans/${record.owner}/${text}`}>
<Link to={`/plans/${text}`}>
{text}
</Link>
);
@@ -179,7 +179,7 @@ class SubscriptionListPage extends BaseListPage {
...this.getColumnSearchProps("user"),
render: (text, record, index) => {
return (
<Link to={`/users/${record.owner}/${text}`}>
<Link to={`/users/${text}`}>
{text}
</Link>
);
@@ -193,7 +193,7 @@ class SubscriptionListPage extends BaseListPage {
...this.getColumnSearchProps("payment"),
render: (text, record, index) => {
return (
<Link to={`/payments/${record.owner}/${text}`}>
<Link to={`/payments/${text}`}>
{text}
</Link>
);
@@ -209,7 +209,7 @@ class SubscriptionListPage extends BaseListPage {
render: (text, record, index) => {
switch (text) {
case "Pending":
return Setting.getTag("processing", i18next.t("subscription:Pending"), <ExclamationCircleOutlined />);
return Setting.getTag("processing", i18next.t("permission:Pending"), <ExclamationCircleOutlined />);
case "Active":
return Setting.getTag("success", i18next.t("subscription:Active"), <SyncOutlined spin />);
case "Upcoming":

View File

@@ -710,6 +710,79 @@ class SyncerEditPage extends React.Component {
"values": [],
},
];
case "AWS IAM":
return [
{
"name": "UserId",
"type": "string",
"casdoorName": "Id",
"isHashed": true,
"values": [],
},
{
"name": "UserName",
"type": "string",
"casdoorName": "Name",
"isHashed": true,
"values": [],
},
{
"name": "UserName",
"type": "string",
"casdoorName": "DisplayName",
"isHashed": true,
"values": [],
},
{
"name": "Tags.Email",
"type": "string",
"casdoorName": "Email",
"isHashed": true,
"values": [],
},
{
"name": "Tags.Phone",
"type": "string",
"casdoorName": "Phone",
"isHashed": true,
"values": [],
},
{
"name": "Tags.FirstName",
"type": "string",
"casdoorName": "FirstName",
"isHashed": true,
"values": [],
},
{
"name": "Tags.LastName",
"type": "string",
"casdoorName": "LastName",
"isHashed": true,
"values": [],
},
{
"name": "Tags.Title",
"type": "string",
"casdoorName": "Title",
"isHashed": true,
"values": [],
},
{
"name": "Tags.Department",
"type": "string",
"casdoorName": "Affiliation",
"isHashed": true,
"values": [],
},
{
"name": "CreateDate",
"type": "string",
"casdoorName": "CreatedTime",
"isHashed": true,
"values": [],
},
];
default:
return [];
}
@@ -753,7 +826,7 @@ class SyncerEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("provider:Type - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.syncer.type} onChange={(value => {
@@ -766,14 +839,14 @@ class SyncerEditPage extends React.Component {
});
})}>
{
["Database", "Keycloak", "WeCom", "Azure AD", "Active Directory", "Google Workspace", "DingTalk", "Lark", "Okta", "SCIM"]
["Database", "Keycloak", "WeCom", "Azure AD", "Active Directory", "Google Workspace", "DingTalk", "Lark", "Okta", "SCIM", "AWS IAM"]
.map((item, index) => <Option key={index} value={item}>{item}</Option>)
}
</Select>
</Col>
</Row>
{
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Active Directory" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" ? null : (
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Active Directory" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" || this.state.syncer.type === "AWS IAM" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Database type"), i18next.t("syncer:Database type - Tooltip"))} :
@@ -828,7 +901,7 @@ class SyncerEditPage extends React.Component {
this.state.syncer.type === "WeCom" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(this.state.syncer.type === "Azure AD" ? i18next.t("provider:Tenant ID") : this.state.syncer.type === "Google Workspace" ? i18next.t("syncer:Admin Email") : this.state.syncer.type === "Active Directory" ? i18next.t("ldap:Server") : this.state.syncer.type === "SCIM" ? i18next.t("syncer:SCIM Server URL") : i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
{Setting.getLabel(this.state.syncer.type === "Azure AD" ? i18next.t("provider:Tenant ID") : this.state.syncer.type === "Google Workspace" ? i18next.t("syncer:Admin Email") : this.state.syncer.type === "Active Directory" ? i18next.t("ldap:Server") : this.state.syncer.type === "SCIM" ? i18next.t("syncer:SCIM Server URL") : this.state.syncer.type === "AWS IAM" ? i18next.t("syncer:AWS Region") : i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.syncer.host} onChange={e => {
@@ -839,7 +912,7 @@ class SyncerEditPage extends React.Component {
)
}
{
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" ? null : (
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" || this.state.syncer.type === "AWS IAM" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(this.state.syncer.type === "Active Directory" ? i18next.t("provider:LDAP port") : i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
@@ -863,7 +936,8 @@ class SyncerEditPage extends React.Component {
this.state.syncer.type === "Azure AD" ? i18next.t("provider:Client ID") :
this.state.syncer.type === "Active Directory" ? i18next.t("syncer:Bind DN") :
this.state.syncer.type === "SCIM" ? i18next.t("syncer:Username (optional)") :
i18next.t("general:User"),
this.state.syncer.type === "AWS IAM" ? i18next.t("syncer:AWS Access Key ID") :
i18next.t("general:User"),
i18next.t("general:User - Tooltip")
)} :
</Col>
@@ -884,7 +958,8 @@ class SyncerEditPage extends React.Component {
this.state.syncer.type === "Azure AD" ? i18next.t("provider:Client secret") :
this.state.syncer.type === "Google Workspace" ? i18next.t("syncer:Service account key") :
this.state.syncer.type === "SCIM" ? i18next.t("syncer:API Token / Password") :
i18next.t("general:Password"),
this.state.syncer.type === "AWS IAM" ? i18next.t("syncer:AWS Secret Access Key") :
i18next.t("general:Password"),
i18next.t("general:Password - Tooltip")
)} :
</Col>
@@ -903,10 +978,10 @@ class SyncerEditPage extends React.Component {
</Col>
</Row>
{
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" ? null : (
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" || this.state.syncer.type === "AWS IAM" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(this.state.syncer.type === "Active Directory" ? i18next.t("syncer:Base DN") : i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :
{Setting.getLabel(this.state.syncer.type === "Active Directory" ? i18next.t("ldap:Base DN") : i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.syncer.database} onChange={e => {
@@ -999,7 +1074,7 @@ class SyncerEditPage extends React.Component {
) : null
}
{
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" ? null : (
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" || this.state.syncer.type === "AWS IAM" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Table"), i18next.t("syncer:Table - Tooltip"))} :

View File

@@ -147,7 +147,7 @@ class SyncerListPage extends BaseListPage {
},
},
{
title: i18next.t("provider:Type"),
title: i18next.t("general:Type"),
dataIndex: "type",
key: "type",
width: "100px",

View File

@@ -203,7 +203,7 @@ class TicketEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("general:Content")}:
{i18next.t("provider:Content")}:
</Col>
<Col span={22} >
<TextArea autoSize={{minRows: 3, maxRows: 10}} value={this.state.ticket.content} disabled={!isAdmin && !isOwner} onChange={e => {

View File

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

View File

@@ -19,6 +19,7 @@ import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as UserBackend from "./backend/UserBackend";
import * as Setting from "./Setting";
import {Button, Card, Col, Input, InputNumber, Row, Select} from "antd";
import PaginateSelect from "./common/PaginateSelect";
import i18next from "i18next";
const {Option} = Select;
@@ -43,7 +44,6 @@ class TransactionEditPage extends React.Component {
if (this.state.mode === "recharge") {
this.getOrganizations();
this.getApplications(this.state.organizationName);
this.getUsers(this.state.organizationName);
}
}
@@ -103,19 +103,6 @@ class TransactionEditPage extends React.Component {
});
}
getUsers(organizationName) {
const targetOrganizationName = organizationName || this.state.organizationName;
UserBackend.getUsers(targetOrganizationName)
.then((res) => {
this.setState({
users: res.data || [],
});
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
submitTransactionEdit(exitAfterSave) {
if (this.state.transaction === null) {
return;
@@ -205,7 +192,6 @@ class TransactionEditPage extends React.Component {
this.updateTransactionField("owner", value);
this.updateTransactionField("application", "");
this.getApplications(value);
this.getUsers(value);
}}>
{
this.state.organizations.map((org, index) => <Option key={index} value={org.name}>{org.name}</Option>)
@@ -283,7 +269,7 @@ class TransactionEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("payment:Type - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Type"), i18next.t("cert:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Input disabled={true} value={this.state.transaction.type} onChange={e => {
@@ -313,7 +299,7 @@ class TransactionEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("transaction:Tag - Tooltip"))} :
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("product:Tag - Tooltip"))} :
</Col>
<Col span={22} >
{isRechargeMode ? (
@@ -340,17 +326,24 @@ class TransactionEditPage extends React.Component {
</Col>
<Col span={22} >
{isRechargeMode ? (
<Select virtual={false} style={{width: "100%"}}
<PaginateSelect
virtual
style={{width: "100%"}}
value={this.state.transaction.user}
disabled={this.state.transaction.tag === "Organization"}
allowClear
fetchPage={UserBackend.getUsers}
buildFetchArgs={({page, pageSize, searchText}) => {
const field = searchText ? "name" : "";
return [this.state.transaction?.organization || this.state.organizationName, page, pageSize, field, searchText];
}}
reloadKey={this.state.transaction?.organization || this.state.organizationName}
optionMapper={(user) => Setting.getOption(user.name, user.name)}
filterOption={false}
onChange={(value) => {
this.updateTransactionField("user", value || "");
}}>
{
this.state.users.map((user, index) => <Option key={index} value={user.name}>{user.name}</Option>)
}
</Select>
}}
/>
) : (
<Input disabled={true} value={this.state.transaction.user} onChange={e => {
}} />
@@ -359,7 +352,7 @@ class TransactionEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("transaction:Amount"), i18next.t("transaction:Amount - Tooltip"))} :
{Setting.getLabel(i18next.t("product:Amount"), i18next.t("transaction:Amount - Tooltip"))} :
</Col>
<Col span={4} >
<InputNumber disabled={!isRechargeMode} value={this.state.transaction.amount ?? 0} onChange={value => {
@@ -369,7 +362,7 @@ class TransactionEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("currency:Currency"), i18next.t("currency:Currency - Tooltip"))} :
{Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.transaction.currency} disabled={!isRechargeMode} onChange={(value => {

View File

@@ -626,7 +626,7 @@ class UserEditPage extends React.Component {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Title"), i18next.t("user:Title - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Title"), i18next.t("general:Title - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.title} onChange={e => {
@@ -686,7 +686,7 @@ class UserEditPage extends React.Component {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Real name"), i18next.t("user:Real name - Tooltip"))} :
{Setting.getLabel(i18next.t("application:Real name"), i18next.t("user:Real name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.realName} disabled={disabled} onChange={e => {
@@ -744,7 +744,7 @@ class UserEditPage extends React.Component {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("user:Tag - Tooltip"))} :
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("product:Tag - Tooltip"))} :
</Col>
<Col span={22} >
{
@@ -835,7 +835,7 @@ class UserEditPage extends React.Component {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Balance credit"), i18next.t("user:Balance credit - Tooltip"))} :
{Setting.getLabel(i18next.t("organization:Balance credit"), i18next.t("organization:Balance credit - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.user.balanceCredit ?? 0} onChange={value => {
@@ -848,7 +848,7 @@ class UserEditPage extends React.Component {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Balance currency"), i18next.t("user:Balance currency - Tooltip"))} :
{Setting.getLabel(i18next.t("organization:Balance currency"), i18next.t("organization:Balance currency - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.user.balanceCurrency || "USD"} onChange={(value => {
@@ -1437,7 +1437,7 @@ class UserEditPage extends React.Component {
type="card"
activeKey={activeKey}
items={tabs.map(tab => ({
label: tab === "" ? i18next.t("user:Default") : tab,
label: tab === "" ? i18next.t("general:Default") : tab,
key: tab,
}))}
/>
@@ -1457,7 +1457,7 @@ class UserEditPage extends React.Component {
}}
style={{marginBottom: "20px", height: "100%"}}
items={tabs.map(tab => ({
label: tab === "" ? i18next.t("user:Default") : tab,
label: tab === "" ? i18next.t("general:Default") : tab,
key: tab,
}))}
/>

View File

@@ -191,7 +191,7 @@ class UserListPage extends BaseListPage {
impersonateUser(user) {
UserBackend.impersonateUser(user).then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Success"));
Setting.showMessage("success", i18next.t("general:Successfully executed"));
Setting.goToLinkSoft(this, "/");
window.location.reload();
} else {
@@ -393,7 +393,7 @@ class UserListPage extends BaseListPage {
...this.getColumnSearchProps("affiliation"),
},
{
title: i18next.t("user:Real name"),
title: i18next.t("application:Real name"),
dataIndex: "realName",
key: "realName",
width: "120px",
@@ -479,7 +479,7 @@ class UserListPage extends BaseListPage {
},
},
{
title: i18next.t("user:Balance credit"),
title: i18next.t("organization:Balance credit"),
dataIndex: "balanceCredit",
key: "balanceCredit",
width: "120px",
@@ -489,7 +489,7 @@ class UserListPage extends BaseListPage {
},
},
{
title: i18next.t("user:Balance currency"),
title: i18next.t("organization:Balance currency"),
dataIndex: "balanceCurrency",
key: "balanceCurrency",
width: "140px",

View File

@@ -73,7 +73,7 @@ class VerificationListPage extends BaseListPage {
},
},
{
title: i18next.t("provider:Type"),
title: i18next.t("general:Type"),
dataIndex: "type",
key: "type",
width: "90px",

View File

@@ -220,7 +220,7 @@ class WebhookEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Method"), i18next.t("webhook:Method - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.webhook.method} onChange={(value => {this.updateWebhookField("method", value);})}>

View File

@@ -44,7 +44,7 @@ class CasLogout extends React.Component {
if (logoutRes.status === "ok") {
logoutTimeOut(logoutRes.data2);
} else {
Setting.showMessage("error", `${i18next.t("login:Failed to log out")}: ${logoutRes.msg}`);
Setting.showMessage("error", `${i18next.t("general:Failed to log out")}: ${logoutRes.msg}`);
}
});
} else {
@@ -67,7 +67,7 @@ class CasLogout extends React.Component {
if (res.status === "ok") {
logoutTimeOut(res.data2);
} else {
Setting.showMessage("error", `${i18next.t("login:Failed to log out")}: ${res.msg}`);
Setting.showMessage("error", `${i18next.t("general:Failed to log out")}: ${res.msg}`);
}
});
}

View File

@@ -488,7 +488,7 @@ class ForgetPage extends React.Component {
>
<Input.Password
prefix={<CheckCircleOutlined />}
placeholder={i18next.t("signup:Confirm")}
placeholder={i18next.t("general:Confirm")}
/>
</Form.Item>
<br />

View File

@@ -286,8 +286,8 @@ class LoginPage extends React.Component {
}
switch (this.state.loginMethod) {
case "verificationCode": return i18next.t("login:Email or phone");
case "verificationCodeEmail": return i18next.t("login:Email");
case "verificationCodePhone": return i18next.t("login:Phone");
case "verificationCodeEmail": return i18next.t("general:Email");
case "verificationCodePhone": return i18next.t("general:Phone");
case "ldap": return i18next.t("login:LDAP username, Email or phone");
default: return i18next.t("login:username, Email or phone");
}

View File

@@ -439,6 +439,10 @@ export function getAuthUrl(application, provider, method, code) {
const redirectOrigin = application.forcedRedirectOrigin ? application.forcedRedirectOrigin : window.location.origin;
let redirectUri = `${redirectOrigin}/callback`;
let scope = authInfo[type].scope;
// Allow provider.scopes to override default scope if specified
if (provider.scopes && provider.scopes.trim() !== "") {
scope = provider.scopes;
}
const isShortState = (provider.type === "WeChat" && navigator.userAgent.includes("MicroMessenger")) || (provider.type === "Twitter");
let applicationName = application.name;
if (application?.isShared) {

View File

@@ -423,7 +423,7 @@ class SignupPage extends React.Component {
<Form.Item
name="name"
className="signup-name"
label={(signupItem.label ? signupItem.label : (signupItem.rule === "Real name" || signupItem.rule === "First, last") ? i18next.t("general:Real name") : i18next.t("general:Display name"))}
label={(signupItem.label ? signupItem.label : (signupItem.rule === "Real name" || signupItem.rule === "First, last") ? i18next.t("application:Real name") : i18next.t("general:Display name"))}
rules={displayNameRules}
>
<Input className="signup-name-input" placeholder={signupItem.placeholder} />
@@ -552,13 +552,13 @@ class SignupPage extends React.Component {
rules={[
{
required: required,
message: i18next.t("signup:Please input your Email!"),
message: i18next.t("login:Please input your Email!"),
},
{
validator: (_, value) => {
if (this.state.email !== "" && !Setting.isValidEmail(this.state.email)) {
this.setState({validEmail: false});
return Promise.reject(i18next.t("signup:The input is not valid Email!"));
return Promise.reject(i18next.t("login:The input is not valid Email!"));
}
if (signupItem.regex) {
@@ -772,7 +772,7 @@ class SignupPage extends React.Component {
<Form.Item
name="confirm"
className="signup-confirm"
label={signupItem.label ? signupItem.label : i18next.t("signup:Confirm")}
label={signupItem.label ? signupItem.label : i18next.t("general:Confirm")}
dependencies={["password"]}
hasFeedback
rules={[

View File

@@ -31,7 +31,7 @@ export function renderMessage(msg) {
type="error"
action={
<Button size="small" type="primary" danger>
{i18next.t("product:Detail")}
{i18next.t("general:Detail")}
</Button>
}
/>

View File

@@ -29,11 +29,11 @@ export const MfaVerifyPushForm = ({mfaProps, application, onFinish, method, user
<Form.Item
name="passcode"
noStyle
rules={[{required: true, message: i18next.t("login:Please input your verification code!")}]}
rules={[{required: true, message: i18next.t("code:Please input your verification code!")}]}
>
<Input
style={{width: "100%", marginTop: 12}}
placeholder={i18next.t("mfa:Verification code")}
placeholder={i18next.t("login:Verification code")}
/>
</Form.Item>
<Form.Item

View File

@@ -144,7 +144,7 @@ const Dashboard = (props) => {
tooltip: {trigger: "axis"},
legend: {data: [
i18next.t("general:Users"),
i18next.t("general:Providers"),
i18next.t("application:Providers"),
i18next.t("general:Applications"),
i18next.t("general:Organizations"),
i18next.t("general:Subscriptions"),
@@ -164,7 +164,7 @@ const Dashboard = (props) => {
series: [
{name: i18next.t("general:Organizations"), type: "line", data: dashboardData.organizationCounts},
{name: i18next.t("general:Users"), type: "line", data: dashboardData.userCounts},
{name: i18next.t("general:Providers"), type: "line", data: dashboardData.providerCounts},
{name: i18next.t("application:Providers"), type: "line", data: dashboardData.providerCounts},
{name: i18next.t("general:Applications"), type: "line", data: dashboardData.applicationCounts},
{name: i18next.t("general:Subscriptions"), type: "line", data: dashboardData.subscriptionCounts},
{name: i18next.t("general:Roles"), type: "line", data: dashboardData.roleCounts},

View File

@@ -7,7 +7,7 @@ const ShortcutsPage = () => {
const items = [
{link: "/organizations", name: i18next.t("general:Organizations"), description: i18next.t("general:User containers")},
{link: "/users", name: i18next.t("general:Users"), description: i18next.t("general:Users under all organizations")},
{link: "/providers", name: i18next.t("general:Providers"), description: i18next.t("general:OAuth providers")},
{link: "/providers", name: i18next.t("application:Providers"), description: i18next.t("general:OAuth providers")},
{link: "/applications", name: i18next.t("general:Applications"), description: i18next.t("general:Applications that require authentication")},
];

View File

@@ -5,7 +5,7 @@ import React from "react";
export const NavItemTree = ({disabled, checkedKeys, defaultExpandedKeys, onCheck}) => {
const NavItemNodes = [
{
title: i18next.t("organization:All"),
title: i18next.t("general:All"),
key: "all",
children: [
{
@@ -32,7 +32,7 @@ export const NavItemTree = ({disabled, checkedKeys, defaultExpandedKeys, onCheck
key: "/applications-top",
children: [
{title: i18next.t("general:Applications"), key: "/applications"},
{title: i18next.t("general:Providers"), key: "/providers"},
{title: i18next.t("application:Providers"), key: "/providers"},
{title: i18next.t("general:Resources"), key: "/resources"},
{title: i18next.t("general:Certs"), key: "/certs"},
],

View File

@@ -0,0 +1,277 @@
// 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 {Select, Spin} from "antd";
import * as Setting from "../Setting";
const SCROLL_BOTTOM_OFFSET = 20;
const defaultOptionMapper = (item) => {
if (item === null) {
return null;
}
if (typeof item === "string") {
return Setting.getOption(item, item);
}
const value = item.value ?? item.name ?? item.id ?? item.key;
const label = item.label ?? item.displayName ?? value;
if (value === undefined) {
return null;
}
return Setting.getOption(label, value);
};
function PaginateSelect(props) {
const {
fetchPage,
buildFetchArgs,
optionMapper = defaultOptionMapper,
pageSize = Setting.MAX_PAGE_SIZE,
debounceMs = Setting.SEARCH_DEBOUNCE_MS,
onError,
onSearch: onSearchProp,
onPopupScroll: onPopupScrollProp,
showSearch = true,
filterOption = false,
notFoundContent,
loading: selectLoading,
dropdownMatchSelectWidth = false,
virtual = false,
reloadKey,
...restProps
} = props;
const [options, setOptions] = React.useState([]);
const [hasMore, setHasMore] = React.useState(true);
const [loading, setLoading] = React.useState(false);
const debounceRef = React.useRef(null);
const latestSearchRef = React.useRef("");
const loadingRef = React.useRef(false);
const requestIdRef = React.useRef(0);
const pageRef = React.useRef(0);
const fetchPageRef = React.useRef(fetchPage);
const buildFetchArgsRef = React.useRef(buildFetchArgs);
const optionMapperRef = React.useRef(optionMapper ?? defaultOptionMapper);
React.useEffect(() => {
fetchPageRef.current = fetchPage;
}, [fetchPage]);
React.useEffect(() => {
buildFetchArgsRef.current = buildFetchArgs;
}, [buildFetchArgs]);
React.useEffect(() => {
optionMapperRef.current = optionMapper ?? defaultOptionMapper;
}, [optionMapper]);
const handleError = React.useCallback((error) => {
if (onError) {
onError(error);
return;
}
if (Setting?.showMessage) {
Setting.showMessage("error", error?.message ?? String(error));
}
}, [onError]);
const extractItems = React.useCallback((response) => {
if (Array.isArray(response)) {
return response;
}
if (Array.isArray(response?.items)) {
return response.items;
}
if (Array.isArray(response?.data)) {
return response.data;
}
if (Array.isArray(response?.list)) {
return response.list;
}
return [];
}, []);
const mergeOptions = React.useCallback((prev, next, reset) => {
if (reset) {
return next;
}
const merged = [...prev];
const indexByValue = new Map();
merged.forEach((opt, idx) => {
if (opt?.value !== undefined) {
indexByValue.set(opt.value, idx);
}
});
next.forEach((opt) => {
if (!opt) {
return;
}
const optionValue = opt.value;
if (optionValue === undefined) {
merged.push(opt);
return;
}
if (indexByValue.has(optionValue)) {
merged[indexByValue.get(optionValue)] = opt;
return;
}
indexByValue.set(optionValue, merged.length);
merged.push(opt);
});
return merged;
}, []);
const loadPage = React.useCallback(async({pageToLoad = 1, reset = false, search = latestSearchRef.current} = {}) => {
const fetcher = fetchPageRef.current;
if (typeof fetcher !== "function") {
return;
}
if (loadingRef.current && !reset) {
return;
}
if (reset) {
loadingRef.current = false;
}
const currentRequestId = requestIdRef.current + 1;
requestIdRef.current = currentRequestId;
loadingRef.current = true;
setLoading(true);
const defaultArgsObject = {
page: pageToLoad,
pageSize,
search,
searchText: search,
query: search,
};
try {
const argsBuilder = buildFetchArgsRef.current;
const builtArgs = argsBuilder ? argsBuilder({
page: pageToLoad,
pageSize,
searchText: search,
}) : defaultArgsObject;
const payload = Array.isArray(builtArgs) ?
await fetcher(...builtArgs) :
await fetcher(builtArgs ?? defaultArgsObject);
if (currentRequestId !== requestIdRef.current) {
return;
}
if (payload?.status && payload.status !== "ok") {
handleError(payload?.msg ?? payload?.error ?? "Request failed");
setHasMore(false);
return;
}
const items = extractItems(payload);
const mapper = optionMapperRef.current ?? defaultOptionMapper;
const mappedOptions = items.map(mapper).filter(Boolean);
setOptions((prev) => mergeOptions(prev, mappedOptions, reset));
pageRef.current = pageToLoad;
const hasMoreFromPayload = typeof payload?.hasMore === "boolean" ? payload.hasMore : null;
const hasMoreFromTotal = typeof payload?.total === "number" ? (pageToLoad * pageSize < payload.total) : null;
const fallbackHasMore = mappedOptions.length === pageSize;
setHasMore(hasMoreFromPayload ?? hasMoreFromTotal ?? fallbackHasMore);
} catch (error) {
if (currentRequestId === requestIdRef.current) {
handleError(error);
}
} finally {
if (currentRequestId === requestIdRef.current) {
loadingRef.current = false;
setLoading(false);
}
}
}, [pageSize, extractItems, mergeOptions, handleError]);
const resetAndLoad = React.useCallback((search = "") => {
latestSearchRef.current = search;
setOptions([]);
setHasMore(true);
pageRef.current = 0;
loadPage({pageToLoad: 1, reset: true, search});
}, [loadPage]);
React.useEffect(() => {
resetAndLoad("");
return () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
};
}, [resetAndLoad, reloadKey]);
const handleSearch = React.useCallback((value) => {
onSearchProp?.(value);
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
const triggerSearch = () => resetAndLoad(value || "");
if (!debounceMs) {
triggerSearch();
return;
}
debounceRef.current = setTimeout(triggerSearch, debounceMs);
}, [debounceMs, onSearchProp, resetAndLoad]);
const handlePopupScroll = React.useCallback((event) => {
onPopupScrollProp?.(event);
const target = event?.target;
if (!target || loadingRef.current || !hasMore) {
return;
}
const reachedBottom = target.scrollTop + target.offsetHeight >= target.scrollHeight - SCROLL_BOTTOM_OFFSET;
if (reachedBottom) {
const nextPage = pageRef.current + 1;
loadPage({pageToLoad: nextPage});
}
}, [hasMore, loadPage, onPopupScrollProp]);
const mergedLoading = selectLoading ?? loading;
const mergedNotFound = mergedLoading ? <Spin size="small" /> : notFoundContent;
return (
<Select
{...restProps}
virtual={virtual}
showSearch={showSearch}
filterOption={filterOption}
options={options}
loading={mergedLoading}
notFoundContent={mergedNotFound}
onSearch={showSearch ? handleSearch : undefined}
onPopupScroll={handlePopupScroll}
dropdownMatchSelectWidth={dropdownMatchSelectWidth}
/>
);
}
export default PaginateSelect;

View File

@@ -5,7 +5,7 @@ import React from "react";
export const WidgetItemTree = ({disabled, checkedKeys, defaultExpandedKeys, onCheck}) => {
const WidgetItemNodes = [
{
title: i18next.t("organization:All"),
title: i18next.t("general:All"),
key: "all",
children: [
{title: i18next.t("general:Tour"), key: "tour"},

View File

@@ -64,7 +64,7 @@ const EnableMfaModal = ({user, mfaType, onSuccess}) => {
const showModal = () => {
if (!isValid()) {
if (mfaType === EmailMfaType) {
Setting.showMessage("error", i18next.t("signup:Please input your Email!"));
Setting.showMessage("error", i18next.t("login:Please input your Email!"));
} else {
Setting.showMessage("error", i18next.t("signup:Please input your phone number!"));
}

View File

@@ -52,9 +52,9 @@ export const CountryCodeSelect = (props) => {
filterOption={(input, option) => (option?.text ?? "").toLowerCase().includes(input.toLowerCase())}
>
{
props.hasDefault ? (<Option key={"All"} value={"All"} label={i18next.t("organization:All")} text={"organization:All"} >
props.hasDefault ? (<Option key={"All"} value={"All"} label={i18next.t("general:All")} text={"general:All"} >
<div style={{display: "flex", justifyContent: "space-between", marginRight: "10px"}}>
{i18next.t("organization:All")}
{i18next.t("general:All")}
</div>
</Option>) : null
}

View File

@@ -58,7 +58,7 @@ function OrganizationSelect(props) {
if (withAll) {
items.unshift({
label: i18next.t("organization:All"),
label: i18next.t("general:All"),
value: "All",
});
}

View File

@@ -21,7 +21,7 @@ import {CheckOutlined} from "@ant-design/icons";
import {CompactTheme, DarkTheme, Light} from "antd-token-previewer/es/icons";
export const Themes = [
{label: "Default", key: "default", icon: <Light style={{fontSize: "24px"}} />}, // i18next.t("theme:Default")
{label: "Default", key: "default", icon: <Light style={{fontSize: "24px"}} />}, // i18next.t("general:Default")
{label: "Dark", key: "dark", icon: <DarkTheme style={{fontSize: "24px"}} />}, // i18next.t("theme:Dark")
{label: "Compact", key: "compact", icon: <CompactTheme style={{fontSize: "24px"}} />}, // i18next.t("theme:Compact")
];

View File

@@ -30,7 +30,7 @@ export const THEMES = {
};
const themeTypes = {
default: "Default", // i18next.t("theme:Default")
default: "Default", // i18next.t("general:Default")
dark: "Dark", // i18next.t("theme:Dark")
lark: "Document", // i18next.t("theme:Document")
comic: "Blossom", // i18next.t("theme:Blossom")

View File

@@ -1,5 +1,6 @@
{
"account": {
"Exit impersonation": "Impersonation beenden",
"Logout": "Abmeldung",
"My Account": "Mein Konto",
"Sign Up": "Anmelden"
@@ -20,18 +21,23 @@
"Add Face ID": "Face ID hinzufügen",
"Add Face ID with Image": "Face ID mit Bild hinzufügen",
"Always": "Immer",
"Array": "Array",
"Authentication": "Authentifizierung",
"Auto signin": "Automatische Anmeldung",
"Auto signin - Tooltip": "Wenn eine angemeldete Session in Casdoor vorhanden ist, wird diese automatisch für die Anmeldung auf Anwendungsebene verwendet",
"Background URL": "Background-URL",
"Background URL - Tooltip": "URL des Hintergrundbildes, das auf der Anmeldeseite angezeigt wird",
"Background URL Mobile": "Hintergrund-URL mobil",
"Background URL Mobile - Tooltip": "URL des Hintergrundbildes für mobile Geräte",
"Basic": "Basis",
"Big icon": "Großes Symbol",
"Binding providers": "Bindungsanbieter",
"CSS style": "CSS-Stil",
"Center": "Zentrum",
"Code resend timeout": "Code-Neusendungs-Timeout",
"Code resend timeout - Tooltip": "Zeitraum (in Sekunden), den Benutzer warten müssen, bevor sie einen weiteren Verifizierungscode anfordern können. Auf 0 setzen, um die globale Standardeinstellung (60 Sekunden) zu verwenden",
"Cookie expire": "Cookie-Ablauf",
"Cookie expire - Tooltip": "Cookie-Ablauf - Hinweis",
"Copy SAML metadata URL": "SAML-Metadaten-URL kopieren",
"Copy prompt page URL": "URL der Prompt-Seite kopieren",
"Copy signin page URL": "Kopieren Sie die URL der Anmeldeseite",
@@ -42,6 +48,8 @@
"Custom CSS Mobile": "Benutzerdefiniertes CSS mobil",
"Custom CSS Mobile - Edit": "Benutzerdefiniertes CSS mobil Bearbeiten",
"Custom CSS Mobile - Tooltip": "CSS-Styling für mobile Geräte",
"Disable SAML attributes": "SAML-Attribute deaktivieren",
"Disable SAML attributes - Tooltip": "SAML-Attribute deaktivieren - Hinweis",
"Disable signin": "Anmeldung deaktivieren",
"Disable signin - Tooltip": "Anmeldung für Benutzer deaktivieren",
"Dynamic": "Dynamisch",
@@ -52,10 +60,12 @@
"Enable SAML C14N10 - Tooltip": "C14N10 anstelle von C14N11 in SAML verwenden",
"Enable SAML POST binding": "SAML POST-Binding aktivieren",
"Enable SAML POST binding - Tooltip": "Das HTTP-POST-Binding verwendet HTML-Formular-Eingabefelder, um SAML-Nachrichten zu senden. Aktivieren Sie diese Option, wenn Ihr SP dies verwendet.",
"Enable SAML assertion signature": "SAML-Assertion-Signatur aktivieren",
"Enable SAML assertion signature - Tooltip": "SAML-Assertion-Signatur aktivieren - Hinweis",
"Enable SAML compression": "Aktivieren Sie SAML-Komprimierung",
"Enable SAML compression - Tooltip": "Ob SAML-Antwortnachrichten komprimiert werden sollen, wenn Casdoor als SAML-IdP verwendet wird",
"Enable exclusive signin": "Enable exclusive signin",
"Enable exclusive signin - Tooltip": "When exclusive signin enabled, user cannot have multiple active session",
"Enable exclusive signin": "Exklusive Anmeldung aktivieren",
"Enable exclusive signin - Tooltip": "Wenn die exklusive Anmeldung aktiviert ist, kann der Benutzer nicht mehrere aktive Sitzungen haben",
"Enable side panel": "Sidepanel aktivieren",
"Enable signin session - Tooltip": "Ob Casdoor eine Sitzung aufrechterhält, nachdem man sich von der Anwendung aus bei Casdoor angemeldet hat",
"Enable signup": "Registrierung aktivieren",
@@ -74,12 +84,12 @@
"Forced redirect origin": "Erzwungene Weiterleitung Ursprung",
"Form position": "Formposition",
"Form position - Tooltip": "Position der Anmelde-, Registrierungs- und Passwort-vergessen-Formulare",
"Generate Face ID": "Face ID generieren",
"Grant types": "Grant-Typen",
"Grant types - Tooltip": "Wählen Sie aus, welche Grant-Typen im OAuth-Protokoll zulässig sind",
"Header HTML": "Header-HTML",
"Header HTML - Edit": "Header-HTML Bearbeiten",
"Header HTML - Tooltip": "Passen Sie den head-Tag Ihrer Anwendungsstartseite an",
"Horizontal": "Horizontal",
"Incremental": "Inkrementell",
"Inline": "Inline",
"Input": "Eingabe",
@@ -91,6 +101,8 @@
"Logged out successfully": "Erfolgreich ausgeloggt",
"MFA remember time": "MFA-Merkezeit",
"MFA remember time - Tooltip": "Konfiguriert die Dauer, für die ein Konto nach einer erfolgreichen MFA-Anmeldung als vertrauenswürdig gespeichert wird",
"Menu mode": "Menümodus",
"Menu mode - Tooltip": "Menümodus - Hinweis",
"Multiple Choices": "Mehrfachauswahl",
"New Application": "Neue Anwendung",
"No verification": "Keine Verifizierung",
@@ -99,12 +111,13 @@
"Order": "Reihenfolge",
"Order - Tooltip": "Je kleiner der Wert, desto höher rangiert er auf der Apps-Seite",
"Org choice mode": "Organisationsauswahlmodus",
"Org choice mode - Tooltip": "Organisationsauswahlmodus",
"Org choice mode - Tooltip": "Organisationsauswahlmodus - Hinweis",
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "Bitte aktivieren Sie zuerst \"Anmeldesitzung\", bevor Sie \"Automatische Anmeldung\" aktivieren.",
"Please input your application!": "Bitte geben Sie Ihre Anwendung ein!",
"Please input your organization!": "Bitte geben Sie Ihre Organisation ein!",
"Please select a HTML file": "Bitte wählen Sie eine HTML-Datei aus",
"Pop up": "Pop-up",
"Providers": "Anbieter",
"Random": "Zufällig",
"Real name": "Echter Name",
"Redirect URL": "Weiterleitungs-URL",
@@ -119,24 +132,26 @@
"SAML hash algorithm": "SAML-Hash-Algorithmus",
"SAML hash algorithm - Tooltip": "Hash-Algorithmus für SAML-Signatur",
"SAML metadata": "SAML-Metadaten",
"SAML metadata - Tooltip": "Die Metadaten des SAML-Protokolls",
"SAML metadata - Tooltip": "Die Metadaten des SAML-Protokolls - Hinweis",
"SAML reply URL": "SAML Reply-URL",
"Security": "Sicherheit",
"Select": "Auswählen",
"Side panel HTML": "Sidepanel-HTML",
"Side panel HTML - Edit": "Sidepanel HTML - Bearbeiten",
"Side panel HTML - Tooltip": "Passen Sie den HTML-Code für das Sidepanel der Login-Seite an",
"Side panel HTML - Tooltip": "Den HTML-Code für die Seitenleiste der Anmeldeseite anpassen - Hinweis",
"Sign Up Error": "Registrierungsfehler",
"Signin": "Anmelden",
"Signin (Default True)": "Anmelden (Standard: Wahr)",
"Signin items": "Anmeldeelemente",
"Signin items - Tooltip": "Anmeldeelemente",
"Signin methods": "Anmeldemethoden",
"Signin methods - Tooltip": "Hinzufügen der zulässigen Anmeldemethoden für Benutzer, standardmäßig sind alle Methoden verfügbar",
"Signin methods - Tooltip": "Erlaubte Anmeldemethoden für Benutzer hinzufügen. Standardmäßig sind alle Methoden verfügbar - Hinweis",
"Signin session": "Anmeldesession",
"Signup items": "Registrierungs Items",
"Signup items - Tooltip": "Items, die Benutzer ausfüllen müssen, wenn sie neue Konten registrieren",
"Signup items - Tooltip": "Elemente für Benutzer, die beim Registrieren neuer Konten ausgefüllt werden müssen - Hinweis",
"Single Choice": "Einfachauswahl",
"Small icon": "Kleines Symbol",
"String": "String",
"Tags - Tooltip": "Nur Benutzer mit einem Tag, das in den Anwendungstags aufgeführt ist, können sich anmelden",
"The application does not allow to sign up new account": "Die Anwendung erlaubt es nicht, ein neues Konto zu registrieren",
"Token expire": "Token läuft ab",
@@ -147,8 +162,10 @@
"Token format - Tooltip": "Das Format des Access-Tokens",
"Token signing method": "Token-Signaturmethode",
"Token signing method - Tooltip": "Signaturmethode des JWT-Tokens muss mit dem Zertifikat übereinstimmen",
"UI Customization": "UI-Anpassung",
"Use Email as NameID": "E-Mail als NameID verwenden",
"Use Email as NameID - Tooltip": "E-Mail als NameID verwenden",
"Vertical": "Vertikal",
"You are unexpected to see this prompt page": "Sie sind unerwartet auf diese Aufforderungsseite gelangt"
},
"cert": {
@@ -233,7 +250,7 @@
"Width": "Breite"
},
"general": {
"A normal user can only modify the permission submitted by itself": "A normal user can only modify the permission submitted by itself",
"A normal user can only modify the permission submitted by itself": "Ein normaler Benutzer kann nur die von ihm selbst eingereichte Berechtigung ändern",
"AI Assistant": "KI-Assistent",
"API key": "API-Schlüssel",
"API key - Tooltip": "API-Schlüssel für den Zugriff auf den Dienst",
@@ -265,13 +282,12 @@
"Business & Payments": "Geschäft & Zahlungen",
"Cancel": "Abbrechen",
"Captcha": "Captcha",
"Cart": "Warenkorb",
"Cert": "Zertifikat",
"Cert - Tooltip": "Das Public-Key-Zertifikat, das vom Client-SDK, das mit dieser Anwendung korrespondiert, verifiziert werden muss",
"Certs": "Zertifikate",
"Clear": "Leeren",
"Click to Upload": "Klicken Sie zum Hochladen",
"Click to cancel sorting": "Klicken Sie, um die Sortierung abzubrechen",
"Click to sort ascending": "Klicken Sie, um aufsteigend zu sortieren",
"Click to sort descending": "Klicken Sie, um absteigend zu sortieren",
"Client IP": "Client-IP",
"Close": "Schließen",
"Confirm": "Bestätigen",
@@ -291,11 +307,12 @@
"Delete": "Löschen",
"Description": "Beschreibung",
"Description - Tooltip": "Detaillierte Beschreibungsinformationen zur Referenz, Casdoor selbst wird es nicht verwenden",
"Detail": "详情",
"Detail": "Details",
"Disable": "Deaktivieren",
"Display name": "Anzeigename",
"Display name - Tooltip": "Ein benutzerfreundlicher, leicht lesbarer Name, der öffentlich in der Benutzeroberfläche angezeigt wird",
"Down": "Nach unten",
"Download template": "Vorlage herunterladen",
"Edit": "Bearbeiten",
"Email": "E-Mail",
"Email - Tooltip": "Gültige E-Mail-Adresse",
@@ -310,18 +327,21 @@
"Enabled successfully": "Erfolgreich aktiviert",
"Enforcers": "Enforcer",
"Failed to add": "Fehler beim hinzufügen",
"Failed to cancel": "Abbrechen fehlgeschlagen",
"Failed to connect to server": "Die Verbindung zum Server konnte nicht hergestellt werden",
"Failed to copy": "Kopieren fehlgeschlagen",
"Failed to delete": "Konnte nicht gelöscht werden",
"Failed to enable": "Aktivierung fehlgeschlagen",
"Failed to get": "Abruf fehlgeschlagen",
"Failed to log out": "Failed to log out",
"Failed to load": "Laden fehlgeschlagen",
"Failed to log out": "Abmelden fehlgeschlagen",
"Failed to remove": "Entfernen fehlgeschlagen",
"Failed to save": "Konnte nicht gespeichert werden",
"Failed to send": "Senden fehlgeschlagen",
"Failed to sync": "Synchronisation fehlgeschlagen",
"Failed to unlink": "Failed to unlink",
"Failed to update": "Failed to update",
"Failed to upload": "Failed to upload",
"Failed to unlink": "Verknüpfung aufheben fehlgeschlagen",
"Failed to update": "Aktualisieren fehlgeschlagen",
"Failed to upload": "Hochladen fehlgeschlagen",
"Failed to verify": "Verifizierung fehlgeschlagen",
"False": "Falsch",
"Favicon": "Favicon",
@@ -334,6 +354,7 @@
"Forget URL - Tooltip": "Benutzerdefinierte URL für die \"Passwort vergessen\" Seite. Wenn nicht festgelegt, wird die standardmäßige Casdoor \"Passwort vergessen\" Seite verwendet. Wenn sie festgelegt ist, wird der \"Passwort vergessen\" Link auf der Login-Seite zu dieser URL umgeleitet",
"Forms": "Formulare",
"Found some texts still not translated? Please help us translate at": "Haben Sie noch Texte gefunden, die nicht übersetzt wurden? Bitte helfen Sie uns beim Übersetzen",
"Generate": "Generieren",
"Go to enable": "Zum Aktivieren gehen",
"Go to writable demo site?": "Gehe zur beschreibbaren Demo-Website?",
"Groups": "Gruppen",
@@ -346,6 +367,7 @@
"IP whitelist": "IP-Whitelist",
"IP whitelist - Tooltip": "IP-Whitelist",
"Identity": "Identität",
"Impersonation": "Identitätswechsel",
"Invitations": "Einladungen",
"Is enabled": "Ist aktiviert",
"Is enabled - Tooltip": "Festlegen, ob es verwendet werden kann",
@@ -378,17 +400,21 @@
"Name": "Name",
"Name - Tooltip": "Eindeutige, auf Strings basierende ID",
"Name format": "Namensformat",
"No verification method": "No verification method",
"No products available": "Keine Produkte verfügbar",
"No sheets found in file": "Keine Tabellenblätter in der Datei gefunden",
"No verification method": "Keine Verifizierungsmethode",
"Non-LDAP": "Nicht-LDAP",
"None": "Keine",
"OAuth providers": "OAuth-Provider",
"OFF": "AUS",
"OK": "OK",
"ON": "EIN",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Only 1 MFA method can be required": "Es kann nur 1 MFA-Methode erforderlich sein",
"Or": "Oder",
"Orders": "Bestellungen",
"Organization": "Organisation",
"Organization - Tooltip": "Ähnlich wie bei Konzepten wie Mietern oder Benutzerpools gehört jeder Benutzer und jede Anwendung einer Organisation an",
"Organization is null": "Organization is null",
"Organization is null": "Organisation ist leer",
"Organizations": "Organisationen",
"Password": "Passwort",
"Password - Tooltip": "Stellen Sie sicher, dass das Passwort korrekt ist",
@@ -411,30 +437,30 @@
"Phone - Tooltip": "Telefonnummer",
"Phone only": "Nur Telefon",
"Phone or Email": "Telefon oder E-Mail",
"Place Order": "Bestellung aufgeben",
"Plain": "Klartext",
"Plan": "Plan",
"Plan - Tooltip": "Abonnementplan",
"Plans": "Pläne",
"Plans - Tooltip": "Pläne",
"Please complete the captcha correctly": "Bitte lösen Sie das Captcha korrekt",
"Please input your search": "Bitte geben Sie Ihre Suche ein",
"Please select at least 1 user first": "Please select at least 1 user first",
"Please select at least 1 user first": "Bitte wählen Sie zuerst mindestens 1 Benutzer aus",
"Preview": "Vorschau",
"Preview - Tooltip": "Vorschau der konfigurierten Effekte",
"Pricing": "Preisgestaltung",
"Pricing - Tooltip": "Preisgestaltung",
"Pricings": "Preise",
"Product Store": "Produktshop",
"Products": "Produkte",
"Provider": "Anbieter",
"Provider - Tooltip": "Zahlungsprovider, die konfiguriert werden müssen, inkl. PayPal, Alipay, WeChat Pay usw.",
"Providers": "Provider",
"Providers - Tooltip": "Provider, die konfiguriert werden müssen, einschließlich Drittanbieter-Logins, Objektspeicherung, Verifizierungscode usw.",
"QR Code": "QR-Code",
"QR code is too large": "QR-Code ist zu groß",
"Real name": "Echter Name",
"Records": "Datensätze",
"Request": "Anfrage",
"Request URI": "Anfrage-URI",
"Reset": "Zurücksetzen",
"Reset to Default": "Auf Standard zurücksetzen",
"Resources": "Ressourcen",
"Role": "Rolle",
@@ -467,10 +493,13 @@
"Sorry, you do not have permission to access this page or logged in status invalid.": "Es tut uns leid, aber Sie haben keine Berechtigung, auf diese Seite zuzugreifen, oder Sie sind nicht angemeldet.",
"State": "Bundesland / Staat",
"State - Tooltip": "Bundesland",
"Status": "Status",
"Subscriptions": "Abonnements",
"Successfully added": "Erfolgreich hinzugefügt",
"Successfully canceled": "Erfolgreich abgebrochen",
"Successfully copied": "Erfolgreich kopiert",
"Successfully deleted": "Erfolgreich gelöscht",
"Successfully executed": "Erfolgreich ausgeführt",
"Successfully removed": "Erfolgreich entfernt",
"Successfully saved": "Erfolgreich gespeichert",
"Successfully sent": "Erfolgreich gesendet",
@@ -485,11 +514,12 @@
"Syncers": "Syncer",
"System Info": "Systeminformationen",
"Tab": "Tab",
"The actions cannot be empty": "The actions cannot be empty",
"The resources cannot be empty": "The resources cannot be empty",
"The users and roles cannot be empty at the same time": "The users and roles cannot be empty at the same time",
"The actions cannot be empty": "Die Aktionen dürfen nicht leer sein",
"The resources cannot be empty": "Die Ressourcen dürfen nicht leer sein",
"The users and roles cannot be empty at the same time": "Benutzer und Rollen dürfen nicht gleichzeitig leer sein",
"There was a problem signing you in..": "Es gab ein Problem beim Anmelden...",
"This is a read-only demo site!": "Dies ist eine schreibgeschützte Demo-Seite!",
"Tickets": "Tickets",
"Timestamp": "Zeitstempel",
"Title": "Titel",
"Title - Tooltip": "Titel der Browserseite",
@@ -500,17 +530,17 @@
"Transactions": "Transaktionen",
"True": "Wahr",
"Type": "Typ",
"Type - Tooltip": "Typ",
"URL": "URL",
"URL - Tooltip": "URL-Link",
"Unknown application name": "Unknown application name",
"Unknown authentication type": "Unknown authentication type",
"Unknown application name": "Unbekannter Anwendungsname",
"Unknown authentication type": "Unbekannter Authentifizierungstyp",
"Up": "Oben",
"Updated time": "Aktualisierungszeit",
"Upload (.xlsx)": "Upload (.xlsx)",
"User": "Nutzer",
"User - Tooltip": "Stellen Sie sicher, dass der Benutzername korrekt ist",
"User Management": "Benutzerverwaltung",
"User already exists": "User already exists",
"User already exists": "Benutzer existiert bereits",
"User containers": "Nutzerpools",
"User type": "Benutzertyp",
"User type - Tooltip": "Tags, denen der Benutzer angehört, standardmäßig auf \"normaler Benutzer\" festgelegt",
@@ -518,9 +548,10 @@
"Users - Tooltip": "Benutzer",
"Users under all organizations": "Benutzer unter allen Organisationen",
"Verifications": "Verifizierungen",
"View": "Anzeigen",
"Webhooks": "Webhooks",
"You can only select one physical group": "Sie können nur eine physische Gruppe auswählen",
"You must select a picture first": "You must select a picture first",
"You must select a picture first": "Sie müssen zuerst ein Bild auswählen",
"empty": "leere",
"remove": "entfernen",
"{total} in total": "Insgesamt {total}"
@@ -532,9 +563,8 @@
"Parent group - Tooltip": "Übergeordnete Gruppe dieser Gruppe",
"Physical": "Physisch",
"Show all": "Alle anzeigen",
"Upload (.xlsx)": "Hochladen (.xlsx)",
"Virtual": "Virtuell",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Sie müssen zuerst alle Untergruppen löschen. Sie können die Untergruppen im linken Gruppenbaum unter [Organisationen] -> [Gruppen] anzeigen."
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Sie müssen zuerst alle Untergruppen löschen. Sie können die Untergruppen im linken Gruppenbaum unter [Organisationen] -\u003e [Gruppen] anzeigen."
},
"home": {
"New users past 30 days": "Neue Benutzer der letzten 30 Tage",
@@ -557,7 +587,6 @@
"You need to first specify a default application for organization: ": "Sie müssen zuerst eine Standardanwendung für die Organisation angeben: "
},
"ldap": {
"Admin": "Administrator",
"Admin - Tooltip": "CN oder ID des LDAP-Serveradministrators",
"Admin Password": "Administratoren-Passwort",
"Admin Password - Tooltip": "LDAP-Server-Administratorpasswort",
@@ -597,13 +626,12 @@
"login": {
"Auto sign in": "Automatische Anmeldung",
"Back button": "Zurück-Button",
"Click the button below to sign in with Telegram": "Klicken Sie auf die Schaltfläche unten, um sich mit Telegram anzumelden",
"Continue with": "Weitermachen mit",
"Email": "E-Mail",
"Email or phone": "E-Mail oder Telefon",
"Face ID": "Face ID",
"Face Recognition": "Gesichtserkennung",
"Face recognition failed": "Gesichtserkennung fehlgeschlagen",
"Failed to log out": "Abmeldung fehlgeschlagen",
"Failed to obtain MetaMask authorization": "MetaMask-Autorisierung fehlgeschlagen",
"Failed to obtain Web3-Onboard authorization": "Web3-Onboard-Autorisierung fehlgeschlagen",
"Forgot password?": "Passwort vergessen?",
@@ -615,20 +643,18 @@
"Model loading failure": "Modell-Ladefehler",
"No account?": "Kein Konto?",
"Or sign in with another account": "Oder mit einem anderen Konto anmelden",
"Phone": "Telefon",
"Please ensure sufficient lighting and align your face in the center of the recognition box": "Stellen Sie sicher, dass ausreichend Licht vorhanden ist und Ihr Gesicht in der Mitte des Erkennungsfeldes positioniert ist.",
"Please ensure that you have a camera device for facial recognition": "Stellen Sie sicher, dass Sie eine Kamera für die Gesichtserkennung haben.",
"Please input your Email or Phone!": "Bitte geben Sie Ihre E-Mail oder Telefonnummer ein!",
"Please input your Email!": "Bitte geben Sie Ihre E-Mail ein!",
"Please input your LDAP username!": "Bitte geben Sie Ihren LDAP-Benutzernamen ein!",
"Please input your Phone!": "Bitte geben Sie Ihre Telefonnummer ein!",
"Please input your RADIUS password!": "Please input your RADIUS password!",
"Please input your RADIUS username!": "Please input your RADIUS username!",
"Please input your RADIUS password!": "Bitte geben Sie Ihr RADIUS-Passwort ein!",
"Please input your RADIUS username!": "Bitte geben Sie Ihren RADIUS-Benutzernamen ein!",
"Please input your code!": "Bitte geben Sie Ihren Code ein!",
"Please input your organization name!": "Bitte geben Sie Ihren Organisationsnamen ein!",
"Please input your password!": "Bitte geben Sie Ihr Passwort ein!",
"Please input your push notification receiver!": "Please input your push notification receiver!",
"Please input your verification code!": "Please input your verification code!",
"Please input your push notification receiver!": "Bitte geben Sie den Empfänger für Push-Benachrichtigungen ein!",
"Please load the webpage using HTTPS, otherwise the camera cannot be accessed": "Bitte laden Sie die Webseite über HTTPS, sonst kann auf die Kamera nicht zugegriffen werden.",
"Please provide permission to access the camera": "Bitte erteilen Sie die Kamerazugriffsberechtigung.",
"Please select an organization": "Bitte wählen Sie eine Organisation aus.",
@@ -639,6 +665,7 @@
"Select organization": "Organisation auswählen",
"Sign In": "Anmelden",
"Sign in with Face ID": "Mit Face ID anmelden",
"Sign in with Telegram": "Mit Telegram anmelden",
"Sign in with WebAuthn": "Melden Sie sich mit WebAuthn an",
"Sign in with {type}": "Melden Sie sich mit {type} an",
"Signin button": "Anmelde-Button",
@@ -671,7 +698,7 @@
"Please confirm the information below": "Bitte bestätigen Sie die folgenden Informationen",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Bitte speichern Sie diesen Wiederherstellungscode. Falls Ihr Gerät keinen Code liefern kann, können Sie MFA mit diesem Code zurücksetzen.",
"Protect your account with Multi-factor authentication": "Schützen Sie Ihr Konto mit MFA",
"Push notification receiver": "Push notification receiver",
"Push notification receiver": "Empfänger für Push-Benachrichtigungen",
"Recovery code": "Wiederherstellungscode",
"Remember this account for {hour} hours": "Dieses Konto für {hour} Stunden merken",
"Scan the QR code with your Authenticator App": "Scannen Sie den QR-Code mit Ihrer Authenticator-App",
@@ -681,18 +708,17 @@
"To ensure the security of your account, it is required to enable multi-factor authentication": "Zur Sicherheit Ihres Kontos ist MFA erforderlich.",
"Use Authenticator App": "Authenticator-App verwenden",
"Use Email": "E-Mail verwenden",
"Use Push Notification": "Use Push Notification",
"Use Radius": "Use Radius",
"Use Push Notification": "Push-Benachrichtigung verwenden",
"Use Radius": "RADIUS verwenden",
"Use SMS": "SMS verwenden",
"Use SMS verification code": "SMS-Verifizierungscode verwenden",
"Use a recovery code": "Wiederherstellungscode verwenden",
"Verification code": "Verification code",
"Verify Code": "Code verifizieren",
"Verify Password": "Passwort verifizieren",
"You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "Sie haben MFA aktiviert. Klicken Sie auf „Code senden“, um fortzufahren.",
"You have enabled Multi-Factor Authentication, please enter the RADIUS password": "You have enabled Multi-Factor Authentication, please enter the RADIUS password",
"You have enabled Multi-Factor Authentication, please enter the RADIUS password": "Sie haben MFA aktiviert. Bitte geben Sie das RADIUS-Passwort ein.",
"You have enabled Multi-Factor Authentication, please enter the TOTP code": "Sie haben MFA aktiviert. Bitte geben Sie den TOTP-Code ein.",
"You have enabled Multi-Factor Authentication, please enter the verification code from push notification": "You have enabled Multi-Factor Authentication, please enter the verification code from push notification",
"You have enabled Multi-Factor Authentication, please enter the verification code from push notification": "Sie haben MFA aktiviert. Bitte geben Sie den Verifizierungscode aus der Push-Benachrichtigung ein",
"Your email is": "Ihre E-Mail ist",
"Your phone is": "Ihr Telefon ist",
"preferred": "bevorzugt"
@@ -705,10 +731,30 @@
"Model text - Tooltip": "Casbin Zugriffskontrollmodell inklusive integrierter Modelle wie ACL, RBAC, ABAC, RESTful, usw. Sie können auch benutzerdefinierte Modelle erstellen. Weitere Informationen finden Sie auf der Casbin-Website",
"New Model": "Neues Modell"
},
"order": {
"Cancel time": "Stornierungszeit",
"Edit Order": "Bestellung bearbeiten",
"New Order": "Neue Bestellung",
"Order not found": "Bestellung nicht gefunden",
"Pay": "Bezahlen",
"Payment failed time": "Zeitpunkt des Zahlungsfehlers",
"Payment time": "Zahlungszeit",
"Price": "Preis",
"Return to Order List": "Zur Bestellliste zurückkehren",
"Timeout time": "Zeitpunkt des Timeouts",
"View Order": "Bestellung anzeigen"
},
"organization": {
"Account items": "Konto Items",
"Account items - Tooltip": "Elemente auf der persönlichen Einstellungsseite",
"All": "Alle",
"Account menu": "Kontomenü",
"Account menu - Tooltip": "Kontomenü - Tooltip",
"Admin navbar items": "Admin-Navigationsleisten-Elemente",
"Admin navbar items - Tooltip": "Admin-Navigationsleisten-Elemente - Tooltip",
"Balance credit": "Guthaben (Credits)",
"Balance credit - Tooltip": "Guthaben (Credits) - Tooltip",
"Balance currency": "Guthabenwährung",
"Balance currency - Tooltip": "Guthabenwährung - Tooltip",
"Edit Organization": "Organisation bearbeiten",
"Follow global theme": "Folge dem globalen Theme",
"Has privilege consent": "Privilegienzustimmung vorhanden",
@@ -719,10 +765,10 @@
"Is profile public": "Ist das Profil öffentlich?",
"Is profile public - Tooltip": "Nach der Schließung können nur globale Administratoren oder Benutzer in der gleichen Organisation auf die Profilseite des Benutzers zugreifen",
"Modify rule": "Regel ändern",
"Navbar items": "Navbar-Elemente",
"Navbar items - Tooltip": "In der Navigationsleiste angezeigte Elemente",
"New Organization": "Neue Organisation",
"Optional": "Optional",
"Org balance": "Organisationsguthaben",
"Org balance - Tooltip": "Organisationsguthaben - Tooltip",
"Password expire days": "Passwort läuft ab in Tagen",
"Password expire days - Tooltip": "Anzahl der Tage vor dem Ablauf des Passworts",
"Prompt": "Aufforderung",
@@ -730,9 +776,12 @@
"Soft deletion": "Softe Löschung",
"Soft deletion - Tooltip": "Wenn aktiviert, werden gelöschte Benutzer nicht vollständig aus der Datenbank entfernt. Stattdessen werden sie als gelöscht markiert",
"Tags": "Tags",
"Tags - Tooltip": "Sammlung von Tags, die für Benutzer zur Auswahl zur Verfügung stehen",
"Use Email as username": "E-Mail als Benutzername verwenden",
"Use Email as username - Tooltip": "E-Mail als Benutzername verwenden, wenn das Feld „Benutzername“ bei der Registrierung nicht sichtbar ist.",
"User balance": "Benutzerguthaben",
"User balance - Tooltip": "Benutzerguthaben - Tooltip",
"User navbar items": "Benutzer-Navigationsleisten-Elemente",
"User navbar items - Tooltip": "Benutzer-Navigationsleisten-Elemente - Tooltip",
"User types": "Benutzertypen",
"User types - Tooltip": "Verfügbare Benutzertypen im System",
"View rule": "Ansichtsregel",
@@ -775,19 +824,17 @@
"Person phone": "Personen-Telefon",
"Person phone - Tooltip": "Die Telefonnummer des Zahlenden",
"Please carefully check your invoice information. Once the invoice is issued, it cannot be withdrawn or modified.": "Bitte prüfen Sie sorgfältig Ihre Rechnungsinformationen. Sobald die Rechnung ausgestellt wurde, kann sie nicht zurückgenommen oder geändert werden.",
"Please click the below button to return to the original website": "Bitte klicken Sie auf den unten stehenden Button, um zur ursprünglichen Website zurückzukehren",
"Please pay the order first!": "Bitte zahlen Sie zuerst die Bestellung!",
"Processing...": "In Bearbeitung...",
"Product": "Produkt",
"Product - Tooltip": "Produktname",
"Products - Tooltip": "Produkte - Tooltip",
"Recharged successfully": "Erfolgreich aufgeladen",
"Result": "Ergebnis",
"Return to Website": "Zurück zur Website",
"The payment has been canceled": "Die Zahlung wurde storniert",
"The payment has failed": "Die Zahlung ist fehlgeschlagen",
"The payment has time out": "Die Zahlung ist abgelaufen",
"The payment has timed out": "Die Zahlung ist abgelaufen",
"The payment is still under processing": "Die Zahlung wird immer noch bearbeitet",
"Type - Tooltip": "Zahlungsmethode, die beim Kauf des Produkts verwendet wurde",
"View Payment": "Zahlung anzeigen",
"You can view your order details or return to the order list": "Sie können Ihre Bestelldetails ansehen oder zur Bestellliste zurückkehren",
"You have successfully completed the payment": "Sie haben die Zahlung erfolgreich abgeschlossen",
"You have successfully recharged": "Sie haben erfolgreich aufgeladen",
"Your current balance is": "Ihr aktuelles Guthaben beträgt",
@@ -797,7 +844,6 @@
"permission": {
"Actions": "Aktionen",
"Actions - Tooltip": "Erlaubte Aktionen",
"Admin": "Administrator",
"Allow": "erlauben",
"Approve time": "Zeit der Genehmigung",
"Approve time - Tooltip": "Die Genehmigungszeit für diese Erlaubnis",
@@ -824,9 +870,10 @@
"New Plan": "Neuer Plan",
"Period": "Zeitraum",
"Period - Tooltip": "Zeitraum",
"Price": "Preis",
"Plan name": "Planname",
"Price - Tooltip": "Preis",
"Related product": "Zugehöriges Produkt",
"View Plan": "Plan anzeigen",
"per month": "pro Monat",
"per year": "pro Jahr"
},
@@ -836,39 +883,55 @@
"Free": "Kostenlos",
"Getting started": "Loslegen",
"New Pricing": "Neue Preisgestaltung",
"Pricing name": "Pricing-Name",
"Trial duration": "Testphase Dauer",
"Trial duration - Tooltip": "Dauer der Testphase",
"View Pricing": "Preisgestaltung anzeigen",
"days trial available!": "Tage Testphase verfügbar!",
"paid-user do not have active subscription or pending subscription, please select a plan to buy": "Bezahlte Benutzer haben keine aktive oder ausstehende Abonnements, bitte wählen Sie einen Plan zum Kauf aus."
},
"product": {
"Add to cart": "In den Warenkorb",
"AirWallex": "AirWallex",
"Alipay": "Alipay",
"Amount": "Betrag",
"Buy": "Kaufen",
"Buy Product": "Produkt kaufen",
"Detail": "Detail",
"Custom amount available": "Benutzerdefinierter Betrag verfügbar",
"Custom price should be greater than zero": "Benutzerdefinierter Preis muss größer als null sein",
"Detail - Tooltip": "Detail des Produkts",
"Disable custom amount": "Benutzerdefinierten Betrag deaktivieren",
"Disable custom amount - Tooltip": "Benutzerdefinierten Betrag deaktivieren - Tooltip",
"Dummy": "Dummy",
"Edit Product": "Produkt bearbeiten",
"Enter preset amounts": "Vorgegebene Beträge eingeben",
"Failed to create order": "Bestellung konnte nicht erstellt werden",
"Image": "Bild",
"Image - Tooltip": "Bild des Produkts",
"Information": "Information",
"Is recharge": "Ist Aufladung",
"Is recharge - Tooltip": "Ob das Produkt zum Aufladen des Guthabens dient",
"New Product": "Neues Produkt",
"Pay": "Zahlen",
"Order created successfully": "Bestellung erfolgreich erstellt",
"PayPal": "PayPal",
"Payment cancelled": "Zahlung storniert",
"Payment failed": "Zahlung fehlgeschlagen",
"Payment providers": "Zahlungsprovider",
"Payment providers - Tooltip": "Provider von Zahlungsdiensten",
"Placing order...": "Bestellung aufgeben...",
"Price": "Preis",
"Price - Tooltip": "Preis",
"Please add at least one recharge option when custom amount is disabled": "Bitte fügen Sie mindestens eine Aufladeoption hinzu, wenn der benutzerdefinierte Betrag deaktiviert ist",
"Please select a currency": "Bitte wählen Sie eine Währung",
"Please select at least one payment provider": "Bitte wählen Sie mindestens einen Zahlungsanbieter aus",
"Processing payment...": "Zahlung wird verarbeitet...",
"Product list cannot be empty": "Produktliste darf nicht leer sein",
"Quantity": "Menge",
"Quantity - Tooltip": "Menge des Produkts",
"Recharge options": "Aufladeoptionen",
"Recharge options - Tooltip": "Aufladeoptionen - Tooltip",
"Return URL": "Rückkeht-URL",
"Return URL - Tooltip": "URL für die Rückkehr nach einem erfolgreichen Kauf",
"SKU": "SKU",
"Select amount": "Betrag auswählen",
"Sold": "Verkauft",
"Sold - Tooltip": "Menge verkauft",
"Stripe": "Stripe",
@@ -876,13 +939,15 @@
"Success URL - Tooltip": "URL, zu der nach dem Kauf zurückgekehrt wird",
"Tag - Tooltip": "Tag des Produkts",
"Test buy page..": "Testkaufseite.",
"The currency of the product you are adding is different from the currency of the items in the cart": "Die Währung des Produkts, das Sie hinzufügen, unterscheidet sich von der Währung der Artikel im Warenkorb",
"There is no payment channel for this product.": "Es gibt keinen Zahlungskanal für dieses Produkt.",
"This product is currently not in sale.": "Dieses Produkt steht derzeit nicht zum Verkauf.",
"This product is currently not purchasable (No options available)": "Dieses Produkt ist derzeit nicht kaufbar (keine Optionen verfügbar)",
"Total Price": "Gesamtpreis",
"View Product": "Produkt anzeigen",
"WeChat Pay": "WeChat Pay"
},
"provider": {
"Access key": "Access-Key",
"Access key - Tooltip": "Anmeldedaten für die Authentifizierung, die Zugriff auf bestimmte Ressourcen gewähren",
"Agent ID": "Agenten-ID",
"Agent ID - Tooltip": "Eindeutige Kennung für den Agenten, verwendet zur Unterscheidung verschiedener Agenten-Entitäten",
"Api Key": "API-Schlüssel",
@@ -917,18 +982,14 @@
"Client ID - Tooltip": "Eindeutige Nummer zur Identifizierung der Client-Anwendung, verwendet vom Server zur Erkennung verschiedener Client-Instanzen",
"Client ID 2": "Client-ID 2",
"Client ID 2 - Tooltip": "Backup- oder sekundäre Client-Kennung, verwendet zur Unterscheidung von Clients in verschiedenen Szenarien",
"Client Secret": "Client Secret",
"Client secret": "Client-Secret",
"Client secret - Tooltip": "Verifizierungsschlüssel für Client-Server-Interaktion, gewährleistet Kommunikationssicherheit",
"Client secret 2": "Client-Geheimnis 2",
"Client secret 2 - Tooltip": "Backup-Client-Verifizierungsschlüssel, verwendet zur Verifizierung bei Ausfall des primären Schlüssels oder in speziellen Szenarien",
"Content": "Inhalt",
"Content - Tooltip": "Spezifische Informationen oder Daten in Nachrichten, Benachrichtigungen oder Dokumenten",
"Copy": "Kopieren",
"Corp ID": "Corp ID",
"Corp Secret": "Corp Secret",
"DB test": "DB test",
"DB test - Tooltip": "DB test - Tooltip",
"DB test": "DB-Test",
"DB test - Tooltip": "DB-Test - Tooltip",
"Disable SSL": "SSL deaktivieren",
"Disable SSL - Tooltip": "Ob die Deaktivierung des SSL-Protokolls bei der Kommunikation mit dem STMP-Server erfolgen soll",
"Domain": "Domäne",
@@ -940,8 +1001,10 @@
"Email regex - Tooltip": "Nur E-Mails, die diesem regulären Ausdruck entsprechen, können sich registrieren oder anmelden",
"Email title": "Email-Titel",
"Email title - Tooltip": "Betreff der E-Mail",
"Enable proxy": "Enable proxy",
"Enable proxy - Tooltip": "Enable socks5 Proxy when sending email or sms",
"Enable PKCE": "PKCE aktivieren",
"Enable PKCE - Tooltip": "Enable PKCE - Tooltip",
"Enable proxy": "Proxy aktivieren",
"Enable proxy - Tooltip": "SOCKS5-Proxy beim Senden von E-Mails oder SMS aktivieren",
"Endpoint": "Endpunkt",
"Endpoint (Intranet)": "Endpunkt (Intranet)",
"Endpoint - Tooltip": "URL des Dienstendpunkts",
@@ -968,13 +1031,14 @@
"Key ID - Tooltip": "Eindeutige Kennung für den Schlüssel",
"Key text": "Schlüsseltext",
"Key text - Tooltip": "Inhalt des Schlüsseltexts",
"LDAP port": "LDAP-Port",
"Metadata": "Metadaten",
"Metadata - Tooltip": "SAML-Metadaten",
"Metadata url": "Metadaten-URL",
"Metadata url - Tooltip": "SAML-Metadaten-URL",
"Method - Tooltip": "Anmeldeverfahren, QR-Code oder Silent-Login",
"Mobile": "Mobil",
"New Provider": "Neuer Anbieter",
"Normal": "Normal",
"Parameter": "Parameter",
"Parameter - Tooltip": "Konfigurationsparameter",
"Parse": "parsen",
@@ -989,19 +1053,18 @@
"Project Id": "Projekt-ID",
"Project Id - Tooltip": "Projektkennung für den Dienst",
"Prompted": "ausgelöst",
"Provider - Tooltip": "Provider-Konfiguration",
"Provider URL": "Anbieter-URL",
"Provider URL - Tooltip": "URL zur Konfiguration des Dienstanbieters, dieses Feld dient nur als Referenz und wird in Casdoor nicht verwendet",
"Provider test successful": "Provider-Test erfolgreich",
"Public key": "Öffentlicher Schlüssel",
"Public key - Tooltip": "Öffentlicher Schlüssel für Verschlüsselung",
"RADIUS Shared Secret - Tooltip": "Shared Secret of RADIUS",
"RADIUS Shared Secret - Tooltip": "Shared Secret von RADIUS",
"Region": "Region",
"Region - Tooltip": "Geografische Region des Dienstes",
"Region ID": "Regions-ID",
"Region ID - Tooltip": "Regions-ID für den Dienstleister",
"Region endpoint for Internet": "Regionsendpunkt für das Internet",
"Region endpoint for Intranet": "Regionales Endpunkt für Intranet",
"Required": "Benötigt",
"SAML 2.0 Endpoint (HTTP)": "SAML 2.0 Endpunkt (HTTP)",
"SMS Test": "SMS-Test",
"SMS Test - Tooltip": "Telefonnummer für den Versand von Test-SMS",
@@ -1014,7 +1077,6 @@
"Scene": "Szene",
"Scene - Tooltip": "Spezifisches Geschäftsszenario, in dem die Funktion oder Operation angewendet wird, verwendet zur Anpassung der logischen Verarbeitung für verschiedene Szenarien",
"Scope": "Umfang",
"Scope - Tooltip": "Definiert die Grenzen der abgedeckten Berechtigungen oder Operationen, verwendet zur Verwaltung des Zugriffs auf Ressourcen oder des Umfangs, in dem die Funktion wirksam ist",
"Secret access key": "Secret-Access-Key",
"Secret access key - Tooltip": "Privater Schlüssel, der mit dem Zugriffsschlüssel gepaart ist, verwendet zum Signieren sensibler Operationen zur Verbesserung der Zugriffssicherheit",
"Secret key": "Secret-Key",
@@ -1049,6 +1111,8 @@
"Sub type - Tooltip": "Weitere Unterkategorie unter dem Haupttyp, verwendet zur präziseren Unterscheidung von Objekten oder Funktionen",
"Subject": "Betreff",
"Subject - Tooltip": "E-Mail-Betreff",
"Subtype": "Untertyp",
"Subtype - Tooltip": "Untertyp - Tooltip",
"Syncer test": "Synchronisierer-Test",
"Syncer test - Tooltip": "Synchronisierer-Test",
"Team ID": "Team-ID",
@@ -1062,12 +1126,10 @@
"Test SMTP Connection": "Testen Sie die SMTP-Verbindung",
"Third-party": "Drittanbieter",
"This field is required": "Dieses Feld ist erforderlich",
"To address": "To address",
"To address - Tooltip": "Email address of \"To\"",
"To address": "An-Adresse",
"To address - Tooltip": "E-Mail-Adresse im Feld „An“",
"Token URL": "Token-URL",
"Token URL - Tooltip": "Benutzerdefinierte OAuth Token-URL",
"Type": "Typ",
"Type - Tooltip": "Kennung zur Unterscheidung der Kategorie von Objekten, Operationen oder Daten, erleichtert Kategorisierung und logische Verarbeitung",
"Use WeChat Media Platform in PC": "WeChat Media Platform am PC verwenden",
"Use WeChat Media Platform in PC - Tooltip": "Ob das Scannen des WeChat Media Platform QR-Codes zum Login erlaubt ist",
"Use WeChat Media Platform to login": "WeChat Media Platform zur Anmeldung verwenden",
@@ -1084,6 +1146,7 @@
"UserInfo URL - Tooltip": "Benutzerdefinierte OAuth UserInfo-URL",
"Wallets": "Brieftaschen",
"Wallets - Tooltip": "Unterstützte digitale Brieftaschen",
"Web": "Web",
"admin (Shared)": "admin (Gemeinsam)"
},
"record": {
@@ -1115,7 +1178,6 @@
"signup": {
"Accept": "Akzeptieren",
"Agreement": "Vereinbarung",
"Confirm": "Bestätigen",
"Decline": "Abnahme",
"Have account?": "Haben Sie ein Konto?",
"Label": "Beschriftung",
@@ -1126,7 +1188,6 @@
"Please click the below button to sign in": "Bitte klicken Sie auf den untenstehenden Button, um sich anzumelden",
"Please confirm your password!": "Bitte bestätige dein Passwort!",
"Please input the correct ID card number!": "Bitte geben Sie die korrekte Ausweisnummer ein!",
"Please input your Email!": "Bitte geben Sie Ihre E-Mail-Adresse ein!",
"Please input your ID card number!": "Bitte geben Sie Ihre Personalausweisnummer ein!",
"Please input your address!": "Bitte geben Sie Ihre Adresse ein!",
"Please input your affiliation!": "Bitte geben Sie Ihre Zugehörigkeit ein!",
@@ -1136,6 +1197,7 @@
"Please input your last name!": "Bitte geben Sie Ihren Nachnamen ein!",
"Please input your phone number!": "Bitte geben Sie Ihre Telefonnummer ein!",
"Please input your real name!": "Bitte geben Sie Ihren richtigen Namen ein!",
"Please input your {label}!": "Bitte geben Sie {label} ein!",
"Please select your country code!": "Bitte wählen Sie Ihren Ländercode aus!",
"Please select your country/region!": "Bitte wählen Sie Ihr Land/Ihre Region aus!",
"Regex": "Regex",
@@ -1148,10 +1210,9 @@
"Text 4": "Text 4",
"Text 5": "Text 5",
"The input Email doesn't match the signup item regex!": "Die eingegebene E-Mail entspricht nicht dem Regex des Registrierungselements!",
"The input doesn't match the signup item regex!": "The input doesn't match the signup item regex!",
"The input doesn't match the signup item regex!": "Die Eingabe entspricht nicht dem Regex des Registrierungselements!",
"The input is not invoice Tax ID!": "Die Eingabe ist keine Rechnungssteuer-ID!",
"The input is not invoice title!": "Der Eingabewert ist nicht die Rechnungsbezeichnung!",
"The input is not valid Email!": "Die Eingabe ist keine gültige E-Mail-Adresse!",
"The input is not valid Phone!": "Die Eingabe ist kein gültiges Telefon!",
"Username": "Benutzername",
"Username - Tooltip": "Benutzername",
@@ -1167,22 +1228,28 @@
"Error": "Fehler",
"Expired": "Abgelaufen",
"New Subscription": "Neues Abonnement",
"Pending": "Ausstehend",
"Period": "Zeitraum",
"Start time": "Startzeit",
"Start time - Tooltip": "Startzeit",
"Subscription plan": "Abonnementplan",
"Subscription pricing": "Abonnement-Preisgestaltung",
"Suspended": "Ausgesetzt",
"Upcoming": "Bevorstehend"
"Upcoming": "Bevorstehend",
"View Subscription": "Abonnement anzeigen"
},
"syncer": {
"API Token / Password": "API Token / Password",
"Admin Email": "Admin-E-Mail",
"Affiliation table": "Zuordnungstabelle",
"Affiliation table - Tooltip": "Datenbanktabellenname der Arbeitseinheit",
"Avatar base URL": "Avatar-Basis-URL",
"Avatar base URL - Tooltip": "URL-Präfix für die Avatar-Bilder",
"Bind DN": "Bind-DN",
"Casdoor column": "Casdoor-Spalte",
"Column name": "Spaltenname",
"Column type": "Spaltentyp",
"Connect successfully": "Verbindung erfolgreich",
"Corp ID": "Corp-ID",
"Corp secret": "Corp-Secret",
"Database": "Datenbank",
"Database - Tooltip": "Der ursprüngliche Datenbankname",
"Database type": "Datenbanktyp",
@@ -1196,12 +1263,15 @@
"Is read-only": "Nur lesbar",
"Is read-only - Tooltip": "Nur lesbar",
"New Syncer": "Neuer Syncer",
"Paste your Google Workspace service account JSON key here": "Fügen Sie hier Ihren Google Workspace Service-Account-JSON-Schlüssel ein",
"SCIM Server URL": "SCIM-Server-URL",
"SSH host": "SSH-Host",
"SSH password": "SSH-Passwort",
"SSH port": "SSH-Port",
"SSH user": "SSH-Benutzer",
"SSL mode": "SSL-Modus",
"SSL mode - Tooltip": "SSL-Modus",
"Service account key": "Service-Account-Schlüssel",
"Sync interval": "Synchronisierungsintervall",
"Sync interval - Tooltip": "Einheit in Sekunden",
"Table": "Tabelle",
@@ -1209,7 +1279,8 @@
"Table columns": "Tabellenspalten",
"Table columns - Tooltip": "Tabellenspalten, die an der Datensynchronisation beteiligt sind. Spalten, die nicht an der Synchronisation beteiligt sind, müssen nicht hinzugefügt werden",
"Test Connection": "Verbindung testen",
"Test DB Connection": "Test DB Connection"
"Test DB Connection": "DB-Verbindung testen",
"Username (optional)": "Benutzername (optional)"
},
"system": {
"API Latency": "API Latenz",
@@ -1233,13 +1304,24 @@
"Compact": "Kompakt",
"Customize theme": "Anpassen des Themes",
"Dark": "Dunkel",
"Default": "Standardeinstellungen",
"Document": "Dokument",
"Is compact": "Ist kompakt",
"Primary color": "Primärfarbe",
"Theme": "Thema",
"Theme - Tooltip": "Stiltheme der Anwendung"
},
"ticket": {
"Closed": "Geschlossen",
"Edit Ticket": "Ticket bearbeiten",
"In Progress": "In Bearbeitung",
"Messages": "Nachrichten",
"New Ticket": "Neues Ticket",
"Open": "Offen",
"Please enter a message": "Bitte geben Sie eine Nachricht ein",
"Press Ctrl+Enter to send": "Drücken Sie Strg+Enter zum Senden",
"Resolved": "Gelöst",
"Type your message here...": "Geben Sie hier Ihre Nachricht ein..."
},
"token": {
"Access token": "Access-Token",
"Access token - Tooltip": "Zugriffstoken für Authentifizierung",
@@ -1257,11 +1339,10 @@
"Token type - Tooltip": "Token-Typ"
},
"transaction": {
"Amount": "Betrag",
"Amount - Tooltip": "Der Betrag der gehandelten Produkte",
"Edit Transaction": "Transaktion bearbeiten",
"New Transaction": "Neue Transaktion",
"Tag - Tooltip": "Das Tag der Transaktion"
"Recharge": "Aufladen"
},
"user": {
"3rd-party logins": "Drittanbieter-Logins",
@@ -1270,16 +1351,6 @@
"Address - Tooltip": "Wohnadresse",
"Address line": "Adresszeile",
"Addresses": "Adressen",
"Addresses - Tooltip": "Mehrere Adressen des Benutzers",
"Tag": "Tag",
"Line 1": "Zeile 1",
"Line 2": "Zeile 2",
"City": "Stadt",
"State": "Bundesland",
"Zip code": "Postleitzahl",
"Home": "Zuhause",
"Work": "Arbeit",
"Other": "Andere",
"Affiliation": "Zugehörigkeit",
"Affiliation - Tooltip": "Arbeitgeber, wie Firmenname oder Organisationsname",
"Balance": "Guthaben",
@@ -1290,6 +1361,7 @@
"Birthday - Tooltip": "Geburtstag",
"Captcha Verify Failed": "Captcha-Überprüfung fehlgeschlagen",
"Captcha Verify Success": "Captcha-Verifizierung Erfolgreich",
"City": "Stadt",
"Country code": "Ländercode",
"Country code - Tooltip": "Telefon-Ländercode",
"Country/Region": "Land/Region",
@@ -1300,7 +1372,6 @@
"Email cannot be empty": "E-Mail darf nicht leer sein",
"Email/phone reset successfully": "E-Mail-/Telefon-Zurücksetzung erfolgreich durchgeführt",
"Empty input!": "Leere Eingabe!",
"Face ID": "Face ID",
"Face IDs": "Face IDs",
"Gender": "Geschlecht",
"Gender - Tooltip": "Geschlecht des Benutzers",
@@ -1315,28 +1386,34 @@
"ID card type": "Ausweistyp",
"ID card type - Tooltip": "Art des Ausweises",
"ID card with person": "Ausweis mit Person",
"ID verification": "Identitätsprüfung",
"ID verification - Tooltip": "Identitätsprüfung - Tooltip",
"Identity verification successful": "Identitätsprüfung erfolgreich",
"Identity verified": "Identität verifiziert",
"Input your email": "Geben Sie Ihre E-Mail-Adresse ein",
"Input your phone number": "Geben Sie Ihre Telefonnummer ein",
"Is admin": "Ist Admin",
"Is admin - Tooltip": "Ist ein Administrator der Organisation, zu der der Benutzer gehört",
"Is deleted": "ist gelöscht",
"Is deleted - Tooltip": "\"Gelöschte Benutzer behalten nur Datenbankdatensätze und können keine Operationen ausführen.\"",
"Is deleted - Tooltip": "Gelöschte Benutzer behalten nur Datenbankdatensätze und können keine Operationen ausführen - Hinweis",
"Is forbidden": "Ist verboten",
"Is forbidden - Tooltip": "Verbotene Benutzer können sich nicht mehr einloggen",
"Is online": "Ist online",
"Is verified": "Ist verifiziert",
"Karma": "Karma",
"Karma - Tooltip": "Punkte zur Messung der Vertrauensstufe des Benutzers, beeinflussen Berechtigungen oder den Umfang der Dienstnutzung",
"Keys": "Schlüssel",
"Language": "Sprache",
"Language - Tooltip": "Spracheinstellung für die Anzeige der Systemoberfläche oder des Inhalts",
"Last change password time": "Letzte Passwortänderung",
"Line 1": "Zeile 1",
"Line 2": "Zeile 2",
"Link": "Link",
"Location": "Ort",
"Location - Tooltip": "Stadt des Wohnsitzes",
"MFA accounts": "MFA-Konten",
"Managed accounts": "Verwaltete Konten",
"Modify password...": "Passwort ändern...",
"Multi-factor authentication": "Mehrfaktorauthentifizierung",
"Need update password": "Passwort-Update erforderlich",
"Need update password - Tooltip": "Benutzer nach dem Login zum Passwort-Update zwingen",
"New Email": "Neue E-Mail",
@@ -1344,14 +1421,19 @@
"New User": "Neuer Benutzer",
"New phone": "Neue Telefonnummer",
"Old Password": "Altes Passwort",
"Other": "Andere",
"Password set successfully": "Passwort erfolgreich festgelegt",
"Phone cannot be empty": "Telefonnummer kann nicht leer sein",
"Please enter your real name": "Bitte geben Sie Ihren echten Namen ein",
"Please fill in ID card information first": "Bitte füllen Sie zuerst die Ausweisinformationen aus",
"Please fill in your real name first": "Bitte geben Sie zuerst Ihren echten Namen ein",
"Please select avatar from resources": "Bitte wählen Sie einen Avatar aus den Ressourcen aus",
"Properties": "Eigenschaften",
"Properties - Tooltip": "Eigenschaften des Benutzers",
"Ranking": "Rang",
"Ranking - Tooltip": "Position des Benutzers in der Rangfolge basierend auf Punkten, Aktivitätsniveau und anderen Metriken",
"Re-enter New": "Neueingabe wiederholen",
"Real name - Tooltip": "Echter Name - Tooltip",
"Register source": "Registrierungsquelle",
"Register source - Tooltip": "Die Quelle, von der aus der Benutzer registriert wurde",
"Register type": "Registrierungstyp",
@@ -1365,18 +1447,14 @@
"Set new profile picture": "Neues Profilbild festlegen",
"Set password...": "Passwort festlegen...",
"Tag": "Markierung",
"Tag - Tooltip": "Tags des Benutzers",
"The password must contain at least one special character": "Das Passwort muss mindestens ein Sonderzeichen enthalten.",
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Das Passwort muss mindestens einen Großbuchstaben, einen Kleinbuchstaben und eine Ziffer enthalten.",
"The password must have at least 6 characters": "Das Passwort muss mindestens 6 Zeichen lang sein.",
"The password must have at least 8 characters": "Das Passwort muss mindestens 8 Zeichen lang sein.",
"The password must not contain any repeated characters": "Das Passwort darf keine wiederholten Zeichen enthalten.",
"This field value doesn't match the pattern rule": "Der Feldwert entspricht nicht dem Muster.",
"Title": "Titel",
"Title - Tooltip": "Titel der Position oder Funktion, die in der Organisation ausgeübt wird",
"Two passwords you typed do not match.": "Zwei von Ihnen eingegebene Passwörter stimmen nicht überein.",
"Unlink": "Link aufheben",
"Upload (.xlsx)": "Hochladen (.xlsx)",
"Upload ID card back picture": "Rückseite des Ausweises hochladen",
"Upload ID card front picture": "Vorderseite des Ausweises hochladen",
"Upload ID card with person picture": "Ausweis mit Person hochladen",
@@ -1384,8 +1462,12 @@
"User Profile": "Benutzerprofil",
"Values": "Werte",
"Verification code sent": "Bestätigungscode gesendet",
"Verified": "Verifiziert",
"Verify Identity": "Identität verifizieren",
"WebAuthn credentials": "WebAuthn-Anmeldeinformationen",
"Work": "Arbeit",
"You have changed the username, please save your change first before modifying the password": "Sie haben den Benutzernamen geändert. Bitte speichern Sie zuerst, bevor Sie das Passwort ändern.",
"Zip code": "Postleitzahl",
"input password": "Eingabe des Passworts"
},
"verification": {
@@ -1404,7 +1486,6 @@
"Headers - Tooltip": "HTTP-Header (Schlüssel-Wert-Paare)",
"Is user extended": "Wurde der Benutzer erweitert?",
"Is user extended - Tooltip": "Sollten die erweiterten Felder des Benutzers in das JSON inkludiert werden?",
"Method - Tooltip": "HTTP Methode",
"New Webhook": "Neue Webhook",
"Object fields": "Objektfelder",
"Object fields - Tooltip": "Anzeigbare Objektfelder",

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