Compare commits

...

34 Commits

Author SHA1 Message Date
Yang Luo
e600ea7efd feat: add i18n support for table column widgets (#4541) 2025-11-22 16:39:44 +08:00
Yang Luo
8002613398 feat: Add exchange rate conversion for balance calculations (#4534) 2025-11-21 22:13:26 +08:00
IsAurora6
a48b1d0c73 feat: Add recharge functionality with editable fields to transaction list page. (#4536) 2025-11-21 22:11:38 +08:00
Yang Luo
d8b5ecba36 feat: add transaction's subtype field and fix product recharge (#4531) 2025-11-21 19:27:07 +08:00
IsAurora6
e3a8a464d5 feat: Add balanceCurrency field to Organization and User models. (#4525) 2025-11-21 14:42:54 +08:00
IsAurora6
a575ba02d6 feat: Fixed a bug in addTransaction and optimized the transactionEdit page. (#4523) 2025-11-21 09:35:12 +08:00
IsAurora6
a9fcfceb8f feat: Add currency icons wherever currency appears, and optimize the display columns in the transaction table. (#4516) 2025-11-20 22:33:00 +08:00
ledigang
712482ffb9 refactor: omit unnecessary reassignment (#4509) 2025-11-20 18:47:03 +08:00
Yang Luo
84e2c760d9 feat: lazy-load Face ID models only when modal opens (#4508) 2025-11-20 18:46:31 +08:00
IsAurora6
4ab85d6781 feat: Distinguish and allow users to configure adminNavItems and userNavItems. (#4503) 2025-11-20 11:05:30 +08:00
Yang Luo
2ede56ac46 fix: refactor out Setting.CurrencyOptions (#4502) 2025-11-19 21:51:28 +08:00
Yang Luo
6a819a9a20 feat: persist hash column when updating users (#4500) 2025-11-19 21:50:32 +08:00
IsAurora6
ddaeac46e8 fix: optimize UpdateUserBalance and fix precision loss for orgBalance/userBalance. (#4499) 2025-11-19 21:13:32 +08:00
IsAurora6
f9d061d905 feat: return transaction IDs in API and disable links for anonymous user in transaction list (#4498) 2025-11-19 17:40:30 +08:00
Yang Luo
5e550e4364 feat: fix bug in createTable() 2025-11-19 17:33:51 +08:00
Yang Luo
146d54d6f6 feat: add Order pages (#4492) 2025-11-19 14:05:52 +08:00
IsAurora6
1df15a2706 fix: Transaction category & type links not navigating. (#4496) 2025-11-19 11:41:36 +08:00
Yang Luo
f7d73bbfdd Improve transaction fields 2025-11-19 09:14:49 +08:00
Yang Luo
a8b7217348 fix: add needSshfields() 2025-11-19 08:37:13 +08:00
Yang Luo
40a3b19cee feat: add Active Directory syncer support (#4495) 2025-11-19 08:30:01 +08:00
Yang Luo
98b45399a7 feat: add Google Workspace syncer (#4494) 2025-11-19 07:37:11 +08:00
Yang Luo
90edb7ab6b feat: refactor syncers into interface (#4490) 2025-11-19 01:28:37 +08:00
marun
e21b995eca feat: update payment providers when organization changes in PlanEditPage (#4462) 2025-11-19 00:14:01 +08:00
Yang Luo
81221f07f0 fix: improve isAllowedInDemoMode() for add-transaction API 2025-11-18 23:55:43 +08:00
Yang Luo
5fc2cdf637 feat: fix bug in GetEnforcer() API 2025-11-18 23:31:53 +08:00
Yang Luo
5e852e0121 feat: improve user edit page UI 2025-11-18 23:31:17 +08:00
Yang Luo
513ac6ffe9 fix: improve user edit page's transaction table UI 2025-11-18 23:31:16 +08:00
Yang Luo
821ba5673d Improve "Generate" button i18n 2025-11-18 23:31:16 +08:00
IsAurora6
d3ee73e48c feat: Add a URL field to the Transaction structure and optimize the display of the Transaction List. (#4487) 2025-11-18 21:45:57 +08:00
Yang Luo
1d719e3759 feat: fix OAuth-registered users to keep empty passwords unhashed (#4482) 2025-11-17 23:12:53 +08:00
Yang Luo
b3355a9fa6 fix: fix undefined owner in syncer edit page getCerts API call (#4471) 2025-11-17 22:51:12 +08:00
Yang Luo
ccc88cdafb feat: populate updated_time for all user creation paths (#4472) 2025-11-17 22:07:47 +08:00
Yang Luo
abf328bbe5 feat: allow setting email_verified in UpdateUser() API 2025-11-17 22:04:33 +08:00
DacongDA
5530253d38 feat: use correct org owner for UpdateOrganizationBalance (#4478) 2025-11-17 18:17:02 +08:00
92 changed files with 3487 additions and 820 deletions

View File

@@ -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
}

View File

@@ -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
View 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()
}

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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
View 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)
}

View File

@@ -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"}
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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()
}

View 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
}

View File

@@ -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
View 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
}

View 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
}

View 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
View 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"

View File

@@ -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() {

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
View 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
}

View File

@@ -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")

View File

@@ -7187,6 +7187,12 @@
"type": "string"
}
},
"userNavItems": {
"type": "array",
"items": {
"type": "string"
}
},
"owner": {
"type": "string"
},

View File

@@ -4724,6 +4724,10 @@ definitions:
type: array
items:
type: string
userNavItems:
type: array
items:
type: string
owner:
type: string
passwordExpireDays:

View File

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

View File

@@ -38,8 +38,68 @@ import {setTwoToneColor} from "@ant-design/icons";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as Cookie from "cookie";
// Ant Design locale imports
import enUS from "antd/locale/en_US";
import zhCN from "antd/locale/zh_CN";
import zhTW from "antd/locale/zh_TW";
import esES from "antd/locale/es_ES";
import frFR from "antd/locale/fr_FR";
import deDE from "antd/locale/de_DE";
import idID from "antd/locale/id_ID";
import jaJP from "antd/locale/ja_JP";
import koKR from "antd/locale/ko_KR";
import ruRU from "antd/locale/ru_RU";
import viVN from "antd/locale/vi_VN";
import ptBR from "antd/locale/pt_BR";
import itIT from "antd/locale/it_IT";
import msMY from "antd/locale/ms_MY";
import trTR from "antd/locale/tr_TR";
import arEG from "antd/locale/ar_EG";
import heIL from "antd/locale/he_IL";
import nlNL from "antd/locale/nl_NL";
import plPL from "antd/locale/pl_PL";
import fiFI from "antd/locale/fi_FI";
import svSE from "antd/locale/sv_SE";
import ukUA from "antd/locale/uk_UA";
import faIR from "antd/locale/fa_IR";
import csCZ from "antd/locale/cs_CZ";
import skSK from "antd/locale/sk_SK";
setTwoToneColor("rgb(87,52,211)");
function getAntdLocale(language) {
const localeMap = {
"en": enUS,
"zh": zhCN,
"zh-tw": zhTW,
"es": esES,
"fr": frFR,
"de": deDE,
"id": idID,
"ja": jaJP,
"ko": koKR,
"ru": ruRU,
"vi": viVN,
"pt": ptBR,
"it": itIT,
"ms": msMY,
"tr": trTR,
"ar": arEG,
"he": heIL,
"nl": nlNL,
"pl": plPL,
"fi": fiFI,
"sv": svSE,
"uk": ukUA,
"fa": faIR,
"cs": csCZ,
"sk": skSK,
"kk": ruRU, // Use Russian for Kazakh as antd doesn't have Kazakh
"az": trTR, // Use Turkish for Azerbaijani as they're similar
};
return localeMap[language] || enUS;
}
class App extends Component {
constructor(props) {
super(props);
@@ -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()

View File

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

View File

@@ -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
View 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")}&nbsp;&nbsp;&nbsp;&nbsp;
<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
View 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")}&nbsp;&nbsp;&nbsp;&nbsp;
<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;

View File

@@ -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"))} :

View File

@@ -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} />
);
},
},

View File

@@ -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"}} >

View File

@@ -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"),

View File

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

View File

@@ -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>

View File

@@ -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} />
);
},
},

View File

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

View File

@@ -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

View File

@@ -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>

View File

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

View File

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

View File

@@ -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",

View File

@@ -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"))} :

View File

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

View File

@@ -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")}&nbsp;&nbsp;&nbsp;&nbsp;
{title}&nbsp;&nbsp;&nbsp;&nbsp;
<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>
);
}

View File

@@ -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")}&nbsp;&nbsp;&nbsp;&nbsp;
<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")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" disabled={!isAdmin} onClick={this.addTransaction.bind(this)}>{i18next.t("general:Add")}</Button>
&nbsp;&nbsp;
<Button type="primary" size="small" disabled={!isAdmin} onClick={this.rechargeTransaction.bind(this)}>{i18next.t("transaction:Recharge")}</Button>
</div>
);
}}
loading={this.state.loading}
onChange={this.handleTableChange}
/>

View File

@@ -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
);
}
}

