forked from casdoor/casdoor
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4db367eaa | ||
|
|
9df81e3ffc | ||
|
|
048d6acc83 | ||
|
|
e440199977 | ||
|
|
cb4e559d51 | ||
|
|
4d1d0b95d6 | ||
|
|
9cc1133a96 | ||
|
|
897c28e8ad | ||
|
|
9d37a7e38e | ||
|
|
ea597296b4 | ||
|
|
427ddd215e | ||
|
|
24de79b100 | ||
|
|
9ab9c7c8e0 | ||
|
|
0728a9716b | ||
|
|
471570f24a | ||
|
|
2fa520844b | ||
|
|
2306acb416 | ||
|
|
d3f3f76290 | ||
|
|
fe93128495 | ||
|
|
7fd890ff14 | ||
|
|
83b56d7ceb | ||
|
|
503e5a75d2 | ||
|
|
5a607b4991 | ||
|
|
ca2dc2825d | ||
|
|
446d0b9047 | ||
|
|
ee708dbf48 | ||
|
|
221ca28488 | ||
|
|
e93d3f6c13 | ||
|
|
e285396d4e | ||
|
|
10320bb49f | ||
|
|
4d27ebd82a | ||
|
|
6d5e6dab0a | ||
|
|
e600ea7efd |
@@ -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, *, *
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
169
controllers/order_pay.go
Normal 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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package deployment
|
||||
|
||||
|
||||
2
go.mod
2
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package i18n
|
||||
|
||||
|
||||
@@ -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
169
idp/telegram.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package object
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
314
object/order_pay.go
Normal 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)
|
||||
}
|
||||
@@ -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"}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package object
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package object
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package object
|
||||
|
||||
|
||||
130
object/transaction_validate.go
Normal file
130
object/transaction_validate.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package radius
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package sync
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package sync_v2
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package sync_v2
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ func GetUploadXlsxPath(fileId string) string {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
return file.Name()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package util
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
220
web/src/App.js
220
web/src/App.js
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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} />)} />
|
||||
|
||||
@@ -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
302
web/src/OrderPayPage.js
Normal 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;
|
||||
@@ -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"}}>
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
146
web/src/ProductStorePage.js
Normal 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;
|
||||
@@ -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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")}
|
||||
<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>
|
||||
|
||||
<Button type="primary" size="small" disabled={!isAdmin} onClick={this.rechargeTransaction.bind(this)}>{i18next.t("transaction:Recharge")}</Button>
|
||||
</div>
|
||||
|
||||
@@ -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"}} >
|
||||
|
||||
@@ -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")}
|
||||
<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()
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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 يومًا",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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í",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "کاربران جدید در ۳۰ روز گذشته",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 הימים האחרונים",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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日間の新規ユーザー",
|
||||
|
||||
@@ -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 күнде жаңа пайдаланушылар",
|
||||
|
||||
@@ -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일간 새 사용자",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 дней",
|
||||
|
||||
@@ -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í",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 днів",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user