forked from casdoor/casdoor
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80b4c0b1a7 | ||
|
|
eb5a422026 | ||
|
|
f7bd70e0a3 | ||
|
|
5e7dbe4b56 | ||
|
|
bd1fca2f32 | ||
|
|
3d4cc42f1f | ||
|
|
1836cab44d | ||
|
|
75b18635f7 | ||
|
|
47cd44c7ce | ||
|
|
090ca97dcd | ||
|
|
bed01b31f1 | ||
|
|
c8f8f88d85 | ||
|
|
7acb303995 | ||
|
|
2607f8d3e5 | ||
|
|
481db33e58 | ||
|
|
f556c7e11f | ||
|
|
f590992f28 | ||
|
|
80f9db0fa2 | ||
|
|
0748661d2a | ||
|
|
83552ed143 | ||
|
|
8cb8541f96 | ||
|
|
5b646a726c | ||
|
|
19b9586670 | ||
|
|
73f8d19c5f | ||
|
|
04da531df3 | ||
|
|
d97558051d | ||
|
|
ac55355290 |
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
2
go.mod
@@ -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
140
i18n/deduplicate_test.go
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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ó.",
|
||||
|
||||
@@ -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é.",
|
||||
|
||||
@@ -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.": "チューリングテストは失敗しました。",
|
||||
|
||||
@@ -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ę.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
327
object/syncer_awsiam.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -134,7 +134,7 @@ class AdapterListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("provider:Type"),
|
||||
title: i18next.t("general:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: "100px",
|
||||
|
||||
@@ -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}}>
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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%"}}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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"),
|
||||
]));
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")}
|
||||
{i18next.t("application:Providers")}
|
||||
<Button id="add-button" type="primary" size="small" onClick={this.addProvider.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"}} >
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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")},
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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"))} :
|
||||
|
||||
@@ -147,7 +147,7 @@ class SyncerListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("provider:Type"),
|
||||
title: i18next.t("general:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: "100px",
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,
|
||||
}))}
|
||||
/>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -73,7 +73,7 @@ class VerificationListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("provider:Type"),
|
||||
title: i18next.t("general:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: "90px",
|
||||
|
||||
@@ -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);})}>
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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={[
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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")},
|
||||
];
|
||||
|
||||
|
||||
@@ -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"},
|
||||
],
|
||||
|
||||
277
web/src/common/PaginateSelect.js
Normal file
277
web/src/common/PaginateSelect.js
Normal 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;
|
||||
@@ -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"},
|
||||
|
||||
@@ -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!"));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ function OrganizationSelect(props) {
|
||||
|
||||
if (withAll) {
|
||||
items.unshift({
|
||||
label: i18next.t("organization:All"),
|
||||
label: i18next.t("general:All"),
|
||||
value: "All",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
];
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user