View File

@@ -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} />
);
},
},

View File

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

View File

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

View File

@@ -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());
}

View File

@@ -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>
{

View File

@@ -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

View File

@@ -69,7 +69,7 @@ export const ResetModal = (props) => {
return (
<Row>
<Button type="default" onClick={showModal}>
<Button type="primary" onClick={showModal}>
{buttonText}
</Button>
<Modal

View File

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

View File

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

View File

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

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "Das Public-Key-Zertifikat, das vom Client-SDK, das mit dieser Anwendung korrespondiert, verifiziert werden muss",
"Certs": "Zertifikate",
"Click to Upload": "Klicken Sie zum Hochladen",
"Click to cancel sorting": "Klicken Sie, um die Sortierung abzubrechen",
"Click to sort ascending": "Klicken Sie, um aufsteigend zu sortieren",
"Click to sort descending": "Klicken Sie, um absteigend zu sortieren",
"Client IP": "Client-IP",
"Close": "Schließen",
"Confirm": "Bestätigen",
@@ -323,6 +326,7 @@
"False": "Falsch",
"Favicon": "Favicon",
"Favicon - Tooltip": "Favicon-URL, die auf allen Casdoor-Seiten der Organisation verwendet wird",
"Filter": "Filtern",
"First name": "Vorname",
"First name - Tooltip": "Der Vorname des Benutzers",
"Forced redirect origin - Tooltip": "Erzwungener Weiterleitungsursprung",
@@ -378,7 +382,9 @@
"Non-LDAP": "Nicht-LDAP",
"None": "Keine",
"OAuth providers": "OAuth-Provider",
"OFF": "AUS",
"OK": "OK",
"ON": "EIN",
"Only 1 MFA method can be required": "Only 1 MFA method can be required",
"Organization": "Organisation",
"Organization - Tooltip": "Ähnlich wie bei Konzepten wie Mietern oder Benutzerpools gehört jeder Benutzer und jede Anwendung einer Organisation an",
@@ -410,6 +416,7 @@
"Plan - Tooltip": "Abonnementplan",
"Plans": "Pläne",
"Plans - Tooltip": "Pläne",
"Please input your search": "Bitte geben Sie Ihre Suche ein",
"Please select at least 1 user first": "Please select at least 1 user first",
"Preview": "Vorschau",
"Preview - Tooltip": "Vorschau der konfigurierten Effekte",
@@ -427,6 +434,7 @@
"Records": "Datensätze",
"Request": "Anfrage",
"Request URI": "Anfrage-URI",
"Reset": "Zurücksetzen",
"Reset to Default": "Auf Standard zurücksetzen",
"Resources": "Ressourcen",
"Role": "Rolle",
@@ -442,6 +450,7 @@
"SSH type - Tooltip": "Der Authentifizierungstyp für SSH-Verbindungen",
"Save": "Speichern",
"Save & Exit": "Speichern und verlassen",
"Search": "Suchen",
"Send": "Senden",
"Session ID": "Session-ID",
"Sessions": "Sitzungen",
@@ -525,7 +534,7 @@
"Show all": "Alle anzeigen",
"Upload (.xlsx)": "Hochladen (.xlsx)",
"Virtual": "Virtuell",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "Sie müssen zuerst alle Untergruppen löschen. Sie können die Untergruppen im linken Gruppenbaum unter [Organisationen] -\u003e [Gruppen] anzeigen."
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "Sie müssen zuerst alle Untergruppen löschen. Sie können die Untergruppen im linken Gruppenbaum unter [Organisationen] -> [Gruppen] anzeigen."
},
"home": {
"New users past 30 days": "Neue Benutzer der letzten 30 Tage",

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "The public key certificate that needs to be verified by the client SDK corresponding to this application",
"Certs": "Certs",
"Click to Upload": "Click to Upload",
"Click to cancel sorting": "Click to cancel sorting",
"Click to sort ascending": "Click to sort ascending",
"Click to sort descending": "Click to sort descending",
"Client IP": "Client IP",
"Close": "Close",
"Confirm": "Confirm",
@@ -323,6 +326,7 @@
"False": "False",
"Favicon": "Favicon",
"Favicon - Tooltip": "Favicon icon URL used in all Casdoor pages of the organization",
"Filter": "Filter",
"First name": "First name",
"First name - Tooltip": "The first name of user",
"Forced redirect origin - Tooltip": "Forced redirect origin",
@@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -269,6 +269,9 @@
"Cert - Tooltip": "该应用所对应的客户端SDK需要验证的公钥证书",
"Certs": "证书",
"Click to Upload": "点击上传",
"Click to cancel sorting": "点击取消排序",
"Click to sort ascending": "点击升序排序",
"Click to sort descending": "点击降序排序",
"Client IP": "客户端IP",
"Close": "关闭",
"Confirm": "确认",
@@ -323,6 +326,7 @@
"False": "假",
"Favicon": "组织Favicon",
"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": "交易的标签"

View File

@@ -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")},
];
};

View File

@@ -138,15 +138,15 @@ class FaceIdTable extends React.Component {
title={() => (
<div>
{i18next.t("user:Face IDs")}&nbsp;&nbsp;&nbsp;&nbsp;
<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>

View File

@@ -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>

View File

@@ -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}}