forked from casdoor/casdoor
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e600ea7efd | ||
|
|
8002613398 | ||
|
|
a48b1d0c73 | ||
|
|
d8b5ecba36 | ||
|
|
e3a8a464d5 | ||
|
|
a575ba02d6 | ||
|
|
a9fcfceb8f | ||
|
|
712482ffb9 | ||
|
|
84e2c760d9 | ||
|
|
4ab85d6781 | ||
|
|
2ede56ac46 | ||
|
|
6a819a9a20 | ||
|
|
ddaeac46e8 | ||
|
|
f9d061d905 | ||
|
|
5e550e4364 | ||
|
|
146d54d6f6 | ||
|
|
1df15a2706 | ||
|
|
f7d73bbfdd | ||
|
|
a8b7217348 | ||
|
|
40a3b19cee | ||
|
|
98b45399a7 | ||
|
|
90edb7ab6b | ||
|
|
e21b995eca | ||
|
|
81221f07f0 | ||
|
|
5fc2cdf637 | ||
|
|
5e852e0121 | ||
|
|
513ac6ffe9 | ||
|
|
821ba5673d | ||
|
|
d3ee73e48c | ||
|
|
1d719e3759 | ||
|
|
b3355a9fa6 | ||
|
|
ccc88cdafb | ||
|
|
abf328bbe5 | ||
|
|
5530253d38 |
@@ -100,6 +100,7 @@ p, *, *, *, /api/metrics, *, *
|
||||
p, *, *, GET, /api/get-pricing, *, *
|
||||
p, *, *, GET, /api/get-plan, *, *
|
||||
p, *, *, GET, /api/get-subscription, *, *
|
||||
p, *, *, GET, /api/get-transactions, *, *
|
||||
p, *, *, GET, /api/get-provider, *, *
|
||||
p, *, *, GET, /api/get-organization-names, *, *
|
||||
p, *, *, GET, /api/get-all-objects, *, *
|
||||
@@ -183,7 +184,7 @@ func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} else if urlPath == "/api/upload-resource" {
|
||||
} else if urlPath == "/api/upload-resource" || urlPath == "/api/add-transaction" {
|
||||
if subOwner == "app" && subName == "app-casibase" {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -83,15 +83,13 @@ func (c *ApiController) GetEnforcer() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if enforcer == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The enforcer: %s does not exist"), id))
|
||||
return
|
||||
}
|
||||
|
||||
if loadModelCfg == "true" && enforcer.Model != "" {
|
||||
err := enforcer.LoadModelCfg()
|
||||
if err != nil {
|
||||
return
|
||||
if enforcer != nil {
|
||||
if loadModelCfg == "true" && enforcer.Model != "" {
|
||||
err = enforcer.LoadModelCfg()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
166
controllers/order.go
Normal file
166
controllers/order.go
Normal file
@@ -0,0 +1,166 @@
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// GetOrders
|
||||
// @Title GetOrders
|
||||
// @Tag Order API
|
||||
// @Description get orders
|
||||
// @Param owner query string true "The owner of orders"
|
||||
// @Success 200 {array} object.Order The Response object
|
||||
// @router /get-orders [get]
|
||||
func (c *ApiController) GetOrders() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
orders, err := object.GetOrders(owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(orders)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
count, err := object.GetOrderCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
orders, err := object.GetPaginationOrders(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(orders, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserOrders
|
||||
// @Title GetUserOrders
|
||||
// @Tag Order API
|
||||
// @Description get orders for a user
|
||||
// @Param owner query string true "The owner of orders"
|
||||
// @Param user query string true "The username of the user"
|
||||
// @Success 200 {array} object.Order The Response object
|
||||
// @router /get-user-orders [get]
|
||||
func (c *ApiController) GetUserOrders() {
|
||||
owner := c.Input().Get("owner")
|
||||
user := c.Input().Get("user")
|
||||
|
||||
orders, err := object.GetUserOrders(owner, user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(orders)
|
||||
}
|
||||
|
||||
// GetOrder
|
||||
// @Title GetOrder
|
||||
// @Tag Order API
|
||||
// @Description get order
|
||||
// @Param id query string true "The id ( owner/name ) of the order"
|
||||
// @Success 200 {object} object.Order The Response object
|
||||
// @router /get-order [get]
|
||||
func (c *ApiController) GetOrder() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
order, err := object.GetOrder(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(order)
|
||||
}
|
||||
|
||||
// UpdateOrder
|
||||
// @Title UpdateOrder
|
||||
// @Tag Order API
|
||||
// @Description update order
|
||||
// @Param id query string true "The id ( owner/name ) of the order"
|
||||
// @Param body body object.Order true "The details of the order"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-order [post]
|
||||
func (c *ApiController) UpdateOrder() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
var order object.Order
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &order)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateOrder(id, &order))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddOrder
|
||||
// @Title AddOrder
|
||||
// @Tag Order API
|
||||
// @Description add order
|
||||
// @Param body body object.Order true "The details of the order"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-order [post]
|
||||
func (c *ApiController) AddOrder() {
|
||||
var order object.Order
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &order)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddOrder(&order))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteOrder
|
||||
// @Title DeleteOrder
|
||||
// @Tag Order API
|
||||
// @Description delete order
|
||||
// @Param body body object.Order true "The details of the order"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-order [post]
|
||||
func (c *ApiController) DeleteOrder() {
|
||||
var order object.Order
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &order)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteOrder(&order))
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -130,6 +130,10 @@ func (c *ApiController) UpdateOrganization() {
|
||||
|
||||
isGlobalAdmin, _ := c.isGlobalAdmin()
|
||||
|
||||
if organization.BalanceCurrency == "" {
|
||||
organization.BalanceCurrency = "USD"
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization, isGlobalAdmin))
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -165,6 +169,10 @@ func (c *ApiController) AddOrganization() {
|
||||
return
|
||||
}
|
||||
|
||||
if organization.BalanceCurrency == "" {
|
||||
organization.BalanceCurrency = "USD"
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddOrganization(&organization))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -153,8 +153,19 @@ func (c *ApiController) AddTransaction() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddTransaction(&transaction, c.GetAcceptLanguage()))
|
||||
c.ServeJSON()
|
||||
affected, transactionId, err := object.AddTransaction(&transaction, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !affected {
|
||||
c.Data["json"] = wrapActionResponse(false)
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(transactionId)
|
||||
}
|
||||
|
||||
// DeleteTransaction
|
||||
|
||||
@@ -872,7 +872,7 @@ func initDefinedTransaction(transaction *Transaction) {
|
||||
}
|
||||
}
|
||||
transaction.CreatedTime = util.GetCurrentTime()
|
||||
_, err = AddTransaction(transaction, "en")
|
||||
_, _, err = AddTransaction(transaction, "en")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
149
object/order.go
Normal file
149
object/order.go
Normal file
@@ -0,0 +1,149 @@
|
||||
// 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/util"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
|
||||
type Order struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
|
||||
// Product Info
|
||||
ProductName string `xorm:"varchar(100)" json:"productName"`
|
||||
|
||||
// User Info
|
||||
User string `xorm:"varchar(100)" json:"user"`
|
||||
|
||||
// Payment Info
|
||||
Payment string `xorm:"varchar(100)" json:"payment"`
|
||||
|
||||
// Order State
|
||||
State string `xorm:"varchar(100)" json:"state"`
|
||||
Message string `xorm:"varchar(2000)" json:"message"`
|
||||
|
||||
// Order Duration
|
||||
StartTime string `xorm:"varchar(100)" json:"startTime"`
|
||||
EndTime string `xorm:"varchar(100)" json:"endTime"`
|
||||
}
|
||||
|
||||
func GetOrderCount(owner, field, value string) (int64, error) {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
return session.Count(&Order{Owner: owner})
|
||||
}
|
||||
|
||||
func GetOrders(owner string) ([]*Order, error) {
|
||||
orders := []*Order{}
|
||||
err := ormer.Engine.Desc("created_time").Find(&orders, &Order{Owner: owner})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orders, nil
|
||||
}
|
||||
|
||||
func GetUserOrders(owner, user string) ([]*Order, error) {
|
||||
orders := []*Order{}
|
||||
err := ormer.Engine.Desc("created_time").Find(&orders, &Order{Owner: owner, User: user})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orders, nil
|
||||
}
|
||||
|
||||
func GetPaginationOrders(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Order, error) {
|
||||
orders := []*Order{}
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Find(&orders, &Order{Owner: owner})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orders, nil
|
||||
}
|
||||
|
||||
func getOrder(owner string, name string) (*Order, error) {
|
||||
if owner == "" || name == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
order := Order{Owner: owner, Name: name}
|
||||
existed, err := ormer.Engine.Get(&order)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existed {
|
||||
return &order, nil
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetOrder(id string) (*Order, error) {
|
||||
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getOrder(owner, name)
|
||||
}
|
||||
|
||||
func UpdateOrder(id string, order *Order) (bool, error) {
|
||||
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if o, err := getOrder(owner, name); err != nil {
|
||||
return false, err
|
||||
} else if o == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(order)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func AddOrder(order *Order) (bool, error) {
|
||||
affected, err := ormer.Engine.Insert(order)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func DeleteOrder(order *Order) (bool, error) {
|
||||
affected, err := ormer.Engine.ID(core.PK{order.Owner, order.Name}).Delete(&Order{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func (order *Order) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", order.Owner, order.Name)
|
||||
}
|
||||
@@ -83,14 +83,16 @@ type Organization struct {
|
||||
DisableSignin bool `json:"disableSignin"`
|
||||
IpRestriction string `json:"ipRestriction"`
|
||||
NavItems []string `xorm:"mediumtext" json:"navItems"`
|
||||
UserNavItems []string `xorm:"mediumtext" json:"userNavItems"`
|
||||
WidgetItems []string `xorm:"mediumtext" json:"widgetItems"`
|
||||
|
||||
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
|
||||
MfaRememberInHours int `json:"mfaRememberInHours"`
|
||||
AccountItems []*AccountItem `xorm:"mediumtext" json:"accountItems"`
|
||||
|
||||
OrgBalance float64 `json:"orgBalance"`
|
||||
UserBalance float64 `json:"userBalance"`
|
||||
OrgBalance float64 `json:"orgBalance"`
|
||||
UserBalance float64 `json:"userBalance"`
|
||||
BalanceCurrency string `xorm:"varchar(100)" json:"balanceCurrency"`
|
||||
}
|
||||
|
||||
func GetOrganizationCount(owner, name, field, value string) (int64, error) {
|
||||
@@ -237,6 +239,7 @@ func UpdateOrganization(id string, organization *Organization, isGlobalAdmin boo
|
||||
|
||||
if !isGlobalAdmin {
|
||||
organization.NavItems = org.NavItems
|
||||
organization.UserNavItems = org.UserNavItems
|
||||
organization.WidgetItems = org.WidgetItems
|
||||
}
|
||||
|
||||
@@ -586,7 +589,7 @@ func (org *Organization) GetInitScore() (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateOrganizationBalance(owner string, name string, balance float64, isOrgBalance bool, lang string) error {
|
||||
func UpdateOrganizationBalance(owner string, name string, balance float64, currency string, isOrgBalance bool, lang string) error {
|
||||
organization, err := getOrganization(owner, name)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -595,12 +598,19 @@ func UpdateOrganizationBalance(owner string, name string, balance float64, isOrg
|
||||
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 columns []string
|
||||
if isOrgBalance {
|
||||
organization.OrgBalance += balance
|
||||
organization.OrgBalance = AddPrices(organization.OrgBalance, convertedBalance)
|
||||
columns = []string{"org_balance"}
|
||||
} else {
|
||||
organization.UserBalance += balance
|
||||
organization.UserBalance = AddPrices(organization.UserBalance, convertedBalance)
|
||||
columns = []string{"user_balance"}
|
||||
}
|
||||
|
||||
|
||||
@@ -384,6 +384,11 @@ func (a *Ormer) createTable() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Order))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Plan))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -207,7 +207,11 @@ func notifyPayment(body []byte, owner string, paymentName string) (*Payment, *pp
|
||||
}
|
||||
|
||||
if payment.IsRecharge {
|
||||
err = UpdateUserBalance(payment.Owner, payment.User, payment.Price, "en")
|
||||
currency := payment.Currency
|
||||
if currency == "" {
|
||||
currency = "USD"
|
||||
}
|
||||
err = UpdateUserBalance(payment.Owner, payment.User, payment.Price, currency, "en")
|
||||
return payment, notifyResult, err
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ func UploadPermissions(owner string, path string) (bool, error) {
|
||||
|
||||
newPermissions := []*Permission{}
|
||||
for index, line := range table {
|
||||
line := line
|
||||
if index == 0 || parseLineItem(&line, 0) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -291,38 +291,68 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
|
||||
transaction := &Transaction{
|
||||
Owner: payment.Owner,
|
||||
Name: payment.Name,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
DisplayName: payment.DisplayName,
|
||||
Provider: provider.Name,
|
||||
Category: provider.Category,
|
||||
Type: provider.Type,
|
||||
|
||||
ProductName: product.Name,
|
||||
ProductDisplayName: product.DisplayName,
|
||||
Detail: product.Detail,
|
||||
Tag: product.Tag,
|
||||
Currency: product.Currency,
|
||||
Amount: payment.Price,
|
||||
ReturnUrl: payment.ReturnUrl,
|
||||
|
||||
User: payment.User,
|
||||
Application: owner,
|
||||
Payment: payment.GetId(),
|
||||
Domain: "",
|
||||
Amount: payment.Price,
|
||||
Currency: product.Currency,
|
||||
Payment: payment.Name,
|
||||
State: pp.PaymentStateCreated,
|
||||
}
|
||||
|
||||
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
|
||||
err = UpdateUserBalance(user.Owner, user.Name, payment.Price, "en")
|
||||
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" {
|
||||
if product.Price > user.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, "en")
|
||||
err = UpdateUserBalance(user.Owner, user.Name, -product.Price, productCurrency, "en")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -339,8 +369,34 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
|
||||
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")
|
||||
affected, _, err = AddTransaction(transaction, "en")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ func UploadRoles(owner string, path string) (bool, error) {
|
||||
|
||||
newRoles := []*Role{}
|
||||
for index, line := range table {
|
||||
line := line
|
||||
if index == 0 || parseLineItem(&line, 0) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -311,26 +311,6 @@ func TestSyncer(syncer Syncer) error {
|
||||
syncer.Password = oldSyncer.Password
|
||||
}
|
||||
|
||||
// For WeCom syncer, test by getting access token
|
||||
if syncer.Type == "WeCom" {
|
||||
_, err := syncer.getWecomAccessToken()
|
||||
return err
|
||||
}
|
||||
|
||||
// For Azure AD syncer, test by getting access token
|
||||
if syncer.Type == "Azure AD" {
|
||||
_, err := syncer.getAzureAdAccessToken()
|
||||
return err
|
||||
}
|
||||
|
||||
err = syncer.initAdapter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = syncer.Ormer.Engine.Ping()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
provider := GetSyncerProvider(&syncer)
|
||||
return provider.TestConnection()
|
||||
}
|
||||
|
||||
312
object/syncer_activedirectory.go
Normal file
312
object/syncer_activedirectory.go
Normal file
@@ -0,0 +1,312 @@
|
||||
// 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 (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
// ActiveDirectorySyncerProvider implements SyncerProvider for Active Directory LDAP-based syncers
|
||||
type ActiveDirectorySyncerProvider struct {
|
||||
Syncer *Syncer
|
||||
}
|
||||
|
||||
// InitAdapter initializes the Active Directory syncer (no database adapter needed)
|
||||
func (p *ActiveDirectorySyncerProvider) InitAdapter() error {
|
||||
// Active Directory syncer doesn't need database adapter
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOriginalUsers retrieves all users from Active Directory via LDAP
|
||||
func (p *ActiveDirectorySyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
|
||||
return p.getActiveDirectoryUsers()
|
||||
}
|
||||
|
||||
// AddUser adds a new user to Active Directory (not supported for read-only LDAP)
|
||||
func (p *ActiveDirectorySyncerProvider) AddUser(user *OriginalUser) (bool, error) {
|
||||
// Active Directory syncer is typically read-only
|
||||
return false, fmt.Errorf("adding users to Active Directory is not supported")
|
||||
}
|
||||
|
||||
// UpdateUser updates an existing user in Active Directory (not supported for read-only LDAP)
|
||||
func (p *ActiveDirectorySyncerProvider) UpdateUser(user *OriginalUser) (bool, error) {
|
||||
// Active Directory syncer is typically read-only
|
||||
return false, fmt.Errorf("updating users in Active Directory is not supported")
|
||||
}
|
||||
|
||||
// TestConnection tests the Active Directory LDAP connection
|
||||
func (p *ActiveDirectorySyncerProvider) TestConnection() error {
|
||||
conn, err := p.getLdapConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
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
|
||||
// syncer.Port should be the LDAP port (usually 389 or 636 for LDAPS)
|
||||
// syncer.User should be the bind DN or username
|
||||
// syncer.Password should be the bind password
|
||||
|
||||
host := p.Syncer.Host
|
||||
if host == "" {
|
||||
return nil, fmt.Errorf("host is required for Active Directory syncer")
|
||||
}
|
||||
|
||||
port := p.Syncer.Port
|
||||
if port == 0 {
|
||||
port = 389 // Default LDAP port
|
||||
}
|
||||
|
||||
user := p.Syncer.User
|
||||
if user == "" {
|
||||
return nil, fmt.Errorf("user (bind DN) is required for Active Directory syncer")
|
||||
}
|
||||
|
||||
password := p.Syncer.Password
|
||||
if password == "" {
|
||||
return nil, fmt.Errorf("password is required for Active Directory syncer")
|
||||
}
|
||||
|
||||
var conn *goldap.Conn
|
||||
var err error
|
||||
|
||||
// Check if SSL is enabled (port 636 typically indicates LDAPS)
|
||||
if port == 636 {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true, // TODO: Make this configurable
|
||||
}
|
||||
conn, err = goldap.DialTLS("tcp", fmt.Sprintf("%s:%d", host, port), tlsConfig)
|
||||
} else {
|
||||
conn, err = goldap.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Active Directory: %w", err)
|
||||
}
|
||||
|
||||
// Bind with the provided credentials
|
||||
err = conn.Bind(user, password)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to bind to Active Directory: %w", err)
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// getActiveDirectoryUsers retrieves all users from Active Directory
|
||||
func (p *ActiveDirectorySyncerProvider) getActiveDirectoryUsers() ([]*OriginalUser, error) {
|
||||
conn, err := p.getLdapConn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Use the Database field to store the base DN for searching
|
||||
baseDN := p.Syncer.Database
|
||||
if baseDN == "" {
|
||||
return nil, fmt.Errorf("database field (base DN) is required for Active Directory syncer")
|
||||
}
|
||||
|
||||
// Search filter for user objects in Active Directory
|
||||
// Filter for users: objectClass=user, objectCategory=person, and not disabled accounts
|
||||
searchFilter := "(&(objectClass=user)(objectCategory=person))"
|
||||
|
||||
// Attributes to retrieve from Active Directory
|
||||
attributes := []string{
|
||||
"sAMAccountName", // Username
|
||||
"userPrincipalName", // UPN (email-like format)
|
||||
"displayName", // Display name
|
||||
"givenName", // First name
|
||||
"sn", // Last name (surname)
|
||||
"mail", // Email address
|
||||
"telephoneNumber", // Phone number
|
||||
"mobile", // Mobile phone
|
||||
"title", // Job title
|
||||
"department", // Department
|
||||
"company", // Company
|
||||
"streetAddress", // Street address
|
||||
"l", // City/Locality
|
||||
"st", // State/Province
|
||||
"postalCode", // Postal code
|
||||
"co", // Country
|
||||
"objectGUID", // Unique identifier
|
||||
"whenCreated", // Creation time
|
||||
"userAccountControl", // Account status
|
||||
}
|
||||
|
||||
searchRequest := goldap.NewSearchRequest(
|
||||
baseDN,
|
||||
goldap.ScopeWholeSubtree,
|
||||
goldap.NeverDerefAliases,
|
||||
0, // No size limit
|
||||
0, // No time limit
|
||||
false, // Types only = false
|
||||
searchFilter,
|
||||
attributes,
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to search Active Directory: %w", err)
|
||||
}
|
||||
|
||||
originalUsers := []*OriginalUser{}
|
||||
for _, entry := range sr.Entries {
|
||||
originalUser := p.adEntryToOriginalUser(entry)
|
||||
originalUsers = append(originalUsers, originalUser)
|
||||
}
|
||||
|
||||
return originalUsers, nil
|
||||
}
|
||||
|
||||
// adEntryToOriginalUser converts an Active Directory LDAP entry to Casdoor OriginalUser
|
||||
func (p *ActiveDirectorySyncerProvider) adEntryToOriginalUser(entry *goldap.Entry) *OriginalUser {
|
||||
user := &OriginalUser{
|
||||
Address: []string{},
|
||||
Properties: map[string]string{},
|
||||
Groups: []string{},
|
||||
}
|
||||
|
||||
// Get basic attributes
|
||||
sAMAccountName := entry.GetAttributeValue("sAMAccountName")
|
||||
userPrincipalName := entry.GetAttributeValue("userPrincipalName")
|
||||
displayName := entry.GetAttributeValue("displayName")
|
||||
givenName := entry.GetAttributeValue("givenName")
|
||||
sn := entry.GetAttributeValue("sn")
|
||||
mail := entry.GetAttributeValue("mail")
|
||||
telephoneNumber := entry.GetAttributeValue("telephoneNumber")
|
||||
mobile := entry.GetAttributeValue("mobile")
|
||||
title := entry.GetAttributeValue("title")
|
||||
department := entry.GetAttributeValue("department")
|
||||
company := entry.GetAttributeValue("company")
|
||||
streetAddress := entry.GetAttributeValue("streetAddress")
|
||||
city := entry.GetAttributeValue("l")
|
||||
state := entry.GetAttributeValue("st")
|
||||
postalCode := entry.GetAttributeValue("postalCode")
|
||||
country := entry.GetAttributeValue("co")
|
||||
objectGUID := entry.GetAttributeValue("objectGUID")
|
||||
whenCreated := entry.GetAttributeValue("whenCreated")
|
||||
userAccountControlStr := entry.GetAttributeValue("userAccountControl")
|
||||
|
||||
// Set user fields
|
||||
// Use sAMAccountName as the primary username
|
||||
user.Name = sAMAccountName
|
||||
|
||||
// Use objectGUID as the unique ID if available, otherwise use sAMAccountName
|
||||
if objectGUID != "" {
|
||||
user.Id = objectGUID
|
||||
} else {
|
||||
user.Id = sAMAccountName
|
||||
}
|
||||
|
||||
user.DisplayName = displayName
|
||||
user.FirstName = givenName
|
||||
user.LastName = sn
|
||||
|
||||
// If display name is empty, construct from first and last name
|
||||
if user.DisplayName == "" && (user.FirstName != "" || user.LastName != "") {
|
||||
user.DisplayName = strings.TrimSpace(fmt.Sprintf("%s %s", user.FirstName, user.LastName))
|
||||
}
|
||||
|
||||
// Set email - prefer mail attribute, fallback to userPrincipalName
|
||||
if mail != "" {
|
||||
user.Email = mail
|
||||
} else if userPrincipalName != "" {
|
||||
user.Email = userPrincipalName
|
||||
}
|
||||
|
||||
// Set phone - prefer mobile, fallback to telephoneNumber
|
||||
if mobile != "" {
|
||||
user.Phone = mobile
|
||||
} else if telephoneNumber != "" {
|
||||
user.Phone = telephoneNumber
|
||||
}
|
||||
|
||||
user.Title = title
|
||||
|
||||
// Set affiliation/department
|
||||
if department != "" {
|
||||
user.Affiliation = department
|
||||
}
|
||||
|
||||
// Construct location from city, state, country
|
||||
locationParts := []string{}
|
||||
if city != "" {
|
||||
locationParts = append(locationParts, city)
|
||||
}
|
||||
if state != "" {
|
||||
locationParts = append(locationParts, state)
|
||||
}
|
||||
if country != "" {
|
||||
locationParts = append(locationParts, country)
|
||||
}
|
||||
if len(locationParts) > 0 {
|
||||
user.Location = strings.Join(locationParts, ", ")
|
||||
}
|
||||
|
||||
// Construct address
|
||||
if streetAddress != "" {
|
||||
addressParts := []string{streetAddress}
|
||||
if city != "" {
|
||||
addressParts = append(addressParts, city)
|
||||
}
|
||||
if state != "" {
|
||||
addressParts = append(addressParts, state)
|
||||
}
|
||||
if postalCode != "" {
|
||||
addressParts = append(addressParts, postalCode)
|
||||
}
|
||||
if country != "" {
|
||||
addressParts = append(addressParts, country)
|
||||
}
|
||||
user.Address = []string{strings.Join(addressParts, ", ")}
|
||||
}
|
||||
|
||||
// Store additional properties
|
||||
if company != "" {
|
||||
user.Properties["company"] = company
|
||||
}
|
||||
if userPrincipalName != "" {
|
||||
user.Properties["userPrincipalName"] = userPrincipalName
|
||||
}
|
||||
|
||||
// Set creation time
|
||||
if whenCreated != "" {
|
||||
user.CreatedTime = whenCreated
|
||||
} else {
|
||||
user.CreatedTime = util.GetCurrentTime()
|
||||
}
|
||||
|
||||
// Parse userAccountControl to determine if account is disabled
|
||||
// Bit 2 (value 2) indicates the account is disabled
|
||||
if userAccountControlStr != "" {
|
||||
userAccountControl := util.ParseInt(userAccountControlStr)
|
||||
// Check if bit 2 is set (account disabled)
|
||||
user.IsForbidden = (userAccountControl & 0x02) != 0
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
@@ -26,6 +26,40 @@ import (
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// AzureAdSyncerProvider implements SyncerProvider for Azure AD API-based syncers
|
||||
type AzureAdSyncerProvider struct {
|
||||
Syncer *Syncer
|
||||
}
|
||||
|
||||
// InitAdapter initializes the Azure AD syncer (no database adapter needed)
|
||||
func (p *AzureAdSyncerProvider) InitAdapter() error {
|
||||
// Azure AD syncer doesn't need database adapter
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOriginalUsers retrieves all users from Azure AD API
|
||||
func (p *AzureAdSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
|
||||
return p.getAzureAdOriginalUsers()
|
||||
}
|
||||
|
||||
// AddUser adds a new user to Azure AD (not supported for read-only API)
|
||||
func (p *AzureAdSyncerProvider) AddUser(user *OriginalUser) (bool, error) {
|
||||
// Azure AD syncer is typically read-only
|
||||
return false, fmt.Errorf("adding users to Azure AD is not supported")
|
||||
}
|
||||
|
||||
// UpdateUser updates an existing user in Azure AD (not supported for read-only API)
|
||||
func (p *AzureAdSyncerProvider) UpdateUser(user *OriginalUser) (bool, error) {
|
||||
// Azure AD syncer is typically read-only
|
||||
return false, fmt.Errorf("updating users in Azure AD is not supported")
|
||||
}
|
||||
|
||||
// TestConnection tests the Azure AD API connection
|
||||
func (p *AzureAdSyncerProvider) TestConnection() error {
|
||||
_, err := p.getAzureAdAccessToken()
|
||||
return err
|
||||
}
|
||||
|
||||
type AzureAdAccessTokenResp struct {
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
@@ -55,22 +89,22 @@ type AzureAdUserListResp struct {
|
||||
}
|
||||
|
||||
// getAzureAdAccessToken gets access token from Azure AD API using client credentials flow
|
||||
func (syncer *Syncer) getAzureAdAccessToken() (string, error) {
|
||||
func (p *AzureAdSyncerProvider) getAzureAdAccessToken() (string, error) {
|
||||
// syncer.Host should be the tenant ID or tenant domain
|
||||
// syncer.User should be the client ID (application ID)
|
||||
// syncer.Password should be the client secret
|
||||
|
||||
tenantId := syncer.Host
|
||||
tenantId := p.Syncer.Host
|
||||
if tenantId == "" {
|
||||
return "", fmt.Errorf("tenant ID (host field) is required for Azure AD syncer")
|
||||
}
|
||||
|
||||
clientId := syncer.User
|
||||
clientId := p.Syncer.User
|
||||
if clientId == "" {
|
||||
return "", fmt.Errorf("client ID (user field) is required for Azure AD syncer")
|
||||
}
|
||||
|
||||
clientSecret := syncer.Password
|
||||
clientSecret := p.Syncer.Password
|
||||
if clientSecret == "" {
|
||||
return "", fmt.Errorf("client secret (password field) is required for Azure AD syncer")
|
||||
}
|
||||
@@ -124,7 +158,7 @@ func (syncer *Syncer) getAzureAdAccessToken() (string, error) {
|
||||
}
|
||||
|
||||
// getAzureAdUsers gets all users from Azure AD using Microsoft Graph API
|
||||
func (syncer *Syncer) getAzureAdUsers(accessToken string) ([]*AzureAdUser, error) {
|
||||
func (p *AzureAdSyncerProvider) getAzureAdUsers(accessToken string) ([]*AzureAdUser, error) {
|
||||
allUsers := []*AzureAdUser{}
|
||||
nextLink := "https://graph.microsoft.com/v1.0/users?$top=999"
|
||||
|
||||
@@ -173,7 +207,7 @@ func (syncer *Syncer) getAzureAdUsers(accessToken string) ([]*AzureAdUser, error
|
||||
}
|
||||
|
||||
// azureAdUserToOriginalUser converts Azure AD user to Casdoor OriginalUser
|
||||
func (syncer *Syncer) azureAdUserToOriginalUser(azureUser *AzureAdUser) *OriginalUser {
|
||||
func (p *AzureAdSyncerProvider) azureAdUserToOriginalUser(azureUser *AzureAdUser) *OriginalUser {
|
||||
user := &OriginalUser{
|
||||
Id: azureUser.Id,
|
||||
Name: azureUser.UserPrincipalName,
|
||||
@@ -212,15 +246,15 @@ func (syncer *Syncer) azureAdUserToOriginalUser(azureUser *AzureAdUser) *Origina
|
||||
}
|
||||
|
||||
// getAzureAdOriginalUsers is the main entry point for Azure AD syncer
|
||||
func (syncer *Syncer) getAzureAdOriginalUsers() ([]*OriginalUser, error) {
|
||||
func (p *AzureAdSyncerProvider) getAzureAdOriginalUsers() ([]*OriginalUser, error) {
|
||||
// Get access token
|
||||
accessToken, err := syncer.getAzureAdAccessToken()
|
||||
accessToken, err := p.getAzureAdAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get all users from Azure AD
|
||||
azureUsers, err := syncer.getAzureAdUsers(accessToken)
|
||||
azureUsers, err := p.getAzureAdUsers(accessToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -228,7 +262,7 @@ func (syncer *Syncer) getAzureAdOriginalUsers() ([]*OriginalUser, error) {
|
||||
// Convert Azure AD users to Casdoor OriginalUser
|
||||
originalUsers := []*OriginalUser{}
|
||||
for _, azureUser := range azureUsers {
|
||||
originalUser := syncer.azureAdUserToOriginalUser(azureUser)
|
||||
originalUser := p.azureAdUserToOriginalUser(azureUser)
|
||||
originalUsers = append(originalUsers, originalUser)
|
||||
}
|
||||
|
||||
|
||||
156
object/syncer_database.go
Normal file
156
object/syncer_database.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// DatabaseSyncerProvider implements SyncerProvider for database-based syncers
|
||||
type DatabaseSyncerProvider struct {
|
||||
Syncer *Syncer
|
||||
}
|
||||
|
||||
// InitAdapter initializes the database adapter
|
||||
func (p *DatabaseSyncerProvider) InitAdapter() error {
|
||||
if p.Syncer.Ormer != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var dataSourceName string
|
||||
if p.Syncer.DatabaseType == "mssql" {
|
||||
dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", p.Syncer.User, p.Syncer.Password, p.Syncer.Host, p.Syncer.Port, p.Syncer.Database)
|
||||
} else if p.Syncer.DatabaseType == "postgres" {
|
||||
sslMode := "disable"
|
||||
if p.Syncer.SslMode != "" {
|
||||
sslMode = p.Syncer.SslMode
|
||||
}
|
||||
dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=%s dbname=%s", p.Syncer.User, p.Syncer.Password, p.Syncer.Host, p.Syncer.Port, sslMode, p.Syncer.Database)
|
||||
} else {
|
||||
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", p.Syncer.User, p.Syncer.Password, p.Syncer.Host, p.Syncer.Port)
|
||||
}
|
||||
|
||||
var db *sql.DB
|
||||
var err error
|
||||
|
||||
if p.Syncer.SshType != "" && (p.Syncer.DatabaseType == "mysql" || p.Syncer.DatabaseType == "postgres" || p.Syncer.DatabaseType == "mssql") {
|
||||
var dial *ssh.Client
|
||||
if p.Syncer.SshType == "password" {
|
||||
dial, err = DialWithPassword(p.Syncer.SshUser, p.Syncer.SshPassword, p.Syncer.SshHost, p.Syncer.SshPort)
|
||||
} else {
|
||||
dial, err = DialWithCert(p.Syncer.SshUser, p.Syncer.Owner+"/"+p.Syncer.Cert, p.Syncer.SshHost, p.Syncer.SshPort)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
} else if p.Syncer.DatabaseType == "postgres" || p.Syncer.DatabaseType == "mssql" {
|
||||
db = sql.OpenDB(dsnConnector{dsn: dataSourceName, driver: &ViaSSHDialer{Client: dial, Context: nil, DatabaseType: p.Syncer.DatabaseType}})
|
||||
}
|
||||
}
|
||||
|
||||
if !isCloudIntranet {
|
||||
dataSourceName = strings.ReplaceAll(dataSourceName, "dbi.", "db.")
|
||||
}
|
||||
|
||||
if db != nil {
|
||||
p.Syncer.Ormer, err = NewAdapterFromDb(p.Syncer.DatabaseType, dataSourceName, p.Syncer.Database, db)
|
||||
} else {
|
||||
p.Syncer.Ormer, err = NewAdapter(p.Syncer.DatabaseType, dataSourceName, p.Syncer.Database)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetOriginalUsers retrieves all users from the database
|
||||
func (p *DatabaseSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
|
||||
var results []map[string]sql.NullString
|
||||
err := p.Syncer.Ormer.Engine.Table(p.Syncer.getTable()).Find(&results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// AddUser adds a new user to the database
|
||||
func (p *DatabaseSyncerProvider) AddUser(user *OriginalUser) (bool, error) {
|
||||
m := p.Syncer.getMapFromOriginalUser(user)
|
||||
affected, err := p.Syncer.Ormer.Engine.Table(p.Syncer.getTable()).Insert(m)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
// UpdateUser updates an existing user in the database
|
||||
func (p *DatabaseSyncerProvider) UpdateUser(user *OriginalUser) (bool, error) {
|
||||
key := p.Syncer.getTargetTablePrimaryKey()
|
||||
m := p.Syncer.getMapFromOriginalUser(user)
|
||||
pkValue := m[key]
|
||||
delete(m, key)
|
||||
|
||||
affected, err := p.Syncer.Ormer.Engine.Table(p.Syncer.getTable()).Where(fmt.Sprintf("%s = ?", key), pkValue).Update(&m)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
// TestConnection tests the database connection
|
||||
func (p *DatabaseSyncerProvider) TestConnection() error {
|
||||
err := p.InitAdapter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = p.Syncer.Ormer.Engine.Ping()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type dsnConnector struct {
|
||||
dsn string
|
||||
driver driver.Driver
|
||||
}
|
||||
|
||||
func (t dsnConnector) Connect(ctx context.Context) (driver.Conn, error) {
|
||||
return t.driver.Open(t.dsn)
|
||||
}
|
||||
|
||||
func (t dsnConnector) Driver() driver.Driver {
|
||||
return t.driver
|
||||
}
|
||||
207
object/syncer_googleworkspace.go
Normal file
207
object/syncer_googleworkspace.go
Normal file
@@ -0,0 +1,207 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"golang.org/x/oauth2/google"
|
||||
"golang.org/x/oauth2/jwt"
|
||||
admin "google.golang.org/api/admin/directory/v1"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
// GoogleWorkspaceSyncerProvider implements SyncerProvider for Google Workspace API-based syncers
|
||||
type GoogleWorkspaceSyncerProvider struct {
|
||||
Syncer *Syncer
|
||||
}
|
||||
|
||||
// InitAdapter initializes the Google Workspace syncer (no database adapter needed)
|
||||
func (p *GoogleWorkspaceSyncerProvider) InitAdapter() error {
|
||||
// Google Workspace syncer doesn't need database adapter
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOriginalUsers retrieves all users from Google Workspace API
|
||||
func (p *GoogleWorkspaceSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
|
||||
return p.getGoogleWorkspaceOriginalUsers()
|
||||
}
|
||||
|
||||
// AddUser adds a new user to Google Workspace (not supported for read-only API)
|
||||
func (p *GoogleWorkspaceSyncerProvider) AddUser(user *OriginalUser) (bool, error) {
|
||||
// Google Workspace syncer is typically read-only
|
||||
return false, fmt.Errorf("adding users to Google Workspace is not supported")
|
||||
}
|
||||
|
||||
// UpdateUser updates an existing user in Google Workspace (not supported for read-only API)
|
||||
func (p *GoogleWorkspaceSyncerProvider) UpdateUser(user *OriginalUser) (bool, error) {
|
||||
// Google Workspace syncer is typically read-only
|
||||
return false, fmt.Errorf("updating users in Google Workspace is not supported")
|
||||
}
|
||||
|
||||
// TestConnection tests the Google Workspace API connection
|
||||
func (p *GoogleWorkspaceSyncerProvider) TestConnection() error {
|
||||
_, err := p.getAdminService()
|
||||
return err
|
||||
}
|
||||
|
||||
// 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)
|
||||
// syncer.User should be the service account email or client_email
|
||||
// syncer.Password should be the service account private key (JSON key file content)
|
||||
|
||||
adminEmail := p.Syncer.Host
|
||||
if adminEmail == "" {
|
||||
return nil, fmt.Errorf("admin email (host field) is required for Google Workspace syncer")
|
||||
}
|
||||
|
||||
// Parse the service account credentials from the password field
|
||||
serviceAccountKey := p.Syncer.Password
|
||||
if serviceAccountKey == "" {
|
||||
return nil, fmt.Errorf("service account key (password field) is required for Google Workspace syncer")
|
||||
}
|
||||
|
||||
// Parse the JSON key
|
||||
var serviceAccount struct {
|
||||
Type string `json:"type"`
|
||||
ClientEmail string `json:"client_email"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(serviceAccountKey), &serviceAccount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse service account key: %v", err)
|
||||
}
|
||||
|
||||
// Create JWT config for service account with domain-wide delegation
|
||||
config := &jwt.Config{
|
||||
Email: serviceAccount.ClientEmail,
|
||||
PrivateKey: []byte(serviceAccount.PrivateKey),
|
||||
Scopes: []string{
|
||||
admin.AdminDirectoryUserReadonlyScope,
|
||||
},
|
||||
TokenURL: google.JWTTokenURL,
|
||||
Subject: adminEmail, // Impersonate the admin user
|
||||
}
|
||||
|
||||
client := config.Client(context.Background())
|
||||
|
||||
// Create Admin SDK service
|
||||
service, err := admin.NewService(context.Background(), option.WithHTTPClient(client))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create admin service: %v", err)
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// getGoogleWorkspaceUsers gets all users from Google Workspace using Admin SDK API
|
||||
func (p *GoogleWorkspaceSyncerProvider) getGoogleWorkspaceUsers(service *admin.Service) ([]*admin.User, error) {
|
||||
allUsers := []*admin.User{}
|
||||
pageToken := ""
|
||||
|
||||
// Get the customer ID (use "my_customer" for the domain)
|
||||
customer := "my_customer"
|
||||
|
||||
for {
|
||||
call := service.Users.List().Customer(customer).MaxResults(500)
|
||||
if pageToken != "" {
|
||||
call = call.PageToken(pageToken)
|
||||
}
|
||||
|
||||
resp, err := call.Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list users: %v", err)
|
||||
}
|
||||
|
||||
allUsers = append(allUsers, resp.Users...)
|
||||
|
||||
// Handle pagination
|
||||
if resp.NextPageToken == "" {
|
||||
break
|
||||
}
|
||||
pageToken = resp.NextPageToken
|
||||
}
|
||||
|
||||
return allUsers, nil
|
||||
}
|
||||
|
||||
// googleWorkspaceUserToOriginalUser converts Google Workspace user to Casdoor OriginalUser
|
||||
func (p *GoogleWorkspaceSyncerProvider) googleWorkspaceUserToOriginalUser(gwUser *admin.User) *OriginalUser {
|
||||
user := &OriginalUser{
|
||||
Id: gwUser.Id,
|
||||
Name: gwUser.PrimaryEmail,
|
||||
Email: gwUser.PrimaryEmail,
|
||||
Avatar: gwUser.ThumbnailPhotoUrl,
|
||||
Address: []string{},
|
||||
Properties: map[string]string{},
|
||||
Groups: []string{},
|
||||
}
|
||||
|
||||
// Set name fields if Name is not nil
|
||||
if gwUser.Name != nil {
|
||||
user.DisplayName = gwUser.Name.FullName
|
||||
user.FirstName = gwUser.Name.GivenName
|
||||
user.LastName = gwUser.Name.FamilyName
|
||||
}
|
||||
|
||||
// Set IsForbidden based on account status
|
||||
user.IsForbidden = gwUser.Suspended
|
||||
|
||||
// Set IsAdmin
|
||||
user.IsAdmin = gwUser.IsAdmin
|
||||
|
||||
// If display name is empty, construct from first and last name
|
||||
if user.DisplayName == "" && (user.FirstName != "" || user.LastName != "") {
|
||||
user.DisplayName = fmt.Sprintf("%s %s", user.FirstName, user.LastName)
|
||||
}
|
||||
|
||||
// Set CreatedTime from Google or current time
|
||||
if gwUser.CreationTime != "" {
|
||||
user.CreatedTime = gwUser.CreationTime
|
||||
} else {
|
||||
user.CreatedTime = util.GetCurrentTime()
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// getGoogleWorkspaceOriginalUsers is the main entry point for Google Workspace syncer
|
||||
func (p *GoogleWorkspaceSyncerProvider) getGoogleWorkspaceOriginalUsers() ([]*OriginalUser, error) {
|
||||
// Get Admin SDK service
|
||||
service, err := p.getAdminService()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get all users from Google Workspace
|
||||
gwUsers, err := p.getGoogleWorkspaceUsers(service)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert Google Workspace users to Casdoor OriginalUser
|
||||
originalUsers := []*OriginalUser{}
|
||||
for _, gwUser := range gwUsers {
|
||||
originalUser := p.googleWorkspaceUserToOriginalUser(gwUser)
|
||||
originalUsers = append(originalUsers, originalUser)
|
||||
}
|
||||
|
||||
return originalUsers, nil
|
||||
}
|
||||
55
object/syncer_interface.go
Normal file
55
object/syncer_interface.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// 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
|
||||
|
||||
// SyncerProvider defines the interface that all syncer implementations must satisfy.
|
||||
// Different syncer types (Database, Keycloak, WeCom, Azure AD) implement this interface.
|
||||
type SyncerProvider interface {
|
||||
// InitAdapter initializes the connection to the external system
|
||||
InitAdapter() error
|
||||
|
||||
// GetOriginalUsers retrieves all users from the external system
|
||||
GetOriginalUsers() ([]*OriginalUser, error)
|
||||
|
||||
// AddUser adds a new user to the external system
|
||||
AddUser(user *OriginalUser) (bool, error)
|
||||
|
||||
// UpdateUser updates an existing user in the external system
|
||||
UpdateUser(user *OriginalUser) (bool, error)
|
||||
|
||||
// TestConnection tests the connection to the external system
|
||||
TestConnection() error
|
||||
}
|
||||
|
||||
// GetSyncerProvider returns the appropriate SyncerProvider implementation based on syncer type
|
||||
func GetSyncerProvider(syncer *Syncer) SyncerProvider {
|
||||
switch syncer.Type {
|
||||
case "WeCom":
|
||||
return &WecomSyncerProvider{Syncer: syncer}
|
||||
case "Azure AD":
|
||||
return &AzureAdSyncerProvider{Syncer: syncer}
|
||||
case "Google Workspace":
|
||||
return &GoogleWorkspaceSyncerProvider{Syncer: syncer}
|
||||
case "Active Directory":
|
||||
return &ActiveDirectorySyncerProvider{Syncer: syncer}
|
||||
case "Keycloak":
|
||||
return &KeycloakSyncerProvider{
|
||||
DatabaseSyncerProvider: DatabaseSyncerProvider{Syncer: syncer},
|
||||
}
|
||||
default:
|
||||
// Default to database syncer for "Database" type and any others
|
||||
return &DatabaseSyncerProvider{Syncer: syncer}
|
||||
}
|
||||
}
|
||||
31
object/syncer_keycloak.go
Normal file
31
object/syncer_keycloak.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// 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
|
||||
|
||||
// KeycloakSyncerProvider implements SyncerProvider for Keycloak database syncers
|
||||
// Keycloak syncer extends DatabaseSyncerProvider with special handling for Keycloak schema
|
||||
type KeycloakSyncerProvider struct {
|
||||
DatabaseSyncerProvider
|
||||
}
|
||||
|
||||
// GetOriginalUsers retrieves all users from Keycloak database
|
||||
// This method overrides the base implementation to handle Keycloak-specific logic
|
||||
func (p *KeycloakSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
|
||||
// Use the base database implementation
|
||||
return p.DatabaseSyncerProvider.GetOriginalUsers()
|
||||
}
|
||||
|
||||
// Note: Keycloak-specific user mapping is handled in syncer_util.go
|
||||
// via getOriginalUsersFromMap which checks syncer.Type == "Keycloak"
|
||||
@@ -15,62 +15,22 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
type OriginalUser = User
|
||||
|
||||
type Credential struct {
|
||||
Value string `json:"value"`
|
||||
Salt string `json:"salt"`
|
||||
}
|
||||
|
||||
func (syncer *Syncer) getOriginalUsers() ([]*OriginalUser, error) {
|
||||
// Handle WeCom syncer separately
|
||||
if syncer.Type == "WeCom" {
|
||||
return syncer.getWecomOriginalUsers()
|
||||
}
|
||||
|
||||
// Handle Azure AD syncer separately
|
||||
if syncer.Type == "Azure AD" {
|
||||
return syncer.getAzureAdOriginalUsers()
|
||||
}
|
||||
|
||||
var results []map[string]sql.NullString
|
||||
err := syncer.Ormer.Engine.Table(syncer.getTable()).Find(&results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Memory leak problem handling
|
||||
// https://github.com/casdoor/casdoor/issues/1256
|
||||
users := syncer.getOriginalUsersFromMap(results)
|
||||
for _, m := range results {
|
||||
for k := range m {
|
||||
delete(m, k)
|
||||
}
|
||||
}
|
||||
|
||||
return users, nil
|
||||
provider := GetSyncerProvider(syncer)
|
||||
return provider.GetOriginalUsers()
|
||||
}
|
||||
|
||||
func (syncer *Syncer) addUser(user *OriginalUser) (bool, error) {
|
||||
m := syncer.getMapFromOriginalUser(user)
|
||||
affected, err := syncer.Ormer.Engine.Table(syncer.getTable()).Insert(m)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return affected != 0, nil
|
||||
provider := GetSyncerProvider(syncer)
|
||||
return provider.AddUser(user)
|
||||
}
|
||||
|
||||
func (syncer *Syncer) getCasdoorColumns() []string {
|
||||
@@ -85,16 +45,8 @@ func (syncer *Syncer) getCasdoorColumns() []string {
|
||||
}
|
||||
|
||||
func (syncer *Syncer) updateUser(user *OriginalUser) (bool, error) {
|
||||
key := syncer.getTargetTablePrimaryKey()
|
||||
m := syncer.getMapFromOriginalUser(user)
|
||||
pkValue := m[key]
|
||||
delete(m, key)
|
||||
|
||||
affected, err := syncer.Ormer.Engine.Table(syncer.getTable()).Where(fmt.Sprintf("%s = ?", key), pkValue).Update(&m)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return affected != 0, nil
|
||||
provider := GetSyncerProvider(syncer)
|
||||
return provider.UpdateUser(user)
|
||||
}
|
||||
|
||||
func (syncer *Syncer) updateUserForOriginalFields(user *User, key string) (bool, error) {
|
||||
@@ -139,80 +91,9 @@ func (syncer *Syncer) calculateHash(user *OriginalUser) string {
|
||||
return util.GetMd5Hash(s)
|
||||
}
|
||||
|
||||
type dsnConnector struct {
|
||||
dsn string
|
||||
driver driver.Driver
|
||||
}
|
||||
|
||||
func (t dsnConnector) Connect(ctx context.Context) (driver.Conn, error) {
|
||||
return t.driver.Open(t.dsn)
|
||||
}
|
||||
|
||||
func (t dsnConnector) Driver() driver.Driver {
|
||||
return t.driver
|
||||
}
|
||||
|
||||
func (syncer *Syncer) initAdapter() error {
|
||||
if syncer.Ormer != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// WeCom syncer doesn't need database adapter
|
||||
if syncer.Type == "WeCom" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Azure AD syncer doesn't need database adapter
|
||||
if syncer.Type == "Azure AD" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var dataSourceName string
|
||||
if syncer.DatabaseType == "mssql" {
|
||||
dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database)
|
||||
} else if syncer.DatabaseType == "postgres" {
|
||||
sslMode := "disable"
|
||||
if syncer.SslMode != "" {
|
||||
sslMode = syncer.SslMode
|
||||
}
|
||||
dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=%s dbname=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, sslMode, syncer.Database)
|
||||
} else {
|
||||
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", syncer.User, syncer.Password, syncer.Host, syncer.Port)
|
||||
}
|
||||
|
||||
var db *sql.DB
|
||||
var err error
|
||||
|
||||
if syncer.SshType != "" && (syncer.DatabaseType == "mysql" || syncer.DatabaseType == "postgres" || syncer.DatabaseType == "mssql") {
|
||||
var dial *ssh.Client
|
||||
if syncer.SshType == "password" {
|
||||
dial, err = DialWithPassword(syncer.SshUser, syncer.SshPassword, syncer.SshHost, syncer.SshPort)
|
||||
} else {
|
||||
dial, err = DialWithCert(syncer.SshUser, syncer.Owner+"/"+syncer.Cert, syncer.SshHost, syncer.SshPort)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if syncer.DatabaseType == "mysql" {
|
||||
dataSourceName = fmt.Sprintf("%s:%s@%s(%s:%d)/", syncer.User, syncer.Password, syncer.Owner+syncer.Name, syncer.Host, syncer.Port)
|
||||
mysql.RegisterDialContext(syncer.Owner+syncer.Name, (&ViaSSHDialer{Client: dial, Context: nil}).MysqlDial)
|
||||
} else if syncer.DatabaseType == "postgres" || syncer.DatabaseType == "mssql" {
|
||||
db = sql.OpenDB(dsnConnector{dsn: dataSourceName, driver: &ViaSSHDialer{Client: dial, Context: nil, DatabaseType: syncer.DatabaseType}})
|
||||
}
|
||||
}
|
||||
|
||||
if !isCloudIntranet {
|
||||
dataSourceName = strings.ReplaceAll(dataSourceName, "dbi.", "db.")
|
||||
}
|
||||
|
||||
if db != nil {
|
||||
syncer.Ormer, err = NewAdapterFromDb(syncer.DatabaseType, dataSourceName, syncer.Database, db)
|
||||
} else {
|
||||
syncer.Ormer, err = NewAdapter(syncer.DatabaseType, dataSourceName, syncer.Database)
|
||||
}
|
||||
|
||||
return err
|
||||
provider := GetSyncerProvider(syncer)
|
||||
return provider.InitAdapter()
|
||||
}
|
||||
|
||||
func RunSyncUsersJob() {
|
||||
|
||||
@@ -26,6 +26,11 @@ import (
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
type Credential struct {
|
||||
Value string `json:"value"`
|
||||
Salt string `json:"salt"`
|
||||
}
|
||||
|
||||
func (syncer *Syncer) getFullAvatarUrl(avatar string) string {
|
||||
if syncer.AvatarBaseUrl == "" {
|
||||
return avatar
|
||||
|
||||
@@ -26,6 +26,40 @@ import (
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// WecomSyncerProvider implements SyncerProvider for WeCom (WeChat Work) API-based syncers
|
||||
type WecomSyncerProvider struct {
|
||||
Syncer *Syncer
|
||||
}
|
||||
|
||||
// InitAdapter initializes the WeCom syncer (no database adapter needed)
|
||||
func (p *WecomSyncerProvider) InitAdapter() error {
|
||||
// WeCom syncer doesn't need database adapter
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOriginalUsers retrieves all users from WeCom API
|
||||
func (p *WecomSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
|
||||
return p.getWecomUsers()
|
||||
}
|
||||
|
||||
// AddUser adds a new user to WeCom (not supported for read-only API)
|
||||
func (p *WecomSyncerProvider) AddUser(user *OriginalUser) (bool, error) {
|
||||
// WeCom syncer is typically read-only
|
||||
return false, fmt.Errorf("adding users to WeCom is not supported")
|
||||
}
|
||||
|
||||
// UpdateUser updates an existing user in WeCom (not supported for read-only API)
|
||||
func (p *WecomSyncerProvider) UpdateUser(user *OriginalUser) (bool, error) {
|
||||
// WeCom syncer is typically read-only
|
||||
return false, fmt.Errorf("updating users in WeCom is not supported")
|
||||
}
|
||||
|
||||
// TestConnection tests the WeCom API connection
|
||||
func (p *WecomSyncerProvider) TestConnection() error {
|
||||
_, err := p.getWecomAccessToken()
|
||||
return err
|
||||
}
|
||||
|
||||
type WecomAccessTokenResp struct {
|
||||
Errcode int `json:"errcode"`
|
||||
Errmsg string `json:"errmsg"`
|
||||
@@ -61,9 +95,9 @@ type WecomDeptListResp struct {
|
||||
}
|
||||
|
||||
// getWecomAccessToken gets access token from WeCom API
|
||||
func (syncer *Syncer) getWecomAccessToken() (string, error) {
|
||||
func (p *WecomSyncerProvider) getWecomAccessToken() (string, error) {
|
||||
apiUrl := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s",
|
||||
url.QueryEscape(syncer.User), url.QueryEscape(syncer.Password))
|
||||
url.QueryEscape(p.Syncer.User), url.QueryEscape(p.Syncer.Password))
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
@@ -100,7 +134,7 @@ func (syncer *Syncer) getWecomAccessToken() (string, error) {
|
||||
}
|
||||
|
||||
// getWecomDepartments gets all department IDs from WeCom API
|
||||
func (syncer *Syncer) getWecomDepartments(accessToken string) ([]int, error) {
|
||||
func (p *WecomSyncerProvider) getWecomDepartments(accessToken string) ([]int, error) {
|
||||
apiUrl := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s",
|
||||
url.QueryEscape(accessToken))
|
||||
|
||||
@@ -144,7 +178,7 @@ func (syncer *Syncer) getWecomDepartments(accessToken string) ([]int, error) {
|
||||
}
|
||||
|
||||
// getWecomUsersFromDept gets users from a specific department
|
||||
func (syncer *Syncer) getWecomUsersFromDept(accessToken string, deptId int) ([]*WecomUser, error) {
|
||||
func (p *WecomSyncerProvider) getWecomUsersFromDept(accessToken string, deptId int) ([]*WecomUser, error) {
|
||||
apiUrl := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token=%s&department_id=%d",
|
||||
url.QueryEscape(accessToken), deptId)
|
||||
|
||||
@@ -183,15 +217,15 @@ func (syncer *Syncer) getWecomUsersFromDept(accessToken string, deptId int) ([]*
|
||||
}
|
||||
|
||||
// getWecomUsers gets all users from WeCom API
|
||||
func (syncer *Syncer) getWecomUsers() ([]*OriginalUser, error) {
|
||||
func (p *WecomSyncerProvider) getWecomUsers() ([]*OriginalUser, error) {
|
||||
// Get access token
|
||||
accessToken, err := syncer.getWecomAccessToken()
|
||||
accessToken, err := p.getWecomAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get all departments
|
||||
deptIds, err := syncer.getWecomDepartments(accessToken)
|
||||
deptIds, err := p.getWecomDepartments(accessToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -199,7 +233,7 @@ func (syncer *Syncer) getWecomUsers() ([]*OriginalUser, error) {
|
||||
// Get users from all departments (deduplicate by userid)
|
||||
userMap := make(map[string]*WecomUser)
|
||||
for _, deptId := range deptIds {
|
||||
users, err := syncer.getWecomUsersFromDept(accessToken, deptId)
|
||||
users, err := p.getWecomUsersFromDept(accessToken, deptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -215,7 +249,7 @@ func (syncer *Syncer) getWecomUsers() ([]*OriginalUser, error) {
|
||||
// Convert WeCom users to Casdoor OriginalUser
|
||||
originalUsers := []*OriginalUser{}
|
||||
for _, wecomUser := range userMap {
|
||||
originalUser := syncer.wecomUserToOriginalUser(wecomUser)
|
||||
originalUser := p.wecomUserToOriginalUser(wecomUser)
|
||||
originalUsers = append(originalUsers, originalUser)
|
||||
}
|
||||
|
||||
@@ -223,7 +257,7 @@ func (syncer *Syncer) getWecomUsers() ([]*OriginalUser, error) {
|
||||
}
|
||||
|
||||
// wecomUserToOriginalUser converts WeCom user to Casdoor OriginalUser
|
||||
func (syncer *Syncer) wecomUserToOriginalUser(wecomUser *WecomUser) *OriginalUser {
|
||||
func (p *WecomSyncerProvider) wecomUserToOriginalUser(wecomUser *WecomUser) *OriginalUser {
|
||||
user := &OriginalUser{
|
||||
Id: wecomUser.UserId,
|
||||
Name: wecomUser.UserId,
|
||||
@@ -263,8 +297,3 @@ func (syncer *Syncer) wecomUserToOriginalUser(wecomUser *WecomUser) *OriginalUse
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// getWecomOriginalUsers is the main entry point for WeCom syncer
|
||||
func (syncer *Syncer) getWecomOriginalUsers() ([]*OriginalUser, error) {
|
||||
return syncer.getWecomUsers()
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/pp"
|
||||
@@ -28,22 +29,20 @@ type Transaction struct {
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
// Transaction Provider Info
|
||||
Provider string `xorm:"varchar(100)" json:"provider"`
|
||||
Category string `xorm:"varchar(100)" json:"category"`
|
||||
Type string `xorm:"varchar(100)" json:"type"`
|
||||
// Product Info
|
||||
ProductName string `xorm:"varchar(100)" json:"productName"`
|
||||
ProductDisplayName string `xorm:"varchar(100)" json:"productDisplayName"`
|
||||
Detail string `xorm:"varchar(255)" json:"detail"`
|
||||
Tag string `xorm:"varchar(100)" json:"tag"`
|
||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||
Amount float64 `json:"amount"`
|
||||
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
|
||||
// User Info
|
||||
User string `xorm:"varchar(100)" json:"user"`
|
||||
|
||||
Application string `xorm:"varchar(100)" json:"application"`
|
||||
Payment string `xorm:"varchar(100)" json:"payment"`
|
||||
Domain string `xorm:"varchar(1000)" json:"domain"`
|
||||
Category string `xorm:"varchar(100)" json:"category"`
|
||||
Type string `xorm:"varchar(100)" json:"type"`
|
||||
Subtype string `xorm:"varchar(100)" json:"subtype"`
|
||||
Provider string `xorm:"varchar(100)" json:"provider"`
|
||||
User string `xorm:"varchar(100)" json:"user"`
|
||||
Tag string `xorm:"varchar(100)" json:"tag"`
|
||||
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||
|
||||
Payment string `xorm:"varchar(100)" json:"payment"`
|
||||
|
||||
State pp.PaymentState `xorm:"varchar(100)" json:"state"`
|
||||
}
|
||||
@@ -142,19 +141,22 @@ func UpdateTransaction(id string, transaction *Transaction, lang string) (bool,
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func AddTransaction(transaction *Transaction, lang string) (bool, error) {
|
||||
func AddTransaction(transaction *Transaction, lang string) (bool, string, error) {
|
||||
transactionId := strings.ReplaceAll(util.GenerateId(), "-", "")
|
||||
transaction.Name = transactionId
|
||||
|
||||
affected, err := ormer.Engine.Insert(transaction)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
if affected != 0 {
|
||||
if err := updateBalanceForTransaction(transaction, transaction.Amount, lang); err != nil {
|
||||
return false, err
|
||||
return false, transactionId, err
|
||||
}
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
return affected != 0, transactionId, nil
|
||||
}
|
||||
|
||||
func DeleteTransaction(transaction *Transaction, lang string) (bool, error) {
|
||||
@@ -176,19 +178,24 @@ func (transaction *Transaction) GetId() string {
|
||||
}
|
||||
|
||||
func updateBalanceForTransaction(transaction *Transaction, amount float64, lang string) error {
|
||||
if transaction.Category == "Organization" {
|
||||
currency := transaction.Currency
|
||||
if currency == "" {
|
||||
currency = "USD"
|
||||
}
|
||||
|
||||
if transaction.Tag == "Organization" {
|
||||
// Update organization's own balance
|
||||
return UpdateOrganizationBalance(transaction.Owner, transaction.Owner, amount, true, lang)
|
||||
} else if transaction.Category == "User" {
|
||||
return UpdateOrganizationBalance("admin", transaction.Owner, amount, currency, true, lang)
|
||||
} else if transaction.Tag == "User" {
|
||||
// Update user's balance
|
||||
if transaction.User == "" {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:User is required for User category transaction"))
|
||||
}
|
||||
if err := UpdateUserBalance(transaction.Owner, transaction.User, amount, lang); err != nil {
|
||||
if err := UpdateUserBalance(transaction.Owner, transaction.User, amount, currency, lang); err != nil {
|
||||
return err
|
||||
}
|
||||
// Update organization's user balance sum
|
||||
return UpdateOrganizationBalance(transaction.Owner, transaction.Owner, amount, false, lang)
|
||||
return UpdateOrganizationBalance("admin", transaction.Owner, amount, currency, false, lang)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ type User struct {
|
||||
Ranking int `json:"ranking"`
|
||||
Balance float64 `json:"balance"`
|
||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||
BalanceCurrency string `xorm:"varchar(100)" json:"balanceCurrency"`
|
||||
IsDefaultAvatar bool `json:"isDefaultAvatar"`
|
||||
IsOnline bool `json:"isOnline"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
@@ -827,7 +828,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
||||
"owner", "display_name", "avatar", "first_name", "last_name",
|
||||
"location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
|
||||
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
|
||||
"signin_wrong_times", "last_change_password_time", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled",
|
||||
"signin_wrong_times", "last_change_password_time", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled", "email_verified",
|
||||
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
|
||||
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "kwai", "line", "amazon",
|
||||
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
|
||||
@@ -870,6 +871,11 @@ func updateUser(id string, user *User, columns []string) (int64, error) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Ensure hash column is included in updates when columns are specified
|
||||
if len(columns) > 0 && !util.InSlice(columns, "hash") {
|
||||
columns = append(columns, "hash")
|
||||
}
|
||||
|
||||
affected, err := ormer.Engine.ID(core.PK{owner, name}).Cols(columns...).Update(user)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -966,6 +972,14 @@ func AddUser(user *User, lang string) (bool, error) {
|
||||
return false, fmt.Errorf(i18n.Translate(lang, "organization:adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option."))
|
||||
}
|
||||
|
||||
if user.BalanceCurrency == "" {
|
||||
if organization.BalanceCurrency != "" {
|
||||
user.BalanceCurrency = organization.BalanceCurrency
|
||||
} else {
|
||||
user.BalanceCurrency = "USD"
|
||||
}
|
||||
}
|
||||
|
||||
if organization.DefaultPassword != "" && user.Password == "123" {
|
||||
user.Password = organization.DefaultPassword
|
||||
}
|
||||
@@ -978,6 +992,10 @@ func AddUser(user *User, lang string) (bool, error) {
|
||||
user.CreatedTime = util.GetCurrentTime()
|
||||
}
|
||||
|
||||
if user.UpdatedTime == "" {
|
||||
user.UpdatedTime = user.CreatedTime
|
||||
}
|
||||
|
||||
err = user.UpdateUserHash()
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -1038,6 +1056,14 @@ func AddUsers(users []*User) (bool, error) {
|
||||
// this function is only used for syncer or batch upload, so no need to encrypt the password
|
||||
// user.UpdateUserPassword(organization)
|
||||
|
||||
if user.CreatedTime == "" {
|
||||
user.CreatedTime = util.GetCurrentTime()
|
||||
}
|
||||
|
||||
if user.UpdatedTime == "" {
|
||||
user.UpdatedTime = user.CreatedTime
|
||||
}
|
||||
|
||||
err := user.UpdateUserHash()
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -1453,7 +1479,7 @@ func GenerateIdForNewUser(application *Application) (string, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func UpdateUserBalance(owner string, name string, balance float64, lang string) error {
|
||||
func UpdateUserBalance(owner string, name string, balance float64, currency string, lang string) error {
|
||||
user, err := getUser(owner, name)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1461,7 +1487,21 @@ func UpdateUserBalance(owner string, name string, balance float64, lang string)
|
||||
if user == nil {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:The user: %s is not found"), fmt.Sprintf("%s/%s", owner, name))
|
||||
}
|
||||
user.Balance += balance
|
||||
|
||||
// Convert the balance amount from transaction currency to user's balance currency
|
||||
balanceCurrency := user.BalanceCurrency
|
||||
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)
|
||||
|
||||
user.Balance = AddPrices(user.Balance, convertedBalance)
|
||||
_, err = UpdateUser(user.GetId(), user, []string{"balance"}, true)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -124,7 +124,6 @@ func chooseFaviconLinkBySizes(links []Link) *Link {
|
||||
var chosenLink *Link
|
||||
|
||||
for _, link := range links {
|
||||
link := link
|
||||
if chosenLink == nil || compareSizes(link.Sizes, chosenLink.Sizes) > 0 {
|
||||
chosenLink = &link
|
||||
}
|
||||
|
||||
@@ -43,6 +43,11 @@ func (user *User) UpdateUserHash() error {
|
||||
}
|
||||
|
||||
func (user *User) UpdateUserPassword(organization *Organization) {
|
||||
// Don't hash empty passwords (e.g., for OAuth users)
|
||||
if user.Password == "" {
|
||||
return
|
||||
}
|
||||
|
||||
credManager := cred.GetCredManager(organization.PasswordType)
|
||||
if credManager != nil {
|
||||
// Use organization salt if available, otherwise generate a random salt for the user
|
||||
|
||||
88
object/util.go
Normal file
88
object/util.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// 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 "math"
|
||||
|
||||
// Fixed exchange rates (temporary implementation as per requirements)
|
||||
// All rates represent how many units of the currency equal 1 USD
|
||||
// Example: EUR: 0.92 means 1 USD = 0.92 EUR
|
||||
var exchangeRates = map[string]float64{
|
||||
"USD": 1.0,
|
||||
"EUR": 0.92,
|
||||
"GBP": 0.79,
|
||||
"JPY": 149.50,
|
||||
"CNY": 7.24,
|
||||
"AUD": 1.52,
|
||||
"CAD": 1.39,
|
||||
"CHF": 0.88,
|
||||
"HKD": 7.82,
|
||||
"SGD": 1.34,
|
||||
"INR": 83.12,
|
||||
"KRW": 1319.50,
|
||||
"BRL": 4.97,
|
||||
"MXN": 17.09,
|
||||
"ZAR": 18.15,
|
||||
"RUB": 92.50,
|
||||
"TRY": 32.15,
|
||||
"NZD": 1.67,
|
||||
"SEK": 10.35,
|
||||
"NOK": 10.72,
|
||||
"DKK": 6.87,
|
||||
"PLN": 3.91,
|
||||
"THB": 34.50,
|
||||
"MYR": 4.47,
|
||||
"IDR": 15750.00,
|
||||
"PHP": 55.50,
|
||||
"VND": 24500.00,
|
||||
}
|
||||
|
||||
// GetExchangeRate returns the exchange rate from fromCurrency to toCurrency
|
||||
func GetExchangeRate(fromCurrency, toCurrency string) float64 {
|
||||
if fromCurrency == toCurrency {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
// Default to USD if currency not found
|
||||
fromRate, fromExists := exchangeRates[fromCurrency]
|
||||
if !fromExists {
|
||||
fromRate = 1.0
|
||||
}
|
||||
|
||||
toRate, toExists := exchangeRates[toCurrency]
|
||||
if !toExists {
|
||||
toRate = 1.0
|
||||
}
|
||||
|
||||
// Convert from source currency to USD, then from USD to target currency
|
||||
// Example: EUR to JPY = (1/0.92) * 149.50 = USD/EUR * JPY/USD
|
||||
return toRate / fromRate
|
||||
}
|
||||
|
||||
// ConvertCurrency converts an amount from one currency to another using exchange rates
|
||||
func ConvertCurrency(amount float64, fromCurrency, toCurrency string) float64 {
|
||||
if fromCurrency == toCurrency {
|
||||
return amount
|
||||
}
|
||||
|
||||
rate := GetExchangeRate(fromCurrency, toCurrency)
|
||||
converted := amount * rate
|
||||
return math.Round(converted*1e8) / 1e8
|
||||
}
|
||||
|
||||
func AddPrices(price1 float64, price2 float64) float64 {
|
||||
res := price1 + price2
|
||||
return math.Round(res*1e8) / 1e8
|
||||
}
|
||||
@@ -201,6 +201,13 @@ func initAPI() {
|
||||
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")
|
||||
beego.Router("/api/get-order", &controllers.ApiController{}, "GET:GetOrder")
|
||||
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/get-payments", &controllers.ApiController{}, "GET:GetPayments")
|
||||
beego.Router("/api/get-user-payments", &controllers.ApiController{}, "GET:GetUserPayments")
|
||||
beego.Router("/api/get-payment", &controllers.ApiController{}, "GET:GetPayment")
|
||||
|
||||
@@ -7187,6 +7187,12 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"userNavItems": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -4724,6 +4724,10 @@ definitions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
userNavItems:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
owner:
|
||||
type: string
|
||||
passwordExpireDays:
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
@@ -113,7 +173,7 @@ class App extends Component {
|
||||
this.setState({selectedMenuKey: "/auth"});
|
||||
} else if (uri.includes("/records") || uri.includes("/tokens") || uri.includes("/sessions")) {
|
||||
this.setState({selectedMenuKey: "/logs"});
|
||||
} else if (uri.includes("/products") || uri.includes("/payments") || uri.includes("/plans") || uri.includes("/pricings") || uri.includes("/subscriptions")) {
|
||||
} else if (uri.includes("/products") || uri.includes("/orders") || uri.includes("/payments") || uri.includes("/plans") || uri.includes("/pricings") || uri.includes("/subscriptions")) {
|
||||
this.setState({selectedMenuKey: "/business"});
|
||||
} else if (uri.includes("/sysinfo") || uri.includes("/forms") || uri.includes("/syncers") || uri.includes("/webhooks")) {
|
||||
this.setState({selectedMenuKey: "/admin"});
|
||||
@@ -390,13 +450,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 +591,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>
|
||||
|
||||
@@ -63,6 +63,8 @@ import TokenEditPage from "./TokenEditPage";
|
||||
import ProductListPage from "./ProductListPage";
|
||||
import ProductEditPage from "./ProductEditPage";
|
||||
import ProductBuyPage from "./ProductBuyPage";
|
||||
import OrderListPage from "./OrderListPage";
|
||||
import OrderEditPage from "./OrderEditPage";
|
||||
import PaymentListPage from "./PaymentListPage";
|
||||
import PaymentEditPage from "./PaymentEditPage";
|
||||
import PaymentResultPage from "./PaymentResultPage";
|
||||
@@ -98,8 +100,9 @@ import VerificationListPage from "./VerificationListPage";
|
||||
|
||||
function ManagementPage(props) {
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const navItems = props.account?.organization?.navItems;
|
||||
const widgetItems = props.account?.organization?.widgetItems;
|
||||
const organization = props.account?.organization;
|
||||
const navItems = Setting.isLocalAdminUser(props.account) ? organization?.navItems : (organization?.userNavItems ?? []);
|
||||
const widgetItems = organization?.widgetItems;
|
||||
|
||||
function logout() {
|
||||
AuthBackend.logout()
|
||||
@@ -271,77 +274,74 @@ function ManagementPage(props) {
|
||||
Setting.getItem(<Link to="/">{i18next.t("general:Dashboard")}</Link>, "/"),
|
||||
Setting.getItem(<Link to="/shortcuts">{i18next.t("general:Shortcuts")}</Link>, "/shortcuts"),
|
||||
Setting.getItem(<Link to="/apps">{i18next.t("general:Apps")}</Link>, "/apps"),
|
||||
]));
|
||||
|
||||
if (Setting.isLocalAdminUser(props.account) && Conf.ShowGithubCorner) {
|
||||
res.push(Setting.getItem(<a href={"https://casdoor.com"}>
|
||||
<span style={{fontWeight: "bold", backgroundColor: "rgba(87,52,211,0.4)", marginTop: "12px", paddingLeft: "5px", paddingRight: "5px", display: "flex", alignItems: "center", height: "40px", borderRadius: "5px"}}>
|
||||
🚀 SaaS Hosting 🔥
|
||||
</span>
|
||||
</a>, "#"));
|
||||
}
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/organizations">{i18next.t("general:User Management")}</Link>, "/orgs", <AppstoreTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/organizations">{i18next.t("general:Organizations")}</Link>, "/organizations"),
|
||||
Setting.getItem(<Link to="/groups">{i18next.t("general:Groups")}</Link>, "/groups"),
|
||||
Setting.getItem(<Link to="/users">{i18next.t("general:Users")}</Link>, "/users"),
|
||||
Setting.getItem(<Link to="/invitations">{i18next.t("general:Invitations")}</Link>, "/invitations"),
|
||||
]));
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/applications">{i18next.t("general:Identity")}</Link>, "/identity", <LockTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/applications">{i18next.t("general:Applications")}</Link>, "/applications"),
|
||||
Setting.getItem(<Link to="/providers">{i18next.t("general:Providers")}</Link>, "/providers"),
|
||||
Setting.getItem(<Link to="/resources">{i18next.t("general:Resources")}</Link>, "/resources"),
|
||||
Setting.getItem(<Link to="/certs">{i18next.t("general:Certs")}</Link>, "/certs"),
|
||||
]));
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/roles">{i18next.t("general:Authorization")}</Link>, "/auth", <SafetyCertificateTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/roles">{i18next.t("general:Roles")}</Link>, "/roles"),
|
||||
Setting.getItem(<Link to="/permissions">{i18next.t("general:Permissions")}</Link>, "/permissions"),
|
||||
Setting.getItem(<Link to="/models">{i18next.t("general:Models")}</Link>, "/models"),
|
||||
Setting.getItem(<Link to="/adapters">{i18next.t("general:Adapters")}</Link>, "/adapters"),
|
||||
Setting.getItem(<Link to="/enforcers">{i18next.t("general:Enforcers")}</Link>, "/enforcers"),
|
||||
].filter(item => {
|
||||
return Setting.isLocalAdminUser(props.account);
|
||||
if (!Setting.isLocalAdminUser(props.account) && ["/models", "/adapters", "/enforcers"].includes(item.key)) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
})));
|
||||
|
||||
if (Setting.isLocalAdminUser(props.account)) {
|
||||
if (Conf.ShowGithubCorner) {
|
||||
res.push(Setting.getItem(<a href={"https://casdoor.com"}>
|
||||
<span style={{fontWeight: "bold", backgroundColor: "rgba(87,52,211,0.4)", marginTop: "12px", paddingLeft: "5px", paddingRight: "5px", display: "flex", alignItems: "center", height: "40px", borderRadius: "5px"}}>
|
||||
🚀 SaaS Hosting 🔥
|
||||
</span>
|
||||
</a>, "#"));
|
||||
}
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sessions">{i18next.t("general:Logging & Auditing")}</Link>, "/logs", <WalletTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/sessions">{i18next.t("general:Sessions")}</Link>, "/sessions"),
|
||||
Conf.CasvisorUrl ? Setting.getItem(<a target="_blank" rel="noreferrer" href={Conf.CasvisorUrl}>{i18next.t("general:Records")}</a>, "/records")
|
||||
: Setting.getItem(<Link to="/records">{i18next.t("general:Records")}</Link>, "/records"),
|
||||
Setting.getItem(<Link to="/tokens">{i18next.t("general:Tokens")}</Link>, "/tokens"),
|
||||
Setting.getItem(<Link to="/verifications">{i18next.t("general:Verifications")}</Link>, "/verifications"),
|
||||
]));
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/organizations">{i18next.t("general:User Management")}</Link>, "/orgs", <AppstoreTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/organizations">{i18next.t("general:Organizations")}</Link>, "/organizations"),
|
||||
Setting.getItem(<Link to="/groups">{i18next.t("general:Groups")}</Link>, "/groups"),
|
||||
Setting.getItem(<Link to="/users">{i18next.t("general:Users")}</Link>, "/users"),
|
||||
Setting.getItem(<Link to="/invitations">{i18next.t("general:Invitations")}</Link>, "/invitations"),
|
||||
]));
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/products">{i18next.t("general:Business & Payments")}</Link>, "/business", <DollarTwoTone twoToneColor={twoToneColor} />, [
|
||||
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"),
|
||||
Setting.getItem(<Link to="/plans">{i18next.t("general:Plans")}</Link>, "/plans"),
|
||||
Setting.getItem(<Link to="/pricings">{i18next.t("general:Pricings")}</Link>, "/pricings"),
|
||||
Setting.getItem(<Link to="/subscriptions">{i18next.t("general:Subscriptions")}</Link>, "/subscriptions"),
|
||||
Setting.getItem(<Link to="/transactions">{i18next.t("general:Transactions")}</Link>, "/transactions"),
|
||||
]));
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/applications">{i18next.t("general:Identity")}</Link>, "/identity", <LockTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/applications">{i18next.t("general:Applications")}</Link>, "/applications"),
|
||||
Setting.getItem(<Link to="/providers">{i18next.t("general:Providers")}</Link>, "/providers"),
|
||||
Setting.getItem(<Link to="/resources">{i18next.t("general:Resources")}</Link>, "/resources"),
|
||||
Setting.getItem(<Link to="/certs">{i18next.t("general:Certs")}</Link>, "/certs"),
|
||||
]));
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/roles">{i18next.t("general:Authorization")}</Link>, "/auth", <SafetyCertificateTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/roles">{i18next.t("general:Roles")}</Link>, "/roles"),
|
||||
Setting.getItem(<Link to="/permissions">{i18next.t("general:Permissions")}</Link>, "/permissions"),
|
||||
Setting.getItem(<Link to="/models">{i18next.t("general:Models")}</Link>, "/models"),
|
||||
Setting.getItem(<Link to="/adapters">{i18next.t("general:Adapters")}</Link>, "/adapters"),
|
||||
Setting.getItem(<Link to="/enforcers">{i18next.t("general:Enforcers")}</Link>, "/enforcers"),
|
||||
].filter(item => {
|
||||
if (!Setting.isLocalAdminUser(props.account) && ["/models", "/adapters", "/enforcers"].includes(item.key)) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
})));
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sessions">{i18next.t("general:Logging & Auditing")}</Link>, "/logs", <WalletTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/sessions">{i18next.t("general:Sessions")}</Link>, "/sessions"),
|
||||
Conf.CasvisorUrl ? Setting.getItem(<a target="_blank" rel="noreferrer" href={Conf.CasvisorUrl}>{i18next.t("general:Records")}</a>, "/records")
|
||||
: Setting.getItem(<Link to="/records">{i18next.t("general:Records")}</Link>, "/records"),
|
||||
Setting.getItem(<Link to="/tokens">{i18next.t("general:Tokens")}</Link>, "/tokens"),
|
||||
Setting.getItem(<Link to="/verifications">{i18next.t("general:Verifications")}</Link>, "/verifications"),
|
||||
]));
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/products">{i18next.t("general:Business & Payments")}</Link>, "/business", <DollarTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/products">{i18next.t("general:Products")}</Link>, "/products"),
|
||||
Setting.getItem(<Link to="/payments">{i18next.t("general:Payments")}</Link>, "/payments"),
|
||||
Setting.getItem(<Link to="/plans">{i18next.t("general:Plans")}</Link>, "/plans"),
|
||||
Setting.getItem(<Link to="/pricings">{i18next.t("general:Pricings")}</Link>, "/pricings"),
|
||||
Setting.getItem(<Link to="/subscriptions">{i18next.t("general:Subscriptions")}</Link>, "/subscriptions"),
|
||||
Setting.getItem(<Link to="/transactions">{i18next.t("general:Transactions")}</Link>, "/transactions"),
|
||||
]));
|
||||
|
||||
if (Setting.isAdminUser(props.account)) {
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sysinfo">{i18next.t("general:Admin")}</Link>, "/admin", <SettingTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/sysinfo">{i18next.t("general:System Info")}</Link>, "/sysinfo"),
|
||||
Setting.getItem(<Link to="/forms">{i18next.t("general:Forms")}</Link>, "/forms"),
|
||||
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
|
||||
Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks"),
|
||||
Setting.getItem(<a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>{i18next.t("general:Swagger")}</a>, "/swagger")]));
|
||||
} else {
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/syncers">{i18next.t("general:Admin")}</Link>, "/admin", <SettingTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/forms">{i18next.t("general:Forms")}</Link>, "/forms"),
|
||||
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
|
||||
Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks")]));
|
||||
}
|
||||
if (Setting.isAdminUser(props.account)) {
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sysinfo">{i18next.t("general:Admin")}</Link>, "/admin", <SettingTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/sysinfo">{i18next.t("general:System Info")}</Link>, "/sysinfo"),
|
||||
Setting.getItem(<Link to="/forms">{i18next.t("general:Forms")}</Link>, "/forms"),
|
||||
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
|
||||
Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks"),
|
||||
Setting.getItem(<a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>{i18next.t("general:Swagger")}</a>, "/swagger")]));
|
||||
} else {
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/syncers">{i18next.t("general:Admin")}</Link>, "/admin", <SettingTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/forms">{i18next.t("general:Forms")}</Link>, "/forms"),
|
||||
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
|
||||
Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks")]));
|
||||
}
|
||||
|
||||
if (navItemsIsAll()) {
|
||||
@@ -428,6 +428,8 @@ function ManagementPage(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="/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} />)} />
|
||||
|
||||
307
web/src/OrderEditPage.js
Normal file
307
web/src/OrderEditPage.js
Normal file
@@ -0,0 +1,307 @@
|
||||
// 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, Card, Col, Input, Row, Select} from "antd";
|
||||
import * as OrderBackend from "./backend/OrderBackend";
|
||||
import * as ProductBackend from "./backend/ProductBackend";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import * as PaymentBackend from "./backend/PaymentBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
class OrderEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||
orderName: props.match.params.orderName,
|
||||
order: null,
|
||||
products: [],
|
||||
users: [],
|
||||
payments: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getOrder();
|
||||
this.getProducts();
|
||||
this.getUsers();
|
||||
this.getPayments();
|
||||
}
|
||||
|
||||
getOrder() {
|
||||
OrderBackend.getOrder(this.state.organizationName, this.state.orderName)
|
||||
.then((res) => {
|
||||
if (res.data === null) {
|
||||
this.props.history.push("/404");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
order: res.data,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getProducts() {
|
||||
ProductBackend.getProducts(this.state.organizationName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
products: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get products: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getUsers() {
|
||||
UserBackend.getUsers(this.state.organizationName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
users: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get users: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getPayments() {
|
||||
PaymentBackend.getPayments(this.state.organizationName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
payments: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get payments: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
parseOrderField(key, value) {
|
||||
if ([""].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
updateOrderField(key, value) {
|
||||
value = this.parseOrderField(key, value);
|
||||
|
||||
const order = this.state.order;
|
||||
order[key] = value;
|
||||
this.setState({
|
||||
order: order,
|
||||
});
|
||||
}
|
||||
|
||||
renderOrder() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{this.state.mode === "add" ? i18next.t("order:New Order") : i18next.t("order:Edit Order")}
|
||||
<Button onClick={() => this.submitOrderEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitOrderEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteOrder()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
} style={{marginLeft: "5px"}} type="inner">
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Organization")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.order.owner} disabled />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Name")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.order.name} onChange={e => {
|
||||
this.updateOrderField("name", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Display name")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.order.displayName} onChange={e => {
|
||||
this.updateOrderField("displayName", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("order:Product")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.order.productName} onChange={(value) => {
|
||||
this.updateOrderField("productName", value);
|
||||
}}>
|
||||
{
|
||||
this.state.products?.map((product, index) => <Option key={index} value={product.name}>{product.displayName}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:User")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.order.user} onChange={(value) => {
|
||||
this.updateOrderField("user", value);
|
||||
}}>
|
||||
{
|
||||
this.state.users?.map((user, index) => <Option key={index} value={user.name}>{user.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("order:Payment")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.order.payment} onChange={(value) => {
|
||||
this.updateOrderField("payment", value);
|
||||
}}>
|
||||
<Option value="">{"(empty)"}</Option>
|
||||
{
|
||||
this.state.payments?.map((payment, index) => <Option key={index} value={payment.name}>{payment.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:State")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.order.state} onChange={(value) => {
|
||||
this.updateOrderField("state", value);
|
||||
}}>
|
||||
{
|
||||
[
|
||||
{id: "Created", name: "Created"},
|
||||
{id: "Paid", name: "Paid"},
|
||||
{id: "Delivered", name: "Delivered"},
|
||||
{id: "Completed", name: "Completed"},
|
||||
{id: "Canceled", name: "Canceled"},
|
||||
{id: "Expired", name: "Expired"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Message")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.order.message} onChange={e => {
|
||||
this.updateOrderField("message", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("order:Start time")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.order.startTime} onChange={e => {
|
||||
this.updateOrderField("startTime", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("order:End time")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.order.endTime} onChange={e => {
|
||||
this.updateOrderField("endTime", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
submitOrderEdit(exitAfterSave) {
|
||||
const order = Setting.deepCopy(this.state.order);
|
||||
OrderBackend.updateOrder(this.state.organizationName, this.state.orderName, order)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully saved"));
|
||||
this.setState({
|
||||
orderName: this.state.order.name,
|
||||
});
|
||||
if (exitAfterSave) {
|
||||
this.props.history.push("/orders");
|
||||
} else {
|
||||
this.props.history.push(`/orders/${this.state.order.owner}/${this.state.order.name}`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteOrder() {
|
||||
OrderBackend.deleteOrder(this.state.order)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push("/orders");
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
this.state.order !== null ? this.renderOrder() : null
|
||||
}
|
||||
<div style={{marginTop: "20px", marginLeft: "40px"}}>
|
||||
<Button size="large" onClick={() => this.submitOrderEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitOrderEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteOrder()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OrderEditPage;
|
||||
274
web/src/OrderListPage.js
Normal file
274
web/src/OrderListPage.js
Normal file
@@ -0,0 +1,274 @@
|
||||
// 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 {Link} from "react-router-dom";
|
||||
import {Button, Table} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as OrderBackend from "./backend/OrderBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
|
||||
class OrderListPage extends BaseListPage {
|
||||
newOrder() {
|
||||
const randomName = Setting.getRandomName();
|
||||
const owner = Setting.getRequestOrganization(this.props.account);
|
||||
return {
|
||||
owner: owner,
|
||||
name: `order_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Order - ${randomName}`,
|
||||
productName: "",
|
||||
user: "",
|
||||
payment: "",
|
||||
state: "Created",
|
||||
message: "",
|
||||
startTime: moment().format(),
|
||||
endTime: "",
|
||||
};
|
||||
}
|
||||
|
||||
addOrder() {
|
||||
const newOrder = this.newOrder();
|
||||
OrderBackend.addOrder(newOrder)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push({pathname: `/orders/${newOrder.owner}/${newOrder.name}`, mode: "add"});
|
||||
Setting.showMessage("success", i18next.t("general:Successfully added"));
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${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) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
|
||||
this.fetch({
|
||||
pagination: {
|
||||
...this.state.pagination,
|
||||
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(orders) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "140px",
|
||||
fixed: "left",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("name"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/orders/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Organization"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
width: "150px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("owner"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/organizations/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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:Display name"),
|
||||
dataIndex: "displayName",
|
||||
key: "displayName",
|
||||
width: "170px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("displayName"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("order:Product"),
|
||||
dataIndex: "productName",
|
||||
key: "productName",
|
||||
width: "170px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("productName"),
|
||||
render: (text, record, index) => {
|
||||
if (text === "") {
|
||||
return "(empty)";
|
||||
}
|
||||
return (
|
||||
<Link to={`/products/${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 === "") {
|
||||
return "(empty)";
|
||||
}
|
||||
return (
|
||||
<Link to={`/users/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:State"),
|
||||
dataIndex: "state",
|
||||
key: "state",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("state"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("order:Start time"),
|
||||
dataIndex: "startTime",
|
||||
key: "startTime",
|
||||
width: "160px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("order:End time"),
|
||||
dataIndex: "endTime",
|
||||
key: "endTime",
|
||||
width: "160px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
if (text === "") {
|
||||
return "(empty)";
|
||||
}
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
key: "op",
|
||||
width: "170px",
|
||||
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>
|
||||
<PopconfirmModal
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteOrder(index)}
|
||||
>
|
||||
</PopconfirmModal>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const paginationProps = {
|
||||
total: this.state.pagination.total,
|
||||
showQuickJumper: true,
|
||||
showSizeChanger: true,
|
||||
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={orders} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Orders")}
|
||||
<Button type="primary" size="small" onClick={this.addOrder.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fetch = (params = {}) => {
|
||||
const field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
this.setState({loading: true});
|
||||
OrderBackend.getOrders(Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
data: res.data,
|
||||
pagination: {
|
||||
...params.pagination,
|
||||
total: res.data2,
|
||||
},
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (Setting.isResponseDenied(res)) {
|
||||
this.setState({
|
||||
isAuthorized: false,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default OrderListPage;
|
||||
@@ -559,6 +559,20 @@ 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 currency"), i18next.t("organization:Balance currency - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={4} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.balanceCurrency || "USD"} onChange={(value => {
|
||||
this.updateOrganizationField("balanceCurrency", value);
|
||||
})}>
|
||||
{
|
||||
Setting.CurrencyOptions.map((item, index) => <Option key={index} value={item.id}>{Setting.getCurrencyWithFlag(item.id)}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Soft deletion"), i18next.t("organization:Soft deletion - Tooltip"))} :
|
||||
@@ -611,7 +625,7 @@ class OrganizationEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Navbar items"), i18next.t("organization:Navbar items - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("organization:Admin navbar items"), i18next.t("organization:Admin navbar items - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<NavItemTree
|
||||
@@ -624,6 +638,21 @@ class OrganizationEditPage extends React.Component {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:User navbar items"), i18next.t("organization:User navbar items - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<NavItemTree
|
||||
disabled={!Setting.isAdminUser(this.props.account)}
|
||||
checkedKeys={this.state.organization.userNavItems ?? []}
|
||||
defaultExpandedKeys={["all"]}
|
||||
onCheck={(checked, _) => {
|
||||
this.updateOrganizationField("userNavItems", checked);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Widget items"), i18next.t("organization:Widget items - Tooltip"))} :
|
||||
|
||||
@@ -51,6 +51,7 @@ class OrganizationListPage extends BaseListPage {
|
||||
enableTour: true,
|
||||
disableSignin: false,
|
||||
mfaRememberInHours: DefaultMfaRememberInHours,
|
||||
balanceCurrency: "USD",
|
||||
accountItems: [
|
||||
{name: "Organization", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "ID", visible: true, viewRule: "Public", modifyRule: "Immutable"},
|
||||
@@ -81,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 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"},
|
||||
{name: "Register source", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
@@ -258,6 +260,16 @@ class OrganizationListPage extends BaseListPage {
|
||||
return text ?? 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("organization:Balance currency"),
|
||||
dataIndex: "balanceCurrency",
|
||||
key: "balanceCurrency",
|
||||
width: "140px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return text || "USD";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("organization:Soft deletion"),
|
||||
dataIndex: "enableSoftDeletion",
|
||||
@@ -266,7 +278,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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -233,9 +233,13 @@ class PaymentEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.payment.currency} onChange={e => {
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.payment.currency} disabled={true} onChange={(value => {
|
||||
// this.updatePaymentField('currency', e.target.value);
|
||||
}} />
|
||||
})}>
|
||||
{
|
||||
Setting.CurrencyOptions.map((item, index) => <Option key={index} value={item.id}>{Setting.getCurrencyWithFlag(item.id)}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
|
||||
@@ -204,6 +204,9 @@ class PaymentListPage extends BaseListPage {
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("currency"),
|
||||
render: (text, record, index) => {
|
||||
return Setting.getCurrencyWithFlag(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:State"),
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -150,6 +150,7 @@ class PlanEditPage extends React.Component {
|
||||
this.updatePlanField("owner", owner);
|
||||
this.getUsers(owner);
|
||||
this.getRoles(owner);
|
||||
this.getPaymentProviders(owner);
|
||||
})}
|
||||
options={this.state.organizations.map((organization) => Setting.getOption(organization.name, organization.name))
|
||||
} />
|
||||
@@ -229,34 +230,7 @@ class PlanEditPage extends React.Component {
|
||||
this.updatePlanField("currency", value);
|
||||
})}>
|
||||
{
|
||||
[
|
||||
{id: "USD", name: "USD"},
|
||||
{id: "CNY", name: "CNY"},
|
||||
{id: "EUR", name: "EUR"},
|
||||
{id: "JPY", name: "JPY"},
|
||||
{id: "GBP", name: "GBP"},
|
||||
{id: "AUD", name: "AUD"},
|
||||
{id: "CAD", name: "CAD"},
|
||||
{id: "CHF", name: "CHF"},
|
||||
{id: "HKD", name: "HKD"},
|
||||
{id: "SGD", name: "SGD"},
|
||||
{id: "BRL", name: "BRL"},
|
||||
{id: "PLN", name: "PLN"},
|
||||
{id: "KRW", name: "KRW"},
|
||||
{id: "INR", name: "INR"},
|
||||
{id: "RUB", name: "RUB"},
|
||||
{id: "MXN", name: "MXN"},
|
||||
{id: "ZAR", name: "ZAR"},
|
||||
{id: "TRY", name: "TRY"},
|
||||
{id: "SEK", name: "SEK"},
|
||||
{id: "NOK", name: "NOK"},
|
||||
{id: "DKK", name: "DKK"},
|
||||
{id: "THB", name: "THB"},
|
||||
{id: "MYR", name: "MYR"},
|
||||
{id: "TWD", name: "TWD"},
|
||||
{id: "CZK", name: "CZK"},
|
||||
{id: "HUF", name: "HUF"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
Setting.CurrencyOptions.map((item, index) => <Option key={index} value={item.id}>{Setting.getCurrencyWithFlag(item.id)}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
|
||||
@@ -136,6 +136,9 @@ class PlanListPage extends BaseListPage {
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("currency"),
|
||||
render: (text, record, index) => {
|
||||
return Setting.getCurrencyWithFlag(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("plan:Price"),
|
||||
@@ -187,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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -118,66 +118,8 @@ class ProductBuyPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
getCurrencySymbol(product) {
|
||||
if (product?.currency === "USD") {
|
||||
return "$";
|
||||
} else if (product?.currency === "CNY") {
|
||||
return "¥";
|
||||
} else if (product?.currency === "EUR") {
|
||||
return "€";
|
||||
} else if (product?.currency === "JPY") {
|
||||
return "¥";
|
||||
} else if (product?.currency === "GBP") {
|
||||
return "£";
|
||||
} else if (product?.currency === "AUD") {
|
||||
return "A$";
|
||||
} else if (product?.currency === "CAD") {
|
||||
return "C$";
|
||||
} else if (product?.currency === "CHF") {
|
||||
return "CHF";
|
||||
} else if (product?.currency === "HKD") {
|
||||
return "HK$";
|
||||
} else if (product?.currency === "SGD") {
|
||||
return "S$";
|
||||
} else if (product?.currency === "BRL") {
|
||||
return "R$";
|
||||
} else if (product?.currency === "PLN") {
|
||||
return "zł";
|
||||
} else if (product?.currency === "KRW") {
|
||||
return "₩";
|
||||
} else if (product?.currency === "INR") {
|
||||
return "₹";
|
||||
} else if (product?.currency === "RUB") {
|
||||
return "₽";
|
||||
} else if (product?.currency === "MXN") {
|
||||
return "$";
|
||||
} else if (product?.currency === "ZAR") {
|
||||
return "R";
|
||||
} else if (product?.currency === "TRY") {
|
||||
return "₺";
|
||||
} else if (product?.currency === "SEK") {
|
||||
return "kr";
|
||||
} else if (product?.currency === "NOK") {
|
||||
return "kr";
|
||||
} else if (product?.currency === "DKK") {
|
||||
return "kr";
|
||||
} else if (product?.currency === "THB") {
|
||||
return "฿";
|
||||
} else if (product?.currency === "MYR") {
|
||||
return "RM";
|
||||
} else if (product?.currency === "TWD") {
|
||||
return "NT$";
|
||||
} else if (product?.currency === "CZK") {
|
||||
return "Kč";
|
||||
} else if (product?.currency === "HUF") {
|
||||
return "Ft";
|
||||
} else {
|
||||
return "(Unknown currency)";
|
||||
}
|
||||
}
|
||||
|
||||
getPrice(product) {
|
||||
return `${this.getCurrencySymbol(product)}${product?.price} (${Setting.getCurrencyText(product)})`;
|
||||
return `${Setting.getCurrencySymbol(product?.currency)}${product?.price} (${Setting.getCurrencyText(product)})`;
|
||||
}
|
||||
|
||||
// Call Weechat Pay via jsapi
|
||||
|
||||
@@ -206,34 +206,7 @@ class ProductEditPage extends React.Component {
|
||||
this.updateProductField("currency", value);
|
||||
})}>
|
||||
{
|
||||
[
|
||||
{id: "USD", name: "USD"},
|
||||
{id: "CNY", name: "CNY"},
|
||||
{id: "EUR", name: "EUR"},
|
||||
{id: "JPY", name: "JPY"},
|
||||
{id: "GBP", name: "GBP"},
|
||||
{id: "AUD", name: "AUD"},
|
||||
{id: "CAD", name: "CAD"},
|
||||
{id: "CHF", name: "CHF"},
|
||||
{id: "HKD", name: "HKD"},
|
||||
{id: "SGD", name: "SGD"},
|
||||
{id: "BRL", name: "BRL"},
|
||||
{id: "PLN", name: "PLN"},
|
||||
{id: "KRW", name: "KRW"},
|
||||
{id: "INR", name: "INR"},
|
||||
{id: "RUB", name: "RUB"},
|
||||
{id: "MXN", name: "MXN"},
|
||||
{id: "ZAR", name: "ZAR"},
|
||||
{id: "TRY", name: "TRY"},
|
||||
{id: "SEK", name: "SEK"},
|
||||
{id: "NOK", name: "NOK"},
|
||||
{id: "DKK", name: "DKK"},
|
||||
{id: "THB", name: "THB"},
|
||||
{id: "MYR", name: "MYR"},
|
||||
{id: "TWD", name: "TWD"},
|
||||
{id: "CZK", name: "CZK"},
|
||||
{id: "HUF", name: "HUF"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{Setting.getCurrencyWithFlag(item.id)}</Option>)
|
||||
Setting.CurrencyOptions.map((item, index) => <Option key={index} value={item.id}>{Setting.getCurrencyWithFlag(item.id)}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1544,6 +1544,35 @@ export function builtInObject(obj) {
|
||||
return obj.owner === "built-in" && BuiltInObjects.includes(obj.name);
|
||||
}
|
||||
|
||||
export const CurrencyOptions = [
|
||||
{id: "USD", name: "USD"},
|
||||
{id: "CNY", name: "CNY"},
|
||||
{id: "EUR", name: "EUR"},
|
||||
{id: "JPY", name: "JPY"},
|
||||
{id: "GBP", name: "GBP"},
|
||||
{id: "AUD", name: "AUD"},
|
||||
{id: "CAD", name: "CAD"},
|
||||
{id: "CHF", name: "CHF"},
|
||||
{id: "HKD", name: "HKD"},
|
||||
{id: "SGD", name: "SGD"},
|
||||
{id: "BRL", name: "BRL"},
|
||||
{id: "PLN", name: "PLN"},
|
||||
{id: "KRW", name: "KRW"},
|
||||
{id: "INR", name: "INR"},
|
||||
{id: "RUB", name: "RUB"},
|
||||
{id: "MXN", name: "MXN"},
|
||||
{id: "ZAR", name: "ZAR"},
|
||||
{id: "TRY", name: "TRY"},
|
||||
{id: "SEK", name: "SEK"},
|
||||
{id: "NOK", name: "NOK"},
|
||||
{id: "DKK", name: "DKK"},
|
||||
{id: "THB", name: "THB"},
|
||||
{id: "MYR", name: "MYR"},
|
||||
{id: "TWD", name: "TWD"},
|
||||
{id: "CZK", name: "CZK"},
|
||||
{id: "HUF", name: "HUF"},
|
||||
];
|
||||
|
||||
export function getCurrencySymbol(currency) {
|
||||
if (currency === "USD" || currency === "usd") {
|
||||
return "$";
|
||||
@@ -1636,15 +1665,19 @@ export function getCurrencyCountryCode(currency) {
|
||||
}
|
||||
|
||||
export function getCurrencyWithFlag(currency) {
|
||||
const translationKey = `currency:${currency}`;
|
||||
const translatedText = i18next.t(translationKey);
|
||||
const currencyText = translatedText === translationKey ? currency : translatedText;
|
||||
|
||||
const countryCode = getCurrencyCountryCode(currency);
|
||||
if (!countryCode) {
|
||||
return currency;
|
||||
return currencyText;
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<img src={`${StaticBaseUrl}/flag-icons/${countryCode}.svg`} alt={`${currency} flag`} height={20} style={{marginRight: 5}} />
|
||||
{currency}
|
||||
{currencyText}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1661,6 +1694,14 @@ export function getFriendlyUserName(account) {
|
||||
}
|
||||
}
|
||||
|
||||
export function isAnonymousUserName(userName) {
|
||||
if (!userName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /^u-[0-9a-f]{8}$/i.test(userName);
|
||||
}
|
||||
|
||||
export function getUserCommonFields() {
|
||||
return ["Owner", "Name", "CreatedTime", "UpdatedTime", "DeletedTime", "Id", "Type", "Password", "PasswordSalt", "DisplayName", "FirstName", "LastName", "Avatar", "PermanentAvatar",
|
||||
"Email", "EmailVerified", "Phone", "Location", "Address", "Affiliation", "Title", "IdCardType", "IdCard", "Homepage", "Bio", "Tag", "Region",
|
||||
|
||||
@@ -61,10 +61,15 @@ class SyncerEditPage extends React.Component {
|
||||
this.setState({
|
||||
syncer: res.data,
|
||||
});
|
||||
|
||||
if (res.data && res.data.organization) {
|
||||
this.getCerts(res.data.organization);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCerts(owner) {
|
||||
// Load certificates for the given organization
|
||||
CertBackend.getCerts(owner)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
@@ -79,9 +84,6 @@ class SyncerEditPage extends React.Component {
|
||||
this.setState({
|
||||
organizations: res.data || [],
|
||||
});
|
||||
if (res.data) {
|
||||
this.getCerts(`${res.data.owner}/${res.data.name}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -96,6 +98,12 @@ class SyncerEditPage extends React.Component {
|
||||
value = this.parseSyncerField(key, value);
|
||||
|
||||
const syncer = this.state.syncer;
|
||||
if (key === "organization" && syncer["organization"] !== value) {
|
||||
// the syncer changed the organization, reset the cert and reload certs
|
||||
syncer["cert"] = "";
|
||||
this.getCerts(value);
|
||||
}
|
||||
|
||||
syncer[key] = value;
|
||||
this.setState({
|
||||
syncer: syncer,
|
||||
@@ -320,11 +328,140 @@ class SyncerEditPage extends React.Component {
|
||||
"values": [],
|
||||
},
|
||||
];
|
||||
case "Google Workspace":
|
||||
return [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"casdoorName": "Id",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "primaryEmail",
|
||||
"type": "string",
|
||||
"casdoorName": "Name",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "name.fullName",
|
||||
"type": "string",
|
||||
"casdoorName": "DisplayName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "name.givenName",
|
||||
"type": "string",
|
||||
"casdoorName": "FirstName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "name.familyName",
|
||||
"type": "string",
|
||||
"casdoorName": "LastName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "suspended",
|
||||
"type": "boolean",
|
||||
"casdoorName": "IsForbidden",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "isAdmin",
|
||||
"type": "boolean",
|
||||
"casdoorName": "IsAdmin",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
];
|
||||
case "Active Directory":
|
||||
return [
|
||||
{
|
||||
"name": "objectGUID",
|
||||
"type": "string",
|
||||
"casdoorName": "Id",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "sAMAccountName",
|
||||
"type": "string",
|
||||
"casdoorName": "Name",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"casdoorName": "DisplayName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "givenName",
|
||||
"type": "string",
|
||||
"casdoorName": "FirstName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "sn",
|
||||
"type": "string",
|
||||
"casdoorName": "LastName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "mail",
|
||||
"type": "string",
|
||||
"casdoorName": "Email",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "mobile",
|
||||
"type": "string",
|
||||
"casdoorName": "Phone",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"casdoorName": "Title",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "department",
|
||||
"type": "string",
|
||||
"casdoorName": "Affiliation",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "userAccountControl",
|
||||
"type": "string",
|
||||
"casdoorName": "IsForbidden",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
needSshfields() {
|
||||
return this.state.syncer.type === "Database" && (this.state.syncer.databaseType === "mysql" || this.state.syncer.databaseType === "mssql" || this.state.syncer.databaseType === "postgres");
|
||||
}
|
||||
|
||||
renderSyncer() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
@@ -372,14 +509,14 @@ class SyncerEditPage extends React.Component {
|
||||
});
|
||||
})}>
|
||||
{
|
||||
["Database", "Keycloak", "WeCom", "Azure AD"]
|
||||
["Database", "Keycloak", "WeCom", "Azure AD", "Active Directory", "Google Workspace"]
|
||||
.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" ? null : (
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Active Directory" || this.state.syncer.type === "Google Workspace" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("syncer:Database type"), i18next.t("syncer:Database type - Tooltip"))} :
|
||||
@@ -434,7 +571,7 @@ class SyncerEditPage extends React.Component {
|
||||
this.state.syncer.type === "WeCom" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(this.state.syncer.type === "Azure AD" ? i18next.t("provider:Tenant ID") : i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
|
||||
{Setting.getLabel(this.state.syncer.type === "Azure AD" ? i18next.t("provider:Tenant ID") : this.state.syncer.type === "Google Workspace" ? i18next.t("syncer:Admin Email") : this.state.syncer.type === "Active Directory" ? i18next.t("ldap:Server") : i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.syncer.host} onChange={e => {
|
||||
@@ -445,10 +582,10 @@ class SyncerEditPage extends React.Component {
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" ? null : (
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
|
||||
{Setting.getLabel(this.state.syncer.type === "Active Directory" ? i18next.t("provider:LDAP port") : i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber value={this.state.syncer.port} onChange={value => {
|
||||
@@ -458,41 +595,55 @@ class SyncerEditPage extends React.Component {
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.syncer.type === "Google Workspace" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(
|
||||
this.state.syncer.type === "WeCom" ? i18next.t("syncer:Corp ID") :
|
||||
this.state.syncer.type === "Azure AD" ? i18next.t("provider:Client ID") :
|
||||
this.state.syncer.type === "Active Directory" ? i18next.t("syncer:Bind DN") :
|
||||
i18next.t("general:User"),
|
||||
i18next.t("general:User - Tooltip")
|
||||
)} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.syncer.user} onChange={e => {
|
||||
this.updateSyncerField("user", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(
|
||||
this.state.syncer.type === "WeCom" ? i18next.t("provider:Corp ID") :
|
||||
this.state.syncer.type === "Azure AD" ? i18next.t("provider:Client ID") :
|
||||
i18next.t("general:User"),
|
||||
i18next.t("general:User - Tooltip")
|
||||
)} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.syncer.user} onChange={e => {
|
||||
this.updateSyncerField("user", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(
|
||||
this.state.syncer.type === "WeCom" ? i18next.t("provider:Corp Secret") :
|
||||
this.state.syncer.type === "Azure AD" ? i18next.t("provider:Client Secret") :
|
||||
i18next.t("general:Password"),
|
||||
this.state.syncer.type === "WeCom" ? i18next.t("syncer:Corp secret") :
|
||||
this.state.syncer.type === "Azure AD" ? i18next.t("provider:Client secret") :
|
||||
this.state.syncer.type === "Google Workspace" ? i18next.t("syncer:Service account key") :
|
||||
i18next.t("general:Password"),
|
||||
i18next.t("general:Password - Tooltip")
|
||||
)} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input.Password value={this.state.syncer.password} onChange={e => {
|
||||
this.updateSyncerField("password", e.target.value);
|
||||
}} />
|
||||
{
|
||||
this.state.syncer.type === "Google Workspace" ? (
|
||||
<Input.TextArea rows={4} value={this.state.syncer.password} onChange={e => {
|
||||
this.updateSyncerField("password", e.target.value);
|
||||
}} placeholder={i18next.t("syncer:Paste your Google Workspace service account JSON key here")} />
|
||||
) : (
|
||||
<Input.Password value={this.state.syncer.password} onChange={e => {
|
||||
this.updateSyncerField("password", e.target.value);
|
||||
}} />
|
||||
)
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" ? null : (
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :
|
||||
{Setting.getLabel(this.state.syncer.type === "Active Directory" ? i18next.t("syncer:Base DN") : i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.syncer.database} onChange={e => {
|
||||
@@ -503,7 +654,7 @@ class SyncerEditPage extends React.Component {
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.syncer.databaseType === "mysql" || this.state.syncer.databaseType === "mssql" || this.state.syncer.databaseType === "postgres" ? (
|
||||
this.needSshfields() ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:SSH type"), i18next.t("general:SSH type - Tooltip"))} :
|
||||
@@ -521,7 +672,7 @@ class SyncerEditPage extends React.Component {
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.syncer.sshType && this.state.syncer.databaseType === "mysql" || this.state.syncer.databaseType === "mssql" || this.state.syncer.databaseType === "postgres" ? (
|
||||
this.state.syncer.sshType && this.needSshfields() ? (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
@@ -554,7 +705,7 @@ class SyncerEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.syncer.sshType === "password" && (this.state.syncer.databaseType === "mysql" || this.state.syncer.databaseType === "mssql" || this.state.syncer.databaseType === "postgres") ?
|
||||
this.state.syncer.sshType === "password" && this.needSshfields() ?
|
||||
(
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
@@ -585,7 +736,7 @@ class SyncerEditPage extends React.Component {
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" ? null : (
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("syncer:Table"), i18next.t("syncer:Table - Tooltip"))} :
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -14,11 +14,14 @@
|
||||
|
||||
import React from "react";
|
||||
import * as TransactionBackend from "./backend/TransactionBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
import {Button, Card, Col, Input, Row} from "antd";
|
||||
import * as Setting from "./Setting";
|
||||
import {Button, Card, Col, Input, InputNumber, Row, Select} from "antd";
|
||||
import i18next from "i18next";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
class TransactionEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -26,15 +29,19 @@ class TransactionEditPage extends React.Component {
|
||||
classes: props,
|
||||
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||
transactionName: props.match.params.transactionName,
|
||||
application: null,
|
||||
transaction: null,
|
||||
providers: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
organizations: [],
|
||||
applications: [],
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getTransaction();
|
||||
if (this.state.mode === "recharge") {
|
||||
this.getOrganizations();
|
||||
this.getApplications(this.state.organizationName);
|
||||
}
|
||||
}
|
||||
|
||||
getTransaction() {
|
||||
@@ -45,15 +52,58 @@ class TransactionEditPage extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", res.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
transaction: res.data,
|
||||
});
|
||||
|
||||
Setting.scrollToDiv("invoice-area");
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
const isGlobalAdmin = Setting.isAdminUser(this.props.account);
|
||||
const owner = isGlobalAdmin ? "admin" : this.state.organizationName;
|
||||
|
||||
OrganizationBackend.getOrganizations(owner)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
organizations: res.data || [],
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
getApplications(organizationName) {
|
||||
const targetOrganizationName = organizationName || this.state.organizationName;
|
||||
ApplicationBackend.getApplicationsByOrganization("admin", targetOrganizationName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
applications: res.data || [],
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
submitTransactionEdit(exitAfterSave) {
|
||||
if (this.state.transaction === null) {
|
||||
return;
|
||||
}
|
||||
const transaction = Setting.deepCopy(this.state.transaction);
|
||||
TransactionBackend.updateTransaction(this.state.transaction.owner, this.state.transactionName, transaction)
|
||||
.then((res) => {
|
||||
@@ -70,7 +120,7 @@ class TransactionEditPage extends React.Component {
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
|
||||
this.updatePaymentField("name", this.state.transactionName);
|
||||
this.updateTransactionField("name", this.state.transactionName);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -79,6 +129,9 @@ class TransactionEditPage extends React.Component {
|
||||
}
|
||||
|
||||
deleteTransaction() {
|
||||
if (this.state.transaction === null) {
|
||||
return;
|
||||
}
|
||||
TransactionBackend.deleteTransaction(this.state.transaction)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
@@ -93,64 +146,68 @@ class TransactionEditPage extends React.Component {
|
||||
}
|
||||
|
||||
parseTransactionField(key, value) {
|
||||
if ([""].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
if (["amount"].includes(key)) {
|
||||
value = parseFloat(value);
|
||||
if (isNaN(value)) {
|
||||
value = 0;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
getApplication() {
|
||||
ApplicationBackend.getApplication("admin", this.state.applicationName)
|
||||
.then((res) => {
|
||||
if (res.data === null) {
|
||||
this.props.history.push("/404");
|
||||
return;
|
||||
}
|
||||
updateTransactionField(key, value) {
|
||||
value = this.parseTransactionField(key, value);
|
||||
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", res.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
const application = res.data;
|
||||
if (application.grantTypes === null || application.grantTypes === undefined || application.grantTypes.length === 0) {
|
||||
application.grantTypes = ["authorization_code"];
|
||||
}
|
||||
|
||||
if (application.tags === null || application.tags === undefined) {
|
||||
application.tags = [];
|
||||
}
|
||||
|
||||
this.setState({
|
||||
application: application,
|
||||
});
|
||||
|
||||
this.getCerts(application);
|
||||
|
||||
this.getSamlMetadata(application.enableSamlPostBinding);
|
||||
});
|
||||
const transaction = this.state.transaction;
|
||||
transaction[key] = value;
|
||||
this.setState({
|
||||
transaction: transaction,
|
||||
});
|
||||
}
|
||||
|
||||
renderTransaction() {
|
||||
const isRechargeMode = this.state.mode === "recharge";
|
||||
const title = isRechargeMode ? i18next.t("transaction:Recharge") : (this.state.mode === "add" ? i18next.t("transaction:New Transaction") : i18next.t("transaction:Edit Transaction"));
|
||||
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{this.state.mode === "add" ? i18next.t("transaction:New Transaction") : i18next.t("transaction:Edit Transaction")}
|
||||
{title}
|
||||
<Button onClick={() => this.submitTransactionEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitTransactionEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteTransaction()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
{(this.state.mode === "add" || isRechargeMode) ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteTransaction()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.owner} onChange={e => {
|
||||
// this.updatePaymentField('organization', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
{isRechargeMode ? (
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.transaction.owner}
|
||||
onChange={(value) => {
|
||||
this.updateTransactionField("owner", value);
|
||||
this.updateTransactionField("application", "");
|
||||
this.getApplications(value);
|
||||
}}>
|
||||
{
|
||||
this.state.organizations.map((org, index) => <Option key={index} value={org.name}>{org.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
) : (
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.owner} onChange={e => {
|
||||
// this.updatePaymentField('organization', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
|
||||
@@ -161,13 +218,69 @@ class TransactionEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
{isRechargeMode ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Application"), i18next.t("general:Application - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.transaction.application}
|
||||
allowClear
|
||||
onChange={(value) => {
|
||||
this.updateTransactionField("application", value || "");
|
||||
}}>
|
||||
{
|
||||
this.state.applications.map((app, index) => <Option key={index} value={app.name}>{app.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
) : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Application"), i18next.t("general:Application - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.application} onChange={e => {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.displayName} onChange={e => {
|
||||
this.updatePaymentField("displayName", e.target.value);
|
||||
<Input disabled={true} value={this.state.transaction.domain} onChange={e => {
|
||||
// this.updatePaymentField('domain', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Category"), i18next.t("provider:Category - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.category} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("payment:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.type} onChange={e => {
|
||||
// this.updatePaymentField('type', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Subtype"), i18next.t("provider:Subtype - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.subtype} onChange={e => {
|
||||
// this.updatePaymentField('subtype', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -183,41 +296,10 @@ class TransactionEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Category"), i18next.t("provider:Category - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.category} onChange={e => {
|
||||
this.updatePaymentField("displayName", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("payment:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.type} onChange={e => {
|
||||
// this.updatePaymentField('type', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("payment:Product"), i18next.t("payment:Product - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.productName} onChange={e => {
|
||||
// this.updatePaymentField('productName', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("product:Detail"), i18next.t("product:Detail - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.detail} onChange={e => {
|
||||
// this.updatePaymentField('currency', e.target.value);
|
||||
<Input disabled={true} value={this.state.transaction.user} onChange={e => {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -226,59 +308,44 @@ class TransactionEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("transaction:Tag - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<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("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.currency} onChange={e => {
|
||||
// this.updatePaymentField('currency', e.target.value);
|
||||
}} />
|
||||
{isRechargeMode ? (
|
||||
<Select virtual={false} style={{width: "100%"}}
|
||||
value={this.state.transaction.tag}
|
||||
onChange={(value) => {
|
||||
this.updateTransactionField("tag", value);
|
||||
}}>
|
||||
<Option value="Organization">Organization</Option>
|
||||
<Option value="User">User</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("transaction:Amount"), i18next.t("transaction:Amount - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.amount} onChange={e => {
|
||||
// this.updatePaymentField('amount', e.target.value);
|
||||
<Col span={4} >
|
||||
<InputNumber disabled={!isRechargeMode} value={this.state.transaction.amount ?? 0} onChange={value => {
|
||||
this.updateTransactionField("amount", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("product:Return URL"), i18next.t("product:Return URL - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("currency:Currency"), i18next.t("currency:Currency - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.user} onChange={e => {
|
||||
// this.updatePaymentField('amount', 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} >
|
||||
<Input disabled={true} value={this.state.transaction.user} onChange={e => {
|
||||
// this.updatePaymentField('amount', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Application"), i18next.t("general:Application - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.application} onChange={e => {
|
||||
// this.updatePaymentField('amount', e.target.value);
|
||||
}} />
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.transaction.currency} disabled={!isRechargeMode} onChange={(value => {
|
||||
this.updateTransactionField("currency", value);
|
||||
})}>
|
||||
{
|
||||
Setting.CurrencyOptions.map((item, index) => <Option key={index} value={item.id}>{Setting.getCurrencyWithFlag(item.id)}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
@@ -309,13 +376,17 @@ class TransactionEditPage extends React.Component {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
this.state.transaction !== null ? this.renderTransaction() : null
|
||||
this.state.transaction !== null ? (
|
||||
<>
|
||||
{this.renderTransaction()}
|
||||
<div style={{marginTop: "20px", marginLeft: "40px"}}>
|
||||
<Button size="large" onClick={() => this.submitTransactionEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitTransactionEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteTransaction()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
<div style={{marginTop: "20px", marginLeft: "40px"}}>
|
||||
<Button size="large" onClick={() => this.submitTransactionEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitTransactionEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteTransaction()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import BaseListPage from "./BaseListPage";
|
||||
import i18next from "i18next";
|
||||
import {Link} from "react-router-dom";
|
||||
import * as Setting from "./Setting";
|
||||
import * as Provider from "./auth/Provider";
|
||||
import {Button, Table} from "antd";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
import React from "react";
|
||||
@@ -25,26 +24,21 @@ import moment from "moment/moment";
|
||||
|
||||
class TransactionListPage extends BaseListPage {
|
||||
newTransaction() {
|
||||
const randomName = Setting.getRandomName();
|
||||
const organizationName = Setting.getRequestOrganization(this.props.account);
|
||||
return {
|
||||
owner: organizationName,
|
||||
name: `transaction_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Transaction - ${randomName}`,
|
||||
provider: "provider_pay_paypal",
|
||||
application: "app-built-in",
|
||||
domain: "https://ai-admin.casibase.com",
|
||||
category: "",
|
||||
type: "PayPal",
|
||||
productName: "computer-1",
|
||||
productDisplayName: "A notebook computer",
|
||||
detail: "This is a computer with excellent CPU, memory and disk",
|
||||
tag: "Promotion-1",
|
||||
currency: "USD",
|
||||
amount: 0,
|
||||
returnUrl: "https://door.casdoor.com/transactions",
|
||||
type: "chat_id",
|
||||
subtype: "message_id",
|
||||
provider: "provider_chatgpt",
|
||||
user: "admin",
|
||||
application: "",
|
||||
payment: "payment_bhn1ra",
|
||||
tag: "AI message",
|
||||
amount: 0.1,
|
||||
currency: "USD",
|
||||
payment: "payment_paypal_001",
|
||||
state: "Paid",
|
||||
};
|
||||
}
|
||||
@@ -74,7 +68,8 @@ class TransactionListPage extends BaseListPage {
|
||||
TransactionBackend.addTransaction(newTransaction)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push({pathname: `/transactions/${newTransaction.owner}/${newTransaction.name}`, mode: "add"});
|
||||
const transactionId = res.data;
|
||||
this.props.history.push({pathname: `/transactions/${newTransaction.owner}/${transactionId}`, mode: "add"});
|
||||
Setting.showMessage("success", i18next.t("general:Successfully added"));
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
|
||||
@@ -86,24 +81,41 @@ class TransactionListPage extends BaseListPage {
|
||||
});
|
||||
}
|
||||
|
||||
rechargeTransaction() {
|
||||
const organizationName = Setting.getRequestOrganization(this.props.account);
|
||||
const newTransaction = {
|
||||
owner: organizationName,
|
||||
createdTime: moment().format(),
|
||||
application: "",
|
||||
domain: "",
|
||||
category: "",
|
||||
type: "",
|
||||
subtype: "",
|
||||
provider: "",
|
||||
user: "",
|
||||
tag: "Organization",
|
||||
amount: 0,
|
||||
currency: "USD",
|
||||
payment: "",
|
||||
state: "Paid",
|
||||
};
|
||||
TransactionBackend.addTransaction(newTransaction)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const transactionId = res.data;
|
||||
this.props.history.push({pathname: `/transactions/${newTransaction.owner}/${transactionId}`, mode: "recharge"});
|
||||
Setting.showMessage("success", i18next.t("general:Successfully added"));
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(transactions) {
|
||||
const columns = [
|
||||
{
|
||||
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}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Organization"),
|
||||
dataIndex: "owner",
|
||||
@@ -121,36 +133,21 @@ class TransactionListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Provider"),
|
||||
dataIndex: "provider",
|
||||
key: "provider",
|
||||
width: "150px",
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "180px",
|
||||
fixed: "left",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("provider"),
|
||||
...this.getColumnSearchProps("name"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/providers/${record.owner}/${text}`}>
|
||||
<Link to={`/transactions/${record.owner}/${record.name}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:User"),
|
||||
dataIndex: "user",
|
||||
key: "user",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("user"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/users/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: i18next.t("general:Created time"),
|
||||
dataIndex: "createdTime",
|
||||
@@ -161,59 +158,6 @@ class TransactionListPage extends BaseListPage {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("provider:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: "140px",
|
||||
align: "center",
|
||||
filterMultiple: false,
|
||||
filters: Setting.getProviderTypeOptions("Payment").map((o) => {return {text: o.id, value: o.name};}),
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
record.category = "Payment";
|
||||
return Provider.getProviderLogoWidget(record);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("payment:Product"),
|
||||
dataIndex: "productDisplayName",
|
||||
key: "productDisplayName",
|
||||
// width: '160px',
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("productDisplayName"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/products/${record.owner}/${record.productName}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("payment:Currency"),
|
||||
dataIndex: "currency",
|
||||
key: "currency",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("currency"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("transaction:Amount"),
|
||||
dataIndex: "amount",
|
||||
key: "amount",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("amount"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:User"),
|
||||
dataIndex: "user",
|
||||
key: "user",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("user"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Application"),
|
||||
dataIndex: "application",
|
||||
@@ -229,6 +173,143 @@ class TransactionListPage extends BaseListPage {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
@@ -237,8 +318,11 @@ class TransactionListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("payment"),
|
||||
render: (text, record, index) => {
|
||||
if (!text) {
|
||||
return text;
|
||||
}
|
||||
return (
|
||||
<Link to={`/payments/${record.owner}/${record.payment}`}>
|
||||
<Link to={`/payments/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
@@ -259,12 +343,14 @@ class TransactionListPage extends BaseListPage {
|
||||
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(`/transactions/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<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>
|
||||
@@ -283,12 +369,17 @@ class TransactionListPage extends BaseListPage {
|
||||
return (
|
||||
<div>
|
||||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={transactions} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Transactions")}
|
||||
<Button type="primary" size="small" onClick={this.addTransaction.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
title={() => {
|
||||
const isAdmin = Setting.isLocalAdminUser(this.props.account);
|
||||
return (
|
||||
<div>
|
||||
{i18next.t("general:Transactions")}
|
||||
<Button type="primary" 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>
|
||||
);
|
||||
}}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
/>
|
||||
|
||||
@@ -316,11 +316,6 @@ class UserEditPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
let isKeysGenerated = false;
|
||||
if (this.state.user.accessKey !== "" && this.state.user.accessKey !== "") {
|
||||
isKeysGenerated = true;
|
||||
}
|
||||
|
||||
if (accountItem.name === "Organization") {
|
||||
return (
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
@@ -465,7 +460,7 @@ class UserEditPage extends React.Component {
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Email"), i18next.t("general:Email - Tooltip"))} :
|
||||
</Col>
|
||||
<Col style={{paddingRight: "20px"}} span={11} >
|
||||
<Col style={{paddingRight: "20px"}} span={5} >
|
||||
<Input
|
||||
value={this.state.user.email}
|
||||
style={{width: "280Px"}}
|
||||
@@ -475,7 +470,7 @@ class UserEditPage extends React.Component {
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={Setting.isMobile() ? 22 : 11} >
|
||||
<Col span={Setting.isMobile() ? 22 : 5} >
|
||||
{/* backend auto get the current user, so admin can not edit. Just self can reset*/}
|
||||
{this.isSelf() ? <ResetModal application={this.state.application} disabled={disabled} buttonText={i18next.t("user:Reset Email...")} destType={"email"} /> : null}
|
||||
</Col>
|
||||
@@ -487,7 +482,7 @@ class UserEditPage extends React.Component {
|
||||
<Col style={{marginTop: "5px"}} span={Setting.isMobile() ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Phone"), i18next.t("general:Phone - Tooltip"))} :
|
||||
</Col>
|
||||
<Col style={{paddingRight: "20px"}} span={11} >
|
||||
<Col style={{paddingRight: "20px"}} span={5} >
|
||||
<Input.Group compact style={{width: "280Px"}}>
|
||||
<CountryCodeSelect
|
||||
style={{width: "30%"}}
|
||||
@@ -506,7 +501,7 @@ class UserEditPage extends React.Component {
|
||||
}} />
|
||||
</Input.Group>
|
||||
</Col>
|
||||
<Col span={Setting.isMobile() ? 24 : 11} >
|
||||
<Col span={Setting.isMobile() ? 24 : 5} >
|
||||
{this.isSelf() ? (<ResetModal application={this.state.application} countryCode={this.getCountryCode()} disabled={disabled} buttonText={i18next.t("user:Reset Phone...")} destType={"phone"} />) : null}
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -750,6 +745,34 @@ class UserEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "Balance currency") {
|
||||
return (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("user:Balance currency"), i18next.t("user:Balance currency - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.user.balanceCurrency || "USD"} onChange={(value => {
|
||||
this.updateUserField("balanceCurrency", value);
|
||||
})}>
|
||||
{
|
||||
Setting.CurrencyOptions.map((item, index) => <Option key={index} value={item.id}>{Setting.getCurrencyWithFlag(item.id)}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "Transactions") {
|
||||
return (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Transactions"), i18next.t("general:Transactions"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<TransactionTable transactions={this.state.transactions} hideTag={true} />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "Score") {
|
||||
return (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
@@ -835,7 +858,7 @@ class UserEditPage extends React.Component {
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Access key"), i18next.t("general:Access key - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
@@ -843,16 +866,18 @@ class UserEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Access secret"), i18next.t("general:Access secret - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.user.accessSecret} disabled={true} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Row style={{marginTop: "20px", marginBottom: "20px"}} >
|
||||
<Col span={22} >
|
||||
<Button onClick={() => this.addUserKeys()}>{i18next.t(isKeysGenerated ? "general:update" : "general:generate")}</Button>
|
||||
<Button type="primary" onClick={() => this.addUserKeys()}>
|
||||
{i18next.t("general:Generate")}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
@@ -1189,19 +1214,6 @@ class UserEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "Transactions") {
|
||||
return (
|
||||
this.state.mode !== "add" && this.state.transactions.length > 0 ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("transaction:Transactions"), i18next.t("transaction:Transactions"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<TransactionTable transactions={this.state.transactions} />
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ class UserListPage extends BaseListPage {
|
||||
signupApplication: this.state.organization.defaultApplication,
|
||||
registerType: "Add User",
|
||||
registerSource: `${this.props.account.owner}/${this.props.account.name}`,
|
||||
balanceCurrency: this.state.organization.balanceCurrency || "USD",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -375,6 +376,16 @@ class UserListPage extends BaseListPage {
|
||||
return text ?? 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("user:Balance currency"),
|
||||
dataIndex: "balanceCurrency",
|
||||
key: "balanceCurrency",
|
||||
width: "140px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return text || "USD";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("user:Is admin"),
|
||||
dataIndex: "isAdmin",
|
||||
@@ -383,7 +394,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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -395,7 +406,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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -407,7 +418,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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
81
web/src/backend/OrderBackend.js
Normal file
81
web/src/backend/OrderBackend.js
Normal file
@@ -0,0 +1,81 @@
|
||||
// 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 * as Setting from "../Setting";
|
||||
|
||||
export function getOrders(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-orders?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
method: "GET",
|
||||
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",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getOrder(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-order?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function updateOrder(owner, name, order) {
|
||||
const newOrder = Setting.deepCopy(order);
|
||||
return fetch(`${Setting.ServerUrl}/api/update-order?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newOrder),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addOrder(order) {
|
||||
const newOrder = Setting.deepCopy(order);
|
||||
return fetch(`${Setting.ServerUrl}/api/add-order`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newOrder),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function deleteOrder(order) {
|
||||
const newOrder = Setting.deepCopy(order);
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-order`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newOrder),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
@@ -35,6 +35,10 @@ const FaceRecognitionModal = (props) => {
|
||||
const [currentFaceIndex, setCurrentFaceIndex] = React.useState();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!visible || modelsLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadModels = async() => {
|
||||
// const MODEL_URL = "https://justadudewhohacks.github.io/face-api.js/models";
|
||||
const MODEL_URL = `${Setting.StaticBaseUrl}/casdoor/models`;
|
||||
@@ -51,7 +55,7 @@ const FaceRecognitionModal = (props) => {
|
||||
});
|
||||
};
|
||||
loadModels();
|
||||
}, []);
|
||||
}, [visible, modelsLoaded]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (withImage) {
|
||||
@@ -304,7 +308,7 @@ const FaceRecognitionModal = (props) => {
|
||||
if (maxScore < 0.9) {
|
||||
message.error(i18next.t("login:Face recognition failed"));
|
||||
}
|
||||
}}> {i18next.t("application:Generate Face ID")}</Button> : null
|
||||
}}> {i18next.t("general:Generate")}</Button> : null
|
||||
}
|
||||
</Space>
|
||||
{
|
||||
|
||||
@@ -142,7 +142,7 @@ export const PasswordModal = (props) => {
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Button type="default" disabled={props.disabled} onClick={showModal}>
|
||||
<Button type="primary" disabled={props.disabled} onClick={showModal}>
|
||||
{hasOldPassword ? i18next.t("user:Modify password...") : i18next.t("user:Set password...")}
|
||||
</Button>
|
||||
<Modal
|
||||
|
||||
@@ -69,7 +69,7 @@ export const ResetModal = (props) => {
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Button type="default" onClick={showModal}>
|
||||
<Button type="primary" onClick={showModal}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
<Modal
|
||||
|
||||
@@ -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",
|
||||
@@ -378,12 +382,15 @@
|
||||
"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",
|
||||
"Organization is null": "Organization is null",
|
||||
"Organizations": "Organizations",
|
||||
"Orders": "Orders",
|
||||
"Password": "Password",
|
||||
"Password - Tooltip": "Make sure the password is correct",
|
||||
"Password complexity options": "Password complexity options",
|
||||
@@ -406,6 +413,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",
|
||||
@@ -427,6 +435,7 @@
|
||||
"Records": "Records",
|
||||
"Request": "Request",
|
||||
"Request URI": "Request URI",
|
||||
"Reset": "Reset",
|
||||
"Reset to Default": "Reset to Default",
|
||||
"Resources": "Resources",
|
||||
"Role": "Role",
|
||||
@@ -442,6 +451,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",
|
||||
@@ -733,6 +743,18 @@
|
||||
"Widget items": "Widget items",
|
||||
"Widget items - Tooltip": "Items displayed in the widget"
|
||||
},
|
||||
"order": {
|
||||
"Edit Order": "Edit Order",
|
||||
"End time": "End time",
|
||||
"End time - Tooltip": "When the order expires (e.g., for time-limited purchases)",
|
||||
"New Order": "New Order",
|
||||
"Payment": "Payment",
|
||||
"Payment - Tooltip": "The payment associated with this order",
|
||||
"Product": "Product",
|
||||
"Product - Tooltip": "The product that was purchased",
|
||||
"Start time": "Start time",
|
||||
"Start time - Tooltip": "When the order starts"
|
||||
},
|
||||
"payment": {
|
||||
"Confirm your invoice information": "Confirm your invoice information",
|
||||
"Currency": "Currency",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
"Favicon - Tooltip": "该组织所有Casdoor页面中所使用的Favicon图标URL",
|
||||
"Filter": "筛选",
|
||||
"First name": "名字",
|
||||
"First name - Tooltip": "用户的名字",
|
||||
"Forced redirect origin - Tooltip": "强制重定向到指定的来源",
|
||||
@@ -360,7 +364,7 @@
|
||||
"Logo - Tooltip": "应用程序向外展示的图标",
|
||||
"Logo dark": "暗黑logo",
|
||||
"Logo dark - Tooltip": "暗黑主题下使用的logo",
|
||||
"MFA items": "MFA 项",
|
||||
"MFA items": "MFA设置项",
|
||||
"MFA items - Tooltip": "多因素认证项目",
|
||||
"Master password": "万能密码",
|
||||
"Master password - Tooltip": "可用来登录该组织下的所有用户,方便管理员以该用户身份登录,以解决技术问题",
|
||||
@@ -378,7 +382,9 @@
|
||||
"Non-LDAP": "禁用LDAP",
|
||||
"None": "无",
|
||||
"OAuth providers": "OAuth提供方",
|
||||
"OFF": "关",
|
||||
"OK": "确定",
|
||||
"ON": "开",
|
||||
"Only 1 MFA method can be required": "只能要求1个MFA方法",
|
||||
"Organization": "组织",
|
||||
"Organization - Tooltip": "类似于租户、用户池等概念,每个用户和应用都从属于一个组织",
|
||||
@@ -410,6 +416,7 @@
|
||||
"Plan - Tooltip": "订阅里的计划",
|
||||
"Plans": "计划",
|
||||
"Plans - Tooltip": "订阅里的计划",
|
||||
"Please input your search": "请输入搜索内容",
|
||||
"Please select at least 1 user first": "请至少选择1个用户",
|
||||
"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 天新增的用户",
|
||||
@@ -1247,8 +1256,8 @@
|
||||
"Token type - Tooltip": "令牌类型"
|
||||
},
|
||||
"transaction": {
|
||||
"Amount": "数量",
|
||||
"Amount - Tooltip": "交易产品的数量",
|
||||
"Amount": "金额",
|
||||
"Amount - Tooltip": "交易产品的金额",
|
||||
"Edit Transaction": "编辑交易",
|
||||
"New Transaction": "添加交易",
|
||||
"Tag - Tooltip": "交易的标签"
|
||||
|
||||
@@ -91,6 +91,7 @@ class AccountTable extends React.Component {
|
||||
{name: "Birthday", label: i18next.t("user:Birthday")},
|
||||
{name: "Education", label: i18next.t("user:Education")},
|
||||
{name: "Balance", label: i18next.t("user:Balance")},
|
||||
{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")},
|
||||
@@ -113,7 +114,6 @@ class AccountTable extends React.Component {
|
||||
{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")},
|
||||
{name: "Transactions", label: i18next.t("transaction:Transactions")},
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -138,15 +138,15 @@ class FaceIdTable extends React.Component {
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("user:Face IDs")}
|
||||
<Button disabled={this.props.table?.length >= 5} style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.setState({openFaceRecognitionModal: true, withImage: false})}>
|
||||
<Button disabled={this.props.table?.length >= 5} style={{marginRight: "10px"}} type="primary" size="small" onClick={() => this.setState({openFaceRecognitionModal: true, withImage: false})}>
|
||||
{i18next.t("application:Add Face ID")}
|
||||
</Button>
|
||||
<Button disabled={this.props.table?.length >= 5} style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.setState({openFaceRecognitionModal: true, withImage: true})}>
|
||||
<Button disabled={this.props.table?.length >= 5} style={{marginRight: "10px"}} size="small" onClick={() => this.setState({openFaceRecognitionModal: true, withImage: true})}>
|
||||
{i18next.t("application:Add Face ID with Image")}
|
||||
</Button>
|
||||
<Upload maxCount={1} accept="image/*" showUploadList={false}
|
||||
beforeUpload={file => {return false;}} onChange={info => {handleUpload(info);}}>
|
||||
<Button id="upload-button" icon={<UploadOutlined />} loading={this.state.uploading} type="primary" size="small">
|
||||
<Button id="upload-button" icon={<UploadOutlined />} loading={this.state.uploading} size="small">
|
||||
{i18next.t("resource:Upload a file...")}
|
||||
</Button>
|
||||
</Upload>
|
||||
|
||||
@@ -179,7 +179,7 @@ class MfaAccountTable extends React.Component {
|
||||
overlayInnerStyle={{padding: 0}}
|
||||
content={<CasdoorAppQrCode accessToken={this.props.accessToken} icon={this.state.icon} />}
|
||||
>
|
||||
<Button style={{marginRight: "10px"}} type="primary" size="small">
|
||||
<Button style={{marginRight: "10px"}} size="small">
|
||||
{i18next.t("general:QR Code")}
|
||||
</Button>
|
||||
</Popover>
|
||||
@@ -187,7 +187,7 @@ class MfaAccountTable extends React.Component {
|
||||
trigger="click"
|
||||
content={<CasdoorAppUrl accessToken={this.props.accessToken} />}
|
||||
>
|
||||
<Button type="primary" size="small">
|
||||
<Button size="small">
|
||||
{i18next.t("general:URL")}
|
||||
</Button>
|
||||
</Popover>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {Table} from "antd";
|
||||
import {Link} from "react-router-dom";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
@@ -31,43 +32,161 @@ class TransactionTable extends React.Component {
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "150px",
|
||||
width: "180px",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Link to={`/transactions/${record.owner}/${record.name}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Created time"),
|
||||
dataIndex: "createdTime",
|
||||
key: "createdTime",
|
||||
width: "160px",
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
render: (text) => Setting.getFormattedDate(text),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Application"),
|
||||
dataIndex: "application",
|
||||
key: "application",
|
||||
width: "120px",
|
||||
render: (text, record) => {
|
||||
if (!text) {
|
||||
return text;
|
||||
}
|
||||
return (
|
||||
<Link to={`/applications/${record.owner}/${record.application}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("transaction:Category"),
|
||||
title: i18next.t("provider:Domain"),
|
||||
dataIndex: "domain",
|
||||
key: "domain",
|
||||
width: "200px",
|
||||
render: (text) => {
|
||||
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",
|
||||
},
|
||||
{
|
||||
title: i18next.t("transaction:Type"),
|
||||
title: i18next.t("provider:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: "120px",
|
||||
width: "140px",
|
||||
render: (text, record) => {
|
||||
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",
|
||||
render: (text, record) => {
|
||||
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",
|
||||
render: (text, record) => {
|
||||
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>
|
||||
);
|
||||
},
|
||||
},
|
||||
...(!this.props.hideTag ? [{
|
||||
title: i18next.t("user:Tag"),
|
||||
dataIndex: "tag",
|
||||
key: "tag",
|
||||
width: "120px",
|
||||
}] : []),
|
||||
{
|
||||
title: i18next.t("transaction:Amount"),
|
||||
dataIndex: "amount",
|
||||
key: "amount",
|
||||
width: "100px",
|
||||
width: "120px",
|
||||
},
|
||||
{
|
||||
title: i18next.t("payment:Currency"),
|
||||
dataIndex: "currency",
|
||||
key: "currency",
|
||||
width: "120px",
|
||||
render: (text, record, index) => {
|
||||
return `${record.currency} ${text}`;
|
||||
return Setting.getCurrencyWithFlag(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("transaction:State"),
|
||||
title: i18next.t("general:Payment"),
|
||||
dataIndex: "payment",
|
||||
key: "payment",
|
||||
width: "120px",
|
||||
render: (text, record) => {
|
||||
if (!text) {
|
||||
return text;
|
||||
}
|
||||
return (
|
||||
<Link to={`/payments/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:State"),
|
||||
dataIndex: "state",
|
||||
key: "state",
|
||||
width: "100px",
|
||||
width: "120px",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -76,7 +195,7 @@ class TransactionTable extends React.Component {
|
||||
scroll={{x: "max-content"}}
|
||||
columns={columns}
|
||||
dataSource={this.props.transactions}
|
||||
rowKey="name"
|
||||
rowKey={(record) => `${record.owner}/${record.name}`}
|
||||
size="middle"
|
||||
bordered
|
||||
pagination={{pageSize: 10}}
|
||||
|
||||
Reference in New Issue
Block a user