Compare commits

...

33 Commits

Author SHA1 Message Date
IsAurora6
e4db367eaa feat: Remove BuyProduct endpoint and legacy purchase logic. (#4591) 2025-11-28 23:51:22 +08:00
IsAurora6
9df81e3ffc feat: feat: add OrderPayPage.js, fix subscription redirect & refine list time format. (#4586) 2025-11-27 20:49:49 +08:00
IsAurora6
048d6acc83 feat: Implement the complete process of product purchase, order placement, and payment. (#4588) 2025-11-27 20:49:34 +08:00
Yang Luo
e440199977 feat: regenerate the Swagger docs 2025-11-25 22:24:32 +08:00
IsAurora6
cb4e559d51 feat: Added PlaceOrder, CancelOrder, and PayOrder methods, and added corresponding buttons to the frontend. (#4583) 2025-11-25 22:22:46 +08:00
zjumathcode
4d1d0b95d6 feat: drop legacy // +build comment (#4582) 2025-11-25 20:21:09 +08:00
Yang Luo
9cc1133a96 feat: upgrade gomail to v2.2.0 2025-11-25 01:03:45 +08:00
Yang Luo
897c28e8ad fix: fix SQL query in Keycloak syncer (#4578) 2025-11-24 23:40:30 +08:00
Yang Luo
9d37a7e38e fix: fix memory leaks in database syncer from unclosed connections (#4574) 2025-11-24 23:38:50 +08:00
Yang Luo
ea597296b4 fix: allow normal users to view their own transactions (#4572) 2025-11-24 01:47:10 +08:00
Yang Luo
427ddd215e feat: add Telegram OAuth provider (#4570) 2025-11-24 01:04:36 +08:00
Yang Luo
24de79b100 Improve getTransactionTableColumns UI 2025-11-23 22:07:33 +08:00
DacongDA
9ab9c7c8e0 fix: show error better for user upload (#4568) 2025-11-23 21:52:44 +08:00
Yang Luo
0728a9716b feat: deduplicate code between TransactionTable and TransactionListPage (#4567) 2025-11-23 21:47:58 +08:00
Yang Luo
471570f24a Improve AddTransaction API return value 2025-11-23 21:02:06 +08:00
Yang Luo
2fa520844b fix: fix product store page to pass owner parameter to API (#4565) 2025-11-23 20:48:15 +08:00
Yang Luo
2306acb416 fix: improve balanceCredit for org and user 2025-11-23 19:51:39 +08:00
Yang Luo
d3f3f76290 fix: add dry run mode to add-transaction API (#4563) 2025-11-23 17:36:51 +08:00
DacongDA
fe93128495 feat: improve user upload UX (#4542) 2025-11-23 16:05:46 +08:00
seth-shi
7fd890ff14 fix: ticket error handling in HandleOfficialAccountEvent() (#4557) 2025-11-23 14:58:23 +08:00
Yang Luo
83b56d7ceb feat: add product store page (#4544) 2025-11-23 14:54:35 +08:00
Yang Luo
503e5a75d2 feat: add User.OriginalToken field to expose OAuth provider access tokens (#4559) 2025-11-23 14:54:02 +08:00
seth-shi
5a607b4991 fix: close file handle in GetUploadXlsxPath to prevent resource leak (#4558) 2025-11-23 14:37:06 +08:00
Yang Luo
ca2dc2825d feat: add SSO logout notifications to user's signup application (#4547) 2025-11-23 00:47:29 +08:00
Yang Luo
446d0b9047 Improve TransactionTable UI 2025-11-23 00:45:47 +08:00
Yang Luo
ee708dbf48 feat: add Organization.OrgBalanceCredit and User.BalanceCredit fields for credit limit enforcement (#4552) 2025-11-23 00:37:44 +08:00
Yang Luo
221ca28488 fix: flatten top navbar to single level when ≤7 items (#4550) 2025-11-23 00:34:17 +08:00
Yang Luo
e93d3f6c13 Improve transaction list page UI 2025-11-22 23:35:04 +08:00
Yang Luo
e285396d4e fix: fix recharge transaction default values (#4546) 2025-11-22 23:27:29 +08:00
Yang Luo
10320bb49f Improve TransactionTable UI 2025-11-22 21:39:56 +08:00
seth-shi
4d27ebd82a feat: Use email as username when organization setting is enabled during login (#4539) 2025-11-22 20:58:27 +08:00
Yang Luo
6d5e6dab0a Fix account table missing item 2025-11-22 20:56:45 +08:00
Yang Luo
e600ea7efd feat: add i18n support for table column widgets (#4541) 2025-11-22 16:39:44 +08:00
107 changed files with 4378 additions and 1296 deletions

View File

@@ -67,7 +67,6 @@ p, *, *, POST, /api/upload-users, *, *
p, *, *, GET, /api/get-resources, *, *
p, *, *, GET, /api/get-records, *, *
p, *, *, GET, /api/get-product, *, *
p, *, *, POST, /api/buy-product, *, *
p, *, *, GET, /api/get-payment, *, *
p, *, *, POST, /api/update-payment, *, *
p, *, *, POST, /api/invoice-payment, *, *
@@ -101,6 +100,7 @@ p, *, *, GET, /api/get-pricing, *, *
p, *, *, GET, /api/get-plan, *, *
p, *, *, GET, /api/get-subscription, *, *
p, *, *, GET, /api/get-transactions, *, *
p, *, *, GET, /api/get-transaction, *, *
p, *, *, GET, /api/get-provider, *, *
p, *, *, GET, /api/get-organization-names, *, *
p, *, *, GET, /api/get-all-objects, *, *

View File

@@ -484,6 +484,21 @@ func (c *ApiController) SsoLogout() {
return
}
// Send SSO logout notifications to all notification providers in the user's signup application
userObj, err := object.GetUser(user)
if err != nil {
c.ResponseError(err.Error())
return
}
if userObj != nil {
err = object.SendSsoLogoutNotifications(userObj)
if err != nil {
c.ResponseError(err.Error())
return
}
}
util.LogInfo(c.Ctx, "API: [%s] logged out from all applications", user)
c.ResponseOk()

View File

@@ -724,6 +724,7 @@ func (c *ApiController) Login() {
return
}
userInfo := &idp.UserInfo{}
var token *oauth2.Token
if provider.Category == "SAML" {
// SAML
userInfo, err = object.ParseSamlResponse(authForm.SamlResponse, provider, c.Ctx.Request.Host)
@@ -754,7 +755,6 @@ func (c *ApiController) Login() {
}
// https://github.com/golang/oauth2/issues/123#issuecomment-103715338
var token *oauth2.Token
token, err = idProvider.GetToken(authForm.Code)
if err != nil {
c.ResponseError(err.Error())
@@ -804,7 +804,7 @@ func (c *ApiController) Login() {
if user != nil && !user.IsDeleted {
// Sign in via OAuth (want to sign up but already have account)
// sync info from 3rd-party if possible
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, provider.UserMapping)
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, token, provider.UserMapping)
if err != nil {
c.ResponseError(err.Error())
return
@@ -867,6 +867,11 @@ func (c *ApiController) Login() {
return
}
// Handle UseEmailAsUsername for OAuth and Web3
if organization.UseEmailAsUsername && userInfo.Email != "" {
userInfo.Username = userInfo.Email
}
// Handle username conflicts
var tmpUser *object.User
tmpUser, err = object.GetUser(util.GetId(application.Organization, userInfo.Username))
@@ -949,7 +954,7 @@ func (c *ApiController) Login() {
}
// sync info from 3rd-party if possible
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, provider.UserMapping)
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, token, provider.UserMapping)
if err != nil {
c.ResponseError(err.Error())
return
@@ -997,7 +1002,7 @@ func (c *ApiController) Login() {
}
// sync info from 3rd-party if possible
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, provider.UserMapping)
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, token, provider.UserMapping)
if err != nil {
c.ResponseError(err.Error())
return
@@ -1213,7 +1218,7 @@ func (c *ApiController) HandleOfficialAccountEvent() {
return
}
if data.Ticket == "" {
c.ResponseError(err.Error())
c.ResponseError("empty ticket")
return
}
@@ -1228,10 +1233,6 @@ func (c *ApiController) HandleOfficialAccountEvent() {
return
}
if data.Ticket == "" {
c.ResponseError("empty ticket")
return
}
if !idp.VerifyWechatSignature(provider.Content, nonce, timestamp, signature) {
c.ResponseError("invalid signature")
return

169
controllers/order_pay.go Normal file
View File

@@ -0,0 +1,169 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"fmt"
"strconv"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// PlaceOrder
// @Title PlaceOrder
// @Tag Order API
// @Description place an order for a product
// @Param productId query string true "The id ( owner/name ) of the product"
// @Param pricingName query string false "The name of the pricing (for subscription)"
// @Param planName query string false "The name of the plan (for subscription)"
// @Param customPrice query number false "Custom price for recharge products"
// @Param userName query string false "The username to place order for (admin only)"
// @Success 200 {object} object.Order The Response object
// @router /place-order [post]
func (c *ApiController) PlaceOrder() {
productId := c.Input().Get("productId")
pricingName := c.Input().Get("pricingName")
planName := c.Input().Get("planName")
customPriceStr := c.Input().Get("customPrice")
paidUserName := c.Input().Get("userName")
if productId == "" {
c.ResponseError(c.T("general:ProductId is required"))
return
}
var customPrice float64
if customPriceStr != "" {
var err error
customPrice, err = strconv.ParseFloat(customPriceStr, 64)
if err != nil {
c.ResponseError(fmt.Sprintf(c.T("general:Invalid customPrice: %s"), customPriceStr))
return
}
}
owner, _, err := util.GetOwnerAndNameFromIdWithError(productId)
if err != nil {
c.ResponseError(err.Error())
return
}
var userId string
if paidUserName != "" {
userId = util.GetId(owner, paidUserName)
if userId != c.GetSessionUsername() && !c.IsAdmin() && userId != c.GetPaidUsername() {
c.ResponseError(c.T("general:Only admin user can specify user"))
return
}
c.SetSession("paidUsername", "")
} else {
userId = c.GetSessionUsername()
}
if userId == "" {
c.ResponseError(c.T("general:Please login first"))
return
}
user, err := object.GetUser(userId)
if err != nil {
c.ResponseError(err.Error())
return
}
if user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
return
}
order, err := object.PlaceOrder(productId, user, pricingName, planName, customPrice)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(order)
}
// PayOrder
// @Title PayOrder
// @Tag Order API
// @Description pay an existing order
// @Param id query string true "The id ( owner/name ) of the order"
// @Param providerName query string true "The name of the provider"
// @Success 200 {object} controllers.Response The Response object
// @router /pay-order [post]
func (c *ApiController) PayOrder() {
id := c.Input().Get("id")
host := c.Ctx.Request.Host
providerName := c.Input().Get("providerName")
paymentEnv := c.Input().Get("paymentEnv")
order, err := object.GetOrder(id)
if err != nil {
c.ResponseError(err.Error())
return
}
if order == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The order: %s does not exist"), id))
return
}
userId := c.GetSessionUsername()
orderUserId := util.GetId(order.Owner, order.User)
if userId != orderUserId && !c.IsAdmin() {
c.ResponseError(c.T("auth:Unauthorized operation"))
return
}
payment, attachInfo, err := object.PayOrder(providerName, host, paymentEnv, order)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(payment, attachInfo)
}
// CancelOrder
// @Title CancelOrder
// @Tag Order API
// @Description cancel an order
// @Param id query string true "The id ( owner/name ) of the order"
// @Success 200 {object} controllers.Response The Response object
// @router /cancel-order [post]
func (c *ApiController) CancelOrder() {
id := c.Input().Get("id")
order, err := object.GetOrder(id)
if err != nil {
c.ResponseError(err.Error())
return
}
if order == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The order: %s does not exist"), id))
return
}
userId := c.GetSessionUsername()
orderUserId := util.GetId(order.Owner, order.User)
if userId != orderUserId && !c.IsAdmin() {
c.ResponseError(c.T("auth:Unauthorized operation"))
return
}
c.Data["json"] = wrapActionResponse(object.CancelOrder(order))
c.ServeJSON()
}

View File

@@ -16,8 +16,6 @@ package controllers
import (
"encoding/json"
"fmt"
"strconv"
"github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
@@ -151,72 +149,3 @@ func (c *ApiController) DeleteProduct() {
c.Data["json"] = wrapActionResponse(object.DeleteProduct(&product))
c.ServeJSON()
}
// BuyProduct
// @Title BuyProduct
// @Tag Product API
// @Description buy product
// @Param id query string true "The id ( owner/name ) of the product"
// @Param providerName query string true "The name of the provider"
// @Success 200 {object} controllers.Response The Response object
// @router /buy-product [post]
func (c *ApiController) BuyProduct() {
id := c.Input().Get("id")
host := c.Ctx.Request.Host
providerName := c.Input().Get("providerName")
paymentEnv := c.Input().Get("paymentEnv")
customPriceStr := c.Input().Get("customPrice")
if customPriceStr == "" {
customPriceStr = "0"
}
customPrice, err := strconv.ParseFloat(customPriceStr, 64)
if err != nil {
c.ResponseError(err.Error())
return
}
// buy `pricingName/planName` for `paidUserName`
pricingName := c.Input().Get("pricingName")
planName := c.Input().Get("planName")
paidUserName := c.Input().Get("userName")
owner, _, err := util.GetOwnerAndNameFromIdWithError(id)
if err != nil {
c.ResponseError(err.Error())
return
}
var userId string
if paidUserName != "" {
userId = util.GetId(owner, paidUserName)
if userId != c.GetSessionUsername() && !c.IsAdmin() && userId != c.GetPaidUsername() {
c.ResponseError(c.T("general:Only admin user can specify user"))
return
}
c.SetSession("paidUsername", "")
} else {
userId = c.GetSessionUsername()
}
if userId == "" {
c.ResponseError(c.T("general:Please login first"))
return
}
user, err := object.GetUser(userId)
if err != nil {
c.ResponseError(err.Error())
return
}
if user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
return
}
payment, attachInfo, err := object.BuyProduct(id, user, providerName, pricingName, planName, host, paymentEnv, customPrice)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(payment, attachInfo)
}

View File

@@ -113,6 +113,27 @@ func (c *ApiController) GetTransaction() {
return
}
if transaction == nil {
c.ResponseOk(nil)
return
}
// Check if non-admin user is trying to access someone else's transaction
if !c.IsAdmin() {
user := c.GetSessionUsername()
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
if userErr != nil {
c.ResponseError(userErr.Error())
return
}
// Only allow users to view their own transactions
if transaction.User != userName {
c.ResponseError(c.T("auth:Unauthorized operation"))
return
}
}
c.ResponseOk(transaction)
}
@@ -143,6 +164,7 @@ func (c *ApiController) UpdateTransaction() {
// @Tag Transaction API
// @Description add transaction
// @Param body body object.Transaction true "The details of the transaction"
// @Param dryRun query string false "Dry run mode: set to 'true' or '1' to validate without committing"
// @Success 200 {object} controllers.Response The Response object
// @router /add-transaction [post]
func (c *ApiController) AddTransaction() {
@@ -153,7 +175,10 @@ func (c *ApiController) AddTransaction() {
return
}
affected, transactionId, err := object.AddTransaction(&transaction, c.GetAcceptLanguage())
dryRunParam := c.Input().Get("dryRun")
dryRun := dryRunParam != ""
affected, transactionId, err := object.AddTransaction(&transaction, c.GetAcceptLanguage(), dryRun)
if err != nil {
c.ResponseError(err.Error())
return

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package deployment

2
go.mod
View File

@@ -15,7 +15,7 @@ require (
github.com/beevik/etree v1.1.0
github.com/casbin/casbin/v2 v2.77.2
github.com/casdoor/go-sms-sender v0.25.0
github.com/casdoor/gomail/v2 v2.1.0
github.com/casdoor/gomail/v2 v2.2.0
github.com/casdoor/ldapserver v1.2.0
github.com/casdoor/notify v1.0.1
github.com/casdoor/oss v1.8.0

4
go.sum
View File

@@ -233,8 +233,8 @@ github.com/casdoor/go-reddit/v2 v2.1.0 h1:kIbfdJ7AA7H0uTQ8s0q4GGZqSS5V9wVE74RrXy
github.com/casdoor/go-reddit/v2 v2.1.0/go.mod h1:eagkvwlZ4Hcsuc/uQsLHYEulz5jN65SVSwV/AIE7zsc=
github.com/casdoor/go-sms-sender v0.25.0 h1:eF4cOCSbjVg7+0uLlJQnna/FQ0BWW+Fp/x4cXhzQu1Y=
github.com/casdoor/go-sms-sender v0.25.0/go.mod h1:bOm4H8/YfJmEHjBatEVQFOnAf0OOn1B0Wi5B7zDhws0=
github.com/casdoor/gomail/v2 v2.1.0 h1:ua97E3CARnF1Ik8ga/Drz9uGZfaElXJumFexiErWUxM=
github.com/casdoor/gomail/v2 v2.1.0/go.mod h1:GFzOD9RhY0nODiiPaQiOa6DfoKtmO9aTesu5qrp26OI=
github.com/casdoor/gomail/v2 v2.2.0 h1:gVMk43qvqq4XYkAJ+CDY5WWKF9yYRipuyXfp7P0HWIg=
github.com/casdoor/gomail/v2 v2.2.0/go.mod h1:GFzOD9RhY0nODiiPaQiOa6DfoKtmO9aTesu5qrp26OI=
github.com/casdoor/ldapserver v1.2.0 h1:HdSYe+ULU6z9K+2BqgTrJKQRR4//ERAXB64ttOun6Ow=
github.com/casdoor/ldapserver v1.2.0/go.mod h1:VwYU2vqQ2pA8sa00PRekH71R2XmgfzMKhmp1XrrDu2s=
github.com/casdoor/notify v1.0.1 h1:p0kzI7OBlvLbL7zWeKIu31LRcEAygNZGKr5gcFfSIoE=

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package i18n

View File

@@ -129,6 +129,8 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) (IdProvider, error
return NewWeb3OnboardIdProvider(), nil
case "Twitter":
return NewTwitterIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Telegram":
return NewTelegramIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
default:
if isGothSupport(idpInfo.Type) {
return NewGothIdProvider(idpInfo.Type, idpInfo.ClientId, idpInfo.ClientSecret, idpInfo.ClientId2, idpInfo.ClientSecret2, redirectUrl, idpInfo.HostUrl)

169
idp/telegram.go Normal file
View File

@@ -0,0 +1,169 @@
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package idp
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"golang.org/x/oauth2"
)
type TelegramIdProvider struct {
Client *http.Client
ClientId string
ClientSecret string
RedirectUrl string
}
func NewTelegramIdProvider(clientId string, clientSecret string, redirectUrl string) *TelegramIdProvider {
idp := &TelegramIdProvider{
ClientId: clientId,
ClientSecret: clientSecret,
RedirectUrl: redirectUrl,
}
return idp
}
func (idp *TelegramIdProvider) SetHttpClient(client *http.Client) {
idp.Client = client
}
// GetToken validates the Telegram auth data and returns a token
// Telegram uses a widget-based authentication, not standard OAuth2
// The "code" parameter contains the JSON-encoded auth data from Telegram
func (idp *TelegramIdProvider) GetToken(code string) (*oauth2.Token, error) {
// Decode the auth data from the code parameter
var authData map[string]interface{}
if err := json.Unmarshal([]byte(code), &authData); err != nil {
return nil, fmt.Errorf("failed to parse Telegram auth data: %v", err)
}
// Verify the data authenticity
if err := idp.verifyTelegramAuth(authData); err != nil {
return nil, fmt.Errorf("failed to verify Telegram auth data: %v", err)
}
// Create a token with the user ID as access token
userId, ok := authData["id"].(float64)
if !ok {
return nil, fmt.Errorf("invalid user id in auth data")
}
// Store the complete auth data in the token for later retrieval
authDataJson, err := json.Marshal(authData)
if err != nil {
return nil, fmt.Errorf("failed to marshal auth data: %v", err)
}
token := &oauth2.Token{
AccessToken: fmt.Sprintf("telegram_%d", int64(userId)),
TokenType: "Bearer",
}
// Store auth data in token extras to avoid additional API calls
token = token.WithExtra(map[string]interface{}{
"telegram_auth_data": string(authDataJson),
})
return token, nil
}
// verifyTelegramAuth verifies the authenticity of Telegram auth data
// According to Telegram docs: https://core.telegram.org/widgets/login#checking-authorization
func (idp *TelegramIdProvider) verifyTelegramAuth(authData map[string]interface{}) error {
// Extract hash from auth data
hash, ok := authData["hash"].(string)
if !ok {
return fmt.Errorf("hash not found in auth data")
}
// Prepare data check string
var dataCheckArr []string
for key, value := range authData {
if key == "hash" {
continue
}
dataCheckArr = append(dataCheckArr, fmt.Sprintf("%s=%v", key, value))
}
sort.Strings(dataCheckArr)
dataCheckString := strings.Join(dataCheckArr, "\n")
// Calculate secret key
secretKey := sha256.Sum256([]byte(idp.ClientSecret))
// Calculate hash
h := hmac.New(sha256.New, secretKey[:])
h.Write([]byte(dataCheckString))
calculatedHash := hex.EncodeToString(h.Sum(nil))
// Compare hashes
if calculatedHash != hash {
return fmt.Errorf("data verification failed")
}
return nil
}
func (idp *TelegramIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
// Extract auth data from token
authDataStr, ok := token.Extra("telegram_auth_data").(string)
if !ok {
return nil, fmt.Errorf("telegram auth data not found in token")
}
// Parse the auth data
var authData map[string]interface{}
if err := json.Unmarshal([]byte(authDataStr), &authData); err != nil {
return nil, fmt.Errorf("failed to parse auth data: %v", err)
}
// Extract user information from auth data
userId, ok := authData["id"].(float64)
if !ok {
return nil, fmt.Errorf("invalid user id in auth data")
}
firstName, _ := authData["first_name"].(string)
lastName, _ := authData["last_name"].(string)
username, _ := authData["username"].(string)
photoUrl, _ := authData["photo_url"].(string)
// Build display name with fallback
displayName := strings.TrimSpace(firstName + " " + lastName)
if displayName == "" {
displayName = username
}
if displayName == "" {
displayName = strconv.FormatInt(int64(userId), 10)
}
userInfo := UserInfo{
Id: strconv.FormatInt(int64(userId), 10),
Username: username,
DisplayName: displayName,
AvatarUrl: photoUrl,
}
return &userInfo, nil
}

View File

@@ -872,7 +872,7 @@ func initDefinedTransaction(transaction *Transaction) {
}
}
transaction.CreatedTime = util.GetCurrentTime()
_, _, err = AddTransaction(transaction, "en")
_, _, err = AddTransaction(transaction, "en", false)
if err != nil {
panic(err)
}

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package object

View File

@@ -16,6 +16,8 @@ package object
import (
"context"
"encoding/json"
"fmt"
"github.com/casdoor/casdoor/notification"
"github.com/casdoor/notify"
@@ -40,3 +42,63 @@ func SendNotification(provider *Provider, content string) error {
err = client.Send(context.Background(), "", content)
return err
}
// SendSsoLogoutNotifications sends logout notifications to all notification providers
// configured in the user's signup application
func SendSsoLogoutNotifications(user *User) error {
if user == nil {
return nil
}
// If user's signup application is empty, don't send notifications
if user.SignupApplication == "" {
return nil
}
// Get the user's signup application
application, err := GetApplication(user.SignupApplication)
if err != nil {
return fmt.Errorf("failed to get signup application: %w", err)
}
if application == nil {
return fmt.Errorf("signup application not found: %s", user.SignupApplication)
}
// Prepare sanitized user data for notification
// Only include safe, non-sensitive fields
sanitizedData := map[string]interface{}{
"owner": user.Owner,
"name": user.Name,
"displayName": user.DisplayName,
"email": user.Email,
"phone": user.Phone,
"id": user.Id,
"event": "sso-logout",
}
userData, err := json.Marshal(sanitizedData)
if err != nil {
return fmt.Errorf("failed to marshal user data: %w", err)
}
content := string(userData)
// Send notifications to all notification providers in the signup application
for _, providerItem := range application.Providers {
if providerItem.Provider == nil {
continue
}
// Only send to notification providers
if providerItem.Provider.Category != "Notification" {
continue
}
// Send the notification using the provider from the providerItem
err = SendNotification(providerItem.Provider, content)
if err != nil {
return fmt.Errorf("failed to send SSO logout notification to provider %s/%s: %w", providerItem.Provider.Owner, providerItem.Provider.Name, err)
}
}
return nil
}

View File

@@ -28,13 +28,20 @@ type Order struct {
DisplayName string `xorm:"varchar(100)" json:"displayName"`
// Product Info
ProductName string `xorm:"varchar(100)" json:"productName"`
ProductName string `xorm:"varchar(100)" json:"productName"`
Products []string `xorm:"varchar(1000)" json:"products"` // Future support for multiple products per order. Using varchar(1000) for simple JSON array storage; can be refactored to separate table if needed
// Subscription Info (for subscription orders)
PricingName string `xorm:"varchar(100)" json:"pricingName"`
PlanName string `xorm:"varchar(100)" json:"planName"`
// User Info
User string `xorm:"varchar(100)" json:"user"`
// Payment Info
Payment string `xorm:"varchar(100)" json:"payment"`
Payment string `xorm:"varchar(100)" json:"payment"`
Price float64 `json:"price"`
Currency string `xorm:"varchar(100)" json:"currency"`
// Order State
State string `xorm:"varchar(100)" json:"state"`
@@ -147,3 +154,15 @@ func DeleteOrder(order *Order) (bool, error) {
func (order *Order) GetId() string {
return fmt.Sprintf("%s/%s", order.Owner, order.Name)
}
func GetOrderByPayment(owner string, paymentName string) (*Order, error) {
order := &Order{}
existed, err := ormer.Engine.Where("owner = ? AND payment = ?", owner, paymentName).Get(order)
if err != nil {
return nil, err
}
if existed {
return order, nil
}
return nil, nil
}

314
object/order_pay.go Normal file
View File

@@ -0,0 +1,314 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/pp"
"github.com/casdoor/casdoor/util"
)
func PlaceOrder(productId string, user *User, pricingName string, planName string, customPrice float64) (*Order, error) {
product, err := GetProduct(productId)
if err != nil {
return nil, err
}
if product == nil {
return nil, fmt.Errorf("the product: %s does not exist", productId)
}
userBalanceCurrency := user.BalanceCurrency
if userBalanceCurrency == "" {
org, err := getOrganization("admin", user.Owner)
if err == nil && org != nil && org.BalanceCurrency != "" {
userBalanceCurrency = org.BalanceCurrency
} else {
userBalanceCurrency = "USD"
}
}
productCurrency := product.Currency
if productCurrency == "" {
productCurrency = "USD"
}
var productPrice float64
if product.IsRecharge {
if customPrice <= 0 {
return nil, fmt.Errorf("the custom price should be greater than zero")
}
productPrice = customPrice
} else {
productPrice = product.Price
}
price := ConvertCurrency(productPrice, productCurrency, userBalanceCurrency)
orderName := fmt.Sprintf("order_%v", util.GenerateTimeId())
order := &Order{
Owner: product.Owner,
Name: orderName,
CreatedTime: util.GetCurrentTime(),
DisplayName: fmt.Sprintf("Order for %s", product.DisplayName),
ProductName: product.Name,
Products: []string{product.Name},
PricingName: pricingName,
PlanName: planName,
User: user.Name,
Payment: "", // Payment will be set when user pays
Price: price,
Currency: userBalanceCurrency,
State: "Created",
Message: "",
StartTime: util.GetCurrentTime(),
EndTime: "",
}
affected, err := AddOrder(order)
if err != nil {
return nil, err
}
if !affected {
return nil, fmt.Errorf("failed to add order: %s", util.StructToJson(order))
}
return order, nil
}
func PayOrder(providerName, host, paymentEnv string, order *Order) (payment *Payment, attachInfo map[string]interface{}, err error) {
if order.State != "Created" {
return nil, nil, fmt.Errorf("cannot pay for order: %s, current state is %s", order.GetId(), order.State)
}
productId := util.GetId(order.Owner, order.ProductName)
product, err := GetProduct(productId)
if err != nil {
return nil, nil, err
}
if product == nil {
return nil, nil, fmt.Errorf("the product: %s does not exist", productId)
}
user, err := GetUser(util.GetId(order.Owner, order.User))
if err != nil {
return nil, nil, err
}
if user == nil {
return nil, nil, fmt.Errorf("the user: %s does not exist", order.User)
}
provider, err := product.getProvider(providerName)
if err != nil {
return nil, nil, err
}
pProvider, err := GetPaymentProvider(provider)
if err != nil {
return nil, nil, err
}
owner := product.Owner
payerName := fmt.Sprintf("%s | %s", user.Name, user.DisplayName)
paymentName := fmt.Sprintf("payment_%v", util.GenerateTimeId())
originFrontend, originBackend := getOriginFromHost(host)
returnUrl := fmt.Sprintf("%s/payments/%s/%s/result", originFrontend, owner, paymentName)
notifyUrl := fmt.Sprintf("%s/api/notify-payment/%s/%s", originBackend, owner, paymentName)
// Create a subscription when pricing and plan are provided
// This allows both free users and paid users to subscribe to plans
if order.PricingName != "" && order.PlanName != "" {
plan, err := GetPlan(util.GetId(owner, order.PlanName))
if err != nil {
return nil, nil, err
}
if plan == nil {
return nil, nil, fmt.Errorf("the plan: %s does not exist", order.PlanName)
}
sub, err := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
if err != nil {
return nil, nil, err
}
affected, err := AddSubscription(sub)
if err != nil {
return nil, nil, err
}
if !affected {
return nil, nil, fmt.Errorf("failed to add subscription: %s", sub.Name)
}
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, order.PricingName, sub.Name)
}
if product.SuccessUrl != "" {
returnUrl = fmt.Sprintf("%s?transactionOwner=%s&transactionName=%s", product.SuccessUrl, owner, paymentName)
}
payReq := &pp.PayReq{
ProviderName: providerName,
ProductName: product.Name,
PayerName: payerName,
PayerId: user.Id,
PayerEmail: user.Email,
PaymentName: paymentName,
ProductDisplayName: product.DisplayName,
ProductDescription: product.Description,
ProductImage: product.Image,
Price: order.Price,
Currency: order.Currency,
ReturnUrl: returnUrl,
NotifyUrl: notifyUrl,
PaymentEnv: paymentEnv,
}
if provider.Type == "WeChat Pay" {
payReq.PayerId, err = getUserExtraProperty(user, "WeChat", idp.BuildWechatOpenIdKey(provider.ClientId2))
if err != nil {
return nil, nil, err
}
} else if provider.Type == "Balance" {
payReq.PayerId = user.GetId()
}
payResp, err := pProvider.Pay(payReq)
if err != nil {
return nil, nil, err
}
payment = &Payment{
Owner: product.Owner,
Name: paymentName,
CreatedTime: util.GetCurrentTime(),
DisplayName: paymentName,
Provider: provider.Name,
Type: provider.Type,
ProductName: product.Name,
ProductDisplayName: product.DisplayName,
Detail: product.Detail,
Tag: product.Tag,
Currency: order.Currency,
Price: order.Price,
ReturnUrl: product.ReturnUrl,
IsRecharge: product.IsRecharge,
User: user.Name,
PayUrl: payResp.PayUrl,
SuccessUrl: returnUrl,
State: pp.PaymentStateCreated,
OutOrderId: payResp.OrderId,
}
transaction := &Transaction{
Owner: payment.Owner,
Name: payment.Name,
CreatedTime: util.GetCurrentTime(),
DisplayName: payment.DisplayName,
Application: owner,
Domain: "",
Amount: payment.Price,
Currency: order.Currency,
Payment: payment.Name,
State: pp.PaymentStateCreated,
}
if product.IsRecharge {
transaction.Category = "Recharge"
transaction.Type = ""
transaction.Subtype = ""
transaction.Provider = ""
transaction.Tag = "User"
transaction.User = payment.User
transaction.State = pp.PaymentStatePaid
} else {
transaction.Category = ""
transaction.Type = provider.Category
transaction.Subtype = provider.Type
transaction.Provider = provider.Name
transaction.Tag = product.Tag
transaction.User = payment.User
}
if provider.Type == "Dummy" {
payment.State = pp.PaymentStatePaid
currency := payment.Currency
if currency == "" {
currency = "USD"
}
err = UpdateUserBalance(user.Owner, user.Name, payment.Price, currency, "en")
if err != nil {
return nil, nil, err
}
} else if provider.Type == "Balance" {
convertedPrice := ConvertCurrency(order.Price, order.Currency, user.BalanceCurrency)
if convertedPrice > user.Balance {
return nil, nil, fmt.Errorf("insufficient user balance")
}
transaction.Amount = -transaction.Amount
err = UpdateUserBalance(user.Owner, user.Name, -convertedPrice, user.BalanceCurrency, "en")
if err != nil {
return nil, nil, err
}
payment.State = pp.PaymentStatePaid
transaction.State = pp.PaymentStatePaid
}
affected, err := AddPayment(payment)
if err != nil {
return nil, nil, err
}
if !affected {
return nil, nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
}
if product.IsRecharge || provider.Type == "Balance" {
affected, _, err = AddTransaction(transaction, "en", false)
if err != nil {
return nil, nil, err
}
if !affected {
return nil, nil, fmt.Errorf("failed to add transaction: %s", util.StructToJson(transaction))
}
}
order.Payment = payment.Name
if provider.Type == "Dummy" || provider.Type == "Balance" {
order.State = "Paid"
order.Message = "Payment successful"
order.EndTime = util.GetCurrentTime()
}
_, err = UpdateOrder(order.GetId(), order)
if err != nil {
return nil, nil, err
}
return payment, payResp.AttachInfo, nil
}
func CancelOrder(order *Order) (bool, error) {
if order.State != "Created" {
return false, fmt.Errorf("cannot cancel order in state: %s", order.State)
}
order.State = "Canceled"
order.Message = "Canceled by user"
order.EndTime = util.GetCurrentTime()
return UpdateOrder(order.GetId(), order)
}

View File

@@ -92,6 +92,7 @@ type Organization struct {
OrgBalance float64 `json:"orgBalance"`
UserBalance float64 `json:"userBalance"`
BalanceCredit float64 `json:"balanceCredit"`
BalanceCurrency string `xorm:"varchar(100)" json:"balanceCurrency"`
}
@@ -606,10 +607,18 @@ func UpdateOrganizationBalance(owner string, name string, balance float64, curre
convertedBalance := ConvertCurrency(balance, currency, balanceCurrency)
var columns []string
var newBalance float64
if isOrgBalance {
organization.OrgBalance = AddPrices(organization.OrgBalance, convertedBalance)
newBalance = AddPrices(organization.OrgBalance, convertedBalance)
// Check organization balance credit limit
if newBalance < organization.BalanceCredit {
return fmt.Errorf(i18n.Translate(lang, "general:Insufficient balance: new organization balance %v would be below credit limit %v"), newBalance, organization.BalanceCredit)
}
organization.OrgBalance = newBalance
columns = []string{"org_balance"}
} else {
// User balance is just a sum of all users' balances, no credit limit check here
// Individual user credit limits are checked in UpdateUserBalance
organization.UserBalance = AddPrices(organization.UserBalance, convertedBalance)
columns = []string{"user_balance"}
}

View File

@@ -245,6 +245,32 @@ func NotifyPayment(body []byte, owner string, paymentName string) (*Payment, err
return nil, err
}
}
// Update order state based on payment status
order, err := GetOrderByPayment(owner, paymentName)
if err != nil {
return nil, err
}
if order != nil {
if payment.State == pp.PaymentStatePaid {
order.State = "Paid"
order.Message = "Payment successful"
order.EndTime = util.GetCurrentTime()
} else if payment.State == pp.PaymentStateError {
order.State = "PaymentFailed"
order.Message = payment.Message
} else if payment.State == pp.PaymentStateCanceled {
order.State = "Canceled"
order.Message = "Payment was cancelled"
} else if payment.State == pp.PaymentStateTimeout {
order.State = "Timeout"
order.Message = "Payment timed out"
}
_, err = UpdateOrder(order.GetId(), order)
if err != nil {
return nil, err
}
}
}
return payment, nil

View File

@@ -17,10 +17,6 @@ package object
import (
"fmt"
"github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/pp"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
)
@@ -168,246 +164,6 @@ func (product *Product) getProvider(providerName string) (*Provider, error) {
return provider, nil
}
func BuyProduct(id string, user *User, providerName, pricingName, planName, host, paymentEnv string, customPrice float64) (payment *Payment, attachInfo map[string]interface{}, err error) {
product, err := GetProduct(id)
if err != nil {
return nil, nil, err
}
if product == nil {
return nil, nil, fmt.Errorf("the product: %s does not exist", id)
}
if product.IsRecharge {
if customPrice <= 0 {
return nil, nil, fmt.Errorf("the custom price should bigger than zero")
} else {
product.Price = customPrice
}
}
provider, err := product.getProvider(providerName)
if err != nil {
return nil, nil, err
}
pProvider, err := GetPaymentProvider(provider)
if err != nil {
return nil, nil, err
}
owner := product.Owner
payerName := fmt.Sprintf("%s | %s", user.Name, user.DisplayName)
paymentName := fmt.Sprintf("payment_%v", util.GenerateTimeId())
originFrontend, originBackend := getOriginFromHost(host)
returnUrl := fmt.Sprintf("%s/payments/%s/%s/result", originFrontend, owner, paymentName)
notifyUrl := fmt.Sprintf("%s/api/notify-payment/%s/%s", originBackend, owner, paymentName)
// Create a subscription when pricing and plan are provided
// This allows both free users and paid users to subscribe to plans
if pricingName != "" && planName != "" {
plan, err := GetPlan(util.GetId(owner, planName))
if err != nil {
return nil, nil, err
}
if plan == nil {
return nil, nil, fmt.Errorf("the plan: %s does not exist", planName)
}
sub, err := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
if err != nil {
return nil, nil, err
}
_, err = AddSubscription(sub)
if err != nil {
return nil, nil, err
}
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, pricingName, sub.Name)
}
if product.SuccessUrl != "" {
returnUrl = fmt.Sprintf("%s?transactionOwner=%s&transactionName=%s", product.SuccessUrl, owner, paymentName)
}
// Create an order
payReq := &pp.PayReq{
ProviderName: providerName,
ProductName: product.Name,
PayerName: payerName,
PayerId: user.Id,
PayerEmail: user.Email,
PaymentName: paymentName,
ProductDisplayName: product.DisplayName,
ProductDescription: product.Description,
ProductImage: product.Image,
Price: product.Price,
Currency: product.Currency,
ReturnUrl: returnUrl,
NotifyUrl: notifyUrl,
PaymentEnv: paymentEnv,
}
// custom process for WeChat & WeChat Pay
if provider.Type == "WeChat Pay" {
payReq.PayerId, err = getUserExtraProperty(user, "WeChat", idp.BuildWechatOpenIdKey(provider.ClientId2))
if err != nil {
return nil, nil, err
}
} else if provider.Type == "Balance" {
payReq.PayerId = user.GetId()
}
payResp, err := pProvider.Pay(payReq)
if err != nil {
return nil, nil, err
}
// Create a Payment linked with Product and Order
payment = &Payment{
Owner: product.Owner,
Name: paymentName,
CreatedTime: util.GetCurrentTime(),
DisplayName: paymentName,
Provider: provider.Name,
Type: provider.Type,
ProductName: product.Name,
ProductDisplayName: product.DisplayName,
Detail: product.Detail,
Tag: product.Tag,
Currency: product.Currency,
Price: product.Price,
ReturnUrl: product.ReturnUrl,
IsRecharge: product.IsRecharge,
User: user.Name,
PayUrl: payResp.PayUrl,
SuccessUrl: returnUrl,
State: pp.PaymentStateCreated,
OutOrderId: payResp.OrderId,
}
transaction := &Transaction{
Owner: payment.Owner,
Name: payment.Name,
CreatedTime: util.GetCurrentTime(),
DisplayName: payment.DisplayName,
Application: owner,
Domain: "",
Amount: payment.Price,
Currency: product.Currency,
Payment: payment.Name,
State: pp.PaymentStateCreated,
}
// Set Category, Type, Subtype, Provider, and Tag based on product type
if product.IsRecharge {
// For recharge products: Category="Recharge", Type/Subtype/Provider are blank, Tag="User", State="Paid"
transaction.Category = "Recharge"
transaction.Type = ""
transaction.Subtype = ""
transaction.Provider = ""
transaction.Tag = "User"
transaction.User = payment.User
transaction.State = pp.PaymentStatePaid
} else {
// For non-recharge products: move provider.Category to Type, provider.Type to Subtype
transaction.Category = ""
transaction.Type = provider.Category
transaction.Subtype = provider.Type
transaction.Provider = provider.Name
transaction.Tag = product.Tag
transaction.User = payment.User
}
if provider.Type == "Dummy" {
payment.State = pp.PaymentStatePaid
currency := payment.Currency
if currency == "" {
currency = "USD"
}
err = UpdateUserBalance(user.Owner, user.Name, payment.Price, currency, "en")
if err != nil {
return nil, nil, err
}
} else if provider.Type == "Balance" {
// Convert product price to user's balance currency for comparison
productCurrency := product.Currency
if productCurrency == "" {
productCurrency = "USD"
}
userBalanceCurrency := user.BalanceCurrency
if userBalanceCurrency == "" {
// Get organization's balance currency as fallback
org, err := getOrganization("admin", user.Owner)
if err == nil && org != nil && org.BalanceCurrency != "" {
userBalanceCurrency = org.BalanceCurrency
} else {
userBalanceCurrency = "USD"
}
}
convertedPrice := ConvertCurrency(product.Price, productCurrency, userBalanceCurrency)
if convertedPrice > user.Balance {
return nil, nil, fmt.Errorf("insufficient user balance")
}
transaction.Amount = -transaction.Amount
err = UpdateUserBalance(user.Owner, user.Name, -product.Price, productCurrency, "en")
if err != nil {
return nil, nil, err
}
payment.State = pp.PaymentStatePaid
transaction.State = pp.PaymentStatePaid
}
affected, err := AddPayment(payment)
if err != nil {
return nil, nil, err
}
if !affected {
return nil, nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
}
// Create an order for this product purchase (not for subscriptions)
if pricingName == "" && planName == "" {
orderName := fmt.Sprintf("order_%v", util.GenerateTimeId())
order := &Order{
Owner: product.Owner,
Name: orderName,
CreatedTime: util.GetCurrentTime(),
DisplayName: fmt.Sprintf("Order for %s", product.DisplayName),
ProductName: product.Name,
User: user.Name,
Payment: paymentName,
State: "Created",
Message: "",
StartTime: util.GetCurrentTime(),
EndTime: "",
}
affected, err = AddOrder(order)
if err != nil {
return nil, nil, err
}
if !affected {
return nil, nil, fmt.Errorf("failed to add order: %s", util.StructToJson(order))
}
}
if product.IsRecharge || provider.Type == "Balance" {
affected, _, err = AddTransaction(transaction, "en")
if err != nil {
return nil, nil, err
}
if !affected {
return nil, nil, fmt.Errorf("failed to add transaction: %s", util.StructToJson(payment))
}
}
return payment, payResp.AttachInfo, nil
}
func ExtendProductWithProviders(product *Product) error {
if product == nil {
return nil

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package object

View File

@@ -20,6 +20,7 @@ import (
"github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
"golang.org/x/crypto/ssh"
)
type TableColumn struct {
@@ -61,7 +62,8 @@ type Syncer struct {
IsReadOnly bool `json:"isReadOnly"`
IsEnabled bool `json:"isEnabled"`
Ormer *Ormer `xorm:"-" json:"-"`
Ormer *Ormer `xorm:"-" json:"-"`
SshClient *ssh.Client `xorm:"-" json:"-"`
}
func GetSyncerCount(owner, organization, field, value string) (int64, error) {
@@ -171,6 +173,9 @@ func UpdateSyncer(id string, syncer *Syncer, isGlobalAdmin bool, lang string) (b
return false, fmt.Errorf(i18n.Translate(lang, "auth:Unauthorized operation"))
}
// Close old syncer connections before updating
_ = s.Close()
session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
if syncer.Password == "***" {
syncer.Password = s.Password
@@ -314,3 +319,27 @@ func TestSyncer(syncer Syncer) error {
provider := GetSyncerProvider(&syncer)
return provider.TestConnection()
}
func (syncer *Syncer) Close() error {
var err error
if syncer.Ormer != nil {
if syncer.Ormer.Engine != nil {
err = syncer.Ormer.Engine.Close()
syncer.Ormer.Engine = nil
}
if syncer.Ormer.Db != nil {
if dbErr := syncer.Ormer.Db.Close(); dbErr != nil && err == nil {
err = dbErr
}
syncer.Ormer.Db = nil
}
syncer.Ormer = nil
}
if syncer.SshClient != nil {
if sshErr := syncer.SshClient.Close(); sshErr != nil && err == nil {
err = sshErr
}
syncer.SshClient = nil
}
return err
}

View File

@@ -61,6 +61,13 @@ func (p *ActiveDirectorySyncerProvider) TestConnection() error {
return nil
}
// Close closes any open connections (no-op for Active Directory LDAP-based syncer)
func (p *ActiveDirectorySyncerProvider) Close() error {
// Active Directory syncer doesn't maintain persistent connections
// LDAP connections are opened and closed per operation
return nil
}
// getLdapConn establishes an LDAP connection to Active Directory
func (p *ActiveDirectorySyncerProvider) getLdapConn() (*goldap.Conn, error) {
// syncer.Host should be the AD server hostname/IP

View File

@@ -60,6 +60,12 @@ func (p *AzureAdSyncerProvider) TestConnection() error {
return err
}
// Close closes any open connections (no-op for Azure AD API-based syncer)
func (p *AzureAdSyncerProvider) Close() error {
// Azure AD syncer doesn't maintain persistent connections
return nil
}
type AzureAdAccessTokenResp struct {
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`

View File

@@ -73,4 +73,6 @@ func addSyncerJob(syncer *Syncer) error {
func deleteSyncerJob(syncer *Syncer) {
clearCron(syncer.Name)
// Close any open connections when deleting the job
_ = syncer.Close()
}

View File

@@ -63,6 +63,9 @@ func (p *DatabaseSyncerProvider) InitAdapter() error {
return err
}
// Store SSH client for proper cleanup
p.Syncer.SshClient = dial
if p.Syncer.DatabaseType == "mysql" {
dataSourceName = fmt.Sprintf("%s:%s@%s(%s:%d)/", p.Syncer.User, p.Syncer.Password, p.Syncer.Owner+p.Syncer.Name, p.Syncer.Host, p.Syncer.Port)
mysql.RegisterDialContext(p.Syncer.Owner+p.Syncer.Name, (&ViaSSHDialer{Client: dial, Context: nil}).MysqlDial)
@@ -95,11 +98,13 @@ func (p *DatabaseSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
// Memory leak problem handling
// https://github.com/casdoor/casdoor/issues/1256
users := p.Syncer.getOriginalUsersFromMap(results)
for _, m := range results {
for k := range m {
delete(m, k)
// Clear map contents to help garbage collection
for i := range results {
for k := range results[i] {
delete(results[i], k)
}
}
results = nil
return users, nil
}
@@ -142,6 +147,11 @@ func (p *DatabaseSyncerProvider) TestConnection() error {
return nil
}
// Close closes the database connection and SSH tunnel
func (p *DatabaseSyncerProvider) Close() error {
return p.Syncer.Close()
}
type dsnConnector struct {
dsn string
driver driver.Driver

View File

@@ -60,6 +60,12 @@ func (p *GoogleWorkspaceSyncerProvider) TestConnection() error {
return err
}
// Close closes any open connections (no-op for Google Workspace API-based syncer)
func (p *GoogleWorkspaceSyncerProvider) Close() error {
// Google Workspace syncer doesn't maintain persistent connections
return nil
}
// getAdminService creates and returns a Google Workspace Admin SDK service
func (p *GoogleWorkspaceSyncerProvider) getAdminService() (*admin.Service, error) {
// syncer.Host should be the admin email (impersonation account)

View File

@@ -31,6 +31,9 @@ type SyncerProvider interface {
// TestConnection tests the connection to the external system
TestConnection() error
// Close closes any open connections and releases resources
Close() error
}
// GetSyncerProvider returns the appropriate SyncerProvider implementation based on syncer type

View File

@@ -12,7 +12,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package object

View File

@@ -246,25 +246,25 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]sql.NullStrin
if syncer.Type == "Keycloak" {
// query and set password and password salt from credential table
sql := fmt.Sprintf("select * from credential where type = 'password' and user_id = '%s'", originalUser.Id)
credentialResult, _ := syncer.Ormer.Engine.QueryString(sql)
if len(credentialResult) > 0 {
credentialResult, err := syncer.Ormer.Engine.QueryString("select * from credential where type = 'password' and user_id = ?", originalUser.Id)
if err == nil && len(credentialResult) > 0 {
credential := Credential{}
_ = json.Unmarshal([]byte(credentialResult[0]["SECRET_DATA"]), &credential)
originalUser.Password = credential.Value
originalUser.PasswordSalt = credential.Salt
if err := json.Unmarshal([]byte(credentialResult[0]["SECRET_DATA"]), &credential); err == nil {
originalUser.Password = credential.Value
originalUser.PasswordSalt = credential.Salt
}
}
// query and set signup application from user group table
sql = fmt.Sprintf("select name from keycloak_group where id = "+
"(select group_id as gid from user_group_membership where user_id = '%s')", originalUser.Id)
groupResult, _ := syncer.Ormer.Engine.QueryString(sql)
if len(groupResult) > 0 {
groupResult, err := syncer.Ormer.Engine.QueryString("select name from keycloak_group where id = "+
"(select group_id as gid from user_group_membership where user_id = ?)", originalUser.Id)
if err == nil && len(groupResult) > 0 {
originalUser.SignupApplication = groupResult[0]["name"]
}
// create time
i, _ := strconv.ParseInt(originalUser.CreatedTime, 10, 64)
tm := time.Unix(i/int64(1000), 0)
originalUser.CreatedTime = tm.Format("2006-01-02T15:04:05+08:00")
if i, err := strconv.ParseInt(originalUser.CreatedTime, 10, 64); err == nil {
tm := time.Unix(i/int64(1000), 0)
originalUser.CreatedTime = tm.Format("2006-01-02T15:04:05+08:00")
}
// enable
value, ok := result["ENABLED"]
if ok {

View File

@@ -60,6 +60,12 @@ func (p *WecomSyncerProvider) TestConnection() error {
return err
}
// Close closes any open connections (no-op for WeCom API-based syncer)
func (p *WecomSyncerProvider) Close() error {
// WeCom syncer doesn't maintain persistent connections
return nil
}
type WecomAccessTokenResp struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`

View File

@@ -141,10 +141,20 @@ func UpdateTransaction(id string, transaction *Transaction, lang string) (bool,
return affected != 0, nil
}
func AddTransaction(transaction *Transaction, lang string) (bool, string, error) {
func AddTransaction(transaction *Transaction, lang string, dryRun bool) (bool, string, error) {
transactionId := strings.ReplaceAll(util.GenerateId(), "-", "")
transaction.Name = transactionId
// In dry run mode, only validate without making changes
if dryRun {
err := validateBalanceForTransaction(transaction, transaction.Amount, lang)
if err != nil {
return false, "", err
}
return true, "", nil
}
affected, err := ormer.Engine.Insert(transaction)
if err != nil {
return false, "", err

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package object

View File

@@ -0,0 +1,130 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"github.com/casdoor/casdoor/i18n"
)
func validateBalanceForTransaction(transaction *Transaction, amount float64, lang string) error {
currency := transaction.Currency
if currency == "" {
currency = "USD"
}
if transaction.Tag == "Organization" {
// Validate organization balance change
return validateOrganizationBalance("admin", transaction.Owner, amount, currency, true, lang)
} else if transaction.Tag == "User" {
// Validate user balance change
if transaction.User == "" {
return fmt.Errorf(i18n.Translate(lang, "general:User is required for User category transaction"))
}
if err := validateUserBalance(transaction.Owner, transaction.User, amount, currency, lang); err != nil {
return err
}
// Validate organization's user balance sum change
return validateOrganizationBalance("admin", transaction.Owner, amount, currency, false, lang)
}
return nil
}
func validateOrganizationBalance(owner string, name string, balance float64, currency string, isOrgBalance bool, lang string) error {
organization, err := getOrganization(owner, name)
if err != nil {
return err
}
if organization == nil {
return fmt.Errorf(i18n.Translate(lang, "auth:the organization: %s is not found"), fmt.Sprintf("%s/%s", owner, name))
}
// Convert the balance amount from transaction currency to organization's balance currency
balanceCurrency := organization.BalanceCurrency
if balanceCurrency == "" {
balanceCurrency = "USD"
}
convertedBalance := ConvertCurrency(balance, currency, balanceCurrency)
var newBalance float64
if isOrgBalance {
newBalance = AddPrices(organization.OrgBalance, convertedBalance)
// Check organization balance credit limit
if newBalance < organization.BalanceCredit {
return fmt.Errorf(i18n.Translate(lang, "general:Insufficient balance: new organization balance %v would be below credit limit %v"), newBalance, organization.BalanceCredit)
}
} else {
// User balance is just a sum of all users' balances, no credit limit check here
// Individual user credit limits are checked in validateUserBalance
newBalance = AddPrices(organization.UserBalance, convertedBalance)
}
// In validation mode, we don't actually update the balance
return nil
}
func validateUserBalance(owner string, name string, balance float64, currency string, lang string) error {
user, err := getUser(owner, name)
if err != nil {
return err
}
if user == nil {
return fmt.Errorf(i18n.Translate(lang, "general:The user: %s is not found"), fmt.Sprintf("%s/%s", owner, name))
}
// Convert the balance amount from transaction currency to user's balance currency
balanceCurrency := user.BalanceCurrency
var org *Organization
if balanceCurrency == "" {
// Get organization's balance currency as fallback
org, err = getOrganization("admin", owner)
if err == nil && org != nil && org.BalanceCurrency != "" {
balanceCurrency = org.BalanceCurrency
} else {
balanceCurrency = "USD"
}
}
convertedBalance := ConvertCurrency(balance, currency, balanceCurrency)
// Calculate new balance
newBalance := AddPrices(user.Balance, convertedBalance)
// Check balance credit limit
// User.BalanceCredit takes precedence over Organization.BalanceCredit
var balanceCredit float64
if user.BalanceCredit != 0 {
balanceCredit = user.BalanceCredit
} else {
// Get organization's balance credit as fallback
if org == nil {
org, err = getOrganization("admin", owner)
if err != nil {
return err
}
}
if org != nil {
balanceCredit = org.BalanceCredit
}
}
// Validate new balance against credit limit
if newBalance < balanceCredit {
return fmt.Errorf(i18n.Translate(lang, "general:Insufficient balance: new balance %v would be below credit limit %v"), newBalance, balanceCredit)
}
// In validation mode, we don't actually update the balance
return nil
}

View File

@@ -92,6 +92,7 @@ type User struct {
Karma int `json:"karma"`
Ranking int `json:"ranking"`
Balance float64 `json:"balance"`
BalanceCredit float64 `json:"balanceCredit"`
Currency string `xorm:"varchar(100)" json:"currency"`
BalanceCurrency string `xorm:"varchar(100)" json:"balanceCurrency"`
IsDefaultAvatar bool `json:"isDefaultAvatar"`
@@ -107,6 +108,7 @@ type User struct {
AccessKey string `xorm:"varchar(100)" json:"accessKey"`
AccessSecret string `xorm:"varchar(100)" json:"accessSecret"`
AccessToken string `xorm:"mediumtext" json:"accessToken"`
OriginalToken string `xorm:"mediumtext" json:"originalToken"`
CreatedIp string `xorm:"varchar(100)" json:"createdIp"`
LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"`
@@ -664,6 +666,9 @@ func GetMaskedUser(user *User, isAdminOrSelf bool, errs ...error) (*User, error)
if user.AccessSecret != "" {
user.AccessSecret = "***"
}
if user.OriginalToken != "" {
user.OriginalToken = "***"
}
}
if user.ManagedAccounts != nil {
@@ -839,7 +844,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
}
}
if isAdmin {
columns = append(columns, "name", "id", "email", "phone", "country_code", "type", "balance", "mfa_items", "register_type", "register_source")
columns = append(columns, "name", "id", "email", "phone", "country_code", "type", "balance", "balance_credit", "balance_currency", "mfa_items", "register_type", "register_source")
}
columns = append(columns, "updated_time")
@@ -1490,9 +1495,10 @@ func UpdateUserBalance(owner string, name string, balance float64, currency stri
// Convert the balance amount from transaction currency to user's balance currency
balanceCurrency := user.BalanceCurrency
var org *Organization
if balanceCurrency == "" {
// Get organization's balance currency as fallback
org, err := getOrganization("admin", owner)
org, err = getOrganization("admin", owner)
if err == nil && org != nil && org.BalanceCurrency != "" {
balanceCurrency = org.BalanceCurrency
} else {
@@ -1501,7 +1507,33 @@ func UpdateUserBalance(owner string, name string, balance float64, currency stri
}
convertedBalance := ConvertCurrency(balance, currency, balanceCurrency)
user.Balance = AddPrices(user.Balance, convertedBalance)
// Calculate new balance
newBalance := AddPrices(user.Balance, convertedBalance)
// Check balance credit limit
// User.BalanceCredit takes precedence over Organization.BalanceCredit
var balanceCredit float64
if user.BalanceCredit != 0 {
balanceCredit = user.BalanceCredit
} else {
// Get organization's balance credit as fallback
if org == nil {
org, err = getOrganization("admin", owner)
if err != nil {
return err
}
}
if org != nil {
balanceCredit = org.BalanceCredit
}
}
// Validate new balance against credit limit
if newBalance < balanceCredit {
return fmt.Errorf(i18n.Translate(lang, "general:Insufficient balance: new balance %v would be below credit limit %v"), newBalance, balanceCredit)
}
user.Balance = newBalance
_, err = UpdateUser(user.GetId(), user, []string{"balance"}, true)
return err
}

View File

@@ -78,6 +78,17 @@ func parseListItem(lines *[]string, i int) []string {
func UploadUsers(owner string, path string, userObj *User, lang string) (bool, error) {
table := xlsx.ReadXlsxFile(path)
if len(table) == 0 {
return false, fmt.Errorf("empty table")
}
for idx, row := range table[0] {
splitRow := strings.Split(row, "#")
if len(splitRow) > 1 {
table[0][idx] = splitRow[1]
}
}
uploadedUsers, err := StringArrayToStruct[User](table)
if err != nil {
return false, err
@@ -146,7 +157,7 @@ func UploadUsers(owner string, path string, userObj *User, lang string) (bool, e
}
if len(newUsers) == 0 {
return false, nil
return false, fmt.Errorf("no users are modified")
}
return AddUsersInBatch(newUsers)

View File

@@ -30,6 +30,7 @@ import (
"github.com/go-webauthn/webauthn/webauthn"
jsoniter "github.com/json-iterator/go"
"github.com/xorm-io/core"
"golang.org/x/oauth2"
)
func GetUserByField(organizationName string, field string, value string) (*User, error) {
@@ -183,7 +184,12 @@ func getUserExtraProperty(user *User, providerType, key string) (string, error)
return extra[key], nil
}
func SetUserOAuthProperties(organization *Organization, user *User, providerType string, userInfo *idp.UserInfo, userMapping ...map[string]string) (bool, error) {
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 != "" {
user.OriginalToken = token.AccessToken
}
if userInfo.Id != "" {
propertyName := fmt.Sprintf("oauth_%s_id", providerType)
setUserProperty(user, propertyName, userInfo.Id)
@@ -859,7 +865,7 @@ func StringArrayToStruct[T any](stringArray [][]string) ([]*T, error) {
instances := []*T{}
var err error
for _, m := range excelMap {
for idx, m := range excelMap {
instance := new(T)
reflectedInstance := reflect.ValueOf(instance).Elem()
@@ -886,7 +892,7 @@ func StringArrayToStruct[T any](stringArray [][]string) ([]*T, error) {
case reflect.Int:
intVal, err := strconv.Atoi(v)
if err != nil {
return nil, err
return nil, fmt.Errorf("line %d - column %s: %s", idx+1, fName, err.Error())
}
fv.SetInt(int64(intVal))
continue
@@ -914,7 +920,7 @@ func StringArrayToStruct[T any](stringArray [][]string) ([]*T, error) {
}
if err != nil {
return nil, err
return nil, fmt.Errorf("line %d: %s", idx, err.Error())
}
}
instances = append(instances, instance)

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package radius

View File

@@ -199,7 +199,6 @@ func initAPI() {
beego.Router("/api/update-product", &controllers.ApiController{}, "POST:UpdateProduct")
beego.Router("/api/add-product", &controllers.ApiController{}, "POST:AddProduct")
beego.Router("/api/delete-product", &controllers.ApiController{}, "POST:DeleteProduct")
beego.Router("/api/buy-product", &controllers.ApiController{}, "POST:BuyProduct")
beego.Router("/api/get-orders", &controllers.ApiController{}, "GET:GetOrders")
beego.Router("/api/get-user-orders", &controllers.ApiController{}, "GET:GetUserOrders")
@@ -207,6 +206,9 @@ func initAPI() {
beego.Router("/api/update-order", &controllers.ApiController{}, "POST:UpdateOrder")
beego.Router("/api/add-order", &controllers.ApiController{}, "POST:AddOrder")
beego.Router("/api/delete-order", &controllers.ApiController{}, "POST:DeleteOrder")
beego.Router("/api/place-order", &controllers.ApiController{}, "POST:PlaceOrder")
beego.Router("/api/cancel-order", &controllers.ApiController{}, "POST:CancelOrder")
beego.Router("/api/pay-order", &controllers.ApiController{}, "POST:PayOrder")
beego.Router("/api/get-payments", &controllers.ApiController{}, "GET:GetPayments")
beego.Router("/api/get-user-payments", &controllers.ApiController{}, "GET:GetUserPayments")

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,60 @@ schemes:
- https
- http
paths:
/.well-known/{application}/jwks:
get:
tags:
- OIDC API
operationId: RootController.GetJwksByApplication
parameters:
- in: path
name: application
description: application name
required: true
type: string
responses:
"200":
description: ""
schema:
$ref: '#/definitions/jose.JSONWebKey'
/.well-known/{application}/openid-configuration:
get:
tags:
- OIDC API
description: Get Oidc Discovery for specific application
operationId: RootController.GetOidcDiscoveryByApplication
parameters:
- in: path
name: application
description: application name
required: true
type: string
responses:
"200":
description: ""
schema:
$ref: '#/definitions/object.OidcDiscovery'
/.well-known/{application}/webfinger:
get:
tags:
- OIDC API
operationId: RootController.GetWebFingerByApplication
parameters:
- in: path
name: application
description: application name
required: true
type: string
- in: query
name: resource
description: resource
required: true
type: string
responses:
"200":
description: ""
schema:
$ref: '#/definitions/object.WebFinger'
/.well-known/jwks:
get:
tags:
@@ -130,6 +184,24 @@ paths:
description: ""
schema:
$ref: '#/definitions/object.Enforcer'
/api/add-form:
post:
tags:
- Form API
description: add form
operationId: ApiController.AddForm
parameters:
- in: body
name: body
description: The details of the form
required: true
schema:
$ref: '#/definitions/object.Form'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/add-group:
post:
tags:
@@ -202,6 +274,24 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/add-order:
post:
tags:
- Order API
description: add order
operationId: ApiController.AddOrder
parameters:
- in: body
name: body
description: The details of the order
required: true
schema:
$ref: '#/definitions/object.Order'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/add-organization:
post:
tags:
@@ -495,6 +585,10 @@ paths:
required: true
schema:
$ref: '#/definitions/object.Transaction'
- in: query
name: dryRun
description: 'Dry run mode: set to ''true'' or ''1'' to validate without committing'
type: string
responses:
"200":
description: The Response object
@@ -578,28 +672,6 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/buy-product:
post:
tags:
- Product API
description: buy product
operationId: ApiController.BuyProduct
parameters:
- in: query
name: id
description: The id ( owner/name ) of the product
required: true
type: string
- in: query
name: providerName
description: The name of the provider
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/check-user-password:
post:
tags:
@@ -682,6 +754,24 @@ paths:
description: ""
schema:
$ref: '#/definitions/object.Enforcer'
/api/delete-form:
post:
tags:
- Form API
description: delete form
operationId: ApiController.DeleteForm
parameters:
- in: body
name: body
description: The details of the form
required: true
schema:
$ref: '#/definitions/object.Form'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/delete-group:
post:
tags:
@@ -765,6 +855,24 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/delete-order:
post:
tags:
- Order API
description: delete order
operationId: ApiController.DeleteOrder
parameters:
- in: body
name: body
description: The details of the order
required: true
schema:
$ref: '#/definitions/object.Order'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/delete-organization:
post:
tags:
@@ -1392,10 +1500,10 @@ paths:
items:
$ref: '#/definitions/object.Enforcer'
/api/get-filtered-policies:
get:
post:
tags:
- Enforcer API
description: get filtered policies
description: get filtered policies with support for multiple filters via POST body
operationId: ApiController.GetFilteredPolicies
parameters:
- in: query
@@ -1403,19 +1511,14 @@ paths:
description: The id ( owner/name ) of enforcer
required: true
type: string
- in: query
name: ptype
description: Policy type, default is 'p'
type: string
- in: query
name: fieldIndex
description: Field index for filtering
type: integer
format: int64
- in: query
name: fieldValues
description: Field values for filtering, comma-separated
type: string
- in: body
name: body
description: Array of filter objects for multiple filters
required: true
schema:
type: array
items:
$ref: '#/definitions/object.Filter'
responses:
"200":
description: ""
@@ -1423,6 +1526,42 @@ paths:
type: array
items:
$ref: '#/definitions/xormadapter.CasbinRule'
/api/get-form:
get:
tags:
- Form API
description: get form
operationId: ApiController.GetForm
parameters:
- in: query
name: id
description: The id (owner/name) of form
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Form'
/api/get-forms:
get:
tags:
- Form API
description: get forms
operationId: ApiController.GetForms
parameters:
- in: query
name: owner
description: The owner of form
required: true
type: string
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Form'
/api/get-global-certs:
get:
tags:
@@ -1436,6 +1575,19 @@ paths:
type: array
items:
$ref: '#/definitions/object.Cert'
/api/get-global-forms:
get:
tags:
- Form API
description: get global forms
operationId: ApiController.GetGlobalForms
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Form'
/api/get-global-providers:
get:
tags:
@@ -1633,6 +1785,42 @@ paths:
type: array
items:
$ref: '#/definitions/object.Model'
/api/get-order:
get:
tags:
- Order API
description: get order
operationId: ApiController.GetOrder
parameters:
- in: query
name: id
description: The id ( owner/name ) of the order
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Order'
/api/get-orders:
get:
tags:
- Order API
description: get orders
operationId: ApiController.GetOrders
parameters:
- in: query
name: owner
description: The owner of orders
required: true
type: string
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Order'
/api/get-organization:
get:
tags:
@@ -1710,9 +1898,9 @@ paths:
/api/get-payment:
get:
tags:
- Verification API
- Payment API
description: get payment
operationId: ApiController.GetVerification
operationId: ApiController.GetPayment
parameters:
- in: query
name: id
@@ -1723,13 +1911,13 @@ paths:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Verification'
$ref: '#/definitions/object.Payment'
/api/get-payments:
get:
tags:
- Verification API
- Payment API
description: get payments
operationId: ApiController.GetVerifications
operationId: ApiController.GetPayments
parameters:
- in: query
name: owner
@@ -1742,7 +1930,7 @@ paths:
schema:
type: array
items:
$ref: '#/definitions/object.Verification'
$ref: '#/definitions/object.Payment'
/api/get-permission:
get:
tags:
@@ -2450,12 +2638,36 @@ paths:
responses:
"200":
description: '{int} int The count of filtered users for an organization'
/api/get-user-orders:
get:
tags:
- Order API
description: get orders for a user
operationId: ApiController.GetUserOrders
parameters:
- in: query
name: owner
description: The owner of orders
required: true
type: string
- in: query
name: user
description: The username of the user
required: true
type: string
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Order'
/api/get-user-payments:
get:
tags:
- Verification API
- Payment API
description: get payments for a user
operationId: ApiController.GetUserVerifications
operationId: ApiController.GetUserPayments
parameters:
- in: query
name: owner
@@ -2478,36 +2690,7 @@ paths:
schema:
type: array
items:
$ref: '#/definitions/object.Verification'
/api/get-user-transactions:
get:
tags:
- Transaction API
description: get transactions for a user
operationId: ApiController.GetUserTransaction
parameters:
- in: query
name: owner
description: The owner of transactions
required: true
type: string
- in: query
name: organization
description: The organization of the user
required: true
type: string
- in: query
name: user
description: The username of the user
required: true
type: string
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Transaction'
$ref: '#/definitions/object.Payment'
/api/get-users:
get:
tags:
@@ -2836,6 +3019,15 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/metrics:
get:
tags:
- System API
description: get Prometheus metrics
operationId: ApiController.GetMetrics
responses:
"200":
description: '{string} Prometheus metrics in text format'
/api/mfa/setup/enable:
post:
tags:
@@ -2999,6 +3191,31 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/send-invitation:
post:
tags:
- Invitation API
description: verify invitation
operationId: ApiController.VerifyInvitation
parameters:
- in: query
name: id
description: The id ( owner/name ) of the invitation
required: true
type: string
- in: body
name: body
description: The details of the invitation
required: true
schema:
type: array
items:
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/send-notification:
post:
tags:
@@ -3120,6 +3337,27 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/sso-logout:
get:
tags:
- Login API
description: logout the current user from all applications
operationId: ApiController.SsoLogout
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
post:
tags:
- Login API
description: logout the current user from all applications
operationId: ApiController.SsoLogout
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/sync-ldap-users:
post:
tags:
@@ -3239,6 +3477,29 @@ paths:
description: ""
schema:
$ref: '#/definitions/object.Enforcer'
/api/update-form:
post:
tags:
- Form API
description: update form
operationId: ApiController.UpdateForm
parameters:
- in: query
name: id
description: The id (owner/name) of the form
required: true
type: string
- in: body
name: body
description: The details of the form
required: true
schema:
$ref: '#/definitions/object.Form'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/update-group:
post:
tags:
@@ -3326,6 +3587,29 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/update-order:
post:
tags:
- Order API
description: update order
operationId: ApiController.UpdateOrder
parameters:
- in: query
name: id
description: The id ( owner/name ) of the order
required: true
type: string
- in: body
name: body
description: The details of the order
required: true
schema:
$ref: '#/definitions/object.Order'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/update-organization:
post:
tags:
@@ -3679,7 +3963,14 @@ paths:
- in: query
name: id
description: The id ( owner/name ) of the user
required: true
type: string
- in: query
name: userId
description: The userId (UUID) of the user
type: string
- in: query
name: owner
description: The owner of the user (required when using userId)
type: string
- in: body
name: body
@@ -3907,10 +4198,10 @@ paths:
schema:
$ref: '#/definitions/controllers.Response'
definitions:
187812.<nil>.string:
217289.<nil>.string:
title: string
type: object
187870.string.string:
217347.string.string:
title: string
type: object
Response:
@@ -4091,18 +4382,25 @@ definitions:
type: string
clientSecret:
type: string
codeResendTimeout:
type: integer
format: int64
createdTime:
type: string
defaultGroup:
type: string
description:
type: string
disableSignin:
type: boolean
displayName:
type: string
enableAutoSignin:
type: boolean
enableCodeSignin:
type: boolean
enableExclusiveSignin:
type: boolean
enableLinkWithEmail:
type: boolean
enablePassword:
@@ -4120,14 +4418,16 @@ definitions:
enableWebAuthn:
type: boolean
expireInHours:
type: integer
format: int64
type: number
format: double
failedSigninFrozenTime:
type: integer
format: int64
failedSigninLimit:
type: integer
format: int64
favicon:
type: string
footerHtml:
type: string
forcedRedirectOrigin:
@@ -4165,6 +4465,9 @@ definitions:
type: string
name:
type: string
order:
type: integer
format: int64
orgChoiceMode:
type: string
organization:
@@ -4182,12 +4485,14 @@ definitions:
items:
type: string
refreshExpireInHours:
type: integer
format: int64
type: number
format: double
samlAttributes:
type: array
items:
$ref: '#/definitions/object.SamlItem'
samlHashAlgorithm:
type: string
samlReplyUrl:
type: string
signinHtml:
@@ -4218,6 +4523,12 @@ definitions:
type: string
themeData:
$ref: '#/definitions/object.ThemeData'
title:
type: string
tokenAttributes:
type: array
items:
$ref: '#/definitions/object.JwtItem'
tokenFields:
type: array
items:
@@ -4308,6 +4619,51 @@ definitions:
format: double
name:
type: string
object.Filter:
title: Filter
type: object
properties:
fieldIndex:
type: integer
format: int64
fieldValues:
type: array
items:
type: string
ptype:
type: string
object.Form:
title: Form
type: object
properties:
createdTime:
type: string
displayName:
type: string
formItems:
type: array
items:
$ref: '#/definitions/object.FormItem'
name:
type: string
owner:
type: string
tag:
type: string
type:
type: string
object.FormItem:
title: FormItem
type: object
properties:
label:
type: string
name:
type: string
visible:
type: boolean
width:
type: string
object.GaugeVecInfo:
title: GaugeVecInfo
type: object
@@ -4453,6 +4809,14 @@ definitions:
format: int64
username:
type: string
object.JwtItem:
title: JwtItem
type: object
properties:
name:
type: string
value:
type: string
object.Ldap:
title: Ldap
type: object
@@ -4466,6 +4830,9 @@ definitions:
type: string
createdTime:
type: string
customAttributes:
additionalProperties:
type: string
defaultGroup:
type: string
enableSsl:
@@ -4513,8 +4880,15 @@ definitions:
type: string
address:
type: string
attributes:
additionalProperties:
type: string
cn:
type: string
country:
type: string
countryName:
type: string
displayName:
type: string
email:
@@ -4660,6 +5034,32 @@ definitions:
type: string
userinfo_endpoint:
type: string
object.Order:
title: Order
type: object
properties:
createdTime:
type: string
displayName:
type: string
endTime:
type: string
message:
type: string
name:
type: string
owner:
type: string
payment:
type: string
productName:
type: string
startTime:
type: string
state:
type: string
user:
type: string
object.Organization:
title: Organization
type: object
@@ -4668,6 +5068,11 @@ definitions:
type: array
items:
$ref: '#/definitions/object.AccountItem'
balanceCredit:
type: number
format: double
balanceCurrency:
type: string
countryCodes:
type: array
items:
@@ -4680,6 +5085,8 @@ definitions:
type: string
defaultPassword:
type: string
disableSignin:
type: boolean
displayName:
type: string
enableSoftDeletion:
@@ -4724,10 +5131,9 @@ definitions:
type: array
items:
type: string
userNavItems:
type: array
items:
type: string
orgBalance:
type: number
format: double
owner:
type: string
passwordExpireDays:
@@ -4753,6 +5159,13 @@ definitions:
$ref: '#/definitions/object.ThemeData'
useEmailAsUsername:
type: boolean
userBalance:
type: number
format: double
userNavItems:
type: array
items:
type: string
userTypes:
type: array
items:
@@ -5058,6 +5471,8 @@ definitions:
type: string
emailRegex:
type: string
enableProxy:
type: boolean
enableSignAuthnRequest:
type: boolean
endpoint:
@@ -5482,26 +5897,22 @@ definitions:
type: string
currency:
type: string
detail:
type: string
displayName:
type: string
domain:
type: string
name:
type: string
owner:
type: string
payment:
type: string
productDisplayName:
type: string
productName:
type: string
provider:
type: string
returnUrl:
type: string
state:
$ref: '#/definitions/pp.PaymentState'
subtype:
type: string
tag:
type: string
type:
@@ -5547,6 +5958,11 @@ definitions:
balance:
type: number
format: double
balanceCredit:
type: number
format: double
balanceCurrency:
type: string
battlenet:
type: string
bilibili:
@@ -5573,6 +5989,24 @@ definitions:
type: string
custom:
type: string
custom2:
type: string
custom3:
type: string
custom4:
type: string
custom5:
type: string
custom6:
type: string
custom7:
type: string
custom8:
type: string
custom9:
type: string
custom10:
type: string
dailymotion:
type: string
deezer:
@@ -5716,6 +6150,18 @@ definitions:
$ref: '#/definitions/object.MfaItem'
mfaPhoneEnabled:
type: boolean
mfaPushEnabled:
type: boolean
mfaPushProvider:
type: string
mfaPushReceiver:
type: string
mfaRadiusEnabled:
type: boolean
mfaRadiusProvider:
type: string
mfaRadiusUsername:
type: string
mfaRememberDeadline:
type: string
microsoftonline:
@@ -5736,6 +6182,8 @@ definitions:
type: string
onedrive:
type: string
originalToken:
type: string
oura:
type: string
owner:
@@ -5776,6 +6224,10 @@ definitions:
type: string
region:
type: string
registerSource:
type: string
registerType:
type: string
roles:
type: array
items:
@@ -5896,13 +6348,13 @@ definitions:
type: object
properties:
aliases:
$ref: '#/definitions/187812.<nil>.string'
$ref: '#/definitions/217289.<nil>.string'
links:
type: array
items:
$ref: '#/definitions/object.WebFingerLink'
properties:
$ref: '#/definitions/187870.string.string'
$ref: '#/definitions/217347.string.string'
subject:
type: string
object.WebFingerLink:
@@ -5978,6 +6430,9 @@ definitions:
sql.DB:
title: DB
type: object
ssh.Client:
title: Client
type: object
util.SystemInfo:
title: SystemInfo
type: object

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package sync

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package sync_v2

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package sync_v2

View File

@@ -21,6 +21,7 @@ func GetUploadXlsxPath(fileId string) string {
if err != nil {
panic(err)
}
defer file.Close()
return file.Name()
}

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package util

View File

@@ -55,7 +55,8 @@
"react-metamask-avatar": "^1.2.1",
"react-router-dom": "^5.3.3",
"react-scripts": "5.0.1",
"react-social-login-buttons": "^3.4.0"
"react-social-login-buttons": "^3.4.0",
"xlsx": "^0.18.5"
},
"scripts": {
"start": "cross-env PORT=7001 craco start",

View File

@@ -129,7 +129,7 @@ class AdapterListPage extends BaseListPage {
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
);
},
},

View File

@@ -38,8 +38,68 @@ import {setTwoToneColor} from "@ant-design/icons";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as Cookie from "cookie";
// Ant Design locale imports
import enUS from "antd/locale/en_US";
import zhCN from "antd/locale/zh_CN";
import zhTW from "antd/locale/zh_TW";
import esES from "antd/locale/es_ES";
import frFR from "antd/locale/fr_FR";
import deDE from "antd/locale/de_DE";
import idID from "antd/locale/id_ID";
import jaJP from "antd/locale/ja_JP";
import koKR from "antd/locale/ko_KR";
import ruRU from "antd/locale/ru_RU";
import viVN from "antd/locale/vi_VN";
import ptBR from "antd/locale/pt_BR";
import itIT from "antd/locale/it_IT";
import msMY from "antd/locale/ms_MY";
import trTR from "antd/locale/tr_TR";
import arEG from "antd/locale/ar_EG";
import heIL from "antd/locale/he_IL";
import nlNL from "antd/locale/nl_NL";
import plPL from "antd/locale/pl_PL";
import fiFI from "antd/locale/fi_FI";
import svSE from "antd/locale/sv_SE";
import ukUA from "antd/locale/uk_UA";
import faIR from "antd/locale/fa_IR";
import csCZ from "antd/locale/cs_CZ";
import skSK from "antd/locale/sk_SK";
setTwoToneColor("rgb(87,52,211)");
function getAntdLocale(language) {
const localeMap = {
"en": enUS,
"zh": zhCN,
"zh-tw": zhTW,
"es": esES,
"fr": frFR,
"de": deDE,
"id": idID,
"ja": jaJP,
"ko": koKR,
"ru": ruRU,
"vi": viVN,
"pt": ptBR,
"it": itIT,
"ms": msMY,
"tr": trTR,
"ar": arEG,
"he": heIL,
"nl": nlNL,
"pl": plPL,
"fi": fiFI,
"sv": svSE,
"uk": ukUA,
"fa": faIR,
"cs": csCZ,
"sk": skSK,
"kk": ruRU, // Use Russian for Kazakh as antd doesn't have Kazakh
"az": trTR, // Use Turkish for Azerbaijani as they're similar
};
return localeMap[language] || enUS;
}
class App extends Component {
constructor(props) {
super(props);
@@ -98,11 +158,133 @@ class App extends Component {
}
}
shouldFlattenMenu() {
const organization = this.state.account?.organization;
const navItems = Setting.isLocalAdminUser(this.state.account) ? organization?.navItems : (organization?.userNavItems ?? []);
// If navItems is "all" or not configured, don't flatten
if (!Array.isArray(navItems) || navItems?.includes("all")) {
return false;
}
// Count how many valid menu items would be visible
// Filter out any invalid or non-existent menu items
const validMenuItems = [
"/", "/shortcuts", "/apps", // Home group
"/organizations", "/groups", "/users", "/invitations", // User Management
"/applications", "/providers", "/resources", "/certs", // Identity
"/roles", "/permissions", "/models", "/adapters", "/enforcers", // Authorization
"/sessions", "/records", "/tokens", "/verifications", // Logging & Auditing
"/products", "/orders", "/payments", "/plans", "/pricings", "/subscriptions", "/transactions", // Business
"/sysinfo", "/forms", "/syncers", "/webhooks", "/swagger", // Admin
];
const count = navItems.filter(item => validMenuItems.includes(item)).length;
return count <= Conf.MaxItemsForFlatMenu;
}
getSelectedMenuKeyForFlatMenu(uri) {
// For flattened menu, return the actual child path instead of parent group
if (uri === "/" || uri.includes("/shortcuts") || uri.includes("/apps")) {
if (uri === "/") {
return "/";
} else if (uri.includes("/shortcuts")) {
return "/shortcuts";
} else if (uri.includes("/apps")) {
return "/apps";
}
} else if (uri.includes("/organizations") || uri.includes("/trees") || uri.includes("/groups") || uri.includes("/users") || uri.includes("/invitations")) {
if (uri.includes("/organizations")) {
return "/organizations";
} else if (uri.includes("/groups")) {
return "/groups";
} else if (uri.includes("/users")) {
return "/users";
} else if (uri.includes("/invitations")) {
return "/invitations";
}
} else if (uri.includes("/applications") || uri.includes("/providers") || uri.includes("/resources") || uri.includes("/certs")) {
if (uri.includes("/applications")) {
return "/applications";
} else if (uri.includes("/providers")) {
return "/providers";
} else if (uri.includes("/resources")) {
return "/resources";
} else if (uri.includes("/certs")) {
return "/certs";
}
} else if (uri.includes("/roles") || uri.includes("/permissions") || uri.includes("/models") || uri.includes("/adapters") || uri.includes("/enforcers")) {
if (uri.includes("/roles")) {
return "/roles";
} else if (uri.includes("/permissions")) {
return "/permissions";
} else if (uri.includes("/models")) {
return "/models";
} else if (uri.includes("/adapters")) {
return "/adapters";
} else if (uri.includes("/enforcers")) {
return "/enforcers";
}
} else if (uri.includes("/records") || uri.includes("/tokens") || uri.includes("/sessions") || uri.includes("/verifications")) {
if (uri.includes("/sessions")) {
return "/sessions";
} else if (uri.includes("/records")) {
return "/records";
} else if (uri.includes("/tokens")) {
return "/tokens";
} else if (uri.includes("/verifications")) {
return "/verifications";
}
} else if (uri.includes("/products") || uri.includes("/orders") || uri.includes("/payments") || uri.includes("/plans") || uri.includes("/pricings") || uri.includes("/subscriptions") || uri.includes("/transactions")) {
if (uri.includes("/products")) {
return "/products";
} else if (uri.includes("/orders")) {
return "/orders";
} else if (uri.includes("/payments")) {
return "/payments";
} else if (uri.includes("/plans")) {
return "/plans";
} else if (uri.includes("/pricings")) {
return "/pricings";
} else if (uri.includes("/subscriptions")) {
return "/subscriptions";
} else if (uri.includes("/transactions")) {
return "/transactions";
}
} else if (uri.includes("/sysinfo") || uri.includes("/forms") || uri.includes("/syncers") || uri.includes("/webhooks")) {
if (uri.includes("/sysinfo")) {
return "/sysinfo";
} else if (uri.includes("/forms")) {
return "/forms";
} else if (uri.includes("/syncers")) {
return "/syncers";
} else if (uri.includes("/webhooks")) {
return "/webhooks";
}
} else if (uri.includes("/signup")) {
return "/signup";
} else if (uri.includes("/login")) {
return "/login";
} else if (uri.includes("/result")) {
return "/result";
}
return -1;
}
updateMenuKey() {
const uri = location.pathname;
this.setState({
uri: uri,
});
// Check if menu should be flattened and use appropriate key selection
if (this.shouldFlattenMenu()) {
const selectedKey = this.getSelectedMenuKeyForFlatMenu(uri);
this.setState({selectedMenuKey: selectedKey});
return;
}
// Original logic for grouped menu
if (uri === "/" || uri.includes("/shortcuts") || uri.includes("/apps")) {
this.setState({selectedMenuKey: "/home"});
} else if (uri.includes("/organizations") || uri.includes("/trees") || uri.includes("/groups") || uri.includes("/users") || uri.includes("/invitations")) {
@@ -111,9 +293,9 @@ class App extends Component {
this.setState({selectedMenuKey: "/identity"});
} else if (uri.includes("/roles") || uri.includes("/permissions") || uri.includes("/models") || uri.includes("/adapters") || uri.includes("/enforcers")) {
this.setState({selectedMenuKey: "/auth"});
} else if (uri.includes("/records") || uri.includes("/tokens") || uri.includes("/sessions")) {
} else if (uri.includes("/records") || uri.includes("/tokens") || uri.includes("/sessions") || uri.includes("/verifications")) {
this.setState({selectedMenuKey: "/logs"});
} else if (uri.includes("/products") || uri.includes("/orders") || uri.includes("/payments") || uri.includes("/plans") || uri.includes("/pricings") || uri.includes("/subscriptions")) {
} else if (uri.includes("/product-store") || uri.includes("/products") || uri.includes("/orders") || uri.includes("/payments") || uri.includes("/plans") || uri.includes("/pricings") || uri.includes("/subscriptions") || uri.includes("/transactions")) {
this.setState({selectedMenuKey: "/business"});
} else if (uri.includes("/sysinfo") || uri.includes("/forms") || uri.includes("/syncers") || uri.includes("/webhooks")) {
this.setState({selectedMenuKey: "/admin"});
@@ -390,13 +572,15 @@ class App extends Component {
}
return (
<ConfigProvider theme={{
token: {
colorPrimary: themeData.colorPrimary,
borderRadius: themeData.borderRadius,
},
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
}}>
<ConfigProvider
locale={getAntdLocale(Setting.getLanguage())}
theme={{
token: {
colorPrimary: themeData.colorPrimary,
borderRadius: themeData.borderRadius,
},
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
}}>
<StyleProvider hashPriority="high" transformers={[legacyLogicalPropertiesTransformer]}>
<Layout id="parent-area">
<Content style={{display: "flex", justifyContent: "center"}}>
@@ -529,14 +713,16 @@ class App extends Component {
<link rel="icon" href={this.state.account.organization?.favicon} />
</Helmet>
}
<ConfigProvider theme={{
token: {
colorPrimary: this.state.themeData.colorPrimary,
colorInfo: this.state.themeData.colorPrimary,
borderRadius: this.state.themeData.borderRadius,
},
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
}}>
<ConfigProvider
locale={getAntdLocale(Setting.getLanguage())}
theme={{
token: {
colorPrimary: this.state.themeData.colorPrimary,
colorInfo: this.state.themeData.colorPrimary,
borderRadius: this.state.themeData.borderRadius,
},
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
}}>
<StyleProvider hashPriority="high" transformers={[legacyLogicalPropertiesTransformer]}>
{
this.renderPage()

View File

@@ -114,7 +114,7 @@ class BaseListPage extends React.Component {
ref={node => {
this.searchInput = node;
}}
placeholder={`Search ${dataIndex}`}
placeholder={i18next.t("general:Please input your search")}
value={selectedKeys[0]}
onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
@@ -129,10 +129,10 @@ class BaseListPage extends React.Component {
size="small"
style={{width: 90}}
>
Search
{i18next.t("general:Search")}
</Button>
<Button onClick={() => this.handleReset(clearFilters)} size="small" style={{width: 90}}>
Reset
{i18next.t("general:Reset")}
</Button>
<Button
type="link"
@@ -145,7 +145,7 @@ class BaseListPage extends React.Component {
});
}}
>
Filter
{i18next.t("general:Filter")}
</Button>
</Space>
</div>

View File

@@ -34,3 +34,6 @@ export const CustomFooter = null;
// Blank or null to hide Ai Assistant button
export const AiAssistantUrl = "https://ai.casbin.com";
// Maximum number of navbar items before switching from flat to grouped menu
export const MaxItemsForFlatMenu = 7;

View File

@@ -61,10 +61,12 @@ import SessionListPage from "./SessionListPage";
import TokenListPage from "./TokenListPage";
import TokenEditPage from "./TokenEditPage";
import ProductListPage from "./ProductListPage";
import ProductStorePage from "./ProductStorePage";
import ProductEditPage from "./ProductEditPage";
import ProductBuyPage from "./ProductBuyPage";
import OrderListPage from "./OrderListPage";
import OrderEditPage from "./OrderEditPage";
import OrderPayPage from "./OrderPayPage";
import PaymentListPage from "./PaymentListPage";
import PaymentEditPage from "./PaymentEditPage";
import PaymentResultPage from "./PaymentResultPage";
@@ -189,6 +191,10 @@ function ManagementPage(props) {
return !Array.isArray(widgetItems) || !!widgetItems?.includes("all");
}
function isSpecialMenuItem(item) {
return item.key === "#" || item.key === "logo";
}
function renderWidgets() {
const widgets = [
Setting.getItem(<ThemeSelect themeAlgorithm={props.themeAlgorithm} onChange={props.setLogoAndThemeAlgorithm} />, "theme"),
@@ -321,6 +327,7 @@ function ManagementPage(props) {
]));
res.push(Setting.getItem(<Link style={{color: textColor}} to="/products">{i18next.t("general:Business & Payments")}</Link>, "/business", <DollarTwoTone twoToneColor={twoToneColor} />, [
Setting.getItem(<Link to="/product-store">{i18next.t("general:Product Store")}</Link>, "/product-store"),
Setting.getItem(<Link to="/products">{i18next.t("general:Products")}</Link>, "/products"),
Setting.getItem(<Link to="/orders">{i18next.t("general:Orders")}</Link>, "/orders"),
Setting.getItem(<Link to="/payments">{i18next.t("general:Payments")}</Link>, "/payments"),
@@ -363,10 +370,36 @@ function ManagementPage(props) {
return item;
});
return resFiltered.filter(item => {
if (item.key === "#" || item.key === "logo") {return true;}
const filteredResult = resFiltered.filter(item => {
if (isSpecialMenuItem(item)) {return true;}
return Array.isArray(item.children) && item.children.length > 0;
});
// Count total end items (leaf nodes)
let totalEndItems = 0;
filteredResult.forEach(item => {
if (Array.isArray(item.children)) {
totalEndItems += item.children.length;
}
});
// If total end items <= MaxItemsForFlatMenu, flatten the menu (show only one level)
if (totalEndItems <= Conf.MaxItemsForFlatMenu) {
const flattenedResult = [];
filteredResult.forEach(item => {
if (isSpecialMenuItem(item)) {
flattenedResult.push(item);
} else if (Array.isArray(item.children)) {
// Add children directly without parent group
item.children.forEach(child => {
flattenedResult.push(child);
});
}
});
return flattenedResult;
}
return filteredResult;
}
function renderLoginIfNotLoggedIn(component) {
@@ -425,11 +458,13 @@ function ManagementPage(props) {
<Route exact path="/sessions" render={(props) => renderLoginIfNotLoggedIn(<SessionListPage account={account} {...props} />)} />
<Route exact path="/tokens" render={(props) => renderLoginIfNotLoggedIn(<TokenListPage account={account} {...props} />)} />
<Route exact path="/tokens/:tokenName" render={(props) => renderLoginIfNotLoggedIn(<TokenEditPage account={account} {...props} />)} />
<Route exact path="/product-store" render={(props) => renderLoginIfNotLoggedIn(<ProductStorePage account={account} {...props} />)} />
<Route exact path="/products" render={(props) => renderLoginIfNotLoggedIn(<ProductListPage account={account} {...props} />)} />
<Route exact path="/products/:organizationName/:productName" render={(props) => renderLoginIfNotLoggedIn(<ProductEditPage account={account} {...props} />)} />
<Route exact path="/products/:organizationName/:productName/buy" render={(props) => renderLoginIfNotLoggedIn(<ProductBuyPage account={account} {...props} />)} />
<Route exact path="/orders" render={(props) => renderLoginIfNotLoggedIn(<OrderListPage account={account} {...props} />)} />
<Route exact path="/orders/:organizationName/:orderName" render={(props) => renderLoginIfNotLoggedIn(<OrderEditPage account={account} {...props} />)} />
<Route exact path="/orders/:organizationName/:orderName/pay" render={(props) => renderLoginIfNotLoggedIn(<OrderPayPage account={account} {...props} />)} />
<Route exact path="/payments" render={(props) => renderLoginIfNotLoggedIn(<PaymentListPage account={account} {...props} />)} />
<Route exact path="/payments/:organizationName/:paymentName" render={(props) => renderLoginIfNotLoggedIn(<PaymentEditPage account={account} {...props} />)} />
<Route exact path="/payments/:organizationName/:paymentName/result" render={(props) => renderLoginIfNotLoggedIn(<PaymentResultPage account={account} {...props} />)} />

View File

@@ -57,6 +57,23 @@ class OrderListPage extends BaseListPage {
});
}
cancelOrder(order) {
OrderBackend.cancelOrder(order.owner, order.name)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully canceled"));
this.fetch({
pagination: this.state.pagination,
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to cancel")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteOrder(i) {
OrderBackend.deleteOrder(this.state.data[i])
.then((res) => {
@@ -199,12 +216,18 @@ class OrderListPage extends BaseListPage {
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "170px",
width: "240px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/orders/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<div style={{display: "flex", flexWrap: "wrap", gap: "8px"}}>
<Button onClick={() => this.props.history.push(`/orders/${record.owner}/${record.name}/pay`)} disabled={record.state !== "Created"}>
{i18next.t("order:Pay")}
</Button>
<Button danger onClick={() => this.cancelOrder(record)} disabled={record.state !== "Created"}>
{i18next.t("general:Cancel")}
</Button>
<Button type="primary" onClick={() => this.props.history.push(`/orders/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteOrder(index)}

302
web/src/OrderPayPage.js Normal file
View File

@@ -0,0 +1,302 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Descriptions, Spin} from "antd";
import i18next from "i18next";
import * as OrderBackend from "./backend/OrderBackend";
import * as ProductBackend from "./backend/ProductBackend";
import * as Setting from "./Setting";
class OrderPayPage extends React.Component {
constructor(props) {
super(props);
this.state = {
owner: props?.match?.params?.organizationName ?? props?.match?.params?.owner ?? null,
orderName: props?.match?.params?.orderName ?? null,
order: null,
product: null,
paymentEnv: "",
isProcessingPayment: false,
};
}
getPaymentEnv() {
let env = "";
const ua = navigator.userAgent.toLowerCase();
// Only support WeChat Pay in WeChat Browser for mobile devices
if (ua.indexOf("micromessenger") !== -1 && ua.indexOf("mobile") !== -1) {
env = "WechatBrowser";
}
this.setState({
paymentEnv: env,
});
}
componentDidMount() {
const params = new URLSearchParams(window.location.search);
if (params.get("created") === "1") {
Setting.showMessage("success", i18next.t("general:Successfully added"));
}
this.getOrder();
this.getPaymentEnv();
}
async getOrder() {
if (!this.state.owner || !this.state.orderName) {
return;
}
const res = await OrderBackend.getOrder(this.state.owner, this.state.orderName);
if (res.status === "ok") {
this.setState({
order: res.data,
}, () => {
this.getProduct();
});
} else {
Setting.showMessage("error", res.msg);
}
}
async getProduct() {
if (!this.state.order || !this.state.order.productName) {
return;
}
const res = await ProductBackend.getProduct(this.state.order.owner, this.state.order.productName);
if (res.status === "ok") {
this.setState({
product: res.data,
});
} else {
Setting.showMessage("error", res.msg);
}
}
getPrice(order) {
return `${Setting.getCurrencySymbol(order?.currency)}${order?.price} (${Setting.getCurrencyText(order)})`;
}
getProductPrice(product) {
return `${Setting.getCurrencySymbol(product?.currency)}${product?.price} (${Setting.getCurrencyText(product)})`;
}
// Call Wechat Pay via jsapi
onBridgeReady(attachInfo) {
const {WeixinJSBridge} = window;
this.setState({
isProcessingPayment: false,
});
WeixinJSBridge.invoke(
"getBrandWCPayRequest", {
"appId": attachInfo.appId,
"timeStamp": attachInfo.timeStamp,
"nonceStr": attachInfo.nonceStr,
"package": attachInfo.package,
"signType": attachInfo.signType,
"paySign": attachInfo.paySign,
},
function(res) {
if (res.err_msg === "get_brand_wcpay_request:ok") {
Setting.goToLink(attachInfo.payment.successUrl);
return;
}
if (res.err_msg === "get_brand_wcpay_request:cancel") {
Setting.showMessage("error", i18next.t("product:Payment cancelled"));
} else {
Setting.showMessage("error", i18next.t("product:Payment failed"));
}
}
);
}
// In WeChat browser, call this function to pay via jsapi
callWechatPay(attachInfo) {
const {WeixinJSBridge} = window;
if (typeof WeixinJSBridge === "undefined") {
document.addEventListener("WeixinJSBridgeReady", () => this.onBridgeReady(attachInfo), false);
} else {
this.onBridgeReady(attachInfo);
}
}
payOrder(provider) {
const {product, order} = this.state;
if (!product || !order) {
return;
}
this.setState({
isProcessingPayment: true,
});
OrderBackend.payOrder(order.owner, order.name, provider.name, this.state.paymentEnv)
.then((res) => {
if (res.status === "ok") {
const payment = res.data;
const attachInfo = res.data2;
let payUrl = payment.payUrl;
if (provider.type === "WeChat Pay") {
if (this.state.paymentEnv === "WechatBrowser") {
attachInfo.payment = payment;
this.callWechatPay(attachInfo);
return;
}
payUrl = `/qrcode/${payment.owner}/${payment.name}?providerName=${provider.name}&payUrl=${encodeURIComponent(payment.payUrl)}&successUrl=${encodeURIComponent(payment.successUrl)}`;
}
Setting.goToLink(payUrl);
} else {
Setting.showMessage("error", `${i18next.t("product:Payment failed")}: ${res.msg}`);
this.setState({
isProcessingPayment: false,
});
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
this.setState({
isProcessingPayment: false,
});
});
}
getPayButton(provider, onClick) {
const providerTypeMap = {
"Dummy": i18next.t("product:Dummy"),
"Alipay": i18next.t("product:Alipay"),
"WeChat Pay": i18next.t("product:WeChat Pay"),
"PayPal": i18next.t("product:PayPal"),
"Stripe": i18next.t("product:Stripe"),
"AirWallex": i18next.t("product:AirWallex"),
};
const text = providerTypeMap[provider.type] || provider.type;
return (
<Button style={{height: "50px", borderWidth: "2px"}} shape="round" icon={
<img style={{marginRight: "10px"}} width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
} size={"large"} onClick={onClick}>
{text}
</Button>
);
}
renderProviderButton(provider) {
return (
<span key={provider.name} style={{width: "200px", marginRight: "20px", marginBottom: "10px"}}>
{this.getPayButton(provider, () => this.payOrder(provider))}
</span>
);
}
renderPaymentMethods() {
const {product} = this.state;
if (!product || !product.providerObjs || product.providerObjs.length === 0) {
return <div>{i18next.t("product:There is no payment channel for this product.")}</div>;
}
return product.providerObjs.map(provider => {
return this.renderProviderButton(provider);
});
}
render() {
const {order, product} = this.state;
if (!order || !product) {
return null;
}
const isSubscriptionOrder = order.pricingName && order.planName;
return (
<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("order:Order Information")}</span>} bordered column={3}>
<Descriptions.Item label={i18next.t("order:Order ID")} span={3}>
<span style={{fontSize: 16}}>
{order.name}
</span>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("order:Order Status")}>
<span style={{fontSize: 16}}>
{order.state}
</span>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Created time")}>
<span style={{fontSize: 16}}>
{Setting.getFormattedDate(order.createdTime)}
</span>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:User")}>
<span style={{fontSize: 16}}>
{order.user}
</span>
</Descriptions.Item>
</Descriptions>
</div>
<div style={{marginBottom: "20px"}}>
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("product:Product Information")}</span>} bordered column={3}>
<Descriptions.Item label={i18next.t("general:Name")} span={3}>
<span style={{fontSize: 20}}>
{Setting.getLanguageText(product?.displayName)}
</span>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Image")} span={3}>
<img src={product?.image} alt={Setting.getLanguageText(product?.displayName)} height={90} style={{marginBottom: "20px"}} />
</Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Price")} span={3}>
<span style={{fontSize: 18, fontWeight: "bold"}}>
{this.getProductPrice(product)}
</span>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Detail")} span={3}>
<span style={{fontSize: 16}}>{Setting.getLanguageText(product?.detail)}</span>
</Descriptions.Item>
</Descriptions>
</div>
{isSubscriptionOrder && (
<div style={{marginBottom: "20px"}}>
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("subscription:Subscription Information")}</span>} bordered column={3}>
<Descriptions.Item label={i18next.t("general:Plan")} span={3}>
<span style={{fontSize: 16}}>{order.planName}</span>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Pricing")} span={3}>
<span style={{fontSize: 16}}>{order.pricingName}</span>
</Descriptions.Item>
</Descriptions>
</div>
)}
<div>
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("payment:Payment Information")}</span>} bordered column={3}>
<Descriptions.Item label={i18next.t("product:Price")} span={3}>
<span style={{fontSize: 28, color: "red", fontWeight: "bold"}}>
{this.getPrice(order)}
</span>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Pay")} span={3}>
{this.renderPaymentMethods()}
</Descriptions.Item>
</Descriptions>
</div>
</Spin>
</div>
);
}
}
export default OrderPayPage;

View File

@@ -559,6 +559,16 @@ class OrganizationEditPage extends React.Component {
<InputNumber value={this.state.organization.userBalance ?? 0} disabled />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("organization:Balance credit"), i18next.t("organization:Balance credit - Tooltip"))} :
</Col>
<Col span={4} >
<InputNumber value={this.state.organization.balanceCredit ?? 0} onChange={value => {
this.updateOrganizationField("balanceCredit", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("organization:Balance currency"), i18next.t("organization:Balance currency - Tooltip"))} :
@@ -808,7 +818,7 @@ class OrganizationEditPage extends React.Component {
}
{this.state.mode !== "add" && this.state.transactions.length > 0 ? (
<Card size="small" title={i18next.t("transaction:Transactions")} style={{marginTop: "20px"}} type="inner">
<TransactionTable transactions={this.state.transactions} />
<TransactionTable transactions={this.state.transactions} includeUser={true} />
</Card>
) : null}
<div style={{marginTop: "20px", marginLeft: "40px"}}>

View File

@@ -82,6 +82,7 @@ class OrganizationListPage extends BaseListPage {
{name: "Karma", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Ranking", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Balance", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Balance credit", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Balance currency", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Register type", visible: true, viewRule: "Public", modifyRule: "Admin"},
@@ -260,6 +261,16 @@ class OrganizationListPage extends BaseListPage {
return text ?? 0;
},
},
{
title: i18next.t("organization:Balance credit"),
dataIndex: "balanceCredit",
key: "balanceCredit",
width: "120px",
sorter: true,
render: (text, record, index) => {
return text ?? 0;
},
},
{
title: i18next.t("organization:Balance currency"),
dataIndex: "balanceCurrency",
@@ -278,7 +289,7 @@ class OrganizationListPage extends BaseListPage {
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
);
},
},

View File

@@ -306,7 +306,7 @@ class PermissionListPage extends BaseListPage {
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
);
},
},

View File

@@ -190,7 +190,7 @@ class PlanListPage extends BaseListPage {
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
);
},
},

View File

@@ -181,7 +181,7 @@ class PricingListPage extends BaseListPage {
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
);
},
},

View File

@@ -18,6 +18,7 @@ import i18next from "i18next";
import * as ProductBackend from "./backend/ProductBackend";
import * as PlanBackend from "./backend/PlanBackend";
import * as PricingBackend from "./backend/PricingBackend";
import * as OrderBackend from "./backend/OrderBackend";
import * as Setting from "./Setting";
class ProductBuyPage extends React.Component {
@@ -122,74 +123,23 @@ class ProductBuyPage extends React.Component {
return `${Setting.getCurrencySymbol(product?.currency)}${product?.price} (${Setting.getCurrencyText(product)})`;
}
// Call Weechat Pay via jsapi
onBridgeReady(attachInfo) {
const {WeixinJSBridge} = window;
// Setting.showMessage("success", "attachInfo is " + JSON.stringify(attachInfo));
this.setState({
isPlacingOrder: false,
});
WeixinJSBridge.invoke(
"getBrandWCPayRequest", {
"appId": attachInfo.appId,
"timeStamp": attachInfo.timeStamp,
"nonceStr": attachInfo.nonceStr,
"package": attachInfo.package,
"signType": attachInfo.signType,
"paySign": attachInfo.paySign,
},
function(res) {
if (res.err_msg === "get_brand_wcpay_request:ok") {
Setting.goToLink(attachInfo.payment.successUrl);
return ;
} else {
if (res.err_msg === "get_brand_wcpay_request:cancel") {
Setting.showMessage("error", i18next.t("product:Payment cancelled"));
} else {
Setting.showMessage("error", i18next.t("product:Payment failed"));
}
}
}
);
}
// In Wechat browser, call this function to pay via jsapi
callWechatPay(attachInfo) {
const {WeixinJSBridge} = window;
if (typeof WeixinJSBridge === "undefined") {
if (document.addEventListener) {
document.addEventListener("WeixinJSBridgeReady", () => this.onBridgeReady(attachInfo), false);
} else if (document.attachEvent) {
document.attachEvent("WeixinJSBridgeReady", () => this.onBridgeReady(attachInfo));
document.attachEvent("onWeixinJSBridgeReady", () => this.onBridgeReady(attachInfo));
}
} else {
this.onBridgeReady(attachInfo);
}
}
buyProduct(product, provider) {
placeOrder(product) {
this.setState({
isPlacingOrder: true,
});
ProductBackend.buyProduct(product.owner, product.name, provider.name, this.state.pricingName ?? "", this.state.planName ?? "", this.state.userName ?? "", this.state.paymentEnv, this.state.customPrice)
const productId = `${product.owner}/${product.name}`;
const pricingName = this.state.pricingName || "";
const planName = this.state.planName || "";
const customPrice = this.state.customPrice || 0;
OrderBackend.placeOrder(productId, pricingName, planName, this.state.userName ?? "", customPrice)
.then((res) => {
if (res.status === "ok") {
const payment = res.data;
const attachInfo = res.data2;
let payUrl = payment.payUrl;
if (provider.type === "WeChat Pay") {
if (this.state.paymentEnv === "WechatBrowser") {
attachInfo.payment = payment;
this.callWechatPay(attachInfo);
return ;
}
payUrl = `/qrcode/${payment.owner}/${payment.name}?providerName=${provider.name}&payUrl=${encodeURIComponent(payment.payUrl)}&successUrl=${encodeURIComponent(payment.successUrl)}`;
}
Setting.goToLink(payUrl);
const order = res.data;
// Redirect to order pay page
Setting.goToLink(`/orders/${order.owner}/${order.name}/pay?created=1`);
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
Setting.showMessage("error", `${i18next.t("product:Payment failed")}: ${res.msg}`);
this.setState({
isPlacingOrder: false,
});
@@ -197,49 +147,13 @@ class ProductBuyPage extends React.Component {
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
this.setState({
isPlacingOrder: false,
});
});
}
getPayButton(provider) {
let text = provider.type;
if (provider.type === "Dummy") {
text = i18next.t("product:Dummy");
} else if (provider.type === "Alipay") {
text = i18next.t("product:Alipay");
} else if (provider.type === "WeChat Pay") {
text = i18next.t("product:WeChat Pay");
} else if (provider.type === "PayPal") {
text = i18next.t("product:PayPal");
} else if (provider.type === "Stripe") {
text = i18next.t("product:Stripe");
} else if (provider.type === "AirWallex") {
text = i18next.t("product:AirWallex");
}
return (
<Button style={{height: "50px", borderWidth: "2px"}} shape="round" icon={
<img style={{marginRight: "10px"}} width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
} size={"large"} >
{
text
}
</Button>
);
}
renderProviderButton(provider, product) {
return (
<span key={provider.name} style={{width: "200px", marginRight: "20px", marginBottom: "10px"}}>
<span style={{width: "200px", cursor: "pointer"}} onClick={() => this.buyProduct(product, provider)}>
{
this.getPayButton(provider)
}
</span>
</span>
);
}
renderPay(product) {
renderPlaceOrderButton(product) {
if (product === undefined || product === null) {
return null;
}
@@ -247,17 +161,32 @@ class ProductBuyPage extends React.Component {
if (product.state !== "Published") {
return i18next.t("product:This product is currently not in sale.");
}
if (product.providerObjs.length === 0) {
return i18next.t("product:There is no payment channel for this product.");
}
return product.providerObjs.map(provider => {
return this.renderProviderButton(provider, product);
});
return (
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
<Button
type="primary"
size="large"
style={{
height: "50px",
fontSize: "18px",
borderRadius: "30px",
paddingLeft: "60px",
paddingRight: "60px",
}}
onClick={() => this.placeOrder(product)}
disabled={this.state.isPlacingOrder}
loading={this.state.isPlacingOrder}
>
{i18next.t("order:Place Order")}
</Button>
</div>
);
}
render() {
const product = this.getProductObj();
const placeOrderButton = this.renderPlaceOrderButton(product);
if (product === null) {
return null;
@@ -299,10 +228,10 @@ class ProductBuyPage extends React.Component {
</React.Fragment>
)
}
<Descriptions.Item label={i18next.t("product:Pay")} span={3}>
{
this.renderPay(product)
}
<Descriptions.Item label={i18next.t("order:Place Order")} span={3}>
<div style={{display: "flex", justifyContent: "center", alignItems: "center", minHeight: "80px"}}>
{placeOrderButton}
</div>
</Descriptions.Item>
</Descriptions>
</Spin>

146
web/src/ProductStorePage.js Normal file
View File

@@ -0,0 +1,146 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, Row, Tag, Typography} from "antd";
import * as Setting from "./Setting";
import * as ProductBackend from "./backend/ProductBackend";
import i18next from "i18next";
const {Text, Title} = Typography;
class ProductStorePage extends React.Component {
constructor(props) {
super(props);
this.state = {
products: [],
loading: true,
};
}
componentDidMount() {
this.getProducts();
}
getProducts() {
const pageSize = 100; // Max products to display in the store
const owner = Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account);
this.setState({loading: true});
ProductBackend.getProducts(owner, 1, pageSize, "state", "Published", "", "")
.then((res) => {
if (res.status === "ok") {
this.setState({
products: res.data,
loading: false,
});
} else {
Setting.showMessage("error", res.msg);
this.setState({loading: false});
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
this.setState({loading: false});
});
}
handleBuyProduct(product) {
this.props.history.push(`/products/${product.owner}/${product.name}/buy`);
}
renderProductCard(product) {
return (
<Col xs={24} sm={12} md={8} lg={6} key={`${product.owner}/${product.name}`} style={{marginBottom: "20px"}}>
<Card
hoverable
onClick={() => this.handleBuyProduct(product)}
style={{cursor: "pointer", height: "100%", display: "flex", flexDirection: "column"}}
cover={
<div style={{height: "200px", overflow: "hidden", display: "flex", alignItems: "center", justifyContent: "center", backgroundColor: "#f0f0f0"}}>
<img
alt={product.displayName}
src={product.image}
style={{width: "100%", height: "100%", objectFit: "contain"}}
/>
</div>
}
actions={[
<Button
key="buy"
type="primary"
onClick={(e) => {
e.stopPropagation();
this.handleBuyProduct(product);
}}
>
{i18next.t("product:Buy")}
</Button>,
]}
bodyStyle={{flex: 1, display: "flex", flexDirection: "column"}}
>
<div style={{flex: 1, display: "flex", flexDirection: "column"}}>
<Title level={5} ellipsis={{rows: 2}} style={{margin: "0 0 4px 0", minHeight: "44px"}}>
{Setting.getLanguageText(product.displayName)}
</Title>
<Text style={{display: "block", marginBottom: 4, minHeight: "40px"}} ellipsis={{rows: 2}}>
{Setting.getLanguageText(product.detail)}
</Text>
{product.tag && (
<Tag color="blue" style={{marginBottom: 4, display: "inline-block"}}>{product.tag}</Tag>
)}
<div style={{marginTop: "auto"}}>
<div style={{marginBottom: 4}}>
<Text strong style={{fontSize: "24px", color: "#ff4d4f"}}>
{Setting.getCurrencySymbol(product.currency)}{product.price}
</Text>
<Text type="secondary" style={{fontSize: "12px", marginLeft: 8}}>
{Setting.getCurrencyWithFlag(product.currency)}
</Text>
</div>
<div>
<Text type="secondary" style={{fontSize: "12px"}}>
{i18next.t("product:Sold")}: {product.sold}
</Text>
</div>
</div>
</div>
</Card>
</Col>
);
}
render() {
return (
<div>
<Row gutter={[16, 16]}>
{this.state.loading ? (
<Col span={24}>
<Card loading={true} />
</Col>
) : this.state.products.length === 0 ? (
<Col span={24}>
<Card>
<Text type="secondary">{i18next.t("general:No products available")}</Text>
</Card>
</Col>
) : (
this.state.products.map(product => this.renderProductCard(product))
)}
</Row>
</div>
);
}
}
export default ProductStorePage;

View File

@@ -192,7 +192,7 @@ class RecordListPage extends BaseListPage {
}
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
);
},
},

View File

@@ -214,7 +214,7 @@ class RoleListPage extends BaseListPage {
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
);
},
},

View File

@@ -435,6 +435,120 @@ export const OtherProviderInfo = {
},
};
export const UserFields = ["owner", "name", "password", "display_name", "id", "type", "email", "phone", "country_code",
"is_admin", "homepage", "birthday", "gender", "password_type", "password_salt", "external_id", "avatar", "first_name", "last_name",
"avatar_type", "permanent_avatar", "email_verified", "region", "location", "address",
"affiliation", "title", "id_card_type", "id_card", "bio", "tag", "language",
"education", "score", "karma", "ranking", "balance", "currency", "is_default_avatar", "is_online",
"is_forbidden", "is_deleted", "signup_application", "hash", "pre_hash", "access_key", "access_secret", "access_token",
"created_ip", "last_signin_time", "last_signin_ip", "github", "google", "qq", "wechat", "facebook", "dingtalk",
"weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs", "baidu", "alipay", "casdoor", "infoflow", "apple",
"azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "kwai", "line", "amazon", "auth0",
"battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru",
"meetup", "microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify",
"soundcloud", "spotify", "strava", "stripe", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk",
"wepay", "xero", "yahoo", "yammer", "yandex", "zoom", "metamask", "web3onboard", "custom", "webauthnCredentials",
"preferred_mfa_type", "recovery_codes", "totp_secret", "mfa_phone_enabled", "mfa_email_enabled", "invitation",
"invitation_code", "face_ids", "ldap", "properties", "roles", "permissions", "groups", "last_change_password_time",
"last_signin_wrong_time", "signin_wrong_times", "managedAccounts", "mfaAccounts", "need_update_password",
"created_time", "updated_time", "deleted_time",
"ip_whitelist"];
export const GetTranslatedUserItems = () => {
return [
{name: "Organization", label: i18next.t("general:Organization")},
{name: "ID", label: i18next.t("general:ID")},
{name: "Name", label: i18next.t("general:Name")},
{name: "Display name", label: i18next.t("general:Display name")},
{name: "First name", label: i18next.t("general:First name")},
{name: "Last name", label: i18next.t("general:Last name")},
{name: "Avatar", label: i18next.t("general:Avatar")},
{name: "User type", label: i18next.t("general:User type")},
{name: "Password", label: i18next.t("general:Password")},
{name: "Email", label: i18next.t("general:Email")},
{name: "Phone", label: i18next.t("general:Phone")},
{name: "Country code", label: i18next.t("user:Country code")},
{name: "Country/Region", label: i18next.t("user:Country/Region")},
{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: "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: "Homepage", label: i18next.t("user:Homepage")},
{name: "Bio", label: i18next.t("user:Bio")},
{name: "Tag", label: i18next.t("user:Tag")},
{name: "Language", label: i18next.t("user:Language")},
{name: "Gender", label: i18next.t("user:Gender")},
{name: "Birthday", label: i18next.t("user:Birthday")},
{name: "Education", label: i18next.t("user:Education")},
{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: "Score", label: i18next.t("user:Score")},
{name: "Karma", label: i18next.t("user:Karma")},
{name: "Ranking", label: i18next.t("user:Ranking")},
{name: "Signup application", label: i18next.t("general:Signup application")},
{name: "API key", label: i18next.t("general:API key")},
{name: "Groups", label: i18next.t("general:Groups")},
{name: "Roles", label: i18next.t("general:Roles")},
{name: "Permissions", label: i18next.t("general:Permissions")},
{name: "3rd-party logins", label: i18next.t("user:3rd-party logins")},
{name: "Properties", label: i18next.t("user:Properties")},
{name: "Is online", label: i18next.t("user:Is online")},
{name: "Is admin", label: i18next.t("user:Is admin")},
{name: "Is forbidden", label: i18next.t("user:Is forbidden")},
{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: "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: "MFA accounts", label: i18next.t("user:MFA accounts")},
{name: "MFA items", label: i18next.t("general:MFA items")},
];
};
export function getUserColumns() {
const items = GetTranslatedUserItems();
return UserFields.map(field => {
let transField = "";
if (field === "webauthnCredentials") {
transField = "WebAuthn credentials";
} else if (field === "region") {
transField = "Country/Region";
} else if (field === "mfaAccounts") {
transField = "MFA accounts";
} else if (field === "face_ids") {
transField = "Face ID";
} else if (field === "managedAccounts") {
transField = "Managed accounts";
} else {
transField = field.toLowerCase().split("_").join(" ");
transField = transField.charAt(0).toUpperCase() + transField.slice(1);
transField = transField.replace("ip", "IP")
.replace("Ip", "IP")
.replace("Id", "ID")
.replace("id", "ID");
}
if (transField === "Owner") {
transField = "Organization";
}
const transFieldItem = items.find(item => item.name === transField);
if (transFieldItem === undefined) {
const toTranslateList = ["general", "user", "organization"].map(ns => `${ns}:${transField}`);
const transResult = toTranslateList.map(item => i18next.t(item) === transField ? null : i18next.t(item))
.find(item => item !== null);
transField = transResult ? transResult : transField;
}
return `${transFieldItem ? transFieldItem.label : transField}#${field}`;
});
}
export function initCountries() {
const countries = require("i18n-iso-countries");
countries.registerLocale(require("i18n-iso-countries/langs/" + getLanguage() + ".json"));

View File

@@ -143,6 +143,9 @@ class SubscriptionListPage extends BaseListPage {
key: "startTime",
width: "140px",
...this.getColumnSearchProps("startTime"),
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("subscription:End time"),
@@ -150,6 +153,9 @@ class SubscriptionListPage extends BaseListPage {
key: "endTime",
width: "140px",
...this.getColumnSearchProps("endTime"),
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("general:Plan"),

View File

@@ -227,7 +227,7 @@ class SyncerListPage extends BaseListPage {
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
);
},
},

View File

@@ -16,6 +16,7 @@ import React from "react";
import * as TransactionBackend from "./backend/TransactionBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
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 i18next from "i18next";
@@ -33,6 +34,7 @@ class TransactionEditPage extends React.Component {
mode: props.location.mode !== undefined ? props.location.mode : "edit",
organizations: [],
applications: [],
users: [],
};
}
@@ -41,6 +43,7 @@ class TransactionEditPage extends React.Component {
if (this.state.mode === "recharge") {
this.getOrganizations();
this.getApplications(this.state.organizationName);
this.getUsers(this.state.organizationName);
}
}
@@ -100,6 +103,19 @@ 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;
@@ -189,6 +205,7 @@ 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>)
@@ -294,15 +311,6 @@ class TransactionEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} :
</Col>
<Col span={22} >
<Input disabled={true} value={this.state.transaction.user} onChange={e => {
}} />
</Col>
</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"))} :
@@ -313,13 +321,38 @@ class TransactionEditPage extends React.Component {
value={this.state.transaction.tag}
onChange={(value) => {
this.updateTransactionField("tag", value);
if (value === "Organization") {
this.updateTransactionField("user", "");
}
}}>
<Option value="Organization">Organization</Option>
<Option value="User">User</Option>
<Option value="Organization">Organization</Option>
</Select>
) : (
<Input disabled={true} value={this.state.transaction.tag} onChange={e => {
// this.updatePaymentField('currency', e.target.value);
}} />
)}
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} :
</Col>
<Col span={22} >
{isRechargeMode ? (
<Select virtual={false} style={{width: "100%"}}
value={this.state.transaction.user}
disabled={this.state.transaction.tag === "Organization"}
allowClear
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 => {
}} />
)}
</Col>

View File

@@ -14,13 +14,12 @@
import BaseListPage from "./BaseListPage";
import i18next from "i18next";
import {Link} from "react-router-dom";
import * as Setting from "./Setting";
import {Button, Table} from "antd";
import PopconfirmModal from "./common/modal/PopconfirmModal";
import React from "react";
import * as TransactionBackend from "./backend/TransactionBackend";
import moment from "moment/moment";
import {getTransactionTableColumns} from "./table/TransactionTableColumns";
class TransactionListPage extends BaseListPage {
newTransaction() {
@@ -86,15 +85,15 @@ class TransactionListPage extends BaseListPage {
const newTransaction = {
owner: organizationName,
createdTime: moment().format(),
application: "",
application: this.props.account.signupApplication || "",
domain: "",
category: "",
category: "Recharge",
type: "",
subtype: "",
provider: "",
user: "",
tag: "Organization",
amount: 0,
user: this.props.account.name || "",
tag: "User",
amount: 100,
currency: "USD",
payment: "",
state: "Paid",
@@ -115,249 +114,20 @@ class TransactionListPage extends BaseListPage {
}
renderTable(transactions) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
const columns = getTransactionTableColumns({
includeOrganization: true,
includeUser: true,
includeTag: true,
includeActions: true,
getColumnSearchProps: this.getColumnSearchProps,
account: this.props.account,
onEdit: (record, isAdmin) => {
this.props.history.push({pathname: `/transactions/${record.owner}/${record.name}`, mode: isAdmin ? "edit" : "view"});
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "180px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
return (
<Link to={`/transactions/${record.owner}/${record.name}`}>
{text}
</Link>
);
},
onDelete: (index) => {
this.deleteTransaction(index);
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
key: "createdTime",
width: "160px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("general:Application"),
dataIndex: "application",
key: "application",
width: "120px",
sorter: true,
...this.getColumnSearchProps("application"),
render: (text, record, index) => {
return (
<Link to={`/applications/${record.owner}/${record.application}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("provider:Domain"),
dataIndex: "domain",
key: "domain",
width: "200px",
sorter: true,
...this.getColumnSearchProps("domain"),
render: (text, record, index) => {
if (!text) {
return null;
}
return (
<a href={text} target="_blank" rel="noopener noreferrer">
{text}
</a>
);
},
},
{
title: i18next.t("provider:Category"),
dataIndex: "category",
key: "category",
width: "120px",
sorter: true,
...this.getColumnSearchProps("category"),
},
{
title: i18next.t("provider:Type"),
dataIndex: "type",
key: "type",
width: "140px",
sorter: true,
...this.getColumnSearchProps("type"),
render: (text, record, index) => {
if (text && record.domain) {
const chatUrl = `${record.domain}/chats/${text}`;
return (
<a href={chatUrl} target="_blank" rel="noopener noreferrer">
{text}
</a>
);
}
return text;
},
},
{
title: i18next.t("provider:Subtype"),
dataIndex: "subtype",
key: "subtype",
width: "140px",
sorter: true,
...this.getColumnSearchProps("subtype"),
render: (text, record, index) => {
if (text && record.domain) {
const messageUrl = `${record.domain}/messages/${text}`;
return (
<a href={messageUrl} target="_blank" rel="noopener noreferrer">
{text}
</a>
);
}
return text;
},
},
{
title: i18next.t("general:Provider"),
dataIndex: "provider",
key: "provider",
width: "150px",
sorter: true,
...this.getColumnSearchProps("provider"),
render: (text, record, index) => {
if (!text) {
return text;
}
if (record.domain) {
const casibaseUrl = `${record.domain}/providers/${text}`;
return (
<a href={casibaseUrl} target="_blank" rel="noopener noreferrer">
{text}
</a>
);
}
return (
<Link to={`/providers/${record.owner}/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:User"),
dataIndex: "user",
key: "user",
width: "120px",
sorter: true,
...this.getColumnSearchProps("user"),
render: (text, record, index) => {
if (!text || Setting.isAnonymousUserName(text)) {
return text;
}
return (
<Link to={`/users/${record.owner}/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("user:Tag"),
dataIndex: "tag",
key: "tag",
width: "120px",
sorter: true,
...this.getColumnSearchProps("tag"),
},
{
title: i18next.t("transaction:Amount"),
dataIndex: "amount",
key: "amount",
width: "120px",
sorter: true,
...this.getColumnSearchProps("amount"),
},
{
title: i18next.t("payment:Currency"),
dataIndex: "currency",
key: "currency",
width: "120px",
sorter: true,
...this.getColumnSearchProps("currency"),
render: (text, record, index) => {
return Setting.getCurrencyWithFlag(text);
},
},
{
title: i18next.t("general:Payment"),
dataIndex: "payment",
key: "payment",
width: "120px",
sorter: true,
...this.getColumnSearchProps("payment"),
render: (text, record, index) => {
if (!text) {
return text;
}
return (
<Link to={`/payments/${record.owner}/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:State"),
dataIndex: "state",
key: "state",
width: "120px",
sorter: true,
...this.getColumnSearchProps("state"),
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "240px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
const isAdmin = Setting.isLocalAdminUser(this.props.account);
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push({pathname: `/transactions/${record.owner}/${record.name}`, mode: isAdmin ? "edit" : "view"})}>{isAdmin ? i18next.t("general:Edit") : i18next.t("general:View")}</Button>
<PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteTransaction(index)}
disabled={!isAdmin}
>
</PopconfirmModal>
</div>
);
},
},
];
});
const paginationProps = {
total: this.state.pagination.total,
@@ -374,7 +144,7 @@ class TransactionListPage extends BaseListPage {
return (
<div>
{i18next.t("general:Transactions")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" disabled={!isAdmin} onClick={this.addTransaction.bind(this)}>{i18next.t("general:Add")}</Button>
<Button size="small" disabled={!isAdmin} onClick={this.addTransaction.bind(this)}>{i18next.t("general:Add")}</Button>
&nbsp;&nbsp;
<Button type="primary" size="small" disabled={!isAdmin} onClick={this.rechargeTransaction.bind(this)}>{i18next.t("transaction:Recharge")}</Button>
</div>

View File

@@ -745,6 +745,19 @@ class UserEditPage extends React.Component {
</Col>
</Row>
);
} else if (accountItem.name === "Balance credit") {
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"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.user.balanceCredit ?? 0} onChange={value => {
this.updateUserField("balanceCredit", value);
}} />
</Col>
</Row>
);
} else if (accountItem.name === "Balance currency") {
return (
<Row style={{marginTop: "20px"}} >

View File

@@ -14,7 +14,7 @@
import React from "react";
import {Link} from "react-router-dom";
import {Button, Space, Switch, Table, Upload} from "antd";
import {Button, Modal, Space, Switch, Table, Upload} from "antd";
import {UploadOutlined} from "@ant-design/icons";
import moment from "moment";
import * as OrganizationBackend from "./backend/OrganizationBackend";
@@ -24,6 +24,7 @@ import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./common/modal/PopconfirmModal";
import AccountAvatar from "./account/AccountAvatar";
import * as XLSX from "xlsx";
class UserListPage extends BaseListPage {
constructor(props) {
@@ -149,19 +150,27 @@ class UserListPage extends BaseListPage {
}
uploadFile(info) {
const {status, response: res} = info.file;
if (status === "done") {
if (res.status === "ok") {
Setting.showMessage("success", "Users uploaded successfully, refreshing the page");
const {pagination} = this.state;
this.fetch({pagination});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to upload")}: ${res.msg}`);
}
const {status, msg} = info;
if (status === "ok") {
Setting.showMessage("success", "Users uploaded successfully, refreshing the page");
const {pagination} = this.state;
this.fetch({pagination});
} else if (status === "error") {
Setting.showMessage("error", i18next.t("general:Failed to upload"));
Setting.showMessage("error", `${i18next.t("general:Failed to upload")}: ${msg}`);
}
this.setState({uploadJsonData: [], uploadColumns: [], showUploadModal: false});
}
generateDownloadTemplate() {
const userObj = {};
const items = Setting.getUserColumns();
items.forEach((item) => {
userObj[item] = null;
});
const worksheet = XLSX.utils.json_to_sheet([userObj]);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1");
XLSX.writeFile(workbook, "import-user.xlsx", {compression: true});
}
getOrganization(organizationName) {
@@ -178,23 +187,82 @@ class UserListPage extends BaseListPage {
}
renderUpload() {
const uploadThis = this;
const props = {
name: "file",
accept: ".xlsx",
method: "post",
action: `${Setting.ServerUrl}/api/upload-users`,
withCredentials: true,
onChange: (info) => {
this.uploadFile(info);
showUploadList: false,
beforeUpload: (file) => {
const reader = new FileReader();
reader.onload = (e) => {
const binary = e.target.result;
try {
const workbook = XLSX.read(binary, {type: "array"});
if (!workbook.SheetNames || workbook.SheetNames.length === 0) {
Setting.showMessage("error", i18next.t("general:No sheets found in file"));
return;
}
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
const jsonData = XLSX.utils.sheet_to_json(worksheet);
this.setState({uploadJsonData: jsonData, file: file});
const columns = Setting.getUserColumns().map(el => {
return {title: el.split("#")[0], dataIndex: el, key: el};
});
this.setState({uploadColumns: columns}, () => {this.setState({showUploadModal: true});});
} catch (err) {
Setting.showMessage("error", `${i18next.t("general:Failed to upload")}: ${err.message}`);
}
};
reader.onerror = (error) => {
Setting.showMessage("error", `${i18next.t("general:Failed to upload")}: ${error?.message || error}`);
};
reader.readAsArrayBuffer(file);
return false;
},
};
return (
<Upload {...props}>
<Button icon={<UploadOutlined />} id="upload-button" size="small">
{i18next.t("user:Upload (.xlsx)")}
</Button>
</Upload>
<>
<Upload {...props}>
<Button icon={<UploadOutlined />} id="upload-button" size="small">
{i18next.t("user:Upload (.xlsx)")}
</Button>
</Upload>
<Modal title={i18next.t("user:Upload (.xlsx)")}
width={"100%"}
closable={true}
open={this.state.showUploadModal}
okText={i18next.t("general:Click to Upload")}
onOk = {() => {
const formData = new FormData();
formData.append("file", this.state.file);
fetch(`${Setting.ServerUrl}/api/upload-users`, {
method: "post",
body: formData,
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
})
.then((res) => res.json())
.then((res) => {uploadThis.uploadFile(res);})
.catch((error) => {
Setting.showMessage("error", `${i18next.t("general:Failed to upload")}: ${error.message}`);
});
}}
cancelText={i18next.t("general:Cancel")}
onCancel={() => {this.setState({showUploadModal: false, uploadJsonData: [], uploadColumns: []});}}
>
<div style={{marginRight: "34px"}}>
<Table scroll={{x: "max-content"}} dataSource={this.state.uploadJsonData} columns={this.state.uploadColumns} />
</div>
</Modal>
</>
);
}
@@ -376,6 +444,16 @@ class UserListPage extends BaseListPage {
return text ?? 0;
},
},
{
title: i18next.t("user:Balance credit"),
dataIndex: "balanceCredit",
key: "balanceCredit",
width: "120px",
sorter: true,
render: (text, record, index) => {
return text ?? 0;
},
},
{
title: i18next.t("user:Balance currency"),
dataIndex: "balanceCurrency",
@@ -394,7 +472,7 @@ class UserListPage extends BaseListPage {
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
);
},
},
@@ -406,7 +484,7 @@ class UserListPage extends BaseListPage {
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
);
},
},
@@ -418,7 +496,7 @@ class UserListPage extends BaseListPage {
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
);
},
},
@@ -473,6 +551,7 @@ class UserListPage extends BaseListPage {
<div>
{i18next.t("general:Users")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "15px"}} type="primary" size="small" onClick={this.addUser.bind(this)}>{i18next.t("general:Add")} </Button>
<Button style={{marginRight: "15px"}} type="primary" size="small" onClick={this.generateDownloadTemplate}>{i18next.t("general:Download template")} </Button>
{
this.renderUpload()
}

View File

@@ -154,7 +154,7 @@ class VerificationListPage extends BaseListPage {
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
);
},
},

View File

@@ -175,7 +175,7 @@ class WebhookListPage extends BaseListPage {
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
);
},
},
@@ -187,7 +187,7 @@ class WebhookListPage extends BaseListPage {
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
);
},
},
@@ -200,7 +200,7 @@ class WebhookListPage extends BaseListPage {
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
);
},
},

View File

@@ -290,6 +290,10 @@ const authInfo = {
scope: "users.read%20tweet.read",
endpoint: "https://twitter.com/i/oauth2/authorize",
},
Telegram: {
scope: "",
endpoint: "https://core.telegram.org/widgets/login",
},
Typetalk: {
scope: "my",
endpoint: "https://typetalk.com/oauth2/authorize",

View File

@@ -24,6 +24,16 @@ export function getOrders(owner, page = "", pageSize = "", field = "", value = "
}).then(res => res.json());
}
export function payOrder(owner, name, providerName, paymentEnv = "") {
return fetch(`${Setting.ServerUrl}/api/pay-order?id=${owner}/${encodeURIComponent(name)}&providerName=${encodeURIComponent(providerName)}&paymentEnv=${paymentEnv}`, {
method: "POST",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function getUserOrders(owner, user) {
return fetch(`${Setting.ServerUrl}/api/get-user-orders?owner=${owner}&user=${user}`, {
method: "GET",
@@ -79,3 +89,23 @@ export function deleteOrder(order) {
},
}).then(res => res.json());
}
export function placeOrder(productId, pricingName = "", planName = "", userName = "", customPrice = 0) {
return fetch(`${Setting.ServerUrl}/api/place-order?productId=${encodeURIComponent(productId)}&pricingName=${encodeURIComponent(pricingName)}&planName=${encodeURIComponent(planName)}&userName=${encodeURIComponent(userName)}&customPrice=${customPrice}`, {
method: "POST",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function cancelOrder(owner, name) {
return fetch(`${Setting.ServerUrl}/api/cancel-order?id=${owner}/${encodeURIComponent(name)}`, {
method: "POST",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}

View File

@@ -69,13 +69,3 @@ export function deleteProduct(product) {
},
}).then(res => res.json());
}
export function buyProduct(owner, name, providerName, pricingName = "", planName = "", userName = "", paymentEnv = "", customPrice = 0) {
return fetch(`${Setting.ServerUrl}/api/buy-product?id=${owner}/${encodeURIComponent(name)}&providerName=${providerName}&pricingName=${pricingName}&planName=${planName}&userName=${userName}&paymentEnv=${paymentEnv}&customPrice=${customPrice}`, {
method: "POST",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "شهادة المفتاح العام التي يجب التحقق منها بواسطة SDK العميل المقابل لهذا التطبيق",
"Certs": "الشهادات",
"Click to Upload": "انقر للتحميل",
"Click to cancel sorting": "انقر لإلغاء الفرز",
"Click to sort ascending": "انقر للفرز تصاعديًا",
"Click to sort descending": "انقر للفرز تنازليًا",
"Client IP": "IP العميل",
"Close": "إغلاق",
"Confirm": "تأكيد",
@@ -323,6 +326,7 @@
"False": "خاطئ",
"Favicon": "أيقونة المفضلة",
"Favicon - Tooltip": "رابط رمز الموقع المستخدم في جميع صفحات Casdoor الخاصة بالمنظمة",
"Filter": "تصفية",
"First name": "الاسم الأول",
"First name - Tooltip": "الاسم الأول للمستخدم",
"Forced redirect origin - Tooltip": "إعادة توجيه الأصل بالقوة - تلميح",
@@ -378,7 +382,9 @@
"Non-LDAP": "غير LDAP",
"None": "لا شيء",
"OAuth providers": "مزودو OAuth",
"OFF": "إيقاف",
"OK": "موافق",
"ON": "تشغيل",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "المنظمة",
"Organization - Tooltip": "يشبه مفاهيم مثل المستأجرين أو تجمعات المستخدمين، كل مستخدم وتطبيق ينتمي إلى منظمة",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "خطة الاشتراك",
"Plans": "الخطط",
"Plans - Tooltip": "الخطط - تلميح",
"Please input your search": "يرجى إدخال بحثك",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "معاينة",
"Preview - Tooltip": "معاينة التأثيرات المُعدّلة",
@@ -427,6 +434,7 @@
"Records": "السجلات",
"Request": "الطلب",
"Request URI": "رابط الطلب",
"Reset": "إعادة تعيين",
"Reset to Default": "إعادة تعيين إلى الافتراضي",
"Resources": "الموارد",
"Role": "الدور",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "نوع المصادقة لاتصال SSH",
"Save": "حفظ",
"Save & Exit": "حفظ وخروج",
"Search": "بحث",
"Send": "إرسال",
"Session ID": "معرف الجلسة",
"Sessions": "الجلسات",
@@ -525,7 +534,7 @@
"Show all": "عرض الكل",
"Upload (.xlsx)": "تحميل (.xlsx)",
"Virtual": "افتراضي",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "تحتاج إلى حذف جميع المجموعات الفرعية أولاً. يمكنك عرض المجموعات الفرعية في شجرة المجموعات على اليسار في صفحة [المنظمات] -\u003e [المجموعات]"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "تحتاج إلى حذف جميع المجموعات الفرعية أولاً. يمكنك عرض المجموعات الفرعية في شجرة المجموعات على اليسار في صفحة [المنظمات] -> [المجموعات]"
},
"home": {
"New users past 30 days": "مستخدمون جدد خلال آخر 30 يومًا",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "Bu tətbiqə uyğun müştəri SDK-sının yoxlaması lazım olan açıq açar sertifikatı",
"Certs": "Sertifikatlar",
"Click to Upload": "Yükləmək üçün klikləyin",
"Click to cancel sorting": "Sıralamanı ləğv etmək üçün klikləyin",
"Click to sort ascending": "Artan sıralama üçün klikləyin",
"Click to sort descending": "Azalan sıralama üçün klikləyin",
"Client IP": "Müştəri IP",
"Close": "Bağla",
"Confirm": "Təsdiqlə",
@@ -323,6 +326,7 @@
"False": "Yanlış",
"Favicon": "Favicon",
"Favicon - Tooltip": "Təşkilatın bütün Casdoor səhifələrində istifadə olunan favicon ikon URL-i",
"Filter": "Filtr",
"First name": "Ad",
"First name - Tooltip": "İstifadəçinin adı",
"Forced redirect origin - Tooltip": "Məcburi yönləndirmə mənbəyi",
@@ -378,7 +382,9 @@
"Non-LDAP": "LDAP deyil",
"None": "Heç biri",
"OAuth providers": "OAuth provayderlər",
"OFF": "BAĞLI",
"OK": "OK",
"ON": "AÇIQ",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Təşkilat",
"Organization - Tooltip": "Kirayəçi və ya istifadəçi hovuzu kimi anlayışlara oxşar, hər istifadəçi və tətbiq bir təşkilata aiddir",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Abunə planı",
"Plans": "Planlar",
"Plans - Tooltip": "Planlar - Tooltip",
"Please input your search": "Xahiş edirik axtarışınızı daxil edin",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Önizləmə",
"Preview - Tooltip": "Konfiqurasiya edilmiş effektləri önizlə",
@@ -427,6 +434,7 @@
"Records": "Qeydlər",
"Request": "Sorğu",
"Request URI": "Sorğu URI",
"Reset": "Sıfırla",
"Reset to Default": "Standarta sıfırla",
"Resources": "Resurslar",
"Role": "Rol",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "SSH bağlantısının auth növü",
"Save": "Yadda saxla",
"Save & Exit": "Yadda saxla və Çıx",
"Search": "Axtar",
"Send": "Göndər",
"Session ID": "Sessiya ID",
"Sessions": "Sessiyalar",
@@ -525,7 +534,7 @@
"Show all": "Hamısını göstər",
"Upload (.xlsx)": "Yüklə (.xlsx)",
"Virtual": "Virtual",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Əvvəlcə bütün alt qrupları silməlisiniz. Alt qrupları [Təşkilatlar] -\u003e [Qruplar] səhifəsinin sol qrup ağacında görə bilərsiniz"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Əvvəlcə bütün alt qrupları silməlisiniz. Alt qrupları [Təşkilatlar] -> [Qruplar] səhifəsinin sol qrup ağacında görə bilərsiniz"
},
"home": {
"New users past 30 days": "Son 30 gündə yeni istifadəçilər",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "Veřejný klíčový certifikát, který musí být ověřen klientským SDK odpovídajícím této aplikaci",
"Certs": "Certifikáty",
"Click to Upload": "Klikněte pro nahrání",
"Click to cancel sorting": "Kliknutím zrušíte řazení",
"Click to sort ascending": "Kliknutím seřadíte vzestupně",
"Click to sort descending": "Kliknutím seřadíte sestupně",
"Client IP": "IP klienta",
"Close": "Zavřít",
"Confirm": "Potvrdit",
@@ -323,6 +326,7 @@
"False": "Nepravda",
"Favicon": "Ikona webu",
"Favicon - Tooltip": "URL ikony favicon použité na všech stránkách Casdoor organizace",
"Filter": "Filtrovat",
"First name": "Křestní jméno",
"First name - Tooltip": "Křestní jméno uživatele",
"Forced redirect origin - Tooltip": "Původ vynuceného přesměrování",
@@ -378,7 +382,9 @@
"Non-LDAP": "Ne-LDAP",
"None": "Žádný",
"OAuth providers": "OAuth poskytovatelé",
"OFF": "VYPNUTO",
"OK": "OK",
"ON": "ZAPNUTO",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Organizace",
"Organization - Tooltip": "Podobné konceptům jako nájemci nebo uživatelské bazény, každý uživatel a aplikace patří do organizace",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Plán předplatného",
"Plans": "Plány",
"Plans - Tooltip": "Plány - popisek",
"Please input your search": "Zadejte prosím vyhledávání",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Náhled",
"Preview - Tooltip": "Náhled nakonfigurovaných efektů",
@@ -427,6 +434,7 @@
"Records": "Záznamy",
"Request": "Požadavek",
"Request URI": "URI požadavku",
"Reset": "Obnovit",
"Reset to Default": "Obnovit výchozí",
"Resources": "Zdroje",
"Role": "Role",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "Typ ověření SSH připojení",
"Save": "Uložit",
"Save & Exit": "Uložit & Ukončit",
"Search": "Hledat",
"Send": "Odeslat",
"Session ID": "ID relace",
"Sessions": "Relace",
@@ -525,7 +534,7 @@
"Show all": "Zobrazit vše",
"Upload (.xlsx)": "Nahrát (.xlsx)",
"Virtual": "Virtuální",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Nejprve musíte odstranit všechny podskupiny. Podskupiny můžete zobrazit ve stromu skupin vlevo na stránce [Organizace] -\u003e [Skupiny]"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Nejprve musíte odstranit všechny podskupiny. Podskupiny můžete zobrazit ve stromu skupin vlevo na stránce [Organizace] -> [Skupiny]"
},
"home": {
"New users past 30 days": "Noví uživatelé za posledních 30 dní",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "Das Public-Key-Zertifikat, das vom Client-SDK, das mit dieser Anwendung korrespondiert, verifiziert werden muss",
"Certs": "Zertifikate",
"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",
@@ -323,6 +326,7 @@
"False": "Falsch",
"Favicon": "Favicon",
"Favicon - Tooltip": "Favicon-URL, die auf allen Casdoor-Seiten der Organisation verwendet wird",
"Filter": "Filtern",
"First name": "Vorname",
"First name - Tooltip": "Der Vorname des Benutzers",
"Forced redirect origin - Tooltip": "Erzwungener Weiterleitungsursprung",
@@ -378,7 +382,9 @@
"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",
"Organization": "Organisation",
"Organization - Tooltip": "Ähnlich wie bei Konzepten wie Mietern oder Benutzerpools gehört jeder Benutzer und jede Anwendung einer Organisation an",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Abonnementplan",
"Plans": "Pläne",
"Plans - Tooltip": "Pläne",
"Please input your search": "Bitte geben Sie Ihre Suche ein",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Vorschau",
"Preview - Tooltip": "Vorschau der konfigurierten Effekte",
@@ -427,6 +434,7 @@
"Records": "Datensätze",
"Request": "Anfrage",
"Request URI": "Anfrage-URI",
"Reset": "Zurücksetzen",
"Reset to Default": "Auf Standard zurücksetzen",
"Resources": "Ressourcen",
"Role": "Rolle",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "Der Authentifizierungstyp für SSH-Verbindungen",
"Save": "Speichern",
"Save & Exit": "Speichern und verlassen",
"Search": "Suchen",
"Send": "Senden",
"Session ID": "Session-ID",
"Sessions": "Sitzungen",
@@ -525,7 +534,7 @@
"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] -\u003e [Groups] page": "Sie müssen zuerst alle Untergruppen löschen. Sie können die Untergruppen im linken Gruppenbaum unter [Organisationen] -\u003e [Gruppen] anzeigen."
"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."
},
"home": {
"New users past 30 days": "Neue Benutzer der letzten 30 Tage",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "The public key certificate that needs to be verified by the client SDK corresponding to this application",
"Certs": "Certs",
"Click to Upload": "Click to Upload",
"Click to cancel sorting": "Click to cancel sorting",
"Click to sort ascending": "Click to sort ascending",
"Click to sort descending": "Click to sort descending",
"Client IP": "Client IP",
"Close": "Close",
"Confirm": "Confirm",
@@ -323,6 +326,7 @@
"False": "False",
"Favicon": "Favicon",
"Favicon - Tooltip": "Favicon icon URL used in all Casdoor pages of the organization",
"Filter": "Filter",
"First name": "First name",
"First name - Tooltip": "The first name of user",
"Forced redirect origin - Tooltip": "Forced redirect origin",
@@ -374,11 +378,14 @@
"Name": "Name",
"Name - Tooltip": "Unique, string-based ID",
"Name format": "Name format",
"No products available": "No products available",
"No verification method": "No verification method",
"Non-LDAP": "Non-LDAP",
"None": "None",
"OAuth providers": "OAuth providers",
"OFF": "OFF",
"OK": "OK",
"ON": "ON",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Organization",
"Organization - Tooltip": "Similar to concepts such as tenants or user pools, each user and application belongs to an organization",
@@ -407,6 +414,7 @@
"Phone only": "Phone only",
"Phone or Email": "Phone or Email",
"Plain": "Plain",
"Please input your search": "Please input your search",
"Plan": "Plan",
"Plan - Tooltip": "Plan in the subscription",
"Plans": "Plans",
@@ -417,6 +425,7 @@
"Pricing": "Pricing",
"Pricing - Tooltip": "Corresponding pricing",
"Pricings": "Pricings",
"Product Store": "Product Store",
"Products": "Products",
"Provider": "Provider",
"Provider - Tooltip": "Payment providers to be configured, including PayPal, Alipay, WeChat Pay, etc.",
@@ -428,6 +437,7 @@
"Records": "Records",
"Request": "Request",
"Request URI": "Request URI",
"Reset": "Reset",
"Reset to Default": "Reset to Default",
"Resources": "Resources",
"Role": "Role",
@@ -443,6 +453,7 @@
"SSH type - Tooltip": "The auth type of SSH connection",
"Save": "Save",
"Save & Exit": "Save & Exit",
"Search": "Search",
"Send": "Send",
"Session ID": "Session ID",
"Sessions": "Sessions",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "El certificado de clave pública que necesita ser verificado por el SDK del cliente correspondiente a esta aplicación",
"Certs": "Certificaciones",
"Click to Upload": "Haz clic para cargar",
"Click to cancel sorting": "Haga clic para cancelar ordenación",
"Click to sort ascending": "Haga clic para ordenar ascendente",
"Click to sort descending": "Haga clic para ordenar descendente",
"Client IP": "IP del cliente",
"Close": "Cerca",
"Confirm": "Confirmar",
@@ -323,6 +326,7 @@
"False": "Falso",
"Favicon": "Favicon",
"Favicon - Tooltip": "URL del icono Favicon utilizado en todas las páginas de Casdoor de la organización",
"Filter": "Filtrar",
"First name": "Nombre de pila",
"First name - Tooltip": "El nombre del usuario",
"Forced redirect origin - Tooltip": "Origen de redirección forzada",
@@ -378,7 +382,9 @@
"Non-LDAP": "No LDAP",
"None": "Ninguno",
"OAuth providers": "Proveedores de OAuth",
"OFF": "DESACTIVADO",
"OK": "Aceptar",
"ON": "ACTIVADO",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Organización",
"Organization - Tooltip": "Similar a conceptos como inquilinos o grupos de usuarios, cada usuario y aplicación pertenece a una organización",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Plan de suscripción",
"Plans": "Planes",
"Plans - Tooltip": "Planes - Información adicional",
"Please input your search": "Por favor ingrese su búsqueda",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Avance",
"Preview - Tooltip": "Vista previa de los efectos configurados",
@@ -427,6 +434,7 @@
"Records": "Registros",
"Request": "Solicitud",
"Request URI": "URI de solicitud",
"Reset": "Restablecer",
"Reset to Default": "Restablecer a predeterminado",
"Resources": "Recursos",
"Role": "Rol",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "El tipo de autenticación de conexión SSH",
"Save": "Guardar",
"Save & Exit": "Guardar y salir",
"Search": "Buscar",
"Send": "Enviar",
"Session ID": "ID de sesión",
"Sessions": "Sesiones",
@@ -525,7 +534,7 @@
"Show all": "Mostrar todos",
"Upload (.xlsx)": "Cargar (.xlsx)",
"Virtual": "Virtual",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Necesitas eliminar todos los subgrupos primero. Puedes ver los subgrupos en el árbol de grupos a la izquierda en la página [Organizaciones] -\u003e [Grupos]"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Necesitas eliminar todos los subgrupos primero. Puedes ver los subgrupos en el árbol de grupos a la izquierda en la página [Organizaciones] -> [Grupos]"
},
"home": {
"New users past 30 days": "Nuevos usuarios en los últimos 30 días",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "گواهی کلید عمومی که نیاز به تأیید توسط SDK کلاینت مربوط به این برنامه دارد",
"Certs": "گواهی‌ها",
"Click to Upload": "برای بارگذاری کلیک کنید",
"Click to cancel sorting": "برای لغو مرتب‌سازی کلیک کنید",
"Click to sort ascending": "برای مرتب‌سازی صعودی کلیک کنید",
"Click to sort descending": "برای مرتب‌سازی نزولی کلیک کنید",
"Client IP": "IP کلاینت",
"Close": "بستن",
"Confirm": "تأیید",
@@ -323,6 +326,7 @@
"False": "غلط",
"Favicon": "آیکون وب",
"Favicon - Tooltip": "آدرس آیکون Favicon استفاده شده در تمام صفحات Casdoor سازمان",
"Filter": "فیلتر",
"First name": "نام",
"First name - Tooltip": "نام کاربر",
"Forced redirect origin - Tooltip": "مبدأ تغییر مسیر اجباری",
@@ -378,7 +382,9 @@
"Non-LDAP": "غیر LDAP",
"None": "هیچ‌کدام",
"OAuth providers": "ارائه‌دهندگان OAuth",
"OFF": "خاموش",
"OK": "تأیید",
"ON": "روشن",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "سازمان",
"Organization - Tooltip": "مشابه مفاهیمی مانند مستأجران یا استخرهای کاربر، هر کاربر و برنامه به یک سازمان تعلق دارند",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "برنامه اشتراک",
"Plans": "طرح‌ها",
"Plans - Tooltip": "طرح‌ها - راهنمای ابزار",
"Please input your search": "لطفا جستجوی خود را وارد کنید",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "پیش‌نمایش",
"Preview - Tooltip": "پیش‌نمایش اثرات پیکربندی شده",
@@ -427,6 +434,7 @@
"Records": "سوابق",
"Request": "درخواست",
"Request URI": "آدرس URI درخواست",
"Reset": "بازنشانی",
"Reset to Default": "بازنشانی به پیش‌فرض",
"Resources": "منابع",
"Role": "نقش",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "نوع احراز هویت اتصال SSH",
"Save": "ذخیره",
"Save & Exit": "ذخیره و خروج",
"Search": "جستجو",
"Send": "ارسال",
"Session ID": "شناسه جلسه",
"Sessions": "جلسات",
@@ -525,7 +534,7 @@
"Show all": "نمایش همه",
"Upload (.xlsx)": "آپلود (.xlsx)",
"Virtual": "مجازی",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "ابتدا باید همه زیرگروه‌ها را حذف کنید. می‌توانید زیرگروه‌ها را در درخت گروه سمت چپ صفحه [سازمان‌ها] -\u003e [گروه‌ها] مشاهده کنید"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "ابتدا باید همه زیرگروه‌ها را حذف کنید. می‌توانید زیرگروه‌ها را در درخت گروه سمت چپ صفحه [سازمان‌ها] -> [گروه‌ها] مشاهده کنید"
},
"home": {
"New users past 30 days": "کاربران جدید در ۳۰ روز گذشته",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "Julkinen avainvarmenne, joka tarkistetaan asiakkaan SDK:n puolella vastaavasti tälle sovellukselle",
"Certs": "Varmenteet",
"Click to Upload": "Klikkaa ladataksesi",
"Click to cancel sorting": "Napsauta peruuttaaksesi lajittelun",
"Click to sort ascending": "Napsauta lajitellaksesi nousevasti",
"Click to sort descending": "Napsauta lajitellaksesi laskevasti",
"Client IP": "Asiakkaan IP",
"Close": "Sulje",
"Confirm": "Vahvista",
@@ -323,6 +326,7 @@
"False": "Epätosi",
"Favicon": "Sivuston ikoni",
"Favicon - Tooltip": "Favicon-kuvakkeen URL, jota käytetään kaikissa Casdoor-sivuissa organisaatiolle",
"Filter": "Suodata",
"First name": "Etunimi",
"First name - Tooltip": "Käyttäjän etunimi",
"Forced redirect origin - Tooltip": "Pakotettu uudelleenohjauksen alkuperä",
@@ -378,7 +382,9 @@
"Non-LDAP": "Ei-LDAP",
"None": "Ei mitään",
"OAuth providers": "OAuth-toimittajat",
"OFF": "POIS",
"OK": "OK",
"ON": "PÄÄLLÄ",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Organisaatio",
"Organization - Tooltip": "Samankaltaisia käsitteitä kuin vuokralaiset tai käyttäjäpoolit, jokainen käyttäjä ja sovellus kuuluu organisaatioon",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Tilaussuunnitelma",
"Plans": "Suunnitelmat",
"Plans - Tooltip": "Suunnitelmat - työkalupala",
"Please input your search": "Anna hakusanasi",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Esikatselu",
"Preview - Tooltip": "Esikatsele määritettyjä vaikutuksia",
@@ -427,6 +434,7 @@
"Records": "Tietueet",
"Request": "Pyyntö",
"Request URI": "Pyyntö URI",
"Reset": "Nollaa",
"Reset to Default": "Palauta oletusarvoon",
"Resources": "Resurssit",
"Role": "Rooli",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "SSH-yhteyden todennustyyppi",
"Save": "Tallenna",
"Save & Exit": "Tallenna ja poistu",
"Search": "Hae",
"Send": "Lähetä",
"Session ID": "Istunnon tunniste",
"Sessions": "Istunnot",
@@ -525,7 +534,7 @@
"Show all": "Näytä kaikki",
"Upload (.xlsx)": "Lataa (.xlsx)",
"Virtual": "Virtuaalinen",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Sinun täytyy poistaa kaikki aliryhmät ensin. Voit tarkastella aliryhmiä vasemmanpuoleisesta ryhmäpuusta sivulla [Organisaatiot] -\u003e [Ryhmät]."
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Sinun täytyy poistaa kaikki aliryhmät ensin. Voit tarkastella aliryhmiä vasemmanpuoleisesta ryhmäpuusta sivulla [Organisaatiot] -> [Ryhmät]."
},
"home": {
"New users past 30 days": "Uudet käyttäjät viimeisen 30 päivän aikana",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "La clé publique du certificat qui doit être vérifiée par le kit de développement client correspondant à cette application",
"Certs": "Certificats",
"Click to Upload": "Cliquer pour télécharger",
"Click to cancel sorting": "Cliquez pour annuler le tri",
"Click to sort ascending": "Cliquez pour trier par ordre croissant",
"Click to sort descending": "Cliquez pour trier par ordre décroissant",
"Client IP": "IP client",
"Close": "Fermer",
"Confirm": "Confirmer",
@@ -323,6 +326,7 @@
"False": "Faux",
"Favicon": "Favicon",
"Favicon - Tooltip": "L'URL de l'icône « favicon » utilisée dans toutes les pages Casdoor de l'organisation",
"Filter": "Filtrer",
"First name": "Prénom",
"First name - Tooltip": "Le prénom de l'utilisateur",
"Forced redirect origin - Tooltip": "Origine de redirection forcée",
@@ -378,7 +382,9 @@
"Non-LDAP": "Non-LDAP",
"None": "Aucun",
"OAuth providers": "Fournisseurs OAuth",
"OFF": "DÉSACTIVÉ",
"OK": "OK",
"ON": "ACTIVÉ",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Organisation",
"Organization - Tooltip": "Similaire à des concepts tels que les locataires (tenants) ou les groupes de compte, chaque compte et application appartient à une organisation",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Plan d'abonnement",
"Plans": "Offres",
"Plans - Tooltip": "Plans - Infobulle",
"Please input your search": "Veuillez saisir votre recherche",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Aperçu",
"Preview - Tooltip": "Prévisualisation des effets configurés",
@@ -427,6 +434,7 @@
"Records": "Enregistrements",
"Request": "Requête",
"Request URI": "URI de requête",
"Reset": "Réinitialiser",
"Reset to Default": "Réinitialiser par défaut",
"Resources": "Ressources",
"Role": "Rôle",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "Type d'authentification de connexion SSH",
"Save": "Enregistrer",
"Save & Exit": "Enregistrer et quitter",
"Search": "Rechercher",
"Send": "Envoyer",
"Session ID": "Identifiant de session",
"Sessions": "Sessions",
@@ -525,7 +534,7 @@
"Show all": "Afficher tout",
"Upload (.xlsx)": "Télécharger (.xlsx)",
"Virtual": "Virtuel",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Vous devez d'abord supprimer tous les sous-groupes. Vous pouvez voir les sous-groupes dans l'arborescence des groupes à gauche de la page [Organisations] -\u003e [Groupes]"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Vous devez d'abord supprimer tous les sous-groupes. Vous pouvez voir les sous-groupes dans l'arborescence des groupes à gauche de la page [Organisations] -> [Groupes]"
},
"home": {
"New users past 30 days": "Nouveaux utilisateurs ces 30 derniers jours",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "התעודה הציבורית שיש לאמת על ידי ה-SDK של הלקוח המתאים ליישום זה",
"Certs": "תעודות",
"Click to Upload": "לחץ להעלאה",
"Click to cancel sorting": "לחץ לביטול המיון",
"Click to sort ascending": "לחץ למיון עולה",
"Click to sort descending": "לחץ למיון יורד",
"Client IP": "IP לקוח",
"Close": "סגור",
"Confirm": "אשר",
@@ -323,6 +326,7 @@
"False": "שקר",
"Favicon": "סמל אתר",
"Favicon - Tooltip": "כתובת סמל Favicon המשמש בכל דפי Casdoor של הארגון",
"Filter": "סינון",
"First name": "שם פרטי",
"First name - Tooltip": "שם פרטי של המשתמש",
"Forced redirect origin - Tooltip": "מקור הפניה מאולץ",
@@ -378,7 +382,9 @@
"Non-LDAP": "לא LDAP",
"None": "אף אחד",
"OAuth providers": "ספקי OAuth",
"OFF": "כבוי",
"OK": "אישור",
"ON": "פעיל",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "ארגון",
"Organization - Tooltip": "דומה למושגים כמו דיירים או בריכות משתמשים, כל משתמש ויישום שייך לארגון",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "תוכנית מנוי",
"Plans": "תוכניות",
"Plans - Tooltip": "תוכניות - תיאור",
"Please input your search": "אנא הזן את החיפוש שלך",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "תצוגה מקדימה",
"Preview - Tooltip": "תצוגה מקדימה של ההגדרות",
@@ -427,6 +434,7 @@
"Records": "רשומות",
"Request": "בקשה",
"Request URI": "כתובת בקשה",
"Reset": "איפוס",
"Reset to Default": "אפס לברירת מחדל",
"Resources": "משאבים",
"Role": "תפקיד",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "סוג האימות של חיבור SSH",
"Save": "שמור",
"Save & Exit": "שמור וצא",
"Search": "חיפוש",
"Send": "שלח",
"Session ID": "מזהה סשן",
"Sessions": "סשנים",
@@ -525,7 +534,7 @@
"Show all": "הצג הכול",
"Upload (.xlsx)": "העלה (.xlsx)",
"Virtual": "וירטואלי",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "עליך למחוק תחילה את כל הקבוצות המשנה. ניתן להציג את הקבוצות המשנה בעץ הקבוצות בצד שמאל בדף [ארגונים] -\u003e [קבוצות]"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "עליך למחוק תחילה את כל הקבוצות המשנה. ניתן להציג את הקבוצות המשנה בעץ הקבוצות בצד שמאל בדף [ארגונים] -> [קבוצות]"
},
"home": {
"New users past 30 days": "משתמשים חדשים ב-30 הימים האחרונים",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "Sertifikat kunci publik yang perlu diverifikasi oleh SDK klien yang sesuai dengan aplikasi ini",
"Certs": "Sertifikat",
"Click to Upload": "Klik untuk Mengunggah",
"Click to cancel sorting": "Klik untuk membatalkan pengurutan",
"Click to sort ascending": "Klik untuk mengurutkan naik",
"Click to sort descending": "Klik untuk mengurutkan turun",
"Client IP": "IP Klien",
"Close": "Tutup",
"Confirm": "Konfirmasi",
@@ -323,6 +326,7 @@
"False": "Salah",
"Favicon": "Favicon",
"Favicon - Tooltip": "URL ikon Favicon yang digunakan di semua halaman Casdoor organisasi",
"Filter": "Saring",
"First name": "Nama depan",
"First name - Tooltip": "Nama depan pengguna",
"Forced redirect origin - Tooltip": "Asal pengalihan paksa",
@@ -378,7 +382,9 @@
"Non-LDAP": "Non-LDAP",
"None": "Tidak ada",
"OAuth providers": "Penyedia OAuth",
"OFF": "MATI",
"OK": "OK",
"ON": "HIDUP",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Organisasi",
"Organization - Tooltip": "Sama seperti konsep seperti penyewa atau grup pengguna, setiap pengguna dan aplikasi termasuk ke dalam suatu organisasi",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Paket berlangganan",
"Plans": "Rencana",
"Plans - Tooltip": "Rencana - Tooltip",
"Please input your search": "Silakan masukkan pencarian Anda",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Tinjauan",
"Preview - Tooltip": "Mengawali pratinjau efek yang sudah dikonfigurasi",
@@ -427,6 +434,7 @@
"Records": "Catatan",
"Request": "Permintaan",
"Request URI": "URI Permintaan",
"Reset": "Atur Ulang",
"Reset to Default": "Setel ulang ke default",
"Resources": "Sumber daya",
"Role": "Peran",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "Tipe autentikasi koneksi SSH",
"Save": "Menyimpan",
"Save & Exit": "Simpan & Keluar",
"Search": "Cari",
"Send": "Kirim",
"Session ID": "ID sesi",
"Sessions": "Sesi-sesi",
@@ -525,7 +534,7 @@
"Show all": "Tampilkan semua",
"Upload (.xlsx)": "Unggah (.xlsx)",
"Virtual": "Virtual",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Anda perlu menghapus semua subgrup terlebih dahulu. Anda dapat melihat subgrup di pohon grup kiri halaman [Organisasi] -\u003e [Grup]"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Anda perlu menghapus semua subgrup terlebih dahulu. Anda dapat melihat subgrup di pohon grup kiri halaman [Organisasi] -> [Grup]"
},
"home": {
"New users past 30 days": "Pengguna baru 30 hari terakhir",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "Il certificato con chiave pubblica che needs essere verificato dall'SDK client corrispondente a questa applicazione",
"Certs": "Certificati",
"Click to Upload": "Clicca per Caricare",
"Click to cancel sorting": "Fare clic per annullare ordinamento",
"Click to sort ascending": "Fare clic per ordinare in modo crescente",
"Click to sort descending": "Fare clic per ordinare in modo decrescente",
"Client IP": "IP client",
"Close": "Chiudi",
"Confirm": "Conferma",
@@ -323,6 +326,7 @@
"False": "Falso",
"Favicon": "Favicon",
"Favicon - Tooltip": "Icona favicon utilizzata in tutte le pagine di Casdoor dell'organizzazione",
"Filter": "Filtra",
"First name": "Nome",
"First name - Tooltip": "Il nome dell'utente",
"Forced redirect origin - Tooltip": "Origine reindirizzamento forzato",
@@ -378,7 +382,9 @@
"Non-LDAP": "Non-LDAP",
"None": "Nessuno",
"OAuth providers": "Provider OAuth",
"OFF": "SPENTO",
"OK": "OK",
"ON": "ACCESO",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Organizzazione",
"Organization - Tooltip": "Simile a concetti come tenant o pool utenti, ogni utente e applicazione appartiene a un'organizzazione",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Piano di abbonamento",
"Plans": "Piani",
"Plans - Tooltip": "Piani - Tooltip",
"Please input your search": "Inserisci la tua ricerca",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Anteprima",
"Preview - Tooltip": "Anteprima degli effetti configurati",
@@ -427,6 +434,7 @@
"Records": "Record",
"Request": "Richiesta",
"Request URI": "URI richiesta",
"Reset": "Reimposta",
"Reset to Default": "Ripristina predefinito",
"Resources": "Risorse",
"Role": "Ruolo",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "Tipo di autenticazione della connessione SSH",
"Save": "Salva",
"Save & Exit": "Salva e Esci",
"Search": "Cerca",
"Send": "Invia",
"Session ID": "ID sessione",
"Sessions": "Sessioni",
@@ -525,7 +534,7 @@
"Show all": "Mostra tutto",
"Upload (.xlsx)": "Carica (.xlsx)",
"Virtual": "Virtuale",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Devi eliminare prima tutti i sottogruppi. Puoi visualizzarli nell'albero a sinistra in [Organizzazioni] -\u003e [Gruppi]"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Devi eliminare prima tutti i sottogruppi. Puoi visualizzarli nell'albero a sinistra in [Organizzazioni] -> [Gruppi]"
},
"home": {
"New users past 30 days": "Nuovi utenti ultimi 30 giorni",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "このアプリケーションに対応するクライアントSDKによって検証する必要がある公開鍵証明書",
"Certs": "証明書",
"Click to Upload": "アップロードするにはクリックしてください",
"Click to cancel sorting": "クリックして並べ替えをキャンセル",
"Click to sort ascending": "クリックして昇順に並べ替え",
"Click to sort descending": "クリックして降順に並べ替え",
"Client IP": "クライアントIP",
"Close": "閉じる",
"Confirm": "確認",
@@ -323,6 +326,7 @@
"False": "偽",
"Favicon": "ファビコン",
"Favicon - Tooltip": "組織のすべてのCasdoorページに使用されるFaviconアイコンのURL",
"Filter": "フィルター",
"First name": "名前",
"First name - Tooltip": "ユーザーの名",
"Forced redirect origin - Tooltip": "強制リダイレクト元",
@@ -378,7 +382,9 @@
"Non-LDAP": "非LDAP",
"None": "なし",
"OAuth providers": "OAuthプロバイダー",
"OFF": "オフ",
"OK": "OK",
"ON": "オン",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "組織",
"Organization - Tooltip": "テナントまたはユーザープールのような概念に似て、各ユーザーとアプリケーションは組織に属しています",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "サブスクリプションプラン",
"Plans": "プラン",
"Plans - Tooltip": "プラン - ツールチップ",
"Please input your search": "検索内容を入力してください",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "プレビュー",
"Preview - Tooltip": "構成されたエフェクトをプレビューする",
@@ -427,6 +434,7 @@
"Records": "記録",
"Request": "リクエスト",
"Request URI": "リクエストURI",
"Reset": "リセット",
"Reset to Default": "デフォルトにリセット",
"Resources": "リソース",
"Role": "ロール",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "SSH接続の認証タイプ",
"Save": "保存",
"Save & Exit": "保存して終了",
"Search": "検索",
"Send": "送信",
"Session ID": "セッションID",
"Sessions": "セッションズ",
@@ -525,7 +534,7 @@
"Show all": "すべて表示",
"Upload (.xlsx)": "アップロード (.xlsx)",
"Virtual": "仮想",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "最初にすべてのサブグループを削除する必要があります。[組織] -\u003e [グループ]ページの左側のグループツリーでサブグループを確認できます"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "最初にすべてのサブグループを削除する必要があります。[組織] -> [グループ]ページの左側のグループツリーでサブグループを確認できます"
},
"home": {
"New users past 30 days": "過去30日間の新規ユーザー",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "Бұл қолданбаға сәйкес келетін клиент SDK тексеретін жария кілт сертификаты",
"Certs": "Сертификаттар",
"Click to Upload": "Жүктеу үшін басыңыз",
"Click to cancel sorting": "Сұрыптауды болдырмау үшін басыңыз",
"Click to sort ascending": "Өсу ретімен сұрыптау үшін басыңыз",
"Click to sort descending": "Кему ретімен сұрыптау үшін басыңыз",
"Client IP": "Клиент IP",
"Close": "Жабу",
"Confirm": "Растау",
@@ -323,6 +326,7 @@
"False": "Жалған",
"Favicon": "Вэб-сайт иконка",
"Favicon - Tooltip": "Ұйымның барлық Casdoor парақтарында қолданылатын favicon белгі URL",
"Filter": "Сүзгі",
"First name": "Аты",
"First name - Tooltip": "Пайдаланушының аты",
"Forced redirect origin - Tooltip": "Мәжбүрлі қайта бағыттау бастамасы",
@@ -378,7 +382,9 @@
"Non-LDAP": "LDAP емес",
"None": "Ешқайсысы",
"OAuth providers": "OAuth провайдерлері",
"OFF": "ӨШІРУЛІ",
"OK": "ОК",
"ON": "ҚОСУЛЫ",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Ұйым",
"Organization - Tooltip": "Тенанттар немесе пайдаланушы жиынтықтары сияқты түсініктерге ұқсас, әр пайдаланушы мен қолданба ұйымға тиесілі",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Жазылым жоспары",
"Plans": "Жоспарлар",
"Plans - Tooltip": "Жоспарлар - Қысқаша түсінік",
"Please input your search": "Іздеу сұрауын енгізіңіз",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Алдын ала қарау",
"Preview - Tooltip": "Теңшеу әсерлерін алдын ала қарау",
@@ -427,6 +434,7 @@
"Records": "Жазбалар",
"Request": "Сұраныс",
"Request URI": "Сұраныс URI",
"Reset": "Қалпына келтіру",
"Reset to Default": "Әдепкіге қайтару",
"Resources": "Ресурстар",
"Role": "Рөл",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "SSH қосылымының растау түрі",
"Save": "Сақтау",
"Save & Exit": "Сақтау және шығу",
"Search": "Іздеу",
"Send": "Жіберу",
"Session ID": "Сессия ID",
"Sessions": "Сессиялар",
@@ -525,7 +534,7 @@
"Show all": "Барлығын көрсету",
"Upload (.xlsx)": "Жүктеу (.xlsx)",
"Virtual": "Виртуалды",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Алдымен барлық ішкі топтарды жою керек. Ішкі топтарды [Ұйымдар] -\u003e [Топтар] парағының сол жақ топ ағашында көре аласыз"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Алдымен барлық ішкі топтарды жою керек. Ішкі топтарды [Ұйымдар] -> [Топтар] парағының сол жақ топ ағашында көре аласыз"
},
"home": {
"New users past 30 days": "Соңғы 30 күнде жаңа пайдаланушылар",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "이 응용 프로그램에 해당하는 클라이언트 SDK에서 확인해야 하는 공개 키 인증서",
"Certs": "증명서",
"Click to Upload": "클릭하여 업로드하세요",
"Click to cancel sorting": "클릭하여 정렬 취소",
"Click to sort ascending": "클릭하여 오름차순 정렬",
"Click to sort descending": "클릭하여 내림차순 정렬",
"Client IP": "클라이언트 IP",
"Close": "닫다",
"Confirm": "확인",
@@ -323,6 +326,7 @@
"False": "거짓",
"Favicon": "파비콘",
"Favicon - Tooltip": "조직의 모든 Casdoor 페이지에서 사용되는 Favicon 아이콘 URL",
"Filter": "필터",
"First name": "이름",
"First name - Tooltip": "사용자의 이름",
"Forced redirect origin - Tooltip": "강제 리디렉션 원본",
@@ -378,7 +382,9 @@
"Non-LDAP": "비-LDAP",
"None": "없음",
"OAuth providers": "OAuth 공급자",
"OFF": "꺼짐",
"OK": "확인",
"ON": "켜짐",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "조직",
"Organization - Tooltip": "각 사용자와 애플리케이션은 테넌트나 사용자 풀과 유사한 개념으로, 조직에 속합니다",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "구독 계획",
"Plans": "플랜",
"Plans - Tooltip": "요금제 - 툴팁",
"Please input your search": "검색어를 입력하세요",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "미리보기",
"Preview - Tooltip": "구성된 효과를 미리보기합니다",
@@ -427,6 +434,7 @@
"Records": "기록",
"Request": "요청",
"Request URI": "요청 URI",
"Reset": "초기화",
"Reset to Default": "기본값으로 재설정",
"Resources": "자원",
"Role": "역할",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "SSH 연결의 인증 유형",
"Save": "저장하다",
"Save & Exit": "저장하고 종료하기",
"Search": "검색",
"Send": "전송",
"Session ID": "세션 ID",
"Sessions": "세션들",
@@ -525,7 +534,7 @@
"Show all": "모두 표시",
"Upload (.xlsx)": "업로드 (.xlsx)",
"Virtual": "가상",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "모든 하위 그룹을 먼저 삭제해야 합니다. [조직] -\u003e [그룹] 페이지의 왼쪽 그룹 트리에서 하위 그룹을 확인할 수 있습니다."
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "모든 하위 그룹을 먼저 삭제해야 합니다. [조직] -> [그룹] 페이지의 왼쪽 그룹 트리에서 하위 그룹을 확인할 수 있습니다."
},
"home": {
"New users past 30 days": "지난 30일간 새 사용자",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "Sijil kekunci awam yang perlu disahkan oleh SDK klien yang sepadan dengan aplikasi ini",
"Certs": "Sijil",
"Click to Upload": "Klik untuk Muat Naik",
"Click to cancel sorting": "Klik untuk batal isih",
"Click to sort ascending": "Klik untuk isih menaik",
"Click to sort descending": "Klik untuk isih menurun",
"Client IP": "IP klien",
"Close": "Tutup",
"Confirm": "Sahkan",
@@ -323,6 +326,7 @@
"False": "Salah",
"Favicon": "Ikon Laman",
"Favicon - Tooltip": "URL ikon favicon yang digunakan dalam semua halaman Casdoor organisasi",
"Filter": "Tapis",
"First name": "Nama pertama",
"First name - Tooltip": "Nama pertama pengguna",
"Forced redirect origin - Tooltip": "Asal ubah hala paksa",
@@ -378,7 +382,9 @@
"Non-LDAP": "Bukan-LDAP",
"None": "Tiada",
"OAuth providers": "Penyedia OAuth",
"OFF": "MATI",
"OK": "OK",
"ON": "HIDUP",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Organisasi",
"Organization - Tooltip": "Sama seperti konsep penyewa atau kumpulan pengguna, setiap pengguna dan aplikasi tergolong dalam organisasi",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Pelan langganan",
"Plans": "Pelan",
"Plans - Tooltip": "Pelan - Tooltip",
"Please input your search": "Sila masukkan carian anda",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Pratonton",
"Preview - Tooltip": "Pratonton kesan yang dikonfigurasi",
@@ -427,6 +434,7 @@
"Records": "Rekod",
"Request": "Permintaan",
"Request URI": "URI permintaan",
"Reset": "Tetapkan Semula",
"Reset to Default": "Set semula ke lalai",
"Resources": "Sumber",
"Role": "Peranan",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "Jenis auth sambungan SSH",
"Save": "Simpan",
"Save & Exit": "Simpan & Keluar",
"Search": "Cari",
"Send": "Hantar",
"Session ID": "ID Sesi",
"Sessions": "Sesi",
@@ -525,7 +534,7 @@
"Show all": "Tunjukkan semua",
"Upload (.xlsx)": "Muat naik (.xlsx)",
"Virtual": "Maya",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Anda perlu padam semua subkumpulan terlebih dahulu. Anda boleh lihat subkumpulan dalam pokok kumpulan kiri halaman [Organisasi] -\u003e [Kumpulan]"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Anda perlu padam semua subkumpulan terlebih dahulu. Anda boleh lihat subkumpulan dalam pokok kumpulan kiri halaman [Organisasi] -> [Kumpulan]"
},
"home": {
"New users past 30 days": "Pengguna baharu 30 hari lalu",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "Het openbare sleutelcertificaat dat moet worden geverifieerd door de client-SDK die hoort bij deze applicatie",
"Certs": "Certificaten",
"Click to Upload": "Klik om te uploaden",
"Click to cancel sorting": "Klik om sortering te annuleren",
"Click to sort ascending": "Klik om oplopend te sorteren",
"Click to sort descending": "Klik om aflopend te sorteren",
"Client IP": "Client-IP",
"Close": "Sluiten",
"Confirm": "Bevestigen",
@@ -323,6 +326,7 @@
"False": "Onwaar",
"Favicon": "Favicon",
"Favicon - Tooltip": "Favicon-pictogram-URL die op alle Casdoor-pagina's van de organisatie wordt gebruikt",
"Filter": "Filter",
"First name": "Voornaam",
"First name - Tooltip": "De voornaam van de gebruiker",
"Forced redirect origin - Tooltip": "Gedwongen omleidings-origin",
@@ -378,7 +382,9 @@
"Non-LDAP": "Niet-LDAP",
"None": "Geen",
"OAuth providers": "OAuth-providers",
"OFF": "UIT",
"OK": "OK",
"ON": "AAN",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Organisatie",
"Organization - Tooltip": "Vergelijkbaar met concepten zoals tenants of gebruikerspools, elke gebruiker en applicatie behoort tot een organisatie",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Abonnementsplan",
"Plans": "Plannen",
"Plans - Tooltip": "Plannen - Tooltip",
"Please input your search": "Voer uw zoekopdracht in",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Voorbeeld",
"Preview - Tooltip": "Bekijk de geconfigureerde effecten",
@@ -427,6 +434,7 @@
"Records": "Records",
"Request": "Verzoek",
"Request URI": "Verzoek-URI",
"Reset": "Resetten",
"Reset to Default": "Herstellen naar standaard",
"Resources": "Bronnen",
"Role": "Rol",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "Het autorisatietype voor SSH-verbinding",
"Save": "Opslaan",
"Save & Exit": "Opslaan & Afsluiten",
"Search": "Zoeken",
"Send": "Verzenden",
"Session ID": "Sessie-ID",
"Sessions": "Sessies",
@@ -525,7 +534,7 @@
"Show all": "Alles weergeven",
"Upload (.xlsx)": "Uploaden (.xlsx)",
"Virtual": "Virtueel",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "U moet eerst alle subgroepen verwijderen. U kunt de subgroepen bekijken in de linkergroepenboom van de [Organisaties] -\u003e [Groepen] pagina"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "U moet eerst alle subgroepen verwijderen. U kunt de subgroepen bekijken in de linkergroepenboom van de [Organisaties] -> [Groepen] pagina"
},
"home": {
"New users past 30 days": "Nieuwe gebruikers afgelopen 30 dagen",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "Certyfikat klucza publicznego, który musi być zweryfikowany przez odpowiednią aplikację SDK po stronie klienta",
"Certs": "Certyfikaty",
"Click to Upload": "Kliknij, aby przesłać",
"Click to cancel sorting": "Kliknij, aby anulować sortowanie",
"Click to sort ascending": "Kliknij, aby posortować rosnąco",
"Click to sort descending": "Kliknij, aby posortować malejąco",
"Client IP": "IP klienta",
"Close": "Zamknij",
"Confirm": "Potwierdź",
@@ -323,6 +326,7 @@
"False": "Fałsz",
"Favicon": "Ikona strony",
"Favicon - Tooltip": "URL ikony favicon używanej na wszystkich stronach Casdoor organizacji",
"Filter": "Filtruj",
"First name": "Imię",
"First name - Tooltip": "Imię użytkownika",
"Forced redirect origin - Tooltip": "Wymuszone źródło przekierowania",
@@ -378,7 +382,9 @@
"Non-LDAP": "Nie-LDAP",
"None": "Brak",
"OAuth providers": "Dostawcy OAuth",
"OFF": "WYŁĄCZONY",
"OK": "OK",
"ON": "WŁĄCZONY",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Organizacja",
"Organization - Tooltip": "Podobne do koncepcji takich jak dzierżawcy lub puli użytkowników, każdy użytkownik i aplikacja należy do organizacji",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Plan subskrypcji",
"Plans": "Plany",
"Plans - Tooltip": "Plany - Tooltip",
"Please input your search": "Proszę wprowadzić wyszukiwanie",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Podgląd",
"Preview - Tooltip": "Podgląd skonfigurowanych efektów",
@@ -427,6 +434,7 @@
"Records": "Rekordy",
"Request": "Żądanie",
"Request URI": "URI żądania",
"Reset": "Resetuj",
"Reset to Default": "Przywróć domyślne",
"Resources": "Zasoby",
"Role": "Rola",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "Typ uwierzytelniania połączenia SSH",
"Save": "Zapisz",
"Save & Exit": "Zapisz i wyjdź",
"Search": "Szukaj",
"Send": "Wyślij",
"Session ID": "ID sesji",
"Sessions": "Sesje",
@@ -525,7 +534,7 @@
"Show all": "Pokaż wszystko",
"Upload (.xlsx)": "Prześlij (.xlsx)",
"Virtual": "Wirtualna",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Musisz najpierw usunąć wszystkie podgrupy. Możesz przeglądać podgrupy w lewym drzewie grup na stronie [Organizacje] -\u003e [Grupy]"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Musisz najpierw usunąć wszystkie podgrupy. Możesz przeglądać podgrupy w lewym drzewie grup na stronie [Organizacje] -> [Grupy]"
},
"home": {
"New users past 30 days": "Nowi użytkownicy w ciągu ostatnich 30 dni",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "O certificado da chave pública que precisa ser verificado pelo SDK do cliente correspondente a esta aplicação",
"Certs": "Certificados",
"Click to Upload": "Clique para Enviar",
"Click to cancel sorting": "Clique para cancelar ordenação",
"Click to sort ascending": "Clique para ordenar em ordem crescente",
"Click to sort descending": "Clique para ordenar em ordem decrescente",
"Client IP": "IP do cliente",
"Close": "Fechar",
"Confirm": "Confirmar",
@@ -323,6 +326,7 @@
"False": "Falso",
"Favicon": "Ícone do site",
"Favicon - Tooltip": "URL do ícone de favicon usado em todas as páginas do Casdoor da organização",
"Filter": "Filtrar",
"First name": "Nome",
"First name - Tooltip": "O primeiro nome do usuário",
"Forced redirect origin - Tooltip": "Origem de redirecionamento forçado",
@@ -378,7 +382,9 @@
"Non-LDAP": "Não-LDAP",
"None": "Nenhum",
"OAuth providers": "Provedores OAuth",
"OFF": "DESLIGADO",
"OK": "OK",
"ON": "LIGADO",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Organização",
"Organization - Tooltip": "Semelhante a conceitos como inquilinos ou grupos de usuários, cada usuário e aplicativo pertence a uma organização",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Plano de assinatura",
"Plans": "Kế hoạch",
"Plans - Tooltip": "Dica: planos",
"Please input your search": "Por favor, insira sua busca",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Visualizar",
"Preview - Tooltip": "Visualizar os efeitos configurados",
@@ -427,6 +434,7 @@
"Records": "Registros",
"Request": "Requisição",
"Request URI": "URI da requisição",
"Reset": "Redefinir",
"Reset to Default": "Redefinir para padrão",
"Resources": "Recursos",
"Role": "Função",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "Tipo de autenticação para conexão SSH",
"Save": "Salvar",
"Save & Exit": "Salvar e Sair",
"Search": "Buscar",
"Send": "Enviar",
"Session ID": "ID da sessão",
"Sessions": "Sessões",
@@ -525,7 +534,7 @@
"Show all": "Mostrar todos",
"Upload (.xlsx)": "Carregar (.xlsx)",
"Virtual": "Virtual",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Você precisa excluir todos os subgrupos primeiro. Você pode visualizar os subgrupos na árvore de grupos à esquerda na página [Organizações] -\u003e [Grupos]"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Você precisa excluir todos os subgrupos primeiro. Você pode visualizar os subgrupos na árvore de grupos à esquerda na página [Organizações] -> [Grupos]"
},
"home": {
"New users past 30 days": "Novos usuários nos últimos 30 dias",

View File

@@ -79,7 +79,7 @@
"Grant types - Tooltip": "Выберите, какие типы грантов разрешены в протоколе OAuth",
"Header HTML": "HTML заголовка",
"Header HTML - Edit": "Редактировать HTML заголовка",
"Header HTML - Tooltip": "Настройте тег \u003chead\u003e страницы входа в приложение",
"Header HTML - Tooltip": "Настройте тег <head> страницы входа в приложение",
"Incremental": "Инкрементный",
"Inline": "Встроенный",
"Input": "Ввод",
@@ -269,6 +269,9 @@
"Cert - Tooltip": "Сертификат открытого ключа, который требуется проверить клиентским SDK, соответствующим этому приложению",
"Certs": "сертификаты",
"Click to Upload": "Нажмите, чтобы загрузить",
"Click to cancel sorting": "Нажмите для отмены сортировки",
"Click to sort ascending": "Нажмите для сортировки по возрастанию",
"Click to sort descending": "Нажмите для сортировки по убыванию",
"Client IP": "IP клиента",
"Close": "Близко",
"Confirm": "Подтвердить",
@@ -323,6 +326,7 @@
"False": "Ложь",
"Favicon": "Фавикон",
"Favicon - Tooltip": "URL иконки Favicon, используемый на всех страницах организации Casdoor",
"Filter": "Фильтр",
"First name": "Имя",
"First name - Tooltip": "Имя пользователя",
"Forced redirect origin - Tooltip": "Принудительный источник перенаправления",
@@ -378,7 +382,9 @@
"Non-LDAP": "Не-LDAP",
"None": "Нет",
"OAuth providers": "Провайдеры OAuth",
"OFF": "ВЫКЛ",
"OK": "ОК",
"ON": "ВКЛ",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Организация",
"Organization - Tooltip": "Аналогично концепциям, таким как арендаторы или группы пользователей, каждый пользователь и приложение принадлежит к организации",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "План подписки",
"Plans": "Планы",
"Plans - Tooltip": "Подсказка: планы",
"Please input your search": "Пожалуйста, введите ваш запрос",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Предварительный просмотр",
"Preview - Tooltip": "Предварительный просмотр настроенных эффектов",
@@ -427,6 +434,7 @@
"Records": "Записи",
"Request": "Запрос",
"Request URI": "URI запроса",
"Reset": "Сброс",
"Reset to Default": "Сбросить к настройкам по умолчанию",
"Resources": "Ресурсы",
"Role": "Роль",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "Тип аутентификации SSH-подключения",
"Save": "Сохранить",
"Save & Exit": "Сохранить и выйти",
"Search": "Поиск",
"Send": "Отправить",
"Session ID": "Идентификатор сессии",
"Sessions": "Сессии",
@@ -525,7 +534,7 @@
"Show all": "Показать все",
"Upload (.xlsx)": "Загрузить (.xlsx)",
"Virtual": "Виртуальная",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Сначала удалите все подгруппы. Подгруппы можно просмотреть в дереве групп слева на странице [Организации] -\u003e [Группы]"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Сначала удалите все подгруппы. Подгруппы можно просмотреть в дереве групп слева на странице [Организации] -> [Группы]"
},
"home": {
"New users past 30 days": "Новые пользователи за 30 дней",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "Certifikát verejného kľúča, ktorý musí byť overený klientským SDK zodpovedajúcim tejto aplikácii",
"Certs": "Certifikáty",
"Click to Upload": "Kliknite na nahranie",
"Click to cancel sorting": "Kliknutím zrušíte triedenie",
"Click to sort ascending": "Kliknutím zoradíte vzostupne",
"Click to sort descending": "Kliknutím zoradíte zostupne",
"Client IP": "IP klienta",
"Close": "Zavrieť",
"Confirm": "Potvrdiť",
@@ -323,6 +326,7 @@
"False": "Nepravda",
"Favicon": "Ikona webu",
"Favicon - Tooltip": "URL ikony favicon používaná na všetkých stránkach Casdoor organizácie",
"Filter": "Filtrovať",
"First name": "Meno",
"First name - Tooltip": "Krstné meno používateľa",
"Forced redirect origin - Tooltip": "Pôvod vynúteného presmerovania",
@@ -378,7 +382,9 @@
"Non-LDAP": "Nie-LDAP",
"None": "Žiadne",
"OAuth providers": "OAuth poskytovatelia",
"OFF": "VYPNUTÉ",
"OK": "OK",
"ON": "ZAPNUTÉ",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Organizácia",
"Organization - Tooltip": "Podobné konceptom ako nájomcovia alebo používateľské pooly, každý používateľ a aplikácia patrí do organizácie",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Plán predplatného",
"Plans": "Plány",
"Plans - Tooltip": "Plány",
"Please input your search": "Zadajte prosím vyhľadávanie",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Náhľad",
"Preview - Tooltip": "Náhľad nakonfigurovaných efektov",
@@ -427,6 +434,7 @@
"Records": "Záznamy",
"Request": "Požiadavka",
"Request URI": "URI požiadavky",
"Reset": "Obnoviť",
"Reset to Default": "Obnoviť predvolené",
"Resources": "Zdroje",
"Role": "Rola",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "Typ autentifikácie SSH pripojenia",
"Save": "Uložiť",
"Save & Exit": "Uložiť a ukončiť",
"Search": "Hľadať",
"Send": "Odoslať",
"Session ID": "ID relácie",
"Sessions": "Relácie",
@@ -525,7 +534,7 @@
"Show all": "Zobraziť všetko",
"Upload (.xlsx)": "Nahrať (.xlsx)",
"Virtual": "Virtuálna",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Najprv musíte odstrániť všetky podprupy. Podprupy môžete zobraziť v ľavom stromu skupín na stránke [Organizácie] -\u003e [Skupiny]"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Najprv musíte odstrániť všetky podprupy. Podprupy môžete zobraziť v ľavom stromu skupín na stránke [Organizácie] -> [Skupiny]"
},
"home": {
"New users past 30 days": "Noví používatelia za posledných 30 dní",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "Det offentliga nyckelcertifikat som behöver verifieras av klient-SDK som motsvarar denna applikation",
"Certs": "Certifikat",
"Click to Upload": "Klicka för att ladda upp",
"Click to cancel sorting": "Klicka för att avbryta sortering",
"Click to sort ascending": "Klicka för att sortera stigande",
"Click to sort descending": "Klicka för att sortera fallande",
"Client IP": "Klient-IP",
"Close": "Stäng",
"Confirm": "Bekräfta",
@@ -323,6 +326,7 @@
"False": "Falskt",
"Favicon": "Favicon",
"Favicon - Tooltip": "Favicon-ikon-URL som används på alla Casdoor-sidor för organisationen",
"Filter": "Filtrera",
"First name": "Förnamn",
"First name - Tooltip": "Användarens förnamn",
"Forced redirect origin - Tooltip": "Tvingad omdirigeringsursprung",
@@ -378,7 +382,9 @@
"Non-LDAP": "Icke-LDAP",
"None": "Ingen",
"OAuth providers": "OAuth-leverantörer",
"OFF": "AV",
"OK": "OK",
"ON": "PÅ",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Organisation",
"Organization - Tooltip": "Liknar koncept som hyresgäster eller användarpooler, varje användare och applikation tillhör en organisation",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Abonnemangsplan",
"Plans": "Planer",
"Plans - Tooltip": "Planer",
"Please input your search": "Vänligen ange din sökning",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Förhandsvisning",
"Preview - Tooltip": "Förhandsgranska de konfigurerade effekterna",
@@ -427,6 +434,7 @@
"Records": "Poster",
"Request": "Förfrågan",
"Request URI": "Förfrågans URI",
"Reset": "Återställ",
"Reset to Default": "Återställ till standard",
"Resources": "Resurser",
"Role": "Roll",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "Autentiseringstyp för SSH-anslutning",
"Save": "Spara",
"Save & Exit": "Spara och avsluta",
"Search": "Sök",
"Send": "Skicka",
"Session ID": "Sessions-ID",
"Sessions": "Sessioner",
@@ -525,7 +534,7 @@
"Show all": "Visa alla",
"Upload (.xlsx)": "Ladda upp (.xlsx)",
"Virtual": "Virtuell",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Du måste ta bort alla undergrupper först. Du kan se undergrupperna i det vänstra gruppträdet på sidan [Organisationer] -\u003e [Grupper]"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Du måste ta bort alla undergrupper först. Du kan se undergrupperna i det vänstra gruppträdet på sidan [Organisationer] -> [Grupper]"
},
"home": {
"New users past 30 days": "Nya användare senaste 30 dagarna",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "Bu uygulamaya karşılık gelen istemci SDK tarafından doğrulanması gereken genel anahtar sertifikası",
"Certs": "Sertifikalar",
"Click to Upload": "Yüklemek için tıklayın",
"Click to cancel sorting": "Sıralamayı iptal etmek için tıklayın",
"Click to sort ascending": "Artan sıralamak için tıklayın",
"Click to sort descending": "Azalan sıralamak için tıklayın",
"Client IP": "İstemci IP'si",
"Close": "Kapat",
"Confirm": "Onayla",
@@ -323,6 +326,7 @@
"False": "Yanlış",
"Favicon": "Favicon",
"Favicon - Tooltip": "Organizasyonun tüm Casdoor sayfalarında kullanılan Favicon simgesi URL'si",
"Filter": "Filtrele",
"First name": "İsim",
"First name - Tooltip": "Kullanıcının adı",
"Forced redirect origin - Tooltip": "Zorunlu yönlendirme kaynağı",
@@ -378,7 +382,9 @@
"Non-LDAP": "LDAP dışı",
"None": "Hiçbiri",
"OAuth providers": "OAuth sağlayıcıları",
"OFF": "KAPALI",
"OK": "Tamam",
"ON": "AÇIK",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Organizasyon",
"Organization - Tooltip": "Kiracılar veya kullanıcı havuzları gibi kavramlara benzer, her kullanıcı ve uygulama bir organizasyona aittir",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Abonelik planı",
"Plans": "Planlar",
"Plans - Tooltip": "Planlar - Araç ipucu",
"Please input your search": "Lütfen aramanızı girin",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Önizleme",
"Preview - Tooltip": "Yapılandırılmış efektleri önizle",
@@ -427,6 +434,7 @@
"Records": "Kayıtlar",
"Request": "İstek",
"Request URI": "İstek URI'si",
"Reset": "Sıfırla",
"Reset to Default": "Varsayılana sıfırla",
"Resources": "Kaynaklar",
"Role": "Rol",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "SSH bağlantısının kimlik doğrulama türü",
"Save": "Kaydet",
"Save & Exit": "Kaydet ve Çık",
"Search": "Ara",
"Send": "Gönder",
"Session ID": "Oturum ID",
"Sessions": "Oturumlar",
@@ -525,7 +534,7 @@
"Show all": "Tümünü göster",
"Upload (.xlsx)": "Yükle (.xlsx)",
"Virtual": "Sanal",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Önce tüm alt grupları silmeniz gerekir. Alt grupları [Organizasyonlar] -\u003e [Gruplar] sayfasının sol grup ağacından görüntüleyebilirsiniz."
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Önce tüm alt grupları silmeniz gerekir. Alt grupları [Organizasyonlar] -> [Gruplar] sayfasının sol grup ağacından görüntüleyebilirsiniz."
},
"home": {
"New users past 30 days": "Son 30 gündeki yeni kullanıcılar",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "Сертифікат відкритого ключа, який потрібно перевірити клієнтським SDK, що відповідає цій програмі",
"Certs": "Сертифікати",
"Click to Upload": "Натисніть, щоб завантажити",
"Click to cancel sorting": "Натисніть для скасування сортування",
"Click to sort ascending": "Натисніть для сортування за зростанням",
"Click to sort descending": "Натисніть для сортування за спаданням",
"Client IP": "IP клієнта",
"Close": "Закрити",
"Confirm": "Підтвердити",
@@ -323,6 +326,7 @@
"False": "Ні",
"Favicon": "Фавікон",
"Favicon - Tooltip": "URL-адреса піктограми Favicon, яка використовується на всіх сторінках Casdoor організації",
"Filter": "Фільтр",
"First name": "Ім'я",
"First name - Tooltip": "Ім'я користувача",
"Forced redirect origin - Tooltip": "Примусове джерело перенаправлення",
@@ -378,7 +382,9 @@
"Non-LDAP": "Не LDAP",
"None": "Жодного",
"OAuth providers": "Постачальники OAuth",
"OFF": "ВИМКНЕНО",
"OK": "в порядку",
"ON": "УВІМКНЕНО",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "організація",
"Organization - Tooltip": "Подібно до таких концепцій, як орендарі або пули користувачів, кожен користувач і програма належать до організації",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "План підписки",
"Plans": "Плани",
"Plans - Tooltip": "Плани - підказка",
"Please input your search": "Будь ласка, введіть ваш запит",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Попередній перегляд",
"Preview - Tooltip": "Перегляньте налаштовані ефекти",
@@ -427,6 +434,7 @@
"Records": "Записи",
"Request": "Запит",
"Request URI": "URI запиту",
"Reset": "Скинути",
"Reset to Default": "Скинути до налаштувань за замовчуванням",
"Resources": "Ресурси",
"Role": "Роль",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "Тип авторизації підключення SSH",
"Save": "зберегти",
"Save & Exit": "зберегти",
"Search": "Пошук",
"Send": "Надіслати",
"Session ID": "Ідентифікатор сеансу",
"Sessions": "Сеанси",
@@ -525,7 +534,7 @@
"Show all": "Покажи все",
"Upload (.xlsx)": "Завантажити (.xlsx)",
"Virtual": "Віртуальний",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Спочатку потрібно видалити всі підгрупи. Підгрупи можна переглянути у лівому дереві груп на сторінці [Організації] -\u003e [Групи]"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Спочатку потрібно видалити всі підгрупи. Підгрупи можна переглянути у лівому дереві груп на сторінці [Організації] -> [Групи]"
},
"home": {
"New users past 30 days": "Нові користувачі за останні 30 днів",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "Chứng chỉ khóa công khai cần được xác minh bởi SDK khách hàng tương ứng với ứng dụng này",
"Certs": "Chứng chỉ",
"Click to Upload": "Nhấp để tải lên",
"Click to cancel sorting": "Nhấp để hủy sắp xếp",
"Click to sort ascending": "Nhấp để sắp xếp tăng dần",
"Click to sort descending": "Nhấp để sắp xếp giảm dần",
"Client IP": "IP khách hàng",
"Close": "Đóng lại",
"Confirm": "Xác nhận",
@@ -323,6 +326,7 @@
"False": "Sai",
"Favicon": "Biểu tượng trang",
"Favicon - Tooltip": "URL biểu tượng Favicon được sử dụng trong tất cả các trang của tổ chức Casdoor",
"Filter": "Lọc",
"First name": "Tên",
"First name - Tooltip": "Tên của người dùng",
"Forced redirect origin - Tooltip": "Nguồn chuyển hướng bắt buộc",
@@ -378,7 +382,9 @@
"Non-LDAP": "Không phải LDAP",
"None": "Không có",
"OAuth providers": "Nhà cung cấp OAuth",
"OFF": "TẮT",
"OK": "OK",
"ON": "BẬT",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Tổ chức",
"Organization - Tooltip": "Tương tự như các khái niệm như người thuê hoặc nhóm người dùng, mỗi người dùng và ứng dụng đều thuộc về một tổ chức",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Gói đăng ký",
"Plans": "Kế hoạch",
"Plans - Tooltip": "Gợi ý các gói",
"Please input your search": "Vui lòng nhập tìm kiếm của bạn",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Xem trước",
"Preview - Tooltip": "Xem trước các hiệu ứng đã cấu hình",
@@ -427,6 +434,7 @@
"Records": "Hồ sơ",
"Request": "Yêu cầu",
"Request URI": "URI yêu cầu",
"Reset": "Đặt lại",
"Reset to Default": "Đặt lại về mặc định",
"Resources": "Tài nguyên",
"Role": "Vai trò",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "Loại xác thực kết nối SSH",
"Save": "Lưu",
"Save & Exit": "Lưu và Thoát",
"Search": "Tìm kiếm",
"Send": "Gửi",
"Session ID": "ID phiên làm việc",
"Sessions": "Phiên",
@@ -525,7 +534,7 @@
"Show all": "Hiển thị tất cả",
"Upload (.xlsx)": "Tải lên (.xlsx)",
"Virtual": "Ảo",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Bạn cần xóa tất cả nhóm con trước. Bạn có thể xem các nhóm con trong cây nhóm bên trái của trang [Tổ chức] -\u003e [Nhóm]"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Bạn cần xóa tất cả nhóm con trước. Bạn có thể xem các nhóm con trong cây nhóm bên trái của trang [Tổ chức] -> [Nhóm]"
},
"home": {
"New users past 30 days": "Người dùng mới trong 30 ngày qua",

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