forked from casdoor/casdoor
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4ce88198f | ||
|
|
a11fa23add | ||
|
|
add6ba32db | ||
|
|
37379dee13 | ||
|
|
2066670b76 | ||
|
|
e751148be2 | ||
|
|
c541d0bcdd | ||
|
|
f0db95d006 | ||
|
|
e4db367eaa | ||
|
|
9df81e3ffc | ||
|
|
048d6acc83 | ||
|
|
e440199977 | ||
|
|
cb4e559d51 | ||
|
|
4d1d0b95d6 | ||
|
|
9cc1133a96 | ||
|
|
897c28e8ad | ||
|
|
9d37a7e38e | ||
|
|
ea597296b4 | ||
|
|
427ddd215e | ||
|
|
24de79b100 | ||
|
|
9ab9c7c8e0 | ||
|
|
0728a9716b | ||
|
|
471570f24a | ||
|
|
2fa520844b | ||
|
|
2306acb416 | ||
|
|
d3f3f76290 | ||
|
|
fe93128495 | ||
|
|
7fd890ff14 | ||
|
|
83b56d7ceb | ||
|
|
503e5a75d2 | ||
|
|
5a607b4991 | ||
|
|
ca2dc2825d | ||
|
|
446d0b9047 | ||
|
|
ee708dbf48 | ||
|
|
221ca28488 | ||
|
|
e93d3f6c13 | ||
|
|
e285396d4e | ||
|
|
10320bb49f | ||
|
|
4d27ebd82a | ||
|
|
6d5e6dab0a | ||
|
|
e600ea7efd | ||
|
|
8002613398 | ||
|
|
a48b1d0c73 | ||
|
|
d8b5ecba36 | ||
|
|
e3a8a464d5 | ||
|
|
a575ba02d6 | ||
|
|
a9fcfceb8f | ||
|
|
712482ffb9 | ||
|
|
84e2c760d9 | ||
|
|
4ab85d6781 | ||
|
|
2ede56ac46 | ||
|
|
6a819a9a20 | ||
|
|
ddaeac46e8 | ||
|
|
f9d061d905 | ||
|
|
5e550e4364 | ||
|
|
146d54d6f6 | ||
|
|
1df15a2706 | ||
|
|
f7d73bbfdd | ||
|
|
a8b7217348 | ||
|
|
40a3b19cee | ||
|
|
98b45399a7 | ||
|
|
90edb7ab6b | ||
|
|
e21b995eca | ||
|
|
81221f07f0 | ||
|
|
5fc2cdf637 | ||
|
|
5e852e0121 | ||
|
|
513ac6ffe9 | ||
|
|
821ba5673d | ||
|
|
d3ee73e48c | ||
|
|
1d719e3759 | ||
|
|
b3355a9fa6 | ||
|
|
ccc88cdafb | ||
|
|
abf328bbe5 | ||
|
|
5530253d38 | ||
|
|
4cef6c5f3f |
@@ -67,7 +67,6 @@ p, *, *, POST, /api/upload-users, *, *
|
||||
p, *, *, GET, /api/get-resources, *, *
|
||||
p, *, *, GET, /api/get-records, *, *
|
||||
p, *, *, GET, /api/get-product, *, *
|
||||
p, *, *, POST, /api/buy-product, *, *
|
||||
p, *, *, GET, /api/get-payment, *, *
|
||||
p, *, *, POST, /api/update-payment, *, *
|
||||
p, *, *, POST, /api/invoice-payment, *, *
|
||||
@@ -100,6 +99,8 @@ 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-transaction, *, *
|
||||
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
|
||||
}
|
||||
|
||||
@@ -484,6 +484,21 @@ func (c *ApiController) SsoLogout() {
|
||||
return
|
||||
}
|
||||
|
||||
// Send SSO logout notifications to all notification providers in the user's signup application
|
||||
userObj, err := object.GetUser(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if userObj != nil {
|
||||
err = object.SendSsoLogoutNotifications(userObj)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
util.LogInfo(c.Ctx, "API: [%s] logged out from all applications", user)
|
||||
|
||||
c.ResponseOk()
|
||||
|
||||
@@ -724,6 +724,7 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
userInfo := &idp.UserInfo{}
|
||||
var token *oauth2.Token
|
||||
if provider.Category == "SAML" {
|
||||
// SAML
|
||||
userInfo, err = object.ParseSamlResponse(authForm.SamlResponse, provider, c.Ctx.Request.Host)
|
||||
@@ -754,7 +755,6 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
|
||||
// https://github.com/golang/oauth2/issues/123#issuecomment-103715338
|
||||
var token *oauth2.Token
|
||||
token, err = idProvider.GetToken(authForm.Code)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -804,7 +804,7 @@ func (c *ApiController) Login() {
|
||||
if user != nil && !user.IsDeleted {
|
||||
// Sign in via OAuth (want to sign up but already have account)
|
||||
// sync info from 3rd-party if possible
|
||||
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, provider.UserMapping)
|
||||
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, token, provider.UserMapping)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -867,6 +867,11 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle UseEmailAsUsername for OAuth and Web3
|
||||
if organization.UseEmailAsUsername && userInfo.Email != "" {
|
||||
userInfo.Username = userInfo.Email
|
||||
}
|
||||
|
||||
// Handle username conflicts
|
||||
var tmpUser *object.User
|
||||
tmpUser, err = object.GetUser(util.GetId(application.Organization, userInfo.Username))
|
||||
@@ -949,7 +954,7 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
|
||||
// sync info from 3rd-party if possible
|
||||
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, provider.UserMapping)
|
||||
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, token, provider.UserMapping)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -997,7 +1002,7 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
|
||||
// sync info from 3rd-party if possible
|
||||
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, provider.UserMapping)
|
||||
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo, token, provider.UserMapping)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -1213,7 +1218,7 @@ func (c *ApiController) HandleOfficialAccountEvent() {
|
||||
return
|
||||
}
|
||||
if data.Ticket == "" {
|
||||
c.ResponseError(err.Error())
|
||||
c.ResponseError("empty ticket")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1228,10 +1233,6 @@ func (c *ApiController) HandleOfficialAccountEvent() {
|
||||
return
|
||||
}
|
||||
|
||||
if data.Ticket == "" {
|
||||
c.ResponseError("empty ticket")
|
||||
return
|
||||
}
|
||||
if !idp.VerifyWechatSignature(provider.Content, nonce, timestamp, signature) {
|
||||
c.ResponseError("invalid signature")
|
||||
return
|
||||
|
||||
@@ -83,15 +83,13 @@ func (c *ApiController) GetEnforcer() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if enforcer == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The enforcer: %s does not exist"), id))
|
||||
return
|
||||
}
|
||||
|
||||
if loadModelCfg == "true" && enforcer.Model != "" {
|
||||
err := enforcer.LoadModelCfg()
|
||||
if err != nil {
|
||||
return
|
||||
if enforcer != nil {
|
||||
if loadModelCfg == "true" && enforcer.Model != "" {
|
||||
err = enforcer.LoadModelCfg()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
166
controllers/order.go
Normal file
166
controllers/order.go
Normal file
@@ -0,0 +1,166 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// GetOrders
|
||||
// @Title GetOrders
|
||||
// @Tag Order API
|
||||
// @Description get orders
|
||||
// @Param owner query string true "The owner of orders"
|
||||
// @Success 200 {array} object.Order The Response object
|
||||
// @router /get-orders [get]
|
||||
func (c *ApiController) GetOrders() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
orders, err := object.GetOrders(owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(orders)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
count, err := object.GetOrderCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
orders, err := object.GetPaginationOrders(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(orders, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserOrders
|
||||
// @Title GetUserOrders
|
||||
// @Tag Order API
|
||||
// @Description get orders for a user
|
||||
// @Param owner query string true "The owner of orders"
|
||||
// @Param user query string true "The username of the user"
|
||||
// @Success 200 {array} object.Order The Response object
|
||||
// @router /get-user-orders [get]
|
||||
func (c *ApiController) GetUserOrders() {
|
||||
owner := c.Input().Get("owner")
|
||||
user := c.Input().Get("user")
|
||||
|
||||
orders, err := object.GetUserOrders(owner, user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(orders)
|
||||
}
|
||||
|
||||
// GetOrder
|
||||
// @Title GetOrder
|
||||
// @Tag Order API
|
||||
// @Description get order
|
||||
// @Param id query string true "The id ( owner/name ) of the order"
|
||||
// @Success 200 {object} object.Order The Response object
|
||||
// @router /get-order [get]
|
||||
func (c *ApiController) GetOrder() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
order, err := object.GetOrder(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(order)
|
||||
}
|
||||
|
||||
// UpdateOrder
|
||||
// @Title UpdateOrder
|
||||
// @Tag Order API
|
||||
// @Description update order
|
||||
// @Param id query string true "The id ( owner/name ) of the order"
|
||||
// @Param body body object.Order true "The details of the order"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-order [post]
|
||||
func (c *ApiController) UpdateOrder() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
var order object.Order
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &order)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateOrder(id, &order))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddOrder
|
||||
// @Title AddOrder
|
||||
// @Tag Order API
|
||||
// @Description add order
|
||||
// @Param body body object.Order true "The details of the order"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-order [post]
|
||||
func (c *ApiController) AddOrder() {
|
||||
var order object.Order
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &order)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddOrder(&order))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteOrder
|
||||
// @Title DeleteOrder
|
||||
// @Tag Order API
|
||||
// @Description delete order
|
||||
// @Param body body object.Order true "The details of the order"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-order [post]
|
||||
func (c *ApiController) DeleteOrder() {
|
||||
var order object.Order
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &order)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteOrder(&order))
|
||||
c.ServeJSON()
|
||||
}
|
||||
169
controllers/order_pay.go
Normal file
169
controllers/order_pay.go
Normal file
@@ -0,0 +1,169 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// PlaceOrder
|
||||
// @Title PlaceOrder
|
||||
// @Tag Order API
|
||||
// @Description place an order for a product
|
||||
// @Param productId query string true "The id ( owner/name ) of the product"
|
||||
// @Param pricingName query string false "The name of the pricing (for subscription)"
|
||||
// @Param planName query string false "The name of the plan (for subscription)"
|
||||
// @Param customPrice query number false "Custom price for recharge products"
|
||||
// @Param userName query string false "The username to place order for (admin only)"
|
||||
// @Success 200 {object} object.Order The Response object
|
||||
// @router /place-order [post]
|
||||
func (c *ApiController) PlaceOrder() {
|
||||
productId := c.Input().Get("productId")
|
||||
pricingName := c.Input().Get("pricingName")
|
||||
planName := c.Input().Get("planName")
|
||||
customPriceStr := c.Input().Get("customPrice")
|
||||
paidUserName := c.Input().Get("userName")
|
||||
|
||||
if productId == "" {
|
||||
c.ResponseError(c.T("general:ProductId is required"))
|
||||
return
|
||||
}
|
||||
|
||||
var customPrice float64
|
||||
if customPriceStr != "" {
|
||||
var err error
|
||||
customPrice, err = strconv.ParseFloat(customPriceStr, 64)
|
||||
if err != nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:Invalid customPrice: %s"), customPriceStr))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
owner, _, err := util.GetOwnerAndNameFromIdWithError(productId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var userId string
|
||||
if paidUserName != "" {
|
||||
userId = util.GetId(owner, paidUserName)
|
||||
if userId != c.GetSessionUsername() && !c.IsAdmin() && userId != c.GetPaidUsername() {
|
||||
c.ResponseError(c.T("general:Only admin user can specify user"))
|
||||
return
|
||||
}
|
||||
|
||||
c.SetSession("paidUsername", "")
|
||||
} else {
|
||||
userId = c.GetSessionUsername()
|
||||
}
|
||||
|
||||
if userId == "" {
|
||||
c.ResponseError(c.T("general:Please login first"))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := object.GetUser(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
|
||||
return
|
||||
}
|
||||
|
||||
order, err := object.PlaceOrder(productId, user, pricingName, planName, customPrice)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(order)
|
||||
}
|
||||
|
||||
// PayOrder
|
||||
// @Title PayOrder
|
||||
// @Tag Order API
|
||||
// @Description pay an existing order
|
||||
// @Param id query string true "The id ( owner/name ) of the order"
|
||||
// @Param providerName query string true "The name of the provider"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /pay-order [post]
|
||||
func (c *ApiController) PayOrder() {
|
||||
id := c.Input().Get("id")
|
||||
host := c.Ctx.Request.Host
|
||||
providerName := c.Input().Get("providerName")
|
||||
paymentEnv := c.Input().Get("paymentEnv")
|
||||
|
||||
order, err := object.GetOrder(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if order == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The order: %s does not exist"), id))
|
||||
return
|
||||
}
|
||||
|
||||
userId := c.GetSessionUsername()
|
||||
orderUserId := util.GetId(order.Owner, order.User)
|
||||
if userId != orderUserId && !c.IsAdmin() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
payment, attachInfo, err := object.PayOrder(providerName, host, paymentEnv, order)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(payment, attachInfo)
|
||||
}
|
||||
|
||||
// CancelOrder
|
||||
// @Title CancelOrder
|
||||
// @Tag Order API
|
||||
// @Description cancel an order
|
||||
// @Param id query string true "The id ( owner/name ) of the order"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /cancel-order [post]
|
||||
func (c *ApiController) CancelOrder() {
|
||||
id := c.Input().Get("id")
|
||||
order, err := object.GetOrder(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if order == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The order: %s does not exist"), id))
|
||||
return
|
||||
}
|
||||
|
||||
userId := c.GetSessionUsername()
|
||||
orderUserId := util.GetId(order.Owner, order.User)
|
||||
if userId != orderUserId && !c.IsAdmin() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.CancelOrder(order))
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@@ -151,72 +149,3 @@ func (c *ApiController) DeleteProduct() {
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteProduct(&product))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// BuyProduct
|
||||
// @Title BuyProduct
|
||||
// @Tag Product API
|
||||
// @Description buy product
|
||||
// @Param id query string true "The id ( owner/name ) of the product"
|
||||
// @Param providerName query string true "The name of the provider"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /buy-product [post]
|
||||
func (c *ApiController) BuyProduct() {
|
||||
id := c.Input().Get("id")
|
||||
host := c.Ctx.Request.Host
|
||||
providerName := c.Input().Get("providerName")
|
||||
paymentEnv := c.Input().Get("paymentEnv")
|
||||
customPriceStr := c.Input().Get("customPrice")
|
||||
if customPriceStr == "" {
|
||||
customPriceStr = "0"
|
||||
}
|
||||
|
||||
customPrice, err := strconv.ParseFloat(customPriceStr, 64)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// buy `pricingName/planName` for `paidUserName`
|
||||
pricingName := c.Input().Get("pricingName")
|
||||
planName := c.Input().Get("planName")
|
||||
paidUserName := c.Input().Get("userName")
|
||||
owner, _, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
var userId string
|
||||
if paidUserName != "" {
|
||||
userId = util.GetId(owner, paidUserName)
|
||||
if userId != c.GetSessionUsername() && !c.IsAdmin() && userId != c.GetPaidUsername() {
|
||||
c.ResponseError(c.T("general:Only admin user can specify user"))
|
||||
return
|
||||
}
|
||||
|
||||
c.SetSession("paidUsername", "")
|
||||
} else {
|
||||
userId = c.GetSessionUsername()
|
||||
}
|
||||
if userId == "" {
|
||||
c.ResponseError(c.T("general:Please login first"))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := object.GetUser(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
|
||||
return
|
||||
}
|
||||
|
||||
payment, attachInfo, err := object.BuyProduct(id, user, providerName, pricingName, planName, host, paymentEnv, customPrice)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(payment, attachInfo)
|
||||
}
|
||||
|
||||
@@ -113,6 +113,27 @@ func (c *ApiController) GetTransaction() {
|
||||
return
|
||||
}
|
||||
|
||||
if transaction == nil {
|
||||
c.ResponseOk(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if non-admin user is trying to access someone else's transaction
|
||||
if !c.IsAdmin() {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Only allow users to view their own transactions
|
||||
if transaction.User != userName {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.ResponseOk(transaction)
|
||||
}
|
||||
|
||||
@@ -143,6 +164,7 @@ func (c *ApiController) UpdateTransaction() {
|
||||
// @Tag Transaction API
|
||||
// @Description add transaction
|
||||
// @Param body body object.Transaction true "The details of the transaction"
|
||||
// @Param dryRun query string false "Dry run mode: set to 'true' or '1' to validate without committing"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-transaction [post]
|
||||
func (c *ApiController) AddTransaction() {
|
||||
@@ -153,8 +175,22 @@ func (c *ApiController) AddTransaction() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddTransaction(&transaction, c.GetAcceptLanguage()))
|
||||
c.ServeJSON()
|
||||
dryRunParam := c.Input().Get("dryRun")
|
||||
dryRun := dryRunParam != ""
|
||||
|
||||
affected, transactionId, err := object.AddTransaction(&transaction, c.GetAcceptLanguage(), dryRun)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !affected {
|
||||
c.Data["json"] = wrapActionResponse(false)
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(transactionId)
|
||||
}
|
||||
|
||||
// DeleteTransaction
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package deployment
|
||||
|
||||
|
||||
15
go.mod
15
go.mod
@@ -4,6 +4,8 @@ go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.3
|
||||
github.com/NdoleStudio/lemonsqueezy-go v1.2.4
|
||||
github.com/PaddleHQ/paddle-go-sdk v1.0.0
|
||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.4
|
||||
github.com/alibabacloud-go/facebody-20191230/v5 v5.1.2
|
||||
@@ -15,7 +17,7 @@ require (
|
||||
github.com/beevik/etree v1.1.0
|
||||
github.com/casbin/casbin/v2 v2.77.2
|
||||
github.com/casdoor/go-sms-sender v0.25.0
|
||||
github.com/casdoor/gomail/v2 v2.1.0
|
||||
github.com/casdoor/gomail/v2 v2.2.0
|
||||
github.com/casdoor/ldapserver v1.2.0
|
||||
github.com/casdoor/notify v1.0.1
|
||||
github.com/casdoor/oss v1.8.0
|
||||
@@ -43,6 +45,7 @@ require (
|
||||
github.com/markbates/goth v1.79.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nyaruka/phonenumbers v1.2.2
|
||||
github.com/polarsource/polar-go v0.12.0
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/prometheus/client_golang v1.11.1
|
||||
github.com/prometheus/client_model v0.4.0
|
||||
@@ -53,7 +56,7 @@ require (
|
||||
github.com/sendgrid/sendgrid-go v3.14.0+incompatible
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/stripe/stripe-go/v74 v74.29.0
|
||||
github.com/tealeg/xlsx v1.0.5
|
||||
github.com/thanhpk/randstr v1.0.4
|
||||
@@ -125,6 +128,8 @@ require (
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
|
||||
github.com/ggicci/httpin v0.19.0 // indirect
|
||||
github.com/ggicci/owl v0.8.2 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.0 // indirect
|
||||
github.com/go-lark/lark v1.9.0 // indirect
|
||||
@@ -152,6 +157,7 @@ require (
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/gregdel/pushover v1.2.1 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
@@ -168,9 +174,9 @@ require (
|
||||
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
|
||||
github.com/markbates/going v1.0.0 // indirect
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-ieproxy v0.0.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mileusna/viber v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
@@ -197,6 +203,7 @@ require (
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.0 // indirect
|
||||
github.com/slack-go/slack v0.12.3 // indirect
|
||||
github.com/spyzhov/ajson v0.8.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
|
||||
36
go.sum
36
go.sum
@@ -78,7 +78,11 @@ github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA4
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/NdoleStudio/lemonsqueezy-go v1.2.4 h1:BhWlCUH+DIPfSn4g/V7f2nFkMCQuzno9DXKZ7YDrXXA=
|
||||
github.com/NdoleStudio/lemonsqueezy-go v1.2.4/go.mod h1:2uZlWgn9sbNxOx3JQWLlPrDOC6NT/wmSTOgL3U/fMMw=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PaddleHQ/paddle-go-sdk v1.0.0 h1:+EXitsPFbRcc0CpQE/MIeudxiVOR8pFe/aOWTEUHDKU=
|
||||
github.com/PaddleHQ/paddle-go-sdk v1.0.0/go.mod h1:kbBBzf0BHEj38QvhtoELqlGip3alKgA/I+vl7RQzB58=
|
||||
github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
|
||||
github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20221121042443-a3fd332d56d9 h1:vuu1KBsr6l7XU3CHsWESP/4B1SNd+VZkrgeFZsUXrsY=
|
||||
@@ -233,8 +237,8 @@ github.com/casdoor/go-reddit/v2 v2.1.0 h1:kIbfdJ7AA7H0uTQ8s0q4GGZqSS5V9wVE74RrXy
|
||||
github.com/casdoor/go-reddit/v2 v2.1.0/go.mod h1:eagkvwlZ4Hcsuc/uQsLHYEulz5jN65SVSwV/AIE7zsc=
|
||||
github.com/casdoor/go-sms-sender v0.25.0 h1:eF4cOCSbjVg7+0uLlJQnna/FQ0BWW+Fp/x4cXhzQu1Y=
|
||||
github.com/casdoor/go-sms-sender v0.25.0/go.mod h1:bOm4H8/YfJmEHjBatEVQFOnAf0OOn1B0Wi5B7zDhws0=
|
||||
github.com/casdoor/gomail/v2 v2.1.0 h1:ua97E3CARnF1Ik8ga/Drz9uGZfaElXJumFexiErWUxM=
|
||||
github.com/casdoor/gomail/v2 v2.1.0/go.mod h1:GFzOD9RhY0nODiiPaQiOa6DfoKtmO9aTesu5qrp26OI=
|
||||
github.com/casdoor/gomail/v2 v2.2.0 h1:gVMk43qvqq4XYkAJ+CDY5WWKF9yYRipuyXfp7P0HWIg=
|
||||
github.com/casdoor/gomail/v2 v2.2.0/go.mod h1:GFzOD9RhY0nODiiPaQiOa6DfoKtmO9aTesu5qrp26OI=
|
||||
github.com/casdoor/ldapserver v1.2.0 h1:HdSYe+ULU6z9K+2BqgTrJKQRR4//ERAXB64ttOun6Ow=
|
||||
github.com/casdoor/ldapserver v1.2.0/go.mod h1:VwYU2vqQ2pA8sa00PRekH71R2XmgfzMKhmp1XrrDu2s=
|
||||
github.com/casdoor/notify v1.0.1 h1:p0kzI7OBlvLbL7zWeKIu31LRcEAygNZGKr5gcFfSIoE=
|
||||
@@ -350,6 +354,10 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
|
||||
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/ggicci/httpin v0.19.0 h1:p0B3SWLVgg770VirYiHB14M5wdRx3zR8mCTzM/TkTQ8=
|
||||
github.com/ggicci/httpin v0.19.0/go.mod h1:hzsQHcbqLabmGOycf7WNw6AAzcVbsMeoOp46bWAbIWc=
|
||||
github.com/ggicci/owl v0.8.2 h1:og+lhqpzSMPDdEB+NJfzoAJARP7qCG3f8uUC3xvGukA=
|
||||
github.com/ggicci/owl v0.8.2/go.mod h1:PHRD57u41vFN5UtFz2SF79yTVoM3HlWpjMiE+ZU2dj4=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
@@ -534,8 +542,9 @@ github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORR
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1 h1:LqbZZ9sNMWVjeXS4NN5oVvhMjDyLhmA1LG86oSo+IqY=
|
||||
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
@@ -552,6 +561,7 @@ github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOj
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
@@ -597,8 +607,9 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||
@@ -617,6 +628,8 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
|
||||
github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
@@ -683,8 +696,9 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI=
|
||||
github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
@@ -693,8 +707,9 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
@@ -788,6 +803,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/polarsource/polar-go v0.12.0 h1:um+6ftOPUMg2TQq9Kv/6fKGBOAl7dOc2YiDdx4Bb0y8=
|
||||
github.com/polarsource/polar-go v0.12.0/go.mod h1:FB11Q4m2n3wIk6l/POOkz0MVOUx1o0Yt4Y97MnQfe0c=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
||||
@@ -894,6 +911,8 @@ github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJ
|
||||
github.com/sony/sonyflake v1.0.0 h1:MpU6Ro7tfXwgn2l5eluf9xQvQJDROTBImNCfRXn/YeM=
|
||||
github.com/sony/sonyflake v1.0.0/go.mod h1:Jv3cfhf/UFtolOTTRd3q4Nl6ENqM+KfyZ5PseKfZGF4=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spyzhov/ajson v0.8.0 h1:sFXyMbi4Y/BKjrsfkUZHSjA2JM1184enheSjjoT/zCc=
|
||||
github.com/spyzhov/ajson v0.8.0/go.mod h1:63V+CGM6f1Bu/p4nLIN8885ojBdt88TbLoSFzyqMuVA=
|
||||
github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE=
|
||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
@@ -916,8 +935,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stripe/stripe-go/v74 v74.29.0 h1:ffJ+1Ta1Ccg7yDDz+SfjixX0KizEEJ/wNVRoFYkdwFY=
|
||||
github.com/stripe/stripe-go/v74 v74.29.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
|
||||
github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
|
||||
@@ -1276,6 +1295,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package i18n
|
||||
|
||||
|
||||
@@ -129,6 +129,8 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) (IdProvider, error
|
||||
return NewWeb3OnboardIdProvider(), nil
|
||||
case "Twitter":
|
||||
return NewTwitterIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
||||
case "Telegram":
|
||||
return NewTelegramIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
||||
default:
|
||||
if isGothSupport(idpInfo.Type) {
|
||||
return NewGothIdProvider(idpInfo.Type, idpInfo.ClientId, idpInfo.ClientSecret, idpInfo.ClientId2, idpInfo.ClientSecret2, redirectUrl, idpInfo.HostUrl)
|
||||
|
||||
169
idp/telegram.go
Normal file
169
idp/telegram.go
Normal file
@@ -0,0 +1,169 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package idp
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type TelegramIdProvider struct {
|
||||
Client *http.Client
|
||||
ClientId string
|
||||
ClientSecret string
|
||||
RedirectUrl string
|
||||
}
|
||||
|
||||
func NewTelegramIdProvider(clientId string, clientSecret string, redirectUrl string) *TelegramIdProvider {
|
||||
idp := &TelegramIdProvider{
|
||||
ClientId: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
RedirectUrl: redirectUrl,
|
||||
}
|
||||
|
||||
return idp
|
||||
}
|
||||
|
||||
func (idp *TelegramIdProvider) SetHttpClient(client *http.Client) {
|
||||
idp.Client = client
|
||||
}
|
||||
|
||||
// GetToken validates the Telegram auth data and returns a token
|
||||
// Telegram uses a widget-based authentication, not standard OAuth2
|
||||
// The "code" parameter contains the JSON-encoded auth data from Telegram
|
||||
func (idp *TelegramIdProvider) GetToken(code string) (*oauth2.Token, error) {
|
||||
// Decode the auth data from the code parameter
|
||||
var authData map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(code), &authData); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Telegram auth data: %v", err)
|
||||
}
|
||||
|
||||
// Verify the data authenticity
|
||||
if err := idp.verifyTelegramAuth(authData); err != nil {
|
||||
return nil, fmt.Errorf("failed to verify Telegram auth data: %v", err)
|
||||
}
|
||||
|
||||
// Create a token with the user ID as access token
|
||||
userId, ok := authData["id"].(float64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid user id in auth data")
|
||||
}
|
||||
|
||||
// Store the complete auth data in the token for later retrieval
|
||||
authDataJson, err := json.Marshal(authData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal auth data: %v", err)
|
||||
}
|
||||
|
||||
token := &oauth2.Token{
|
||||
AccessToken: fmt.Sprintf("telegram_%d", int64(userId)),
|
||||
TokenType: "Bearer",
|
||||
}
|
||||
|
||||
// Store auth data in token extras to avoid additional API calls
|
||||
token = token.WithExtra(map[string]interface{}{
|
||||
"telegram_auth_data": string(authDataJson),
|
||||
})
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// verifyTelegramAuth verifies the authenticity of Telegram auth data
|
||||
// According to Telegram docs: https://core.telegram.org/widgets/login#checking-authorization
|
||||
func (idp *TelegramIdProvider) verifyTelegramAuth(authData map[string]interface{}) error {
|
||||
// Extract hash from auth data
|
||||
hash, ok := authData["hash"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("hash not found in auth data")
|
||||
}
|
||||
|
||||
// Prepare data check string
|
||||
var dataCheckArr []string
|
||||
for key, value := range authData {
|
||||
if key == "hash" {
|
||||
continue
|
||||
}
|
||||
dataCheckArr = append(dataCheckArr, fmt.Sprintf("%s=%v", key, value))
|
||||
}
|
||||
sort.Strings(dataCheckArr)
|
||||
dataCheckString := strings.Join(dataCheckArr, "\n")
|
||||
|
||||
// Calculate secret key
|
||||
secretKey := sha256.Sum256([]byte(idp.ClientSecret))
|
||||
|
||||
// Calculate hash
|
||||
h := hmac.New(sha256.New, secretKey[:])
|
||||
h.Write([]byte(dataCheckString))
|
||||
calculatedHash := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
// Compare hashes
|
||||
if calculatedHash != hash {
|
||||
return fmt.Errorf("data verification failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (idp *TelegramIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
|
||||
// Extract auth data from token
|
||||
authDataStr, ok := token.Extra("telegram_auth_data").(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("telegram auth data not found in token")
|
||||
}
|
||||
|
||||
// Parse the auth data
|
||||
var authData map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(authDataStr), &authData); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse auth data: %v", err)
|
||||
}
|
||||
|
||||
// Extract user information from auth data
|
||||
userId, ok := authData["id"].(float64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid user id in auth data")
|
||||
}
|
||||
|
||||
firstName, _ := authData["first_name"].(string)
|
||||
lastName, _ := authData["last_name"].(string)
|
||||
username, _ := authData["username"].(string)
|
||||
photoUrl, _ := authData["photo_url"].(string)
|
||||
|
||||
// Build display name with fallback
|
||||
displayName := strings.TrimSpace(firstName + " " + lastName)
|
||||
if displayName == "" {
|
||||
displayName = username
|
||||
}
|
||||
if displayName == "" {
|
||||
displayName = strconv.FormatInt(int64(userId), 10)
|
||||
}
|
||||
|
||||
userInfo := UserInfo{
|
||||
Id: strconv.FormatInt(int64(userId), 10),
|
||||
Username: username,
|
||||
DisplayName: displayName,
|
||||
AvatarUrl: photoUrl,
|
||||
}
|
||||
|
||||
return &userInfo, nil
|
||||
}
|
||||
@@ -872,7 +872,7 @@ func initDefinedTransaction(transaction *Transaction) {
|
||||
}
|
||||
}
|
||||
transaction.CreatedTime = util.GetCurrentTime()
|
||||
_, err = AddTransaction(transaction, "en")
|
||||
_, _, err = AddTransaction(transaction, "en", false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package object
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ package object
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/notification"
|
||||
"github.com/casdoor/notify"
|
||||
@@ -40,3 +42,63 @@ func SendNotification(provider *Provider, content string) error {
|
||||
err = client.Send(context.Background(), "", content)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendSsoLogoutNotifications sends logout notifications to all notification providers
|
||||
// configured in the user's signup application
|
||||
func SendSsoLogoutNotifications(user *User) error {
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If user's signup application is empty, don't send notifications
|
||||
if user.SignupApplication == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the user's signup application
|
||||
application, err := GetApplication(user.SignupApplication)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get signup application: %w", err)
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
return fmt.Errorf("signup application not found: %s", user.SignupApplication)
|
||||
}
|
||||
|
||||
// Prepare sanitized user data for notification
|
||||
// Only include safe, non-sensitive fields
|
||||
sanitizedData := map[string]interface{}{
|
||||
"owner": user.Owner,
|
||||
"name": user.Name,
|
||||
"displayName": user.DisplayName,
|
||||
"email": user.Email,
|
||||
"phone": user.Phone,
|
||||
"id": user.Id,
|
||||
"event": "sso-logout",
|
||||
}
|
||||
userData, err := json.Marshal(sanitizedData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal user data: %w", err)
|
||||
}
|
||||
content := string(userData)
|
||||
|
||||
// Send notifications to all notification providers in the signup application
|
||||
for _, providerItem := range application.Providers {
|
||||
if providerItem.Provider == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only send to notification providers
|
||||
if providerItem.Provider.Category != "Notification" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Send the notification using the provider from the providerItem
|
||||
err = SendNotification(providerItem.Provider, content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send SSO logout notification to provider %s/%s: %w", providerItem.Provider.Owner, providerItem.Provider.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
168
object/order.go
Normal file
168
object/order.go
Normal file
@@ -0,0 +1,168 @@
|
||||
// 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"`
|
||||
Products []string `xorm:"varchar(1000)" json:"products"` // Future support for multiple products per order. Using varchar(1000) for simple JSON array storage; can be refactored to separate table if needed
|
||||
|
||||
// Subscription Info (for subscription orders)
|
||||
PricingName string `xorm:"varchar(100)" json:"pricingName"`
|
||||
PlanName string `xorm:"varchar(100)" json:"planName"`
|
||||
|
||||
// User Info
|
||||
User string `xorm:"varchar(100)" json:"user"`
|
||||
|
||||
// Payment Info
|
||||
Payment string `xorm:"varchar(100)" json:"payment"`
|
||||
Price float64 `json:"price"`
|
||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func GetOrderByPayment(owner string, paymentName string) (*Order, error) {
|
||||
order := &Order{}
|
||||
existed, err := ormer.Engine.Where("owner = ? AND payment = ?", owner, paymentName).Get(order)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existed {
|
||||
return order, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
314
object/order_pay.go
Normal file
314
object/order_pay.go
Normal file
@@ -0,0 +1,314 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/idp"
|
||||
"github.com/casdoor/casdoor/pp"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func PlaceOrder(productId string, user *User, pricingName string, planName string, customPrice float64) (*Order, error) {
|
||||
product, err := GetProduct(productId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if product == nil {
|
||||
return nil, fmt.Errorf("the product: %s does not exist", productId)
|
||||
}
|
||||
|
||||
userBalanceCurrency := user.BalanceCurrency
|
||||
if userBalanceCurrency == "" {
|
||||
org, err := getOrganization("admin", user.Owner)
|
||||
if err == nil && org != nil && org.BalanceCurrency != "" {
|
||||
userBalanceCurrency = org.BalanceCurrency
|
||||
} else {
|
||||
userBalanceCurrency = "USD"
|
||||
}
|
||||
}
|
||||
|
||||
productCurrency := product.Currency
|
||||
if productCurrency == "" {
|
||||
productCurrency = "USD"
|
||||
}
|
||||
|
||||
var productPrice float64
|
||||
if product.IsRecharge {
|
||||
if customPrice <= 0 {
|
||||
return nil, fmt.Errorf("the custom price should be greater than zero")
|
||||
}
|
||||
productPrice = customPrice
|
||||
} else {
|
||||
productPrice = product.Price
|
||||
}
|
||||
price := ConvertCurrency(productPrice, productCurrency, userBalanceCurrency)
|
||||
|
||||
orderName := fmt.Sprintf("order_%v", util.GenerateTimeId())
|
||||
order := &Order{
|
||||
Owner: product.Owner,
|
||||
Name: orderName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
DisplayName: fmt.Sprintf("Order for %s", product.DisplayName),
|
||||
ProductName: product.Name,
|
||||
Products: []string{product.Name},
|
||||
PricingName: pricingName,
|
||||
PlanName: planName,
|
||||
User: user.Name,
|
||||
Payment: "", // Payment will be set when user pays
|
||||
Price: price,
|
||||
Currency: userBalanceCurrency,
|
||||
State: "Created",
|
||||
Message: "",
|
||||
StartTime: util.GetCurrentTime(),
|
||||
EndTime: "",
|
||||
}
|
||||
|
||||
affected, err := AddOrder(order)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !affected {
|
||||
return nil, fmt.Errorf("failed to add order: %s", util.StructToJson(order))
|
||||
}
|
||||
|
||||
return order, nil
|
||||
}
|
||||
|
||||
func PayOrder(providerName, host, paymentEnv string, order *Order) (payment *Payment, attachInfo map[string]interface{}, err error) {
|
||||
if order.State != "Created" {
|
||||
return nil, nil, fmt.Errorf("cannot pay for order: %s, current state is %s", order.GetId(), order.State)
|
||||
}
|
||||
|
||||
productId := util.GetId(order.Owner, order.ProductName)
|
||||
product, err := GetProduct(productId)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if product == nil {
|
||||
return nil, nil, fmt.Errorf("the product: %s does not exist", productId)
|
||||
}
|
||||
|
||||
user, err := GetUser(util.GetId(order.Owner, order.User))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, nil, fmt.Errorf("the user: %s does not exist", order.User)
|
||||
}
|
||||
|
||||
provider, err := product.getProvider(providerName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pProvider, err := GetPaymentProvider(provider)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
owner := product.Owner
|
||||
payerName := fmt.Sprintf("%s | %s", user.Name, user.DisplayName)
|
||||
paymentName := fmt.Sprintf("payment_%v", util.GenerateTimeId())
|
||||
|
||||
originFrontend, originBackend := getOriginFromHost(host)
|
||||
returnUrl := fmt.Sprintf("%s/payments/%s/%s/result", originFrontend, owner, paymentName)
|
||||
notifyUrl := fmt.Sprintf("%s/api/notify-payment/%s/%s", originBackend, owner, paymentName)
|
||||
|
||||
// Create a subscription when pricing and plan are provided
|
||||
// This allows both free users and paid users to subscribe to plans
|
||||
if order.PricingName != "" && order.PlanName != "" {
|
||||
plan, err := GetPlan(util.GetId(owner, order.PlanName))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if plan == nil {
|
||||
return nil, nil, fmt.Errorf("the plan: %s does not exist", order.PlanName)
|
||||
}
|
||||
|
||||
sub, err := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
affected, err := AddSubscription(sub)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !affected {
|
||||
return nil, nil, fmt.Errorf("failed to add subscription: %s", sub.Name)
|
||||
}
|
||||
|
||||
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, order.PricingName, sub.Name)
|
||||
}
|
||||
|
||||
if product.SuccessUrl != "" {
|
||||
returnUrl = fmt.Sprintf("%s?transactionOwner=%s&transactionName=%s", product.SuccessUrl, owner, paymentName)
|
||||
}
|
||||
|
||||
payReq := &pp.PayReq{
|
||||
ProviderName: providerName,
|
||||
ProductName: product.Name,
|
||||
PayerName: payerName,
|
||||
PayerId: user.Id,
|
||||
PayerEmail: user.Email,
|
||||
PaymentName: paymentName,
|
||||
ProductDisplayName: product.DisplayName,
|
||||
ProductDescription: product.Description,
|
||||
ProductImage: product.Image,
|
||||
Price: order.Price,
|
||||
Currency: order.Currency,
|
||||
ReturnUrl: returnUrl,
|
||||
NotifyUrl: notifyUrl,
|
||||
PaymentEnv: paymentEnv,
|
||||
}
|
||||
|
||||
if provider.Type == "WeChat Pay" {
|
||||
payReq.PayerId, err = getUserExtraProperty(user, "WeChat", idp.BuildWechatOpenIdKey(provider.ClientId2))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
} else if provider.Type == "Balance" {
|
||||
payReq.PayerId = user.GetId()
|
||||
}
|
||||
|
||||
payResp, err := pProvider.Pay(payReq)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
payment = &Payment{
|
||||
Owner: product.Owner,
|
||||
Name: paymentName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
DisplayName: paymentName,
|
||||
|
||||
Provider: provider.Name,
|
||||
Type: provider.Type,
|
||||
|
||||
ProductName: product.Name,
|
||||
ProductDisplayName: product.DisplayName,
|
||||
Detail: product.Detail,
|
||||
Tag: product.Tag,
|
||||
Currency: order.Currency,
|
||||
Price: order.Price,
|
||||
ReturnUrl: product.ReturnUrl,
|
||||
IsRecharge: product.IsRecharge,
|
||||
|
||||
User: user.Name,
|
||||
PayUrl: payResp.PayUrl,
|
||||
SuccessUrl: returnUrl,
|
||||
State: pp.PaymentStateCreated,
|
||||
OutOrderId: payResp.OrderId,
|
||||
}
|
||||
|
||||
transaction := &Transaction{
|
||||
Owner: payment.Owner,
|
||||
Name: payment.Name,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
DisplayName: payment.DisplayName,
|
||||
Application: owner,
|
||||
Domain: "",
|
||||
Amount: payment.Price,
|
||||
Currency: order.Currency,
|
||||
Payment: payment.Name,
|
||||
State: pp.PaymentStateCreated,
|
||||
}
|
||||
|
||||
if product.IsRecharge {
|
||||
transaction.Category = "Recharge"
|
||||
transaction.Type = ""
|
||||
transaction.Subtype = ""
|
||||
transaction.Provider = ""
|
||||
transaction.Tag = "User"
|
||||
transaction.User = payment.User
|
||||
transaction.State = pp.PaymentStatePaid
|
||||
} else {
|
||||
transaction.Category = ""
|
||||
transaction.Type = provider.Category
|
||||
transaction.Subtype = provider.Type
|
||||
transaction.Provider = provider.Name
|
||||
transaction.Tag = product.Tag
|
||||
transaction.User = payment.User
|
||||
}
|
||||
|
||||
if provider.Type == "Dummy" {
|
||||
payment.State = pp.PaymentStatePaid
|
||||
currency := payment.Currency
|
||||
if currency == "" {
|
||||
currency = "USD"
|
||||
}
|
||||
err = UpdateUserBalance(user.Owner, user.Name, payment.Price, currency, "en")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
} else if provider.Type == "Balance" {
|
||||
convertedPrice := ConvertCurrency(order.Price, order.Currency, user.BalanceCurrency)
|
||||
if convertedPrice > user.Balance {
|
||||
return nil, nil, fmt.Errorf("insufficient user balance")
|
||||
}
|
||||
transaction.Amount = -transaction.Amount
|
||||
err = UpdateUserBalance(user.Owner, user.Name, -convertedPrice, user.BalanceCurrency, "en")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
payment.State = pp.PaymentStatePaid
|
||||
transaction.State = pp.PaymentStatePaid
|
||||
}
|
||||
|
||||
affected, err := AddPayment(payment)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if !affected {
|
||||
return nil, nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
|
||||
}
|
||||
|
||||
if product.IsRecharge || provider.Type == "Balance" {
|
||||
affected, _, err = AddTransaction(transaction, "en", false)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !affected {
|
||||
return nil, nil, fmt.Errorf("failed to add transaction: %s", util.StructToJson(transaction))
|
||||
}
|
||||
}
|
||||
|
||||
order.Payment = payment.Name
|
||||
if provider.Type == "Dummy" || provider.Type == "Balance" {
|
||||
order.State = "Paid"
|
||||
order.Message = "Payment successful"
|
||||
order.EndTime = util.GetCurrentTime()
|
||||
}
|
||||
_, err = UpdateOrder(order.GetId(), order)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return payment, payResp.AttachInfo, nil
|
||||
}
|
||||
|
||||
func CancelOrder(order *Order) (bool, error) {
|
||||
if order.State != "Created" {
|
||||
return false, fmt.Errorf("cannot cancel order in state: %s", order.State)
|
||||
}
|
||||
|
||||
order.State = "Canceled"
|
||||
order.Message = "Canceled by user"
|
||||
order.EndTime = util.GetCurrentTime()
|
||||
return UpdateOrder(order.GetId(), order)
|
||||
}
|
||||
@@ -83,14 +83,17 @@ 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"`
|
||||
BalanceCredit float64 `json:"balanceCredit"`
|
||||
BalanceCurrency string `xorm:"varchar(100)" json:"balanceCurrency"`
|
||||
}
|
||||
|
||||
func GetOrganizationCount(owner, name, field, value string) (int64, error) {
|
||||
@@ -237,6 +240,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 +590,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 +599,27 @@ 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
|
||||
var newBalance float64
|
||||
if isOrgBalance {
|
||||
organization.OrgBalance += balance
|
||||
newBalance = AddPrices(organization.OrgBalance, convertedBalance)
|
||||
// Check organization balance credit limit
|
||||
if newBalance < organization.BalanceCredit {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:Insufficient balance: new organization balance %v would be below credit limit %v"), newBalance, organization.BalanceCredit)
|
||||
}
|
||||
organization.OrgBalance = newBalance
|
||||
columns = []string{"org_balance"}
|
||||
} else {
|
||||
organization.UserBalance += balance
|
||||
// User balance is just a sum of all users' balances, no credit limit check here
|
||||
// Individual user credit limits are checked in UpdateUserBalance
|
||||
organization.UserBalance = AddPrices(organization.UserBalance, convertedBalance)
|
||||
columns = []string{"user_balance"}
|
||||
}
|
||||
|
||||
|
||||
@@ -384,6 +384,11 @@ func (a *Ormer) createTable() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Order))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Plan))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -207,7 +207,11 @@ func notifyPayment(body []byte, owner string, paymentName string) (*Payment, *pp
|
||||
}
|
||||
|
||||
if payment.IsRecharge {
|
||||
err = UpdateUserBalance(payment.Owner, payment.User, payment.Price, "en")
|
||||
currency := payment.Currency
|
||||
if currency == "" {
|
||||
currency = "USD"
|
||||
}
|
||||
err = UpdateUserBalance(payment.Owner, payment.User, payment.Price, currency, "en")
|
||||
return payment, notifyResult, err
|
||||
}
|
||||
|
||||
@@ -241,6 +245,32 @@ func NotifyPayment(body []byte, owner string, paymentName string) (*Payment, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Update order state based on payment status
|
||||
order, err := GetOrderByPayment(owner, paymentName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if order != nil {
|
||||
if payment.State == pp.PaymentStatePaid {
|
||||
order.State = "Paid"
|
||||
order.Message = "Payment successful"
|
||||
order.EndTime = util.GetCurrentTime()
|
||||
} else if payment.State == pp.PaymentStateError {
|
||||
order.State = "PaymentFailed"
|
||||
order.Message = payment.Message
|
||||
} else if payment.State == pp.PaymentStateCanceled {
|
||||
order.State = "Canceled"
|
||||
order.Message = "Payment was cancelled"
|
||||
} else if payment.State == pp.PaymentStateTimeout {
|
||||
order.State = "Timeout"
|
||||
order.Message = "Payment timed out"
|
||||
}
|
||||
_, err = UpdateOrder(order.GetId(), order)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return payment, nil
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -17,10 +17,6 @@ package object
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/idp"
|
||||
|
||||
"github.com/casdoor/casdoor/pp"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
@@ -168,190 +164,6 @@ func (product *Product) getProvider(providerName string) (*Provider, error) {
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func BuyProduct(id string, user *User, providerName, pricingName, planName, host, paymentEnv string, customPrice float64) (payment *Payment, attachInfo map[string]interface{}, err error) {
|
||||
product, err := GetProduct(id)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if product == nil {
|
||||
return nil, nil, fmt.Errorf("the product: %s does not exist", id)
|
||||
}
|
||||
|
||||
if product.IsRecharge {
|
||||
if customPrice <= 0 {
|
||||
return nil, nil, fmt.Errorf("the custom price should bigger than zero")
|
||||
} else {
|
||||
product.Price = customPrice
|
||||
}
|
||||
}
|
||||
|
||||
provider, err := product.getProvider(providerName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pProvider, err := GetPaymentProvider(provider)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
owner := product.Owner
|
||||
payerName := fmt.Sprintf("%s | %s", user.Name, user.DisplayName)
|
||||
paymentName := fmt.Sprintf("payment_%v", util.GenerateTimeId())
|
||||
|
||||
originFrontend, originBackend := getOriginFromHost(host)
|
||||
returnUrl := fmt.Sprintf("%s/payments/%s/%s/result", originFrontend, owner, paymentName)
|
||||
notifyUrl := fmt.Sprintf("%s/api/notify-payment/%s/%s", originBackend, owner, paymentName)
|
||||
|
||||
// Create a subscription when pricing and plan are provided
|
||||
// This allows both free users and paid users to subscribe to plans
|
||||
if pricingName != "" && planName != "" {
|
||||
plan, err := GetPlan(util.GetId(owner, planName))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if plan == nil {
|
||||
return nil, nil, fmt.Errorf("the plan: %s does not exist", planName)
|
||||
}
|
||||
|
||||
sub, err := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
_, err = AddSubscription(sub)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, pricingName, sub.Name)
|
||||
}
|
||||
|
||||
if product.SuccessUrl != "" {
|
||||
returnUrl = fmt.Sprintf("%s?transactionOwner=%s&transactionName=%s", product.SuccessUrl, owner, paymentName)
|
||||
}
|
||||
// Create an order
|
||||
payReq := &pp.PayReq{
|
||||
ProviderName: providerName,
|
||||
ProductName: product.Name,
|
||||
PayerName: payerName,
|
||||
PayerId: user.Id,
|
||||
PayerEmail: user.Email,
|
||||
PaymentName: paymentName,
|
||||
ProductDisplayName: product.DisplayName,
|
||||
ProductDescription: product.Description,
|
||||
ProductImage: product.Image,
|
||||
Price: product.Price,
|
||||
Currency: product.Currency,
|
||||
ReturnUrl: returnUrl,
|
||||
NotifyUrl: notifyUrl,
|
||||
PaymentEnv: paymentEnv,
|
||||
}
|
||||
|
||||
// custom process for WeChat & WeChat Pay
|
||||
if provider.Type == "WeChat Pay" {
|
||||
payReq.PayerId, err = getUserExtraProperty(user, "WeChat", idp.BuildWechatOpenIdKey(provider.ClientId2))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
} else if provider.Type == "Balance" {
|
||||
payReq.PayerId = user.GetId()
|
||||
}
|
||||
|
||||
payResp, err := pProvider.Pay(payReq)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// Create a Payment linked with Product and Order
|
||||
payment = &Payment{
|
||||
Owner: product.Owner,
|
||||
Name: paymentName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
DisplayName: paymentName,
|
||||
|
||||
Provider: provider.Name,
|
||||
Type: provider.Type,
|
||||
|
||||
ProductName: product.Name,
|
||||
ProductDisplayName: product.DisplayName,
|
||||
Detail: product.Detail,
|
||||
Tag: product.Tag,
|
||||
Currency: product.Currency,
|
||||
Price: product.Price,
|
||||
ReturnUrl: product.ReturnUrl,
|
||||
IsRecharge: product.IsRecharge,
|
||||
|
||||
User: user.Name,
|
||||
PayUrl: payResp.PayUrl,
|
||||
SuccessUrl: returnUrl,
|
||||
State: pp.PaymentStateCreated,
|
||||
OutOrderId: payResp.OrderId,
|
||||
}
|
||||
|
||||
transaction := &Transaction{
|
||||
Owner: payment.Owner,
|
||||
Name: payment.Name,
|
||||
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(),
|
||||
|
||||
State: pp.PaymentStateCreated,
|
||||
}
|
||||
|
||||
if provider.Type == "Dummy" {
|
||||
payment.State = pp.PaymentStatePaid
|
||||
err = UpdateUserBalance(user.Owner, user.Name, payment.Price, "en")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
} else if provider.Type == "Balance" {
|
||||
if product.Price > user.Balance {
|
||||
return nil, nil, fmt.Errorf("insufficient user balance")
|
||||
}
|
||||
transaction.Amount = -transaction.Amount
|
||||
err = UpdateUserBalance(user.Owner, user.Name, -product.Price, "en")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
payment.State = pp.PaymentStatePaid
|
||||
transaction.State = pp.PaymentStatePaid
|
||||
}
|
||||
|
||||
affected, err := AddPayment(payment)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if !affected {
|
||||
return nil, nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
|
||||
}
|
||||
|
||||
if product.IsRecharge || provider.Type == "Balance" {
|
||||
affected, err = AddTransaction(transaction, "en")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !affected {
|
||||
return nil, nil, fmt.Errorf("failed to add transaction: %s", util.StructToJson(payment))
|
||||
}
|
||||
}
|
||||
|
||||
return payment, payResp.AttachInfo, nil
|
||||
}
|
||||
|
||||
func ExtendProductWithProviders(product *Product) error {
|
||||
if product == nil {
|
||||
return nil
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package object
|
||||
|
||||
|
||||
@@ -345,6 +345,30 @@ func GetPaymentProvider(p *Provider) (pp.PaymentProvider, error) {
|
||||
return nil, err
|
||||
}
|
||||
return pp, nil
|
||||
} else if typ == "Polar" {
|
||||
pp, err := pp.NewPolarPaymentProvider(p.ClientSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pp, nil
|
||||
} else if typ == "Paddle" {
|
||||
pp, err := pp.NewPaddlePaymentProvider(p.ClientSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pp, nil
|
||||
} else if typ == "FastSpring" {
|
||||
pp, err := pp.NewFastSpringPaymentProvider(p.ClientId, p.ClientSecret, p.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pp, nil
|
||||
} else if typ == "Lemon Squeezy" {
|
||||
pp, err := pp.NewLemonSqueezyPaymentProvider(p.ClientId, p.ClientSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pp, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("the payment provider type: %s is not supported", p.Type)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type TableColumn struct {
|
||||
@@ -61,7 +62,8 @@ type Syncer struct {
|
||||
IsReadOnly bool `json:"isReadOnly"`
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
|
||||
Ormer *Ormer `xorm:"-" json:"-"`
|
||||
Ormer *Ormer `xorm:"-" json:"-"`
|
||||
SshClient *ssh.Client `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
func GetSyncerCount(owner, organization, field, value string) (int64, error) {
|
||||
@@ -171,6 +173,9 @@ func UpdateSyncer(id string, syncer *Syncer, isGlobalAdmin bool, lang string) (b
|
||||
return false, fmt.Errorf(i18n.Translate(lang, "auth:Unauthorized operation"))
|
||||
}
|
||||
|
||||
// Close old syncer connections before updating
|
||||
_ = s.Close()
|
||||
|
||||
session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
|
||||
if syncer.Password == "***" {
|
||||
syncer.Password = s.Password
|
||||
@@ -311,26 +316,30 @@ 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()
|
||||
}
|
||||
|
||||
func (syncer *Syncer) Close() error {
|
||||
var err error
|
||||
if syncer.Ormer != nil {
|
||||
if syncer.Ormer.Engine != nil {
|
||||
err = syncer.Ormer.Engine.Close()
|
||||
syncer.Ormer.Engine = nil
|
||||
}
|
||||
if syncer.Ormer.Db != nil {
|
||||
if dbErr := syncer.Ormer.Db.Close(); dbErr != nil && err == nil {
|
||||
err = dbErr
|
||||
}
|
||||
syncer.Ormer.Db = nil
|
||||
}
|
||||
syncer.Ormer = nil
|
||||
}
|
||||
if syncer.SshClient != nil {
|
||||
if sshErr := syncer.SshClient.Close(); sshErr != nil && err == nil {
|
||||
err = sshErr
|
||||
}
|
||||
syncer.SshClient = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
319
object/syncer_activedirectory.go
Normal file
319
object/syncer_activedirectory.go
Normal file
@@ -0,0 +1,319 @@
|
||||
// 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
|
||||
}
|
||||
|
||||
// Close closes any open connections (no-op for Active Directory LDAP-based syncer)
|
||||
func (p *ActiveDirectorySyncerProvider) Close() error {
|
||||
// Active Directory syncer doesn't maintain persistent connections
|
||||
// LDAP connections are opened and closed per operation
|
||||
return nil
|
||||
}
|
||||
|
||||
// getLdapConn establishes an LDAP connection to Active Directory
|
||||
func (p *ActiveDirectorySyncerProvider) getLdapConn() (*goldap.Conn, error) {
|
||||
// syncer.Host should be the AD server hostname/IP
|
||||
// syncer.Port should be the LDAP port (usually 389 or 636 for LDAPS)
|
||||
// syncer.User should be the bind DN or username
|
||||
// syncer.Password should be the bind password
|
||||
|
||||
host := p.Syncer.Host
|
||||
if host == "" {
|
||||
return nil, fmt.Errorf("host is required for Active Directory syncer")
|
||||
}
|
||||
|
||||
port := p.Syncer.Port
|
||||
if port == 0 {
|
||||
port = 389 // Default LDAP port
|
||||
}
|
||||
|
||||
user := p.Syncer.User
|
||||
if user == "" {
|
||||
return nil, fmt.Errorf("user (bind DN) is required for Active Directory syncer")
|
||||
}
|
||||
|
||||
password := p.Syncer.Password
|
||||
if password == "" {
|
||||
return nil, fmt.Errorf("password is required for Active Directory syncer")
|
||||
}
|
||||
|
||||
var conn *goldap.Conn
|
||||
var err error
|
||||
|
||||
// Check if SSL is enabled (port 636 typically indicates LDAPS)
|
||||
if port == 636 {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true, // TODO: Make this configurable
|
||||
}
|
||||
conn, err = goldap.DialTLS("tcp", fmt.Sprintf("%s:%d", host, port), tlsConfig)
|
||||
} else {
|
||||
conn, err = goldap.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Active Directory: %w", err)
|
||||
}
|
||||
|
||||
// Bind with the provided credentials
|
||||
err = conn.Bind(user, password)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to bind to Active Directory: %w", err)
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// getActiveDirectoryUsers retrieves all users from Active Directory
|
||||
func (p *ActiveDirectorySyncerProvider) getActiveDirectoryUsers() ([]*OriginalUser, error) {
|
||||
conn, err := p.getLdapConn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Use the Database field to store the base DN for searching
|
||||
baseDN := p.Syncer.Database
|
||||
if baseDN == "" {
|
||||
return nil, fmt.Errorf("database field (base DN) is required for Active Directory syncer")
|
||||
}
|
||||
|
||||
// Search filter for user objects in Active Directory
|
||||
// Filter for users: objectClass=user, objectCategory=person, and not disabled accounts
|
||||
searchFilter := "(&(objectClass=user)(objectCategory=person))"
|
||||
|
||||
// Attributes to retrieve from Active Directory
|
||||
attributes := []string{
|
||||
"sAMAccountName", // Username
|
||||
"userPrincipalName", // UPN (email-like format)
|
||||
"displayName", // Display name
|
||||
"givenName", // First name
|
||||
"sn", // Last name (surname)
|
||||
"mail", // Email address
|
||||
"telephoneNumber", // Phone number
|
||||
"mobile", // Mobile phone
|
||||
"title", // Job title
|
||||
"department", // Department
|
||||
"company", // Company
|
||||
"streetAddress", // Street address
|
||||
"l", // City/Locality
|
||||
"st", // State/Province
|
||||
"postalCode", // Postal code
|
||||
"co", // Country
|
||||
"objectGUID", // Unique identifier
|
||||
"whenCreated", // Creation time
|
||||
"userAccountControl", // Account status
|
||||
}
|
||||
|
||||
searchRequest := goldap.NewSearchRequest(
|
||||
baseDN,
|
||||
goldap.ScopeWholeSubtree,
|
||||
goldap.NeverDerefAliases,
|
||||
0, // No size limit
|
||||
0, // No time limit
|
||||
false, // Types only = false
|
||||
searchFilter,
|
||||
attributes,
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to search Active Directory: %w", err)
|
||||
}
|
||||
|
||||
originalUsers := []*OriginalUser{}
|
||||
for _, entry := range sr.Entries {
|
||||
originalUser := p.adEntryToOriginalUser(entry)
|
||||
originalUsers = append(originalUsers, originalUser)
|
||||
}
|
||||
|
||||
return originalUsers, nil
|
||||
}
|
||||
|
||||
// adEntryToOriginalUser converts an Active Directory LDAP entry to Casdoor OriginalUser
|
||||
func (p *ActiveDirectorySyncerProvider) adEntryToOriginalUser(entry *goldap.Entry) *OriginalUser {
|
||||
user := &OriginalUser{
|
||||
Address: []string{},
|
||||
Properties: map[string]string{},
|
||||
Groups: []string{},
|
||||
}
|
||||
|
||||
// Get basic attributes
|
||||
sAMAccountName := entry.GetAttributeValue("sAMAccountName")
|
||||
userPrincipalName := entry.GetAttributeValue("userPrincipalName")
|
||||
displayName := entry.GetAttributeValue("displayName")
|
||||
givenName := entry.GetAttributeValue("givenName")
|
||||
sn := entry.GetAttributeValue("sn")
|
||||
mail := entry.GetAttributeValue("mail")
|
||||
telephoneNumber := entry.GetAttributeValue("telephoneNumber")
|
||||
mobile := entry.GetAttributeValue("mobile")
|
||||
title := entry.GetAttributeValue("title")
|
||||
department := entry.GetAttributeValue("department")
|
||||
company := entry.GetAttributeValue("company")
|
||||
streetAddress := entry.GetAttributeValue("streetAddress")
|
||||
city := entry.GetAttributeValue("l")
|
||||
state := entry.GetAttributeValue("st")
|
||||
postalCode := entry.GetAttributeValue("postalCode")
|
||||
country := entry.GetAttributeValue("co")
|
||||
objectGUID := entry.GetAttributeValue("objectGUID")
|
||||
whenCreated := entry.GetAttributeValue("whenCreated")
|
||||
userAccountControlStr := entry.GetAttributeValue("userAccountControl")
|
||||
|
||||
// Set user fields
|
||||
// Use sAMAccountName as the primary username
|
||||
user.Name = sAMAccountName
|
||||
|
||||
// Use objectGUID as the unique ID if available, otherwise use sAMAccountName
|
||||
if objectGUID != "" {
|
||||
user.Id = objectGUID
|
||||
} else {
|
||||
user.Id = sAMAccountName
|
||||
}
|
||||
|
||||
user.DisplayName = displayName
|
||||
user.FirstName = givenName
|
||||
user.LastName = sn
|
||||
|
||||
// If display name is empty, construct from first and last name
|
||||
if user.DisplayName == "" && (user.FirstName != "" || user.LastName != "") {
|
||||
user.DisplayName = strings.TrimSpace(fmt.Sprintf("%s %s", user.FirstName, user.LastName))
|
||||
}
|
||||
|
||||
// Set email - prefer mail attribute, fallback to userPrincipalName
|
||||
if mail != "" {
|
||||
user.Email = mail
|
||||
} else if userPrincipalName != "" {
|
||||
user.Email = userPrincipalName
|
||||
}
|
||||
|
||||
// Set phone - prefer mobile, fallback to telephoneNumber
|
||||
if mobile != "" {
|
||||
user.Phone = mobile
|
||||
} else if telephoneNumber != "" {
|
||||
user.Phone = telephoneNumber
|
||||
}
|
||||
|
||||
user.Title = title
|
||||
|
||||
// Set affiliation/department
|
||||
if department != "" {
|
||||
user.Affiliation = department
|
||||
}
|
||||
|
||||
// Construct location from city, state, country
|
||||
locationParts := []string{}
|
||||
if city != "" {
|
||||
locationParts = append(locationParts, city)
|
||||
}
|
||||
if state != "" {
|
||||
locationParts = append(locationParts, state)
|
||||
}
|
||||
if country != "" {
|
||||
locationParts = append(locationParts, country)
|
||||
}
|
||||
if len(locationParts) > 0 {
|
||||
user.Location = strings.Join(locationParts, ", ")
|
||||
}
|
||||
|
||||
// Construct address
|
||||
if streetAddress != "" {
|
||||
addressParts := []string{streetAddress}
|
||||
if city != "" {
|
||||
addressParts = append(addressParts, city)
|
||||
}
|
||||
if state != "" {
|
||||
addressParts = append(addressParts, state)
|
||||
}
|
||||
if postalCode != "" {
|
||||
addressParts = append(addressParts, postalCode)
|
||||
}
|
||||
if country != "" {
|
||||
addressParts = append(addressParts, country)
|
||||
}
|
||||
user.Address = []string{strings.Join(addressParts, ", ")}
|
||||
}
|
||||
|
||||
// Store additional properties
|
||||
if company != "" {
|
||||
user.Properties["company"] = company
|
||||
}
|
||||
if userPrincipalName != "" {
|
||||
user.Properties["userPrincipalName"] = userPrincipalName
|
||||
}
|
||||
|
||||
// Set creation time
|
||||
if whenCreated != "" {
|
||||
user.CreatedTime = whenCreated
|
||||
} else {
|
||||
user.CreatedTime = util.GetCurrentTime()
|
||||
}
|
||||
|
||||
// Parse userAccountControl to determine if account is disabled
|
||||
// Bit 2 (value 2) indicates the account is disabled
|
||||
if userAccountControlStr != "" {
|
||||
userAccountControl := util.ParseInt(userAccountControlStr)
|
||||
// Check if bit 2 is set (account disabled)
|
||||
user.IsForbidden = (userAccountControl & 0x02) != 0
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
@@ -26,6 +26,46 @@ 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
|
||||
}
|
||||
|
||||
// Close closes any open connections (no-op for Azure AD API-based syncer)
|
||||
func (p *AzureAdSyncerProvider) Close() error {
|
||||
// Azure AD syncer doesn't maintain persistent connections
|
||||
return nil
|
||||
}
|
||||
|
||||
type AzureAdAccessTokenResp struct {
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
@@ -55,22 +95,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 +164,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 +213,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 +252,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 +268,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -73,4 +73,6 @@ func addSyncerJob(syncer *Syncer) error {
|
||||
|
||||
func deleteSyncerJob(syncer *Syncer) {
|
||||
clearCron(syncer.Name)
|
||||
// Close any open connections when deleting the job
|
||||
_ = syncer.Close()
|
||||
}
|
||||
|
||||
166
object/syncer_database.go
Normal file
166
object/syncer_database.go
Normal file
@@ -0,0 +1,166 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package 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
|
||||
}
|
||||
|
||||
// Store SSH client for proper cleanup
|
||||
p.Syncer.SshClient = dial
|
||||
|
||||
if p.Syncer.DatabaseType == "mysql" {
|
||||
dataSourceName = fmt.Sprintf("%s:%s@%s(%s:%d)/", p.Syncer.User, p.Syncer.Password, p.Syncer.Owner+p.Syncer.Name, p.Syncer.Host, p.Syncer.Port)
|
||||
mysql.RegisterDialContext(p.Syncer.Owner+p.Syncer.Name, (&ViaSSHDialer{Client: dial, Context: nil}).MysqlDial)
|
||||
} 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)
|
||||
// Clear map contents to help garbage collection
|
||||
for i := range results {
|
||||
for k := range results[i] {
|
||||
delete(results[i], k)
|
||||
}
|
||||
}
|
||||
results = nil
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Close closes the database connection and SSH tunnel
|
||||
func (p *DatabaseSyncerProvider) Close() error {
|
||||
return p.Syncer.Close()
|
||||
}
|
||||
|
||||
type dsnConnector struct {
|
||||
dsn string
|
||||
driver driver.Driver
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
213
object/syncer_googleworkspace.go
Normal file
213
object/syncer_googleworkspace.go
Normal file
@@ -0,0 +1,213 @@
|
||||
// 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
|
||||
}
|
||||
|
||||
// Close closes any open connections (no-op for Google Workspace API-based syncer)
|
||||
func (p *GoogleWorkspaceSyncerProvider) Close() error {
|
||||
// Google Workspace syncer doesn't maintain persistent connections
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAdminService creates and returns a Google Workspace Admin SDK service
|
||||
func (p *GoogleWorkspaceSyncerProvider) getAdminService() (*admin.Service, error) {
|
||||
// syncer.Host should be the admin email (impersonation account)
|
||||
// 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
|
||||
}
|
||||
58
object/syncer_interface.go
Normal file
58
object/syncer_interface.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// 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
|
||||
|
||||
// Close closes any open connections and releases resources
|
||||
Close() error
|
||||
}
|
||||
|
||||
// GetSyncerProvider returns the appropriate SyncerProvider implementation based on syncer type
|
||||
func GetSyncerProvider(syncer *Syncer) SyncerProvider {
|
||||
switch syncer.Type {
|
||||
case "WeCom":
|
||||
return &WecomSyncerProvider{Syncer: syncer}
|
||||
case "Azure AD":
|
||||
return &AzureAdSyncerProvider{Syncer: syncer}
|
||||
case "Google Workspace":
|
||||
return &GoogleWorkspaceSyncerProvider{Syncer: syncer}
|
||||
case "Active Directory":
|
||||
return &ActiveDirectorySyncerProvider{Syncer: syncer}
|
||||
case "Keycloak":
|
||||
return &KeycloakSyncerProvider{
|
||||
DatabaseSyncerProvider: DatabaseSyncerProvider{Syncer: syncer},
|
||||
}
|
||||
default:
|
||||
// Default to database syncer for "Database" type and any others
|
||||
return &DatabaseSyncerProvider{Syncer: syncer}
|
||||
}
|
||||
}
|
||||
31
object/syncer_keycloak.go
Normal file
31
object/syncer_keycloak.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
// KeycloakSyncerProvider implements SyncerProvider for Keycloak database syncers
|
||||
// Keycloak syncer extends DatabaseSyncerProvider with special handling for Keycloak schema
|
||||
type KeycloakSyncerProvider struct {
|
||||
DatabaseSyncerProvider
|
||||
}
|
||||
|
||||
// GetOriginalUsers retrieves all users from Keycloak database
|
||||
// This method overrides the base implementation to handle Keycloak-specific logic
|
||||
func (p *KeycloakSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
|
||||
// Use the base database implementation
|
||||
return p.DatabaseSyncerProvider.GetOriginalUsers()
|
||||
}
|
||||
|
||||
// Note: Keycloak-specific user mapping is handled in syncer_util.go
|
||||
// via getOriginalUsersFromMap which checks syncer.Type == "Keycloak"
|
||||
@@ -15,62 +15,22 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
type OriginalUser = User
|
||||
|
||||
type Credential struct {
|
||||
Value string `json:"value"`
|
||||
Salt string `json:"salt"`
|
||||
}
|
||||
|
||||
func (syncer *Syncer) getOriginalUsers() ([]*OriginalUser, error) {
|
||||
// Handle WeCom syncer separately
|
||||
if syncer.Type == "WeCom" {
|
||||
return syncer.getWecomOriginalUsers()
|
||||
}
|
||||
|
||||
// Handle Azure AD syncer separately
|
||||
if syncer.Type == "Azure AD" {
|
||||
return syncer.getAzureAdOriginalUsers()
|
||||
}
|
||||
|
||||
var results []map[string]sql.NullString
|
||||
err := syncer.Ormer.Engine.Table(syncer.getTable()).Find(&results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Memory leak problem handling
|
||||
// https://github.com/casdoor/casdoor/issues/1256
|
||||
users := syncer.getOriginalUsersFromMap(results)
|
||||
for _, m := range results {
|
||||
for k := range m {
|
||||
delete(m, k)
|
||||
}
|
||||
}
|
||||
|
||||
return users, nil
|
||||
provider := GetSyncerProvider(syncer)
|
||||
return provider.GetOriginalUsers()
|
||||
}
|
||||
|
||||
func (syncer *Syncer) addUser(user *OriginalUser) (bool, error) {
|
||||
m := syncer.getMapFromOriginalUser(user)
|
||||
affected, err := syncer.Ormer.Engine.Table(syncer.getTable()).Insert(m)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return affected != 0, nil
|
||||
provider := GetSyncerProvider(syncer)
|
||||
return provider.AddUser(user)
|
||||
}
|
||||
|
||||
func (syncer *Syncer) getCasdoorColumns() []string {
|
||||
@@ -85,16 +45,8 @@ func (syncer *Syncer) getCasdoorColumns() []string {
|
||||
}
|
||||
|
||||
func (syncer *Syncer) updateUser(user *OriginalUser) (bool, error) {
|
||||
key := syncer.getTargetTablePrimaryKey()
|
||||
m := syncer.getMapFromOriginalUser(user)
|
||||
pkValue := m[key]
|
||||
delete(m, key)
|
||||
|
||||
affected, err := syncer.Ormer.Engine.Table(syncer.getTable()).Where(fmt.Sprintf("%s = ?", key), pkValue).Update(&m)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return affected != 0, nil
|
||||
provider := GetSyncerProvider(syncer)
|
||||
return provider.UpdateUser(user)
|
||||
}
|
||||
|
||||
func (syncer *Syncer) updateUserForOriginalFields(user *User, key string) (bool, error) {
|
||||
@@ -139,80 +91,9 @@ func (syncer *Syncer) calculateHash(user *OriginalUser) string {
|
||||
return util.GetMd5Hash(s)
|
||||
}
|
||||
|
||||
type dsnConnector struct {
|
||||
dsn string
|
||||
driver driver.Driver
|
||||
}
|
||||
|
||||
func (t dsnConnector) Connect(ctx context.Context) (driver.Conn, error) {
|
||||
return t.driver.Open(t.dsn)
|
||||
}
|
||||
|
||||
func (t dsnConnector) Driver() driver.Driver {
|
||||
return t.driver
|
||||
}
|
||||
|
||||
func (syncer *Syncer) initAdapter() error {
|
||||
if syncer.Ormer != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// WeCom syncer doesn't need database adapter
|
||||
if syncer.Type == "WeCom" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Azure AD syncer doesn't need database adapter
|
||||
if syncer.Type == "Azure AD" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var dataSourceName string
|
||||
if syncer.DatabaseType == "mssql" {
|
||||
dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database)
|
||||
} else if syncer.DatabaseType == "postgres" {
|
||||
sslMode := "disable"
|
||||
if syncer.SslMode != "" {
|
||||
sslMode = syncer.SslMode
|
||||
}
|
||||
dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=%s dbname=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, sslMode, syncer.Database)
|
||||
} else {
|
||||
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", syncer.User, syncer.Password, syncer.Host, syncer.Port)
|
||||
}
|
||||
|
||||
var db *sql.DB
|
||||
var err error
|
||||
|
||||
if syncer.SshType != "" && (syncer.DatabaseType == "mysql" || syncer.DatabaseType == "postgres" || syncer.DatabaseType == "mssql") {
|
||||
var dial *ssh.Client
|
||||
if syncer.SshType == "password" {
|
||||
dial, err = DialWithPassword(syncer.SshUser, syncer.SshPassword, syncer.SshHost, syncer.SshPort)
|
||||
} else {
|
||||
dial, err = DialWithCert(syncer.SshUser, syncer.Owner+"/"+syncer.Cert, syncer.SshHost, syncer.SshPort)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if syncer.DatabaseType == "mysql" {
|
||||
dataSourceName = fmt.Sprintf("%s:%s@%s(%s:%d)/", syncer.User, syncer.Password, syncer.Owner+syncer.Name, syncer.Host, syncer.Port)
|
||||
mysql.RegisterDialContext(syncer.Owner+syncer.Name, (&ViaSSHDialer{Client: dial, Context: nil}).MysqlDial)
|
||||
} else if syncer.DatabaseType == "postgres" || syncer.DatabaseType == "mssql" {
|
||||
db = sql.OpenDB(dsnConnector{dsn: dataSourceName, driver: &ViaSSHDialer{Client: dial, Context: nil, DatabaseType: syncer.DatabaseType}})
|
||||
}
|
||||
}
|
||||
|
||||
if !isCloudIntranet {
|
||||
dataSourceName = strings.ReplaceAll(dataSourceName, "dbi.", "db.")
|
||||
}
|
||||
|
||||
if db != nil {
|
||||
syncer.Ormer, err = NewAdapterFromDb(syncer.DatabaseType, dataSourceName, syncer.Database, db)
|
||||
} else {
|
||||
syncer.Ormer, err = NewAdapter(syncer.DatabaseType, dataSourceName, syncer.Database)
|
||||
}
|
||||
|
||||
return err
|
||||
provider := GetSyncerProvider(syncer)
|
||||
return provider.InitAdapter()
|
||||
}
|
||||
|
||||
func RunSyncUsersJob() {
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package object
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -241,25 +246,25 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]sql.NullStrin
|
||||
|
||||
if syncer.Type == "Keycloak" {
|
||||
// query and set password and password salt from credential table
|
||||
sql := fmt.Sprintf("select * from credential where type = 'password' and user_id = '%s'", originalUser.Id)
|
||||
credentialResult, _ := syncer.Ormer.Engine.QueryString(sql)
|
||||
if len(credentialResult) > 0 {
|
||||
credentialResult, err := syncer.Ormer.Engine.QueryString("select * from credential where type = 'password' and user_id = ?", originalUser.Id)
|
||||
if err == nil && len(credentialResult) > 0 {
|
||||
credential := Credential{}
|
||||
_ = json.Unmarshal([]byte(credentialResult[0]["SECRET_DATA"]), &credential)
|
||||
originalUser.Password = credential.Value
|
||||
originalUser.PasswordSalt = credential.Salt
|
||||
if err := json.Unmarshal([]byte(credentialResult[0]["SECRET_DATA"]), &credential); err == nil {
|
||||
originalUser.Password = credential.Value
|
||||
originalUser.PasswordSalt = credential.Salt
|
||||
}
|
||||
}
|
||||
// query and set signup application from user group table
|
||||
sql = fmt.Sprintf("select name from keycloak_group where id = "+
|
||||
"(select group_id as gid from user_group_membership where user_id = '%s')", originalUser.Id)
|
||||
groupResult, _ := syncer.Ormer.Engine.QueryString(sql)
|
||||
if len(groupResult) > 0 {
|
||||
groupResult, err := syncer.Ormer.Engine.QueryString("select name from keycloak_group where id = "+
|
||||
"(select group_id as gid from user_group_membership where user_id = ?)", originalUser.Id)
|
||||
if err == nil && len(groupResult) > 0 {
|
||||
originalUser.SignupApplication = groupResult[0]["name"]
|
||||
}
|
||||
// create time
|
||||
i, _ := strconv.ParseInt(originalUser.CreatedTime, 10, 64)
|
||||
tm := time.Unix(i/int64(1000), 0)
|
||||
originalUser.CreatedTime = tm.Format("2006-01-02T15:04:05+08:00")
|
||||
if i, err := strconv.ParseInt(originalUser.CreatedTime, 10, 64); err == nil {
|
||||
tm := time.Unix(i/int64(1000), 0)
|
||||
originalUser.CreatedTime = tm.Format("2006-01-02T15:04:05+08:00")
|
||||
}
|
||||
// enable
|
||||
value, ok := result["ENABLED"]
|
||||
if ok {
|
||||
|
||||
@@ -26,6 +26,46 @@ 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
|
||||
}
|
||||
|
||||
// Close closes any open connections (no-op for WeCom API-based syncer)
|
||||
func (p *WecomSyncerProvider) Close() error {
|
||||
// WeCom syncer doesn't maintain persistent connections
|
||||
return nil
|
||||
}
|
||||
|
||||
type WecomAccessTokenResp struct {
|
||||
Errcode int `json:"errcode"`
|
||||
Errmsg string `json:"errmsg"`
|
||||
@@ -61,9 +101,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 +140,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 +184,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 +223,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 +239,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 +255,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 +263,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 +303,3 @@ func (syncer *Syncer) wecomUserToOriginalUser(wecomUser *WecomUser) *OriginalUse
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// getWecomOriginalUsers is the main entry point for WeCom syncer
|
||||
func (syncer *Syncer) getWecomOriginalUsers() ([]*OriginalUser, error) {
|
||||
return syncer.getWecomUsers()
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/pp"
|
||||
@@ -28,22 +29,20 @@ type Transaction struct {
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
// Transaction Provider Info
|
||||
Provider string `xorm:"varchar(100)" json:"provider"`
|
||||
Category string `xorm:"varchar(100)" json:"category"`
|
||||
Type string `xorm:"varchar(100)" json:"type"`
|
||||
// Product Info
|
||||
ProductName string `xorm:"varchar(100)" json:"productName"`
|
||||
ProductDisplayName string `xorm:"varchar(100)" json:"productDisplayName"`
|
||||
Detail string `xorm:"varchar(255)" json:"detail"`
|
||||
Tag string `xorm:"varchar(100)" json:"tag"`
|
||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||
Amount float64 `json:"amount"`
|
||||
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
|
||||
// User Info
|
||||
User string `xorm:"varchar(100)" json:"user"`
|
||||
|
||||
Application string `xorm:"varchar(100)" json:"application"`
|
||||
Payment string `xorm:"varchar(100)" json:"payment"`
|
||||
Domain string `xorm:"varchar(1000)" json:"domain"`
|
||||
Category string `xorm:"varchar(100)" json:"category"`
|
||||
Type string `xorm:"varchar(100)" json:"type"`
|
||||
Subtype string `xorm:"varchar(100)" json:"subtype"`
|
||||
Provider string `xorm:"varchar(100)" json:"provider"`
|
||||
User string `xorm:"varchar(100)" json:"user"`
|
||||
Tag string `xorm:"varchar(100)" json:"tag"`
|
||||
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||
|
||||
Payment string `xorm:"varchar(100)" json:"payment"`
|
||||
|
||||
State pp.PaymentState `xorm:"varchar(100)" json:"state"`
|
||||
}
|
||||
@@ -142,19 +141,32 @@ 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, dryRun bool) (bool, string, error) {
|
||||
transactionId := strings.ReplaceAll(util.GenerateId(), "-", "")
|
||||
transaction.Name = transactionId
|
||||
|
||||
// In dry run mode, only validate without making changes
|
||||
if dryRun {
|
||||
err := validateBalanceForTransaction(transaction, transaction.Amount, lang)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
affected, err := ormer.Engine.Insert(transaction)
|
||||
if err != nil {
|
||||
return false, err
|
||||
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 +188,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
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package object
|
||||
|
||||
|
||||
130
object/transaction_validate.go
Normal file
130
object/transaction_validate.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
)
|
||||
|
||||
func validateBalanceForTransaction(transaction *Transaction, amount float64, lang string) error {
|
||||
currency := transaction.Currency
|
||||
if currency == "" {
|
||||
currency = "USD"
|
||||
}
|
||||
|
||||
if transaction.Tag == "Organization" {
|
||||
// Validate organization balance change
|
||||
return validateOrganizationBalance("admin", transaction.Owner, amount, currency, true, lang)
|
||||
} else if transaction.Tag == "User" {
|
||||
// Validate user balance change
|
||||
if transaction.User == "" {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:User is required for User category transaction"))
|
||||
}
|
||||
if err := validateUserBalance(transaction.Owner, transaction.User, amount, currency, lang); err != nil {
|
||||
return err
|
||||
}
|
||||
// Validate organization's user balance sum change
|
||||
return validateOrganizationBalance("admin", transaction.Owner, amount, currency, false, lang)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateOrganizationBalance(owner string, name string, balance float64, currency string, isOrgBalance bool, lang string) error {
|
||||
organization, err := getOrganization(owner, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if organization == nil {
|
||||
return fmt.Errorf(i18n.Translate(lang, "auth:the organization: %s is not found"), fmt.Sprintf("%s/%s", owner, name))
|
||||
}
|
||||
|
||||
// Convert the balance amount from transaction currency to organization's balance currency
|
||||
balanceCurrency := organization.BalanceCurrency
|
||||
if balanceCurrency == "" {
|
||||
balanceCurrency = "USD"
|
||||
}
|
||||
convertedBalance := ConvertCurrency(balance, currency, balanceCurrency)
|
||||
|
||||
var newBalance float64
|
||||
if isOrgBalance {
|
||||
newBalance = AddPrices(organization.OrgBalance, convertedBalance)
|
||||
// Check organization balance credit limit
|
||||
if newBalance < organization.BalanceCredit {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:Insufficient balance: new organization balance %v would be below credit limit %v"), newBalance, organization.BalanceCredit)
|
||||
}
|
||||
} else {
|
||||
// User balance is just a sum of all users' balances, no credit limit check here
|
||||
// Individual user credit limits are checked in validateUserBalance
|
||||
newBalance = AddPrices(organization.UserBalance, convertedBalance)
|
||||
}
|
||||
|
||||
// In validation mode, we don't actually update the balance
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateUserBalance(owner string, name string, balance float64, currency string, lang string) error {
|
||||
user, err := getUser(owner, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user == nil {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:The user: %s is not found"), fmt.Sprintf("%s/%s", owner, name))
|
||||
}
|
||||
|
||||
// Convert the balance amount from transaction currency to user's balance currency
|
||||
balanceCurrency := user.BalanceCurrency
|
||||
var org *Organization
|
||||
if balanceCurrency == "" {
|
||||
// Get organization's balance currency as fallback
|
||||
org, err = getOrganization("admin", owner)
|
||||
if err == nil && org != nil && org.BalanceCurrency != "" {
|
||||
balanceCurrency = org.BalanceCurrency
|
||||
} else {
|
||||
balanceCurrency = "USD"
|
||||
}
|
||||
}
|
||||
convertedBalance := ConvertCurrency(balance, currency, balanceCurrency)
|
||||
|
||||
// Calculate new balance
|
||||
newBalance := AddPrices(user.Balance, convertedBalance)
|
||||
|
||||
// Check balance credit limit
|
||||
// User.BalanceCredit takes precedence over Organization.BalanceCredit
|
||||
var balanceCredit float64
|
||||
if user.BalanceCredit != 0 {
|
||||
balanceCredit = user.BalanceCredit
|
||||
} else {
|
||||
// Get organization's balance credit as fallback
|
||||
if org == nil {
|
||||
org, err = getOrganization("admin", owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if org != nil {
|
||||
balanceCredit = org.BalanceCredit
|
||||
}
|
||||
}
|
||||
|
||||
// Validate new balance against credit limit
|
||||
if newBalance < balanceCredit {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:Insufficient balance: new balance %v would be below credit limit %v"), newBalance, balanceCredit)
|
||||
}
|
||||
|
||||
// In validation mode, we don't actually update the balance
|
||||
return nil
|
||||
}
|
||||
@@ -92,7 +92,9 @@ type User struct {
|
||||
Karma int `json:"karma"`
|
||||
Ranking int `json:"ranking"`
|
||||
Balance float64 `json:"balance"`
|
||||
BalanceCredit float64 `json:"balanceCredit"`
|
||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||
BalanceCurrency string `xorm:"varchar(100)" json:"balanceCurrency"`
|
||||
IsDefaultAvatar bool `json:"isDefaultAvatar"`
|
||||
IsOnline bool `json:"isOnline"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
@@ -106,6 +108,7 @@ type User struct {
|
||||
AccessKey string `xorm:"varchar(100)" json:"accessKey"`
|
||||
AccessSecret string `xorm:"varchar(100)" json:"accessSecret"`
|
||||
AccessToken string `xorm:"mediumtext" json:"accessToken"`
|
||||
OriginalToken string `xorm:"mediumtext" json:"originalToken"`
|
||||
|
||||
CreatedIp string `xorm:"varchar(100)" json:"createdIp"`
|
||||
LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"`
|
||||
@@ -663,6 +666,9 @@ func GetMaskedUser(user *User, isAdminOrSelf bool, errs ...error) (*User, error)
|
||||
if user.AccessSecret != "" {
|
||||
user.AccessSecret = "***"
|
||||
}
|
||||
if user.OriginalToken != "" {
|
||||
user.OriginalToken = "***"
|
||||
}
|
||||
}
|
||||
|
||||
if user.ManagedAccounts != nil {
|
||||
@@ -827,7 +833,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",
|
||||
@@ -838,7 +844,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
||||
}
|
||||
}
|
||||
if isAdmin {
|
||||
columns = append(columns, "name", "id", "email", "phone", "country_code", "type", "balance", "mfa_items", "register_type", "register_source")
|
||||
columns = append(columns, "name", "id", "email", "phone", "country_code", "type", "balance", "balance_credit", "balance_currency", "mfa_items", "register_type", "register_source")
|
||||
}
|
||||
|
||||
columns = append(columns, "updated_time")
|
||||
@@ -870,6 +876,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 +977,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 +997,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 +1061,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 +1484,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 +1492,48 @@ 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
|
||||
var org *Organization
|
||||
if balanceCurrency == "" {
|
||||
// Get organization's balance currency as fallback
|
||||
org, err = getOrganization("admin", owner)
|
||||
if err == nil && org != nil && org.BalanceCurrency != "" {
|
||||
balanceCurrency = org.BalanceCurrency
|
||||
} else {
|
||||
balanceCurrency = "USD"
|
||||
}
|
||||
}
|
||||
convertedBalance := ConvertCurrency(balance, currency, balanceCurrency)
|
||||
|
||||
// Calculate new balance
|
||||
newBalance := AddPrices(user.Balance, convertedBalance)
|
||||
|
||||
// Check balance credit limit
|
||||
// User.BalanceCredit takes precedence over Organization.BalanceCredit
|
||||
var balanceCredit float64
|
||||
if user.BalanceCredit != 0 {
|
||||
balanceCredit = user.BalanceCredit
|
||||
} else {
|
||||
// Get organization's balance credit as fallback
|
||||
if org == nil {
|
||||
org, err = getOrganization("admin", owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if org != nil {
|
||||
balanceCredit = org.BalanceCredit
|
||||
}
|
||||
}
|
||||
|
||||
// Validate new balance against credit limit
|
||||
if newBalance < balanceCredit {
|
||||
return fmt.Errorf(i18n.Translate(lang, "general:Insufficient balance: new balance %v would be below credit limit %v"), newBalance, balanceCredit)
|
||||
}
|
||||
|
||||
user.Balance = newBalance
|
||||
_, err = UpdateUser(user.GetId(), user, []string{"balance"}, true)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -124,7 +124,6 @@ func chooseFaviconLinkBySizes(links []Link) *Link {
|
||||
var chosenLink *Link
|
||||
|
||||
for _, link := range links {
|
||||
link := link
|
||||
if chosenLink == nil || compareSizes(link.Sizes, chosenLink.Sizes) > 0 {
|
||||
chosenLink = &link
|
||||
}
|
||||
|
||||
@@ -43,6 +43,11 @@ func (user *User) UpdateUserHash() error {
|
||||
}
|
||||
|
||||
func (user *User) UpdateUserPassword(organization *Organization) {
|
||||
// Don't hash empty passwords (e.g., for OAuth users)
|
||||
if user.Password == "" {
|
||||
return
|
||||
}
|
||||
|
||||
credManager := cred.GetCredManager(organization.PasswordType)
|
||||
if credManager != nil {
|
||||
// Use organization salt if available, otherwise generate a random salt for the user
|
||||
|
||||
@@ -78,9 +78,15 @@ func parseListItem(lines *[]string, i int) []string {
|
||||
func UploadUsers(owner string, path string, userObj *User, lang string) (bool, error) {
|
||||
table := xlsx.ReadXlsxFile(path)
|
||||
|
||||
oldUserMap, err := getUserMap(owner)
|
||||
if err != nil {
|
||||
return false, err
|
||||
if len(table) == 0 {
|
||||
return false, fmt.Errorf("empty table")
|
||||
}
|
||||
|
||||
for idx, row := range table[0] {
|
||||
splitRow := strings.Split(row, "#")
|
||||
if len(splitRow) > 1 {
|
||||
table[0][idx] = splitRow[1]
|
||||
}
|
||||
}
|
||||
|
||||
uploadedUsers, err := StringArrayToStruct[User](table)
|
||||
@@ -104,6 +110,11 @@ func UploadUsers(owner string, path string, userObj *User, lang string) (bool, e
|
||||
return false, fmt.Errorf(i18n.Translate(lang, "auth:The organization: %s does not exist"), organizationName)
|
||||
}
|
||||
|
||||
oldUserMap, err := getUserMap(organizationName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
newUsers := []*User{}
|
||||
for _, user := range uploadedUsers {
|
||||
if _, ok := oldUserMap[user.GetId()]; !ok {
|
||||
@@ -146,7 +157,7 @@ func UploadUsers(owner string, path string, userObj *User, lang string) (bool, e
|
||||
}
|
||||
|
||||
if len(newUsers) == 0 {
|
||||
return false, nil
|
||||
return false, fmt.Errorf("no users are modified")
|
||||
}
|
||||
|
||||
return AddUsersInBatch(newUsers)
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/xorm-io/core"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func GetUserByField(organizationName string, field string, value string) (*User, error) {
|
||||
@@ -183,7 +184,12 @@ func getUserExtraProperty(user *User, providerType, key string) (string, error)
|
||||
return extra[key], nil
|
||||
}
|
||||
|
||||
func SetUserOAuthProperties(organization *Organization, user *User, providerType string, userInfo *idp.UserInfo, userMapping ...map[string]string) (bool, error) {
|
||||
func SetUserOAuthProperties(organization *Organization, user *User, providerType string, userInfo *idp.UserInfo, token *oauth2.Token, userMapping ...map[string]string) (bool, error) {
|
||||
// Store the original OAuth provider token if available
|
||||
if token != nil && token.AccessToken != "" {
|
||||
user.OriginalToken = token.AccessToken
|
||||
}
|
||||
|
||||
if userInfo.Id != "" {
|
||||
propertyName := fmt.Sprintf("oauth_%s_id", providerType)
|
||||
setUserProperty(user, propertyName, userInfo.Id)
|
||||
@@ -859,7 +865,7 @@ func StringArrayToStruct[T any](stringArray [][]string) ([]*T, error) {
|
||||
instances := []*T{}
|
||||
var err error
|
||||
|
||||
for _, m := range excelMap {
|
||||
for idx, m := range excelMap {
|
||||
instance := new(T)
|
||||
reflectedInstance := reflect.ValueOf(instance).Elem()
|
||||
|
||||
@@ -886,7 +892,7 @@ func StringArrayToStruct[T any](stringArray [][]string) ([]*T, error) {
|
||||
case reflect.Int:
|
||||
intVal, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("line %d - column %s: %s", idx+1, fName, err.Error())
|
||||
}
|
||||
fv.SetInt(int64(intVal))
|
||||
continue
|
||||
@@ -914,7 +920,7 @@ func StringArrayToStruct[T any](stringArray [][]string) ([]*T, error) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("line %d: %s", idx, err.Error())
|
||||
}
|
||||
}
|
||||
instances = append(instances, instance)
|
||||
|
||||
88
object/util.go
Normal file
88
object/util.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import "math"
|
||||
|
||||
// Fixed exchange rates (temporary implementation as per requirements)
|
||||
// All rates represent how many units of the currency equal 1 USD
|
||||
// Example: EUR: 0.92 means 1 USD = 0.92 EUR
|
||||
var exchangeRates = map[string]float64{
|
||||
"USD": 1.0,
|
||||
"EUR": 0.92,
|
||||
"GBP": 0.79,
|
||||
"JPY": 149.50,
|
||||
"CNY": 7.24,
|
||||
"AUD": 1.52,
|
||||
"CAD": 1.39,
|
||||
"CHF": 0.88,
|
||||
"HKD": 7.82,
|
||||
"SGD": 1.34,
|
||||
"INR": 83.12,
|
||||
"KRW": 1319.50,
|
||||
"BRL": 4.97,
|
||||
"MXN": 17.09,
|
||||
"ZAR": 18.15,
|
||||
"RUB": 92.50,
|
||||
"TRY": 32.15,
|
||||
"NZD": 1.67,
|
||||
"SEK": 10.35,
|
||||
"NOK": 10.72,
|
||||
"DKK": 6.87,
|
||||
"PLN": 3.91,
|
||||
"THB": 34.50,
|
||||
"MYR": 4.47,
|
||||
"IDR": 15750.00,
|
||||
"PHP": 55.50,
|
||||
"VND": 24500.00,
|
||||
}
|
||||
|
||||
// GetExchangeRate returns the exchange rate from fromCurrency to toCurrency
|
||||
func GetExchangeRate(fromCurrency, toCurrency string) float64 {
|
||||
if fromCurrency == toCurrency {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
// Default to USD if currency not found
|
||||
fromRate, fromExists := exchangeRates[fromCurrency]
|
||||
if !fromExists {
|
||||
fromRate = 1.0
|
||||
}
|
||||
|
||||
toRate, toExists := exchangeRates[toCurrency]
|
||||
if !toExists {
|
||||
toRate = 1.0
|
||||
}
|
||||
|
||||
// Convert from source currency to USD, then from USD to target currency
|
||||
// Example: EUR to JPY = (1/0.92) * 149.50 = USD/EUR * JPY/USD
|
||||
return toRate / fromRate
|
||||
}
|
||||
|
||||
// ConvertCurrency converts an amount from one currency to another using exchange rates
|
||||
func ConvertCurrency(amount float64, fromCurrency, toCurrency string) float64 {
|
||||
if fromCurrency == toCurrency {
|
||||
return amount
|
||||
}
|
||||
|
||||
rate := GetExchangeRate(fromCurrency, toCurrency)
|
||||
converted := amount * rate
|
||||
return math.Round(converted*1e8) / 1e8
|
||||
}
|
||||
|
||||
func AddPrices(price1 float64, price2 float64) float64 {
|
||||
res := price1 + price2
|
||||
return math.Round(res*1e8) / 1e8
|
||||
}
|
||||
255
pp/fastspring.go
Normal file
255
pp/fastspring.go
Normal file
@@ -0,0 +1,255 @@
|
||||
// Copyright 2024 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 pp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type FastSpringPaymentProvider struct {
|
||||
ApiUsername string
|
||||
ApiPassword string
|
||||
StorefrontPath string
|
||||
}
|
||||
|
||||
func NewFastSpringPaymentProvider(apiUsername string, apiPassword string, storefrontPath string) (*FastSpringPaymentProvider, error) {
|
||||
pp := &FastSpringPaymentProvider{
|
||||
ApiUsername: apiUsername,
|
||||
ApiPassword: apiPassword,
|
||||
StorefrontPath: storefrontPath,
|
||||
}
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
type fastSpringSessionRequest struct {
|
||||
Account *fastSpringAccount `json:"account,omitempty"`
|
||||
Items []fastSpringItem `json:"items"`
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
type fastSpringAccount struct {
|
||||
Contact *fastSpringContact `json:"contact,omitempty"`
|
||||
}
|
||||
|
||||
type fastSpringContact struct {
|
||||
Email string `json:"email,omitempty"`
|
||||
First string `json:"first,omitempty"`
|
||||
Last string `json:"last,omitempty"`
|
||||
}
|
||||
|
||||
type fastSpringItem struct {
|
||||
Product string `json:"product"`
|
||||
Quantity int `json:"quantity"`
|
||||
Pricing *fastSpringPricing `json:"pricing,omitempty"`
|
||||
}
|
||||
|
||||
type fastSpringPricing struct {
|
||||
Price map[string]float64 `json:"price,omitempty"`
|
||||
}
|
||||
|
||||
type fastSpringSessionResponse struct {
|
||||
ID string `json:"id"`
|
||||
Account string `json:"account"`
|
||||
AccountURL string `json:"accountUrl"`
|
||||
}
|
||||
|
||||
type fastSpringOrderResponse struct {
|
||||
ID string `json:"id"`
|
||||
Reference string `json:"reference"`
|
||||
Total float64 `json:"total"`
|
||||
TotalDisplay string `json:"totalDisplay"`
|
||||
TotalInPayoutCurrency float64 `json:"totalInPayoutCurrency"`
|
||||
Currency string `json:"currency"`
|
||||
PayoutCurrency string `json:"payoutCurrency"`
|
||||
Completed bool `json:"completed"`
|
||||
Changed int64 `json:"changed"`
|
||||
Tags map[string]string `json:"tags"`
|
||||
Items []fastSpringOrderItem `json:"items"`
|
||||
}
|
||||
|
||||
type fastSpringOrderItem struct {
|
||||
Product string `json:"product"`
|
||||
Quantity int `json:"quantity"`
|
||||
Display string `json:"display"`
|
||||
Subtotal float64 `json:"subtotal"`
|
||||
Discount float64 `json:"discount"`
|
||||
Price float64 `json:"price"`
|
||||
}
|
||||
|
||||
func (pp *FastSpringPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
|
||||
description := joinAttachString([]string{r.ProductName, r.ProductDisplayName, r.ProviderName})
|
||||
|
||||
// Create session request
|
||||
sessionReq := fastSpringSessionRequest{
|
||||
Items: []fastSpringItem{
|
||||
{
|
||||
Product: r.ProductName,
|
||||
Quantity: 1,
|
||||
Pricing: &fastSpringPricing{
|
||||
Price: map[string]float64{
|
||||
strings.ToUpper(r.Currency): r.Price,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Tags: map[string]string{
|
||||
"payment_name": r.PaymentName,
|
||||
"product_description": description,
|
||||
"product_name": r.ProductName,
|
||||
"product_display_name": r.ProductDisplayName,
|
||||
"provider_name": r.ProviderName,
|
||||
},
|
||||
}
|
||||
|
||||
if r.PayerEmail != "" || r.PayerName != "" {
|
||||
sessionReq.Account = &fastSpringAccount{
|
||||
Contact: &fastSpringContact{
|
||||
Email: r.PayerEmail,
|
||||
First: r.PayerName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(sessionReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create HTTP request to FastSpring API
|
||||
req, err := http.NewRequest("POST", "https://api.fastspring.com/sessions", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.SetBasicAuth(pp.ApiUsername, pp.ApiPassword)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
return nil, fmt.Errorf("fastspring API error: %s", string(body))
|
||||
}
|
||||
|
||||
var sessionResp fastSpringSessionResponse
|
||||
if err := json.Unmarshal(body, &sessionResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build checkout URL
|
||||
checkoutURL := fmt.Sprintf("https://%s/session/%s", pp.StorefrontPath, sessionResp.ID)
|
||||
|
||||
payResp := &PayResp{
|
||||
PayUrl: checkoutURL,
|
||||
OrderId: sessionResp.ID,
|
||||
}
|
||||
return payResp, nil
|
||||
}
|
||||
|
||||
func (pp *FastSpringPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
|
||||
// Fetch order details from FastSpring API
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://api.fastspring.com/orders/%s", orderId), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.SetBasicAuth(pp.ApiUsername, pp.ApiPassword)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
// Order not found - still pending
|
||||
return &NotifyResult{PaymentStatus: PaymentStateCreated}, nil
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("fastspring API error: %s", string(respBody))
|
||||
}
|
||||
|
||||
var orderResp fastSpringOrderResponse
|
||||
if err := json.Unmarshal(respBody, &orderResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if order is completed
|
||||
if !orderResp.Completed {
|
||||
return &NotifyResult{PaymentStatus: PaymentStateCreated}, nil
|
||||
}
|
||||
|
||||
// Extract payment details from tags
|
||||
var (
|
||||
paymentName string
|
||||
productName string
|
||||
productDisplayName string
|
||||
providerName string
|
||||
)
|
||||
|
||||
if orderResp.Tags != nil {
|
||||
paymentName = orderResp.Tags["payment_name"]
|
||||
productName = orderResp.Tags["product_name"]
|
||||
productDisplayName = orderResp.Tags["product_display_name"]
|
||||
providerName = orderResp.Tags["provider_name"]
|
||||
}
|
||||
|
||||
return &NotifyResult{
|
||||
PaymentName: paymentName,
|
||||
PaymentStatus: PaymentStatePaid,
|
||||
|
||||
ProductName: productName,
|
||||
ProductDisplayName: productDisplayName,
|
||||
ProviderName: providerName,
|
||||
|
||||
Price: orderResp.Total,
|
||||
Currency: orderResp.Currency,
|
||||
|
||||
OrderId: orderId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (pp *FastSpringPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (pp *FastSpringPaymentProvider) GetResponseError(err error) string {
|
||||
if err == nil {
|
||||
return "success"
|
||||
}
|
||||
return "fail"
|
||||
}
|
||||
194
pp/lemonsqueezy.go
Normal file
194
pp/lemonsqueezy.go
Normal file
@@ -0,0 +1,194 @@
|
||||
// Copyright 2024 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 pp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/NdoleStudio/lemonsqueezy-go"
|
||||
)
|
||||
|
||||
type LemonSqueezyPaymentProvider struct {
|
||||
Client *lemonsqueezy.Client
|
||||
StoreID int
|
||||
}
|
||||
|
||||
func NewLemonSqueezyPaymentProvider(storeId string, apiKey string) (*LemonSqueezyPaymentProvider, error) {
|
||||
client := lemonsqueezy.New(
|
||||
lemonsqueezy.WithAPIKey(apiKey),
|
||||
)
|
||||
|
||||
storeID, err := strconv.Atoi(storeId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid store ID: %w", err)
|
||||
}
|
||||
|
||||
pp := &LemonSqueezyPaymentProvider{
|
||||
Client: client,
|
||||
StoreID: storeID,
|
||||
}
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
func (pp *LemonSqueezyPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Parse variant ID from the product name (expected to be the variant ID)
|
||||
variantID, err := strconv.Atoi(r.ProductName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid variant ID in product name: %w", err)
|
||||
}
|
||||
|
||||
// Store product info in custom data for later retrieval
|
||||
description := joinAttachString([]string{r.ProductName, r.ProductDisplayName, r.ProviderName})
|
||||
|
||||
customData := map[string]any{
|
||||
"payment_name": r.PaymentName,
|
||||
"product_description": description,
|
||||
"product_name": r.ProductName,
|
||||
"product_display_name": r.ProductDisplayName,
|
||||
"provider_name": r.ProviderName,
|
||||
"price": priceFloat64ToString(r.Price),
|
||||
"currency": r.Currency,
|
||||
}
|
||||
|
||||
// Create checkout attributes
|
||||
attributes := &lemonsqueezy.CheckoutCreateAttributes{
|
||||
ProductOptions: lemonsqueezy.CheckoutCreateProductOptions{
|
||||
Name: r.ProductDisplayName,
|
||||
Description: r.ProductDescription,
|
||||
RedirectURL: r.ReturnUrl,
|
||||
},
|
||||
CheckoutData: lemonsqueezy.CheckoutCreateData{
|
||||
Email: r.PayerEmail,
|
||||
Name: r.PayerName,
|
||||
Custom: customData,
|
||||
},
|
||||
}
|
||||
|
||||
// Create checkout
|
||||
checkout, _, err := pp.Client.Checkouts.Create(ctx, pp.StoreID, variantID, attributes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if checkout == nil {
|
||||
return nil, fmt.Errorf("lemonsqueezy checkout response is nil")
|
||||
}
|
||||
|
||||
payResp := &PayResp{
|
||||
PayUrl: checkout.Data.Attributes.URL,
|
||||
OrderId: checkout.Data.ID,
|
||||
}
|
||||
return payResp, nil
|
||||
}
|
||||
|
||||
func (pp *LemonSqueezyPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Get checkout status
|
||||
checkout, _, err := pp.Client.Checkouts.Get(ctx, orderId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if checkout == nil {
|
||||
return nil, fmt.Errorf("lemonsqueezy checkout not found for order: %s", orderId)
|
||||
}
|
||||
|
||||
// Check if checkout has expired
|
||||
if checkout.Data.Attributes.ExpiresAt != nil {
|
||||
if time.Now().After(*checkout.Data.Attributes.ExpiresAt) {
|
||||
return &NotifyResult{PaymentStatus: PaymentStateTimeout}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Extract payment details from custom data
|
||||
var (
|
||||
paymentName string
|
||||
productName string
|
||||
productDisplayName string
|
||||
providerName string
|
||||
price float64
|
||||
currency string
|
||||
)
|
||||
|
||||
if checkout.Data.Attributes.CheckoutData.Custom != nil {
|
||||
if customData, ok := checkout.Data.Attributes.CheckoutData.Custom.(map[string]any); ok {
|
||||
if v, ok := customData["payment_name"]; ok {
|
||||
if str, ok := v.(string); ok {
|
||||
paymentName = str
|
||||
}
|
||||
}
|
||||
if v, ok := customData["product_name"]; ok {
|
||||
if str, ok := v.(string); ok {
|
||||
productName = str
|
||||
}
|
||||
}
|
||||
if v, ok := customData["product_display_name"]; ok {
|
||||
if str, ok := v.(string); ok {
|
||||
productDisplayName = str
|
||||
}
|
||||
}
|
||||
if v, ok := customData["provider_name"]; ok {
|
||||
if str, ok := v.(string); ok {
|
||||
providerName = str
|
||||
}
|
||||
}
|
||||
if v, ok := customData["price"]; ok {
|
||||
if str, ok := v.(string); ok {
|
||||
price = priceStringToFloat64(str)
|
||||
}
|
||||
}
|
||||
if v, ok := customData["currency"]; ok {
|
||||
if str, ok := v.(string); ok {
|
||||
currency = str
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lemon Squeezy checkouts don't have a direct status field for payment completion.
|
||||
// The checkout remains valid until it expires or is used.
|
||||
// For proper payment status tracking, webhooks should be configured.
|
||||
// Here we return PaymentStateCreated to indicate the checkout is still pending.
|
||||
return &NotifyResult{
|
||||
PaymentName: paymentName,
|
||||
PaymentStatus: PaymentStateCreated,
|
||||
|
||||
ProductName: productName,
|
||||
ProductDisplayName: productDisplayName,
|
||||
ProviderName: providerName,
|
||||
|
||||
Price: price,
|
||||
Currency: currency,
|
||||
|
||||
OrderId: orderId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (pp *LemonSqueezyPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (pp *LemonSqueezyPaymentProvider) GetResponseError(err error) string {
|
||||
if err == nil {
|
||||
return "success"
|
||||
}
|
||||
return "fail"
|
||||
}
|
||||
223
pp/paddle.go
Normal file
223
pp/paddle.go
Normal file
@@ -0,0 +1,223 @@
|
||||
// Copyright 2024 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 pp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/PaddleHQ/paddle-go-sdk"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
)
|
||||
|
||||
type PaddlePaymentProvider struct {
|
||||
Client *paddle.SDK
|
||||
}
|
||||
|
||||
func NewPaddlePaymentProvider(apiKey string) (*PaddlePaymentProvider, error) {
|
||||
var client *paddle.SDK
|
||||
var err error
|
||||
|
||||
if conf.GetConfigString("runmode") == "prod" {
|
||||
client, err = paddle.New(apiKey)
|
||||
} else {
|
||||
client, err = paddle.NewSandbox(apiKey)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pp := &PaddlePaymentProvider{
|
||||
Client: client,
|
||||
}
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
func (pp *PaddlePaymentProvider) Pay(r *PayReq) (*PayResp, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Store product info in custom_data for later retrieval
|
||||
description := joinAttachString([]string{r.ProductName, r.ProductDisplayName, r.ProviderName})
|
||||
|
||||
customData := paddle.CustomData{
|
||||
"payment_name": r.PaymentName,
|
||||
"product_description": description,
|
||||
"product_name": r.ProductName,
|
||||
"product_display_name": r.ProductDisplayName,
|
||||
"provider_name": r.ProviderName,
|
||||
}
|
||||
|
||||
// Convert price to amount string in cents (lowest denomination)
|
||||
amountInCents := fmt.Sprintf("%d", priceFloat64ToInt64(r.Price))
|
||||
|
||||
// Map currency string to paddle CurrencyCode
|
||||
currencyCode := paddle.CurrencyCode(strings.ToUpper(r.Currency))
|
||||
|
||||
// Create a non-catalog price and product for this transaction
|
||||
items := []paddle.CreateTransactionItems{
|
||||
*paddle.NewCreateTransactionItemsNonCatalogPriceAndProduct(&paddle.NonCatalogPriceAndProduct{
|
||||
Quantity: 1,
|
||||
Price: paddle.TransactionPriceCreateWithProduct{
|
||||
Description: description,
|
||||
Name: &r.ProductDisplayName,
|
||||
TaxMode: paddle.TaxModeAccountSetting,
|
||||
UnitPrice: paddle.Money{
|
||||
Amount: amountInCents,
|
||||
CurrencyCode: currencyCode,
|
||||
},
|
||||
Product: paddle.TransactionSubscriptionProductCreate{
|
||||
Name: r.ProductDisplayName,
|
||||
Description: &r.ProductDescription,
|
||||
TaxCategory: paddle.TaxCategoryStandard,
|
||||
ImageURL: &r.ProductImage,
|
||||
CustomData: customData,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
checkoutSettings := &paddle.TransactionCheckout{
|
||||
URL: &r.ReturnUrl,
|
||||
}
|
||||
|
||||
req := &paddle.CreateTransactionRequest{
|
||||
Items: items,
|
||||
CustomData: customData,
|
||||
Checkout: checkoutSettings,
|
||||
}
|
||||
|
||||
res, err := pp.Client.CreateTransaction(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res == nil {
|
||||
return nil, fmt.Errorf("paddle transaction response is nil")
|
||||
}
|
||||
|
||||
// Get checkout URL from the transaction
|
||||
checkoutURL := ""
|
||||
if res.Checkout != nil && res.Checkout.URL != nil {
|
||||
checkoutURL = *res.Checkout.URL
|
||||
}
|
||||
|
||||
payResp := &PayResp{
|
||||
PayUrl: checkoutURL,
|
||||
OrderId: res.ID,
|
||||
}
|
||||
return payResp, nil
|
||||
}
|
||||
|
||||
func (pp *PaddlePaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Get transaction status
|
||||
req := &paddle.GetTransactionRequest{
|
||||
TransactionID: orderId,
|
||||
}
|
||||
res, err := pp.Client.GetTransaction(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res == nil {
|
||||
return nil, fmt.Errorf("paddle transaction not found for order: %s", orderId)
|
||||
}
|
||||
|
||||
// Map Paddle status to payment state
|
||||
switch res.Status {
|
||||
case paddle.TransactionStatusDraft, paddle.TransactionStatusReady:
|
||||
return &NotifyResult{PaymentStatus: PaymentStateCreated}, nil
|
||||
case paddle.TransactionStatusCompleted, paddle.TransactionStatusPaid:
|
||||
// Payment successful, continue to extract payment details below
|
||||
case paddle.TransactionStatusBilled:
|
||||
// Billed but not yet paid
|
||||
return &NotifyResult{PaymentStatus: PaymentStateCreated}, nil
|
||||
case paddle.TransactionStatusCanceled:
|
||||
return &NotifyResult{PaymentStatus: PaymentStateCanceled, NotifyMessage: "Transaction canceled"}, nil
|
||||
case paddle.TransactionStatusPastDue:
|
||||
return &NotifyResult{PaymentStatus: PaymentStateError, NotifyMessage: "Payment past due"}, nil
|
||||
default:
|
||||
return &NotifyResult{PaymentStatus: PaymentStateError, NotifyMessage: fmt.Sprintf("unexpected paddle transaction status: %v", res.Status)}, nil
|
||||
}
|
||||
|
||||
// Extract payment details from transaction for successful payment
|
||||
var (
|
||||
paymentName string
|
||||
productName string
|
||||
productDisplayName string
|
||||
providerName string
|
||||
)
|
||||
|
||||
if res.CustomData != nil {
|
||||
if v, ok := res.CustomData["payment_name"]; ok {
|
||||
if str, ok := v.(string); ok {
|
||||
paymentName = str
|
||||
}
|
||||
}
|
||||
if v, ok := res.CustomData["product_name"]; ok {
|
||||
if str, ok := v.(string); ok {
|
||||
productName = str
|
||||
}
|
||||
}
|
||||
if v, ok := res.CustomData["product_display_name"]; ok {
|
||||
if str, ok := v.(string); ok {
|
||||
productDisplayName = str
|
||||
}
|
||||
}
|
||||
if v, ok := res.CustomData["provider_name"]; ok {
|
||||
if str, ok := v.(string); ok {
|
||||
providerName = str
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get price from transaction details
|
||||
var price float64
|
||||
var currency string
|
||||
|
||||
if len(res.Details.LineItems) > 0 {
|
||||
// Get the total amount from transaction details
|
||||
price = priceStringToFloat64(res.Details.Totals.Total) / 100
|
||||
currency = string(res.CurrencyCode)
|
||||
}
|
||||
|
||||
return &NotifyResult{
|
||||
PaymentName: paymentName,
|
||||
PaymentStatus: PaymentStatePaid,
|
||||
|
||||
ProductName: productName,
|
||||
ProductDisplayName: productDisplayName,
|
||||
ProviderName: providerName,
|
||||
|
||||
Price: price,
|
||||
Currency: currency,
|
||||
|
||||
OrderId: orderId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (pp *PaddlePaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (pp *PaddlePaymentProvider) GetResponseError(err error) string {
|
||||
if err == nil {
|
||||
return "success"
|
||||
}
|
||||
return "fail"
|
||||
}
|
||||
157
pp/polar.go
Normal file
157
pp/polar.go
Normal file
@@ -0,0 +1,157 @@
|
||||
// Copyright 2024 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 pp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
polargo "github.com/polarsource/polar-go"
|
||||
"github.com/polarsource/polar-go/models/components"
|
||||
)
|
||||
|
||||
type PolarPaymentProvider struct {
|
||||
Client *polargo.Polar
|
||||
}
|
||||
|
||||
func NewPolarPaymentProvider(accessToken string) (*PolarPaymentProvider, error) {
|
||||
client := polargo.New(
|
||||
polargo.WithSecurity(accessToken),
|
||||
)
|
||||
|
||||
pp := &PolarPaymentProvider{
|
||||
Client: client,
|
||||
}
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
func (pp *PolarPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Store product info in metadata for later retrieval
|
||||
description := joinAttachString([]string{r.ProductName, r.ProductDisplayName, r.ProviderName})
|
||||
|
||||
metadata := map[string]components.CheckoutCreateMetadata{
|
||||
"payment_name": components.CreateCheckoutCreateMetadataStr(r.PaymentName),
|
||||
"product_description": components.CreateCheckoutCreateMetadataStr(description),
|
||||
"product_name": components.CreateCheckoutCreateMetadataStr(r.ProductName),
|
||||
"product_display_name": components.CreateCheckoutCreateMetadataStr(r.ProductDisplayName),
|
||||
"provider_name": components.CreateCheckoutCreateMetadataStr(r.ProviderName),
|
||||
}
|
||||
|
||||
checkoutCreate := components.CheckoutCreate{
|
||||
CustomerName: polargo.Pointer(r.PayerName),
|
||||
CustomerEmail: polargo.Pointer(r.PayerEmail),
|
||||
SuccessURL: polargo.Pointer(r.ReturnUrl),
|
||||
Metadata: metadata,
|
||||
Amount: polargo.Pointer(priceFloat64ToInt64(r.Price)),
|
||||
}
|
||||
|
||||
res, err := pp.Client.Checkouts.Create(ctx, checkoutCreate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.Checkout == nil {
|
||||
return nil, fmt.Errorf("polar checkout response is nil")
|
||||
}
|
||||
|
||||
payResp := &PayResp{
|
||||
PayUrl: res.Checkout.URL,
|
||||
OrderId: res.Checkout.ID,
|
||||
}
|
||||
return payResp, nil
|
||||
}
|
||||
|
||||
func (pp *PolarPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Get checkout session status
|
||||
res, err := pp.Client.Checkouts.Get(ctx, orderId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.Checkout == nil {
|
||||
return nil, fmt.Errorf("polar checkout not found for order: %s", orderId)
|
||||
}
|
||||
|
||||
checkout := res.Checkout
|
||||
|
||||
// Map Polar status to payment state
|
||||
switch checkout.Status {
|
||||
case components.CheckoutStatusOpen:
|
||||
return &NotifyResult{PaymentStatus: PaymentStateCreated}, nil
|
||||
case components.CheckoutStatusSucceeded:
|
||||
// Payment successful, continue to extract payment details below
|
||||
case components.CheckoutStatusConfirmed:
|
||||
// Payment confirmed but not yet succeeded
|
||||
return &NotifyResult{PaymentStatus: PaymentStateCreated}, nil
|
||||
case components.CheckoutStatusExpired:
|
||||
return &NotifyResult{PaymentStatus: PaymentStateTimeout}, nil
|
||||
case components.CheckoutStatusFailed:
|
||||
return &NotifyResult{PaymentStatus: PaymentStateError, NotifyMessage: "Payment failed"}, nil
|
||||
default:
|
||||
return &NotifyResult{PaymentStatus: PaymentStateError, NotifyMessage: fmt.Sprintf("unexpected polar checkout status: %v", checkout.Status)}, nil
|
||||
}
|
||||
|
||||
// Extract payment details from checkout for successful payment
|
||||
var (
|
||||
paymentName string
|
||||
productName string
|
||||
productDisplayName string
|
||||
providerName string
|
||||
)
|
||||
|
||||
if checkout.Metadata != nil {
|
||||
if v, ok := checkout.Metadata["payment_name"]; ok && v.Str != nil {
|
||||
paymentName = *v.Str
|
||||
}
|
||||
if v, ok := checkout.Metadata["product_name"]; ok && v.Str != nil {
|
||||
productName = *v.Str
|
||||
}
|
||||
if v, ok := checkout.Metadata["product_display_name"]; ok && v.Str != nil {
|
||||
productDisplayName = *v.Str
|
||||
}
|
||||
if v, ok := checkout.Metadata["provider_name"]; ok && v.Str != nil {
|
||||
providerName = *v.Str
|
||||
}
|
||||
}
|
||||
|
||||
return &NotifyResult{
|
||||
PaymentName: paymentName,
|
||||
PaymentStatus: PaymentStatePaid,
|
||||
|
||||
ProductName: productName,
|
||||
ProductDisplayName: productDisplayName,
|
||||
ProviderName: providerName,
|
||||
|
||||
Price: priceInt64ToFloat64(checkout.TotalAmount),
|
||||
Currency: checkout.Currency,
|
||||
|
||||
OrderId: orderId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (pp *PolarPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (pp *PolarPaymentProvider) GetResponseError(err error) string {
|
||||
if err == nil {
|
||||
return "success"
|
||||
}
|
||||
return "fail"
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package radius
|
||||
|
||||
|
||||
@@ -199,7 +199,16 @@ func initAPI() {
|
||||
beego.Router("/api/update-product", &controllers.ApiController{}, "POST:UpdateProduct")
|
||||
beego.Router("/api/add-product", &controllers.ApiController{}, "POST:AddProduct")
|
||||
beego.Router("/api/delete-product", &controllers.ApiController{}, "POST:DeleteProduct")
|
||||
beego.Router("/api/buy-product", &controllers.ApiController{}, "POST:BuyProduct")
|
||||
|
||||
beego.Router("/api/get-orders", &controllers.ApiController{}, "GET:GetOrders")
|
||||
beego.Router("/api/get-user-orders", &controllers.ApiController{}, "GET:GetUserOrders")
|
||||
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/place-order", &controllers.ApiController{}, "POST:PlaceOrder")
|
||||
beego.Router("/api/cancel-order", &controllers.ApiController{}, "POST:CancelOrder")
|
||||
beego.Router("/api/pay-order", &controllers.ApiController{}, "POST:PayOrder")
|
||||
|
||||
beego.Router("/api/get-payments", &controllers.ApiController{}, "GET:GetPayments")
|
||||
beego.Router("/api/get-user-payments", &controllers.ApiController{}, "GET:GetUserPayments")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,60 @@ schemes:
|
||||
- https
|
||||
- http
|
||||
paths:
|
||||
/.well-known/{application}/jwks:
|
||||
get:
|
||||
tags:
|
||||
- OIDC API
|
||||
operationId: RootController.GetJwksByApplication
|
||||
parameters:
|
||||
- in: path
|
||||
name: application
|
||||
description: application name
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
schema:
|
||||
$ref: '#/definitions/jose.JSONWebKey'
|
||||
/.well-known/{application}/openid-configuration:
|
||||
get:
|
||||
tags:
|
||||
- OIDC API
|
||||
description: Get Oidc Discovery for specific application
|
||||
operationId: RootController.GetOidcDiscoveryByApplication
|
||||
parameters:
|
||||
- in: path
|
||||
name: application
|
||||
description: application name
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
schema:
|
||||
$ref: '#/definitions/object.OidcDiscovery'
|
||||
/.well-known/{application}/webfinger:
|
||||
get:
|
||||
tags:
|
||||
- OIDC API
|
||||
operationId: RootController.GetWebFingerByApplication
|
||||
parameters:
|
||||
- in: path
|
||||
name: application
|
||||
description: application name
|
||||
required: true
|
||||
type: string
|
||||
- in: query
|
||||
name: resource
|
||||
description: resource
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
schema:
|
||||
$ref: '#/definitions/object.WebFinger'
|
||||
/.well-known/jwks:
|
||||
get:
|
||||
tags:
|
||||
@@ -130,6 +184,24 @@ paths:
|
||||
description: ""
|
||||
schema:
|
||||
$ref: '#/definitions/object.Enforcer'
|
||||
/api/add-form:
|
||||
post:
|
||||
tags:
|
||||
- Form API
|
||||
description: add form
|
||||
operationId: ApiController.AddForm
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the form
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Form'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/add-group:
|
||||
post:
|
||||
tags:
|
||||
@@ -202,6 +274,24 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/add-order:
|
||||
post:
|
||||
tags:
|
||||
- Order API
|
||||
description: add order
|
||||
operationId: ApiController.AddOrder
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the order
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Order'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/add-organization:
|
||||
post:
|
||||
tags:
|
||||
@@ -495,6 +585,10 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Transaction'
|
||||
- in: query
|
||||
name: dryRun
|
||||
description: 'Dry run mode: set to ''true'' or ''1'' to validate without committing'
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
@@ -578,28 +672,6 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/buy-product:
|
||||
post:
|
||||
tags:
|
||||
- Product API
|
||||
description: buy product
|
||||
operationId: ApiController.BuyProduct
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id ( owner/name ) of the product
|
||||
required: true
|
||||
type: string
|
||||
- in: query
|
||||
name: providerName
|
||||
description: The name of the provider
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/check-user-password:
|
||||
post:
|
||||
tags:
|
||||
@@ -682,6 +754,24 @@ paths:
|
||||
description: ""
|
||||
schema:
|
||||
$ref: '#/definitions/object.Enforcer'
|
||||
/api/delete-form:
|
||||
post:
|
||||
tags:
|
||||
- Form API
|
||||
description: delete form
|
||||
operationId: ApiController.DeleteForm
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the form
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Form'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/delete-group:
|
||||
post:
|
||||
tags:
|
||||
@@ -765,6 +855,24 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/delete-order:
|
||||
post:
|
||||
tags:
|
||||
- Order API
|
||||
description: delete order
|
||||
operationId: ApiController.DeleteOrder
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the order
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Order'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/delete-organization:
|
||||
post:
|
||||
tags:
|
||||
@@ -1392,10 +1500,10 @@ paths:
|
||||
items:
|
||||
$ref: '#/definitions/object.Enforcer'
|
||||
/api/get-filtered-policies:
|
||||
get:
|
||||
post:
|
||||
tags:
|
||||
- Enforcer API
|
||||
description: get filtered policies
|
||||
description: get filtered policies with support for multiple filters via POST body
|
||||
operationId: ApiController.GetFilteredPolicies
|
||||
parameters:
|
||||
- in: query
|
||||
@@ -1403,19 +1511,14 @@ paths:
|
||||
description: The id ( owner/name ) of enforcer
|
||||
required: true
|
||||
type: string
|
||||
- in: query
|
||||
name: ptype
|
||||
description: Policy type, default is 'p'
|
||||
type: string
|
||||
- in: query
|
||||
name: fieldIndex
|
||||
description: Field index for filtering
|
||||
type: integer
|
||||
format: int64
|
||||
- in: query
|
||||
name: fieldValues
|
||||
description: Field values for filtering, comma-separated
|
||||
type: string
|
||||
- in: body
|
||||
name: body
|
||||
description: Array of filter objects for multiple filters
|
||||
required: true
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Filter'
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
@@ -1423,6 +1526,42 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/xormadapter.CasbinRule'
|
||||
/api/get-form:
|
||||
get:
|
||||
tags:
|
||||
- Form API
|
||||
description: get form
|
||||
operationId: ApiController.GetForm
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id (owner/name) of form
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/object.Form'
|
||||
/api/get-forms:
|
||||
get:
|
||||
tags:
|
||||
- Form API
|
||||
description: get forms
|
||||
operationId: ApiController.GetForms
|
||||
parameters:
|
||||
- in: query
|
||||
name: owner
|
||||
description: The owner of form
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Form'
|
||||
/api/get-global-certs:
|
||||
get:
|
||||
tags:
|
||||
@@ -1436,6 +1575,19 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Cert'
|
||||
/api/get-global-forms:
|
||||
get:
|
||||
tags:
|
||||
- Form API
|
||||
description: get global forms
|
||||
operationId: ApiController.GetGlobalForms
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Form'
|
||||
/api/get-global-providers:
|
||||
get:
|
||||
tags:
|
||||
@@ -1633,6 +1785,42 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Model'
|
||||
/api/get-order:
|
||||
get:
|
||||
tags:
|
||||
- Order API
|
||||
description: get order
|
||||
operationId: ApiController.GetOrder
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id ( owner/name ) of the order
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/object.Order'
|
||||
/api/get-orders:
|
||||
get:
|
||||
tags:
|
||||
- Order API
|
||||
description: get orders
|
||||
operationId: ApiController.GetOrders
|
||||
parameters:
|
||||
- in: query
|
||||
name: owner
|
||||
description: The owner of orders
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Order'
|
||||
/api/get-organization:
|
||||
get:
|
||||
tags:
|
||||
@@ -1710,9 +1898,9 @@ paths:
|
||||
/api/get-payment:
|
||||
get:
|
||||
tags:
|
||||
- Verification API
|
||||
- Payment API
|
||||
description: get payment
|
||||
operationId: ApiController.GetVerification
|
||||
operationId: ApiController.GetPayment
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
@@ -1723,13 +1911,13 @@ paths:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/object.Verification'
|
||||
$ref: '#/definitions/object.Payment'
|
||||
/api/get-payments:
|
||||
get:
|
||||
tags:
|
||||
- Verification API
|
||||
- Payment API
|
||||
description: get payments
|
||||
operationId: ApiController.GetVerifications
|
||||
operationId: ApiController.GetPayments
|
||||
parameters:
|
||||
- in: query
|
||||
name: owner
|
||||
@@ -1742,7 +1930,7 @@ paths:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Verification'
|
||||
$ref: '#/definitions/object.Payment'
|
||||
/api/get-permission:
|
||||
get:
|
||||
tags:
|
||||
@@ -2450,12 +2638,36 @@ paths:
|
||||
responses:
|
||||
"200":
|
||||
description: '{int} int The count of filtered users for an organization'
|
||||
/api/get-user-orders:
|
||||
get:
|
||||
tags:
|
||||
- Order API
|
||||
description: get orders for a user
|
||||
operationId: ApiController.GetUserOrders
|
||||
parameters:
|
||||
- in: query
|
||||
name: owner
|
||||
description: The owner of orders
|
||||
required: true
|
||||
type: string
|
||||
- in: query
|
||||
name: user
|
||||
description: The username of the user
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Order'
|
||||
/api/get-user-payments:
|
||||
get:
|
||||
tags:
|
||||
- Verification API
|
||||
- Payment API
|
||||
description: get payments for a user
|
||||
operationId: ApiController.GetUserVerifications
|
||||
operationId: ApiController.GetUserPayments
|
||||
parameters:
|
||||
- in: query
|
||||
name: owner
|
||||
@@ -2478,36 +2690,7 @@ paths:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Verification'
|
||||
/api/get-user-transactions:
|
||||
get:
|
||||
tags:
|
||||
- Transaction API
|
||||
description: get transactions for a user
|
||||
operationId: ApiController.GetUserTransaction
|
||||
parameters:
|
||||
- in: query
|
||||
name: owner
|
||||
description: The owner of transactions
|
||||
required: true
|
||||
type: string
|
||||
- in: query
|
||||
name: organization
|
||||
description: The organization of the user
|
||||
required: true
|
||||
type: string
|
||||
- in: query
|
||||
name: user
|
||||
description: The username of the user
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.Transaction'
|
||||
$ref: '#/definitions/object.Payment'
|
||||
/api/get-users:
|
||||
get:
|
||||
tags:
|
||||
@@ -2836,6 +3019,15 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/metrics:
|
||||
get:
|
||||
tags:
|
||||
- System API
|
||||
description: get Prometheus metrics
|
||||
operationId: ApiController.GetMetrics
|
||||
responses:
|
||||
"200":
|
||||
description: '{string} Prometheus metrics in text format'
|
||||
/api/mfa/setup/enable:
|
||||
post:
|
||||
tags:
|
||||
@@ -2999,6 +3191,31 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/send-invitation:
|
||||
post:
|
||||
tags:
|
||||
- Invitation API
|
||||
description: verify invitation
|
||||
operationId: ApiController.VerifyInvitation
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id ( owner/name ) of the invitation
|
||||
required: true
|
||||
type: string
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the invitation
|
||||
required: true
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/send-notification:
|
||||
post:
|
||||
tags:
|
||||
@@ -3120,6 +3337,27 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/sso-logout:
|
||||
get:
|
||||
tags:
|
||||
- Login API
|
||||
description: logout the current user from all applications
|
||||
operationId: ApiController.SsoLogout
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
post:
|
||||
tags:
|
||||
- Login API
|
||||
description: logout the current user from all applications
|
||||
operationId: ApiController.SsoLogout
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/sync-ldap-users:
|
||||
post:
|
||||
tags:
|
||||
@@ -3239,6 +3477,29 @@ paths:
|
||||
description: ""
|
||||
schema:
|
||||
$ref: '#/definitions/object.Enforcer'
|
||||
/api/update-form:
|
||||
post:
|
||||
tags:
|
||||
- Form API
|
||||
description: update form
|
||||
operationId: ApiController.UpdateForm
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id (owner/name) of the form
|
||||
required: true
|
||||
type: string
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the form
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Form'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/update-group:
|
||||
post:
|
||||
tags:
|
||||
@@ -3326,6 +3587,29 @@ paths:
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/update-order:
|
||||
post:
|
||||
tags:
|
||||
- Order API
|
||||
description: update order
|
||||
operationId: ApiController.UpdateOrder
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id ( owner/name ) of the order
|
||||
required: true
|
||||
type: string
|
||||
- in: body
|
||||
name: body
|
||||
description: The details of the order
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/object.Order'
|
||||
responses:
|
||||
"200":
|
||||
description: The Response object
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
/api/update-organization:
|
||||
post:
|
||||
tags:
|
||||
@@ -3679,7 +3963,14 @@ paths:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id ( owner/name ) of the user
|
||||
required: true
|
||||
type: string
|
||||
- in: query
|
||||
name: userId
|
||||
description: The userId (UUID) of the user
|
||||
type: string
|
||||
- in: query
|
||||
name: owner
|
||||
description: The owner of the user (required when using userId)
|
||||
type: string
|
||||
- in: body
|
||||
name: body
|
||||
@@ -3907,10 +4198,10 @@ paths:
|
||||
schema:
|
||||
$ref: '#/definitions/controllers.Response'
|
||||
definitions:
|
||||
187812.<nil>.string:
|
||||
217289.<nil>.string:
|
||||
title: string
|
||||
type: object
|
||||
187870.string.string:
|
||||
217347.string.string:
|
||||
title: string
|
||||
type: object
|
||||
Response:
|
||||
@@ -4091,18 +4382,25 @@ definitions:
|
||||
type: string
|
||||
clientSecret:
|
||||
type: string
|
||||
codeResendTimeout:
|
||||
type: integer
|
||||
format: int64
|
||||
createdTime:
|
||||
type: string
|
||||
defaultGroup:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
disableSignin:
|
||||
type: boolean
|
||||
displayName:
|
||||
type: string
|
||||
enableAutoSignin:
|
||||
type: boolean
|
||||
enableCodeSignin:
|
||||
type: boolean
|
||||
enableExclusiveSignin:
|
||||
type: boolean
|
||||
enableLinkWithEmail:
|
||||
type: boolean
|
||||
enablePassword:
|
||||
@@ -4120,14 +4418,16 @@ definitions:
|
||||
enableWebAuthn:
|
||||
type: boolean
|
||||
expireInHours:
|
||||
type: integer
|
||||
format: int64
|
||||
type: number
|
||||
format: double
|
||||
failedSigninFrozenTime:
|
||||
type: integer
|
||||
format: int64
|
||||
failedSigninLimit:
|
||||
type: integer
|
||||
format: int64
|
||||
favicon:
|
||||
type: string
|
||||
footerHtml:
|
||||
type: string
|
||||
forcedRedirectOrigin:
|
||||
@@ -4165,6 +4465,9 @@ definitions:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
order:
|
||||
type: integer
|
||||
format: int64
|
||||
orgChoiceMode:
|
||||
type: string
|
||||
organization:
|
||||
@@ -4182,12 +4485,14 @@ definitions:
|
||||
items:
|
||||
type: string
|
||||
refreshExpireInHours:
|
||||
type: integer
|
||||
format: int64
|
||||
type: number
|
||||
format: double
|
||||
samlAttributes:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.SamlItem'
|
||||
samlHashAlgorithm:
|
||||
type: string
|
||||
samlReplyUrl:
|
||||
type: string
|
||||
signinHtml:
|
||||
@@ -4218,6 +4523,12 @@ definitions:
|
||||
type: string
|
||||
themeData:
|
||||
$ref: '#/definitions/object.ThemeData'
|
||||
title:
|
||||
type: string
|
||||
tokenAttributes:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.JwtItem'
|
||||
tokenFields:
|
||||
type: array
|
||||
items:
|
||||
@@ -4308,6 +4619,51 @@ definitions:
|
||||
format: double
|
||||
name:
|
||||
type: string
|
||||
object.Filter:
|
||||
title: Filter
|
||||
type: object
|
||||
properties:
|
||||
fieldIndex:
|
||||
type: integer
|
||||
format: int64
|
||||
fieldValues:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
ptype:
|
||||
type: string
|
||||
object.Form:
|
||||
title: Form
|
||||
type: object
|
||||
properties:
|
||||
createdTime:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
formItems:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.FormItem'
|
||||
name:
|
||||
type: string
|
||||
owner:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
object.FormItem:
|
||||
title: FormItem
|
||||
type: object
|
||||
properties:
|
||||
label:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
visible:
|
||||
type: boolean
|
||||
width:
|
||||
type: string
|
||||
object.GaugeVecInfo:
|
||||
title: GaugeVecInfo
|
||||
type: object
|
||||
@@ -4453,6 +4809,14 @@ definitions:
|
||||
format: int64
|
||||
username:
|
||||
type: string
|
||||
object.JwtItem:
|
||||
title: JwtItem
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
object.Ldap:
|
||||
title: Ldap
|
||||
type: object
|
||||
@@ -4466,6 +4830,9 @@ definitions:
|
||||
type: string
|
||||
createdTime:
|
||||
type: string
|
||||
customAttributes:
|
||||
additionalProperties:
|
||||
type: string
|
||||
defaultGroup:
|
||||
type: string
|
||||
enableSsl:
|
||||
@@ -4513,8 +4880,15 @@ definitions:
|
||||
type: string
|
||||
address:
|
||||
type: string
|
||||
attributes:
|
||||
additionalProperties:
|
||||
type: string
|
||||
cn:
|
||||
type: string
|
||||
country:
|
||||
type: string
|
||||
countryName:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
email:
|
||||
@@ -4660,6 +5034,32 @@ definitions:
|
||||
type: string
|
||||
userinfo_endpoint:
|
||||
type: string
|
||||
object.Order:
|
||||
title: Order
|
||||
type: object
|
||||
properties:
|
||||
createdTime:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
endTime:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
owner:
|
||||
type: string
|
||||
payment:
|
||||
type: string
|
||||
productName:
|
||||
type: string
|
||||
startTime:
|
||||
type: string
|
||||
state:
|
||||
type: string
|
||||
user:
|
||||
type: string
|
||||
object.Organization:
|
||||
title: Organization
|
||||
type: object
|
||||
@@ -4668,6 +5068,11 @@ definitions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.AccountItem'
|
||||
balanceCredit:
|
||||
type: number
|
||||
format: double
|
||||
balanceCurrency:
|
||||
type: string
|
||||
countryCodes:
|
||||
type: array
|
||||
items:
|
||||
@@ -4680,6 +5085,8 @@ definitions:
|
||||
type: string
|
||||
defaultPassword:
|
||||
type: string
|
||||
disableSignin:
|
||||
type: boolean
|
||||
displayName:
|
||||
type: string
|
||||
enableSoftDeletion:
|
||||
@@ -4724,6 +5131,9 @@ definitions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
orgBalance:
|
||||
type: number
|
||||
format: double
|
||||
owner:
|
||||
type: string
|
||||
passwordExpireDays:
|
||||
@@ -4749,6 +5159,13 @@ definitions:
|
||||
$ref: '#/definitions/object.ThemeData'
|
||||
useEmailAsUsername:
|
||||
type: boolean
|
||||
userBalance:
|
||||
type: number
|
||||
format: double
|
||||
userNavItems:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
userTypes:
|
||||
type: array
|
||||
items:
|
||||
@@ -5054,6 +5471,8 @@ definitions:
|
||||
type: string
|
||||
emailRegex:
|
||||
type: string
|
||||
enableProxy:
|
||||
type: boolean
|
||||
enableSignAuthnRequest:
|
||||
type: boolean
|
||||
endpoint:
|
||||
@@ -5478,26 +5897,22 @@ definitions:
|
||||
type: string
|
||||
currency:
|
||||
type: string
|
||||
detail:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
domain:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
owner:
|
||||
type: string
|
||||
payment:
|
||||
type: string
|
||||
productDisplayName:
|
||||
type: string
|
||||
productName:
|
||||
type: string
|
||||
provider:
|
||||
type: string
|
||||
returnUrl:
|
||||
type: string
|
||||
state:
|
||||
$ref: '#/definitions/pp.PaymentState'
|
||||
subtype:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
type:
|
||||
@@ -5543,6 +5958,11 @@ definitions:
|
||||
balance:
|
||||
type: number
|
||||
format: double
|
||||
balanceCredit:
|
||||
type: number
|
||||
format: double
|
||||
balanceCurrency:
|
||||
type: string
|
||||
battlenet:
|
||||
type: string
|
||||
bilibili:
|
||||
@@ -5569,6 +5989,24 @@ definitions:
|
||||
type: string
|
||||
custom:
|
||||
type: string
|
||||
custom2:
|
||||
type: string
|
||||
custom3:
|
||||
type: string
|
||||
custom4:
|
||||
type: string
|
||||
custom5:
|
||||
type: string
|
||||
custom6:
|
||||
type: string
|
||||
custom7:
|
||||
type: string
|
||||
custom8:
|
||||
type: string
|
||||
custom9:
|
||||
type: string
|
||||
custom10:
|
||||
type: string
|
||||
dailymotion:
|
||||
type: string
|
||||
deezer:
|
||||
@@ -5712,6 +6150,18 @@ definitions:
|
||||
$ref: '#/definitions/object.MfaItem'
|
||||
mfaPhoneEnabled:
|
||||
type: boolean
|
||||
mfaPushEnabled:
|
||||
type: boolean
|
||||
mfaPushProvider:
|
||||
type: string
|
||||
mfaPushReceiver:
|
||||
type: string
|
||||
mfaRadiusEnabled:
|
||||
type: boolean
|
||||
mfaRadiusProvider:
|
||||
type: string
|
||||
mfaRadiusUsername:
|
||||
type: string
|
||||
mfaRememberDeadline:
|
||||
type: string
|
||||
microsoftonline:
|
||||
@@ -5732,6 +6182,8 @@ definitions:
|
||||
type: string
|
||||
onedrive:
|
||||
type: string
|
||||
originalToken:
|
||||
type: string
|
||||
oura:
|
||||
type: string
|
||||
owner:
|
||||
@@ -5772,6 +6224,10 @@ definitions:
|
||||
type: string
|
||||
region:
|
||||
type: string
|
||||
registerSource:
|
||||
type: string
|
||||
registerType:
|
||||
type: string
|
||||
roles:
|
||||
type: array
|
||||
items:
|
||||
@@ -5892,13 +6348,13 @@ definitions:
|
||||
type: object
|
||||
properties:
|
||||
aliases:
|
||||
$ref: '#/definitions/187812.<nil>.string'
|
||||
$ref: '#/definitions/217289.<nil>.string'
|
||||
links:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/object.WebFingerLink'
|
||||
properties:
|
||||
$ref: '#/definitions/187870.string.string'
|
||||
$ref: '#/definitions/217347.string.string'
|
||||
subject:
|
||||
type: string
|
||||
object.WebFingerLink:
|
||||
@@ -5974,6 +6430,9 @@ definitions:
|
||||
sql.DB:
|
||||
title: DB
|
||||
type: object
|
||||
ssh.Client:
|
||||
title: Client
|
||||
type: object
|
||||
util.SystemInfo:
|
||||
title: SystemInfo
|
||||
type: object
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package sync
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package sync_v2
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package sync_v2
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ func GetUploadXlsxPath(fileId string) string {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
return file.Name()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package util
|
||||
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
"react-metamask-avatar": "^1.2.1",
|
||||
"react-router-dom": "^5.3.3",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-social-login-buttons": "^3.4.0"
|
||||
"react-social-login-buttons": "^3.4.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env PORT=7001 craco start",
|
||||
|
||||
@@ -129,7 +129,7 @@ class AdapterListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
220
web/src/App.js
220
web/src/App.js
@@ -38,8 +38,68 @@ import {setTwoToneColor} from "@ant-design/icons";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
import * as Cookie from "cookie";
|
||||
|
||||
// Ant Design locale imports
|
||||
import enUS from "antd/locale/en_US";
|
||||
import zhCN from "antd/locale/zh_CN";
|
||||
import zhTW from "antd/locale/zh_TW";
|
||||
import esES from "antd/locale/es_ES";
|
||||
import frFR from "antd/locale/fr_FR";
|
||||
import deDE from "antd/locale/de_DE";
|
||||
import idID from "antd/locale/id_ID";
|
||||
import jaJP from "antd/locale/ja_JP";
|
||||
import koKR from "antd/locale/ko_KR";
|
||||
import ruRU from "antd/locale/ru_RU";
|
||||
import viVN from "antd/locale/vi_VN";
|
||||
import ptBR from "antd/locale/pt_BR";
|
||||
import itIT from "antd/locale/it_IT";
|
||||
import msMY from "antd/locale/ms_MY";
|
||||
import trTR from "antd/locale/tr_TR";
|
||||
import arEG from "antd/locale/ar_EG";
|
||||
import heIL from "antd/locale/he_IL";
|
||||
import nlNL from "antd/locale/nl_NL";
|
||||
import plPL from "antd/locale/pl_PL";
|
||||
import fiFI from "antd/locale/fi_FI";
|
||||
import svSE from "antd/locale/sv_SE";
|
||||
import ukUA from "antd/locale/uk_UA";
|
||||
import faIR from "antd/locale/fa_IR";
|
||||
import csCZ from "antd/locale/cs_CZ";
|
||||
import skSK from "antd/locale/sk_SK";
|
||||
|
||||
setTwoToneColor("rgb(87,52,211)");
|
||||
|
||||
function getAntdLocale(language) {
|
||||
const localeMap = {
|
||||
"en": enUS,
|
||||
"zh": zhCN,
|
||||
"zh-tw": zhTW,
|
||||
"es": esES,
|
||||
"fr": frFR,
|
||||
"de": deDE,
|
||||
"id": idID,
|
||||
"ja": jaJP,
|
||||
"ko": koKR,
|
||||
"ru": ruRU,
|
||||
"vi": viVN,
|
||||
"pt": ptBR,
|
||||
"it": itIT,
|
||||
"ms": msMY,
|
||||
"tr": trTR,
|
||||
"ar": arEG,
|
||||
"he": heIL,
|
||||
"nl": nlNL,
|
||||
"pl": plPL,
|
||||
"fi": fiFI,
|
||||
"sv": svSE,
|
||||
"uk": ukUA,
|
||||
"fa": faIR,
|
||||
"cs": csCZ,
|
||||
"sk": skSK,
|
||||
"kk": ruRU, // Use Russian for Kazakh as antd doesn't have Kazakh
|
||||
"az": trTR, // Use Turkish for Azerbaijani as they're similar
|
||||
};
|
||||
return localeMap[language] || enUS;
|
||||
}
|
||||
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -98,11 +158,133 @@ class App extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
shouldFlattenMenu() {
|
||||
const organization = this.state.account?.organization;
|
||||
const navItems = Setting.isLocalAdminUser(this.state.account) ? organization?.navItems : (organization?.userNavItems ?? []);
|
||||
|
||||
// If navItems is "all" or not configured, don't flatten
|
||||
if (!Array.isArray(navItems) || navItems?.includes("all")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Count how many valid menu items would be visible
|
||||
// Filter out any invalid or non-existent menu items
|
||||
const validMenuItems = [
|
||||
"/", "/shortcuts", "/apps", // Home group
|
||||
"/organizations", "/groups", "/users", "/invitations", // User Management
|
||||
"/applications", "/providers", "/resources", "/certs", // Identity
|
||||
"/roles", "/permissions", "/models", "/adapters", "/enforcers", // Authorization
|
||||
"/sessions", "/records", "/tokens", "/verifications", // Logging & Auditing
|
||||
"/products", "/orders", "/payments", "/plans", "/pricings", "/subscriptions", "/transactions", // Business
|
||||
"/sysinfo", "/forms", "/syncers", "/webhooks", "/swagger", // Admin
|
||||
];
|
||||
|
||||
const count = navItems.filter(item => validMenuItems.includes(item)).length;
|
||||
return count <= Conf.MaxItemsForFlatMenu;
|
||||
}
|
||||
|
||||
getSelectedMenuKeyForFlatMenu(uri) {
|
||||
// For flattened menu, return the actual child path instead of parent group
|
||||
if (uri === "/" || uri.includes("/shortcuts") || uri.includes("/apps")) {
|
||||
if (uri === "/") {
|
||||
return "/";
|
||||
} else if (uri.includes("/shortcuts")) {
|
||||
return "/shortcuts";
|
||||
} else if (uri.includes("/apps")) {
|
||||
return "/apps";
|
||||
}
|
||||
} else if (uri.includes("/organizations") || uri.includes("/trees") || uri.includes("/groups") || uri.includes("/users") || uri.includes("/invitations")) {
|
||||
if (uri.includes("/organizations")) {
|
||||
return "/organizations";
|
||||
} else if (uri.includes("/groups")) {
|
||||
return "/groups";
|
||||
} else if (uri.includes("/users")) {
|
||||
return "/users";
|
||||
} else if (uri.includes("/invitations")) {
|
||||
return "/invitations";
|
||||
}
|
||||
} else if (uri.includes("/applications") || uri.includes("/providers") || uri.includes("/resources") || uri.includes("/certs")) {
|
||||
if (uri.includes("/applications")) {
|
||||
return "/applications";
|
||||
} else if (uri.includes("/providers")) {
|
||||
return "/providers";
|
||||
} else if (uri.includes("/resources")) {
|
||||
return "/resources";
|
||||
} else if (uri.includes("/certs")) {
|
||||
return "/certs";
|
||||
}
|
||||
} else if (uri.includes("/roles") || uri.includes("/permissions") || uri.includes("/models") || uri.includes("/adapters") || uri.includes("/enforcers")) {
|
||||
if (uri.includes("/roles")) {
|
||||
return "/roles";
|
||||
} else if (uri.includes("/permissions")) {
|
||||
return "/permissions";
|
||||
} else if (uri.includes("/models")) {
|
||||
return "/models";
|
||||
} else if (uri.includes("/adapters")) {
|
||||
return "/adapters";
|
||||
} else if (uri.includes("/enforcers")) {
|
||||
return "/enforcers";
|
||||
}
|
||||
} else if (uri.includes("/records") || uri.includes("/tokens") || uri.includes("/sessions") || uri.includes("/verifications")) {
|
||||
if (uri.includes("/sessions")) {
|
||||
return "/sessions";
|
||||
} else if (uri.includes("/records")) {
|
||||
return "/records";
|
||||
} else if (uri.includes("/tokens")) {
|
||||
return "/tokens";
|
||||
} else if (uri.includes("/verifications")) {
|
||||
return "/verifications";
|
||||
}
|
||||
} else if (uri.includes("/products") || uri.includes("/orders") || uri.includes("/payments") || uri.includes("/plans") || uri.includes("/pricings") || uri.includes("/subscriptions") || uri.includes("/transactions")) {
|
||||
if (uri.includes("/products")) {
|
||||
return "/products";
|
||||
} else if (uri.includes("/orders")) {
|
||||
return "/orders";
|
||||
} else if (uri.includes("/payments")) {
|
||||
return "/payments";
|
||||
} else if (uri.includes("/plans")) {
|
||||
return "/plans";
|
||||
} else if (uri.includes("/pricings")) {
|
||||
return "/pricings";
|
||||
} else if (uri.includes("/subscriptions")) {
|
||||
return "/subscriptions";
|
||||
} else if (uri.includes("/transactions")) {
|
||||
return "/transactions";
|
||||
}
|
||||
} else if (uri.includes("/sysinfo") || uri.includes("/forms") || uri.includes("/syncers") || uri.includes("/webhooks")) {
|
||||
if (uri.includes("/sysinfo")) {
|
||||
return "/sysinfo";
|
||||
} else if (uri.includes("/forms")) {
|
||||
return "/forms";
|
||||
} else if (uri.includes("/syncers")) {
|
||||
return "/syncers";
|
||||
} else if (uri.includes("/webhooks")) {
|
||||
return "/webhooks";
|
||||
}
|
||||
} else if (uri.includes("/signup")) {
|
||||
return "/signup";
|
||||
} else if (uri.includes("/login")) {
|
||||
return "/login";
|
||||
} else if (uri.includes("/result")) {
|
||||
return "/result";
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
updateMenuKey() {
|
||||
const uri = location.pathname;
|
||||
this.setState({
|
||||
uri: uri,
|
||||
});
|
||||
|
||||
// Check if menu should be flattened and use appropriate key selection
|
||||
if (this.shouldFlattenMenu()) {
|
||||
const selectedKey = this.getSelectedMenuKeyForFlatMenu(uri);
|
||||
this.setState({selectedMenuKey: selectedKey});
|
||||
return;
|
||||
}
|
||||
|
||||
// Original logic for grouped menu
|
||||
if (uri === "/" || uri.includes("/shortcuts") || uri.includes("/apps")) {
|
||||
this.setState({selectedMenuKey: "/home"});
|
||||
} else if (uri.includes("/organizations") || uri.includes("/trees") || uri.includes("/groups") || uri.includes("/users") || uri.includes("/invitations")) {
|
||||
@@ -111,9 +293,9 @@ class App extends Component {
|
||||
this.setState({selectedMenuKey: "/identity"});
|
||||
} else if (uri.includes("/roles") || uri.includes("/permissions") || uri.includes("/models") || uri.includes("/adapters") || uri.includes("/enforcers")) {
|
||||
this.setState({selectedMenuKey: "/auth"});
|
||||
} else if (uri.includes("/records") || uri.includes("/tokens") || uri.includes("/sessions")) {
|
||||
} else if (uri.includes("/records") || uri.includes("/tokens") || uri.includes("/sessions") || uri.includes("/verifications")) {
|
||||
this.setState({selectedMenuKey: "/logs"});
|
||||
} else if (uri.includes("/products") || uri.includes("/payments") || uri.includes("/plans") || uri.includes("/pricings") || uri.includes("/subscriptions")) {
|
||||
} else if (uri.includes("/product-store") || uri.includes("/products") || uri.includes("/orders") || uri.includes("/payments") || uri.includes("/plans") || uri.includes("/pricings") || uri.includes("/subscriptions") || uri.includes("/transactions")) {
|
||||
this.setState({selectedMenuKey: "/business"});
|
||||
} else if (uri.includes("/sysinfo") || uri.includes("/forms") || uri.includes("/syncers") || uri.includes("/webhooks")) {
|
||||
this.setState({selectedMenuKey: "/admin"});
|
||||
@@ -390,13 +572,15 @@ class App extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigProvider theme={{
|
||||
token: {
|
||||
colorPrimary: themeData.colorPrimary,
|
||||
borderRadius: themeData.borderRadius,
|
||||
},
|
||||
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
|
||||
}}>
|
||||
<ConfigProvider
|
||||
locale={getAntdLocale(Setting.getLanguage())}
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: themeData.colorPrimary,
|
||||
borderRadius: themeData.borderRadius,
|
||||
},
|
||||
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
|
||||
}}>
|
||||
<StyleProvider hashPriority="high" transformers={[legacyLogicalPropertiesTransformer]}>
|
||||
<Layout id="parent-area">
|
||||
<Content style={{display: "flex", justifyContent: "center"}}>
|
||||
@@ -529,14 +713,16 @@ class App extends Component {
|
||||
<link rel="icon" href={this.state.account.organization?.favicon} />
|
||||
</Helmet>
|
||||
}
|
||||
<ConfigProvider theme={{
|
||||
token: {
|
||||
colorPrimary: this.state.themeData.colorPrimary,
|
||||
colorInfo: this.state.themeData.colorPrimary,
|
||||
borderRadius: this.state.themeData.borderRadius,
|
||||
},
|
||||
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
|
||||
}}>
|
||||
<ConfigProvider
|
||||
locale={getAntdLocale(Setting.getLanguage())}
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: this.state.themeData.colorPrimary,
|
||||
colorInfo: this.state.themeData.colorPrimary,
|
||||
borderRadius: this.state.themeData.borderRadius,
|
||||
},
|
||||
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
|
||||
}}>
|
||||
<StyleProvider hashPriority="high" transformers={[legacyLogicalPropertiesTransformer]}>
|
||||
{
|
||||
this.renderPage()
|
||||
|
||||
@@ -180,7 +180,7 @@ class ApplicationEditPage extends React.Component {
|
||||
}
|
||||
|
||||
getGroups() {
|
||||
GroupBackend.getGroups(this.state.organizationName)
|
||||
GroupBackend.getGroups(this.state.owner)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
|
||||
@@ -114,7 +114,7 @@ class BaseListPage extends React.Component {
|
||||
ref={node => {
|
||||
this.searchInput = node;
|
||||
}}
|
||||
placeholder={`Search ${dataIndex}`}
|
||||
placeholder={i18next.t("general:Please input your search")}
|
||||
value={selectedKeys[0]}
|
||||
onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])}
|
||||
onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
|
||||
@@ -129,10 +129,10 @@ class BaseListPage extends React.Component {
|
||||
size="small"
|
||||
style={{width: 90}}
|
||||
>
|
||||
Search
|
||||
{i18next.t("general:Search")}
|
||||
</Button>
|
||||
<Button onClick={() => this.handleReset(clearFilters)} size="small" style={{width: 90}}>
|
||||
Reset
|
||||
{i18next.t("general:Reset")}
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
@@ -145,7 +145,7 @@ class BaseListPage extends React.Component {
|
||||
});
|
||||
}}
|
||||
>
|
||||
Filter
|
||||
{i18next.t("general:Filter")}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
@@ -34,3 +34,6 @@ export const CustomFooter = null;
|
||||
|
||||
// Blank or null to hide Ai Assistant button
|
||||
export const AiAssistantUrl = "https://ai.casbin.com";
|
||||
|
||||
// Maximum number of navbar items before switching from flat to grouped menu
|
||||
export const MaxItemsForFlatMenu = 7;
|
||||
|
||||
@@ -61,8 +61,12 @@ import SessionListPage from "./SessionListPage";
|
||||
import TokenListPage from "./TokenListPage";
|
||||
import TokenEditPage from "./TokenEditPage";
|
||||
import ProductListPage from "./ProductListPage";
|
||||
import ProductStorePage from "./ProductStorePage";
|
||||
import ProductEditPage from "./ProductEditPage";
|
||||
import ProductBuyPage from "./ProductBuyPage";
|
||||
import OrderListPage from "./OrderListPage";
|
||||
import OrderEditPage from "./OrderEditPage";
|
||||
import OrderPayPage from "./OrderPayPage";
|
||||
import PaymentListPage from "./PaymentListPage";
|
||||
import PaymentEditPage from "./PaymentEditPage";
|
||||
import PaymentResultPage from "./PaymentResultPage";
|
||||
@@ -98,8 +102,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()
|
||||
@@ -186,6 +191,10 @@ function ManagementPage(props) {
|
||||
return !Array.isArray(widgetItems) || !!widgetItems?.includes("all");
|
||||
}
|
||||
|
||||
function isSpecialMenuItem(item) {
|
||||
return item.key === "#" || item.key === "logo";
|
||||
}
|
||||
|
||||
function renderWidgets() {
|
||||
const widgets = [
|
||||
Setting.getItem(<ThemeSelect themeAlgorithm={props.themeAlgorithm} onChange={props.setLogoAndThemeAlgorithm} />, "theme"),
|
||||
@@ -271,77 +280,75 @@ 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="/product-store">{i18next.t("general:Product Store")}</Link>, "/product-store"),
|
||||
Setting.getItem(<Link to="/products">{i18next.t("general:Products")}</Link>, "/products"),
|
||||
Setting.getItem(<Link to="/orders">{i18next.t("general:Orders")}</Link>, "/orders"),
|
||||
Setting.getItem(<Link to="/payments">{i18next.t("general:Payments")}</Link>, "/payments"),
|
||||
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()) {
|
||||
@@ -363,10 +370,36 @@ function ManagementPage(props) {
|
||||
return item;
|
||||
});
|
||||
|
||||
return resFiltered.filter(item => {
|
||||
if (item.key === "#" || item.key === "logo") {return true;}
|
||||
const filteredResult = resFiltered.filter(item => {
|
||||
if (isSpecialMenuItem(item)) {return true;}
|
||||
return Array.isArray(item.children) && item.children.length > 0;
|
||||
});
|
||||
|
||||
// Count total end items (leaf nodes)
|
||||
let totalEndItems = 0;
|
||||
filteredResult.forEach(item => {
|
||||
if (Array.isArray(item.children)) {
|
||||
totalEndItems += item.children.length;
|
||||
}
|
||||
});
|
||||
|
||||
// If total end items <= MaxItemsForFlatMenu, flatten the menu (show only one level)
|
||||
if (totalEndItems <= Conf.MaxItemsForFlatMenu) {
|
||||
const flattenedResult = [];
|
||||
filteredResult.forEach(item => {
|
||||
if (isSpecialMenuItem(item)) {
|
||||
flattenedResult.push(item);
|
||||
} else if (Array.isArray(item.children)) {
|
||||
// Add children directly without parent group
|
||||
item.children.forEach(child => {
|
||||
flattenedResult.push(child);
|
||||
});
|
||||
}
|
||||
});
|
||||
return flattenedResult;
|
||||
}
|
||||
|
||||
return filteredResult;
|
||||
}
|
||||
|
||||
function renderLoginIfNotLoggedIn(component) {
|
||||
@@ -425,9 +458,13 @@ function ManagementPage(props) {
|
||||
<Route exact path="/sessions" render={(props) => renderLoginIfNotLoggedIn(<SessionListPage account={account} {...props} />)} />
|
||||
<Route exact path="/tokens" render={(props) => renderLoginIfNotLoggedIn(<TokenListPage account={account} {...props} />)} />
|
||||
<Route exact path="/tokens/:tokenName" render={(props) => renderLoginIfNotLoggedIn(<TokenEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/product-store" render={(props) => renderLoginIfNotLoggedIn(<ProductStorePage account={account} {...props} />)} />
|
||||
<Route exact path="/products" render={(props) => renderLoginIfNotLoggedIn(<ProductListPage account={account} {...props} />)} />
|
||||
<Route exact path="/products/:organizationName/:productName" render={(props) => renderLoginIfNotLoggedIn(<ProductEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/products/:organizationName/:productName/buy" render={(props) => renderLoginIfNotLoggedIn(<ProductBuyPage account={account} {...props} />)} />
|
||||
<Route exact path="/orders" render={(props) => renderLoginIfNotLoggedIn(<OrderListPage account={account} {...props} />)} />
|
||||
<Route exact path="/orders/:organizationName/:orderName" render={(props) => renderLoginIfNotLoggedIn(<OrderEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/orders/:organizationName/:orderName/pay" render={(props) => renderLoginIfNotLoggedIn(<OrderPayPage account={account} {...props} />)} />
|
||||
<Route exact path="/payments" render={(props) => renderLoginIfNotLoggedIn(<PaymentListPage account={account} {...props} />)} />
|
||||
<Route exact path="/payments/:organizationName/:paymentName" render={(props) => renderLoginIfNotLoggedIn(<PaymentEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/payments/:organizationName/:paymentName/result" render={(props) => renderLoginIfNotLoggedIn(<PaymentResultPage account={account} {...props} />)} />
|
||||
|
||||
307
web/src/OrderEditPage.js
Normal file
307
web/src/OrderEditPage.js
Normal file
@@ -0,0 +1,307 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, Row, Select} from "antd";
|
||||
import * as OrderBackend from "./backend/OrderBackend";
|
||||
import * as ProductBackend from "./backend/ProductBackend";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import * as PaymentBackend from "./backend/PaymentBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
class OrderEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||
orderName: props.match.params.orderName,
|
||||
order: null,
|
||||
products: [],
|
||||
users: [],
|
||||
payments: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getOrder();
|
||||
this.getProducts();
|
||||
this.getUsers();
|
||||
this.getPayments();
|
||||
}
|
||||
|
||||
getOrder() {
|
||||
OrderBackend.getOrder(this.state.organizationName, this.state.orderName)
|
||||
.then((res) => {
|
||||
if (res.data === null) {
|
||||
this.props.history.push("/404");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
order: res.data,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getProducts() {
|
||||
ProductBackend.getProducts(this.state.organizationName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
products: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get products: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getUsers() {
|
||||
UserBackend.getUsers(this.state.organizationName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
users: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get users: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getPayments() {
|
||||
PaymentBackend.getPayments(this.state.organizationName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
payments: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get payments: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
parseOrderField(key, value) {
|
||||
if ([""].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
updateOrderField(key, value) {
|
||||
value = this.parseOrderField(key, value);
|
||||
|
||||
const order = this.state.order;
|
||||
order[key] = value;
|
||||
this.setState({
|
||||
order: order,
|
||||
});
|
||||
}
|
||||
|
||||
renderOrder() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{this.state.mode === "add" ? i18next.t("order:New Order") : i18next.t("order:Edit Order")}
|
||||
<Button onClick={() => this.submitOrderEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitOrderEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteOrder()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
} style={{marginLeft: "5px"}} type="inner">
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Organization")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.order.owner} disabled />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Name")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.order.name} onChange={e => {
|
||||
this.updateOrderField("name", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Display name")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.order.displayName} onChange={e => {
|
||||
this.updateOrderField("displayName", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("order:Product")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.order.productName} onChange={(value) => {
|
||||
this.updateOrderField("productName", value);
|
||||
}}>
|
||||
{
|
||||
this.state.products?.map((product, index) => <Option key={index} value={product.name}>{product.displayName}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:User")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.order.user} onChange={(value) => {
|
||||
this.updateOrderField("user", value);
|
||||
}}>
|
||||
{
|
||||
this.state.users?.map((user, index) => <Option key={index} value={user.name}>{user.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("order:Payment")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.order.payment} onChange={(value) => {
|
||||
this.updateOrderField("payment", value);
|
||||
}}>
|
||||
<Option value="">{"(empty)"}</Option>
|
||||
{
|
||||
this.state.payments?.map((payment, index) => <Option key={index} value={payment.name}>{payment.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:State")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.order.state} onChange={(value) => {
|
||||
this.updateOrderField("state", value);
|
||||
}}>
|
||||
{
|
||||
[
|
||||
{id: "Created", name: "Created"},
|
||||
{id: "Paid", name: "Paid"},
|
||||
{id: "Delivered", name: "Delivered"},
|
||||
{id: "Completed", name: "Completed"},
|
||||
{id: "Canceled", name: "Canceled"},
|
||||
{id: "Expired", name: "Expired"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Message")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.order.message} onChange={e => {
|
||||
this.updateOrderField("message", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("order:Start time")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.order.startTime} onChange={e => {
|
||||
this.updateOrderField("startTime", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("order:End time")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.order.endTime} onChange={e => {
|
||||
this.updateOrderField("endTime", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
submitOrderEdit(exitAfterSave) {
|
||||
const order = Setting.deepCopy(this.state.order);
|
||||
OrderBackend.updateOrder(this.state.organizationName, this.state.orderName, order)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully saved"));
|
||||
this.setState({
|
||||
orderName: this.state.order.name,
|
||||
});
|
||||
if (exitAfterSave) {
|
||||
this.props.history.push("/orders");
|
||||
} else {
|
||||
this.props.history.push(`/orders/${this.state.order.owner}/${this.state.order.name}`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteOrder() {
|
||||
OrderBackend.deleteOrder(this.state.order)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push("/orders");
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
this.state.order !== null ? this.renderOrder() : null
|
||||
}
|
||||
<div style={{marginTop: "20px", marginLeft: "40px"}}>
|
||||
<Button size="large" onClick={() => this.submitOrderEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitOrderEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteOrder()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OrderEditPage;
|
||||
297
web/src/OrderListPage.js
Normal file
297
web/src/OrderListPage.js
Normal file
@@ -0,0 +1,297 @@
|
||||
// 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}`);
|
||||
});
|
||||
}
|
||||
|
||||
cancelOrder(order) {
|
||||
OrderBackend.cancelOrder(order.owner, order.name)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully canceled"));
|
||||
this.fetch({
|
||||
pagination: this.state.pagination,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to cancel")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteOrder(i) {
|
||||
OrderBackend.deleteOrder(this.state.data[i])
|
||||
.then((res) => {
|
||||
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: "240px",
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div style={{display: "flex", flexWrap: "wrap", gap: "8px"}}>
|
||||
<Button onClick={() => this.props.history.push(`/orders/${record.owner}/${record.name}/pay`)} disabled={record.state !== "Created"}>
|
||||
{i18next.t("order:Pay")}
|
||||
</Button>
|
||||
<Button danger onClick={() => this.cancelOrder(record)} disabled={record.state !== "Created"}>
|
||||
{i18next.t("general:Cancel")}
|
||||
</Button>
|
||||
<Button type="primary" onClick={() => this.props.history.push(`/orders/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<PopconfirmModal
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteOrder(index)}
|
||||
>
|
||||
</PopconfirmModal>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const paginationProps = {
|
||||
total: this.state.pagination.total,
|
||||
showQuickJumper: true,
|
||||
showSizeChanger: true,
|
||||
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={orders} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Orders")}
|
||||
<Button type="primary" size="small" onClick={this.addOrder.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fetch = (params = {}) => {
|
||||
const field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
this.setState({loading: true});
|
||||
OrderBackend.getOrders(Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
data: res.data,
|
||||
pagination: {
|
||||
...params.pagination,
|
||||
total: res.data2,
|
||||
},
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (Setting.isResponseDenied(res)) {
|
||||
this.setState({
|
||||
isAuthorized: false,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default OrderListPage;
|
||||
302
web/src/OrderPayPage.js
Normal file
302
web/src/OrderPayPage.js
Normal file
@@ -0,0 +1,302 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Descriptions, Spin} from "antd";
|
||||
import i18next from "i18next";
|
||||
import * as OrderBackend from "./backend/OrderBackend";
|
||||
import * as ProductBackend from "./backend/ProductBackend";
|
||||
import * as Setting from "./Setting";
|
||||
|
||||
class OrderPayPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
owner: props?.match?.params?.organizationName ?? props?.match?.params?.owner ?? null,
|
||||
orderName: props?.match?.params?.orderName ?? null,
|
||||
order: null,
|
||||
product: null,
|
||||
paymentEnv: "",
|
||||
isProcessingPayment: false,
|
||||
};
|
||||
}
|
||||
|
||||
getPaymentEnv() {
|
||||
let env = "";
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
// Only support WeChat Pay in WeChat Browser for mobile devices
|
||||
if (ua.indexOf("micromessenger") !== -1 && ua.indexOf("mobile") !== -1) {
|
||||
env = "WechatBrowser";
|
||||
}
|
||||
this.setState({
|
||||
paymentEnv: env,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get("created") === "1") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully added"));
|
||||
}
|
||||
this.getOrder();
|
||||
this.getPaymentEnv();
|
||||
}
|
||||
|
||||
async getOrder() {
|
||||
if (!this.state.owner || !this.state.orderName) {
|
||||
return;
|
||||
}
|
||||
const res = await OrderBackend.getOrder(this.state.owner, this.state.orderName);
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
order: res.data,
|
||||
}, () => {
|
||||
this.getProduct();
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
}
|
||||
|
||||
async getProduct() {
|
||||
if (!this.state.order || !this.state.order.productName) {
|
||||
return;
|
||||
}
|
||||
const res = await ProductBackend.getProduct(this.state.order.owner, this.state.order.productName);
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
product: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
}
|
||||
|
||||
getPrice(order) {
|
||||
return `${Setting.getCurrencySymbol(order?.currency)}${order?.price} (${Setting.getCurrencyText(order)})`;
|
||||
}
|
||||
|
||||
getProductPrice(product) {
|
||||
return `${Setting.getCurrencySymbol(product?.currency)}${product?.price} (${Setting.getCurrencyText(product)})`;
|
||||
}
|
||||
|
||||
// Call Wechat Pay via jsapi
|
||||
onBridgeReady(attachInfo) {
|
||||
const {WeixinJSBridge} = window;
|
||||
this.setState({
|
||||
isProcessingPayment: false,
|
||||
});
|
||||
WeixinJSBridge.invoke(
|
||||
"getBrandWCPayRequest", {
|
||||
"appId": attachInfo.appId,
|
||||
"timeStamp": attachInfo.timeStamp,
|
||||
"nonceStr": attachInfo.nonceStr,
|
||||
"package": attachInfo.package,
|
||||
"signType": attachInfo.signType,
|
||||
"paySign": attachInfo.paySign,
|
||||
},
|
||||
function(res) {
|
||||
if (res.err_msg === "get_brand_wcpay_request:ok") {
|
||||
Setting.goToLink(attachInfo.payment.successUrl);
|
||||
return;
|
||||
}
|
||||
if (res.err_msg === "get_brand_wcpay_request:cancel") {
|
||||
Setting.showMessage("error", i18next.t("product:Payment cancelled"));
|
||||
} else {
|
||||
Setting.showMessage("error", i18next.t("product:Payment failed"));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// In WeChat browser, call this function to pay via jsapi
|
||||
callWechatPay(attachInfo) {
|
||||
const {WeixinJSBridge} = window;
|
||||
if (typeof WeixinJSBridge === "undefined") {
|
||||
document.addEventListener("WeixinJSBridgeReady", () => this.onBridgeReady(attachInfo), false);
|
||||
} else {
|
||||
this.onBridgeReady(attachInfo);
|
||||
}
|
||||
}
|
||||
|
||||
payOrder(provider) {
|
||||
const {product, order} = this.state;
|
||||
if (!product || !order) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isProcessingPayment: true,
|
||||
});
|
||||
|
||||
OrderBackend.payOrder(order.owner, order.name, provider.name, this.state.paymentEnv)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const payment = res.data;
|
||||
const attachInfo = res.data2;
|
||||
|
||||
let payUrl = payment.payUrl;
|
||||
if (provider.type === "WeChat Pay") {
|
||||
if (this.state.paymentEnv === "WechatBrowser") {
|
||||
attachInfo.payment = payment;
|
||||
this.callWechatPay(attachInfo);
|
||||
return;
|
||||
}
|
||||
payUrl = `/qrcode/${payment.owner}/${payment.name}?providerName=${provider.name}&payUrl=${encodeURIComponent(payment.payUrl)}&successUrl=${encodeURIComponent(payment.successUrl)}`;
|
||||
}
|
||||
Setting.goToLink(payUrl);
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("product:Payment failed")}: ${res.msg}`);
|
||||
this.setState({
|
||||
isProcessingPayment: false,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
this.setState({
|
||||
isProcessingPayment: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getPayButton(provider, onClick) {
|
||||
const providerTypeMap = {
|
||||
"Dummy": i18next.t("product:Dummy"),
|
||||
"Alipay": i18next.t("product:Alipay"),
|
||||
"WeChat Pay": i18next.t("product:WeChat Pay"),
|
||||
"PayPal": i18next.t("product:PayPal"),
|
||||
"Stripe": i18next.t("product:Stripe"),
|
||||
"AirWallex": i18next.t("product:AirWallex"),
|
||||
};
|
||||
const text = providerTypeMap[provider.type] || provider.type;
|
||||
|
||||
return (
|
||||
<Button style={{height: "50px", borderWidth: "2px"}} shape="round" icon={
|
||||
<img style={{marginRight: "10px"}} width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
|
||||
} size={"large"} onClick={onClick}>
|
||||
{text}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
renderProviderButton(provider) {
|
||||
return (
|
||||
<span key={provider.name} style={{width: "200px", marginRight: "20px", marginBottom: "10px"}}>
|
||||
{this.getPayButton(provider, () => this.payOrder(provider))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
renderPaymentMethods() {
|
||||
const {product} = this.state;
|
||||
if (!product || !product.providerObjs || product.providerObjs.length === 0) {
|
||||
return <div>{i18next.t("product:There is no payment channel for this product.")}</div>;
|
||||
}
|
||||
|
||||
return product.providerObjs.map(provider => {
|
||||
return this.renderProviderButton(provider);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {order, product} = this.state;
|
||||
|
||||
if (!order || !product) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isSubscriptionOrder = order.pricingName && order.planName;
|
||||
|
||||
return (
|
||||
<div className="login-content">
|
||||
<Spin spinning={this.state.isProcessingPayment} size="large" tip={i18next.t("product:Processing payment...")} style={{paddingTop: "10%"}} >
|
||||
<div style={{marginBottom: "20px"}}>
|
||||
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("order:Order Information")}</span>} bordered column={3}>
|
||||
<Descriptions.Item label={i18next.t("order:Order ID")} span={3}>
|
||||
<span style={{fontSize: 16}}>
|
||||
{order.name}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("order:Order Status")}>
|
||||
<span style={{fontSize: 16}}>
|
||||
{order.state}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("general:Created time")}>
|
||||
<span style={{fontSize: 16}}>
|
||||
{Setting.getFormattedDate(order.createdTime)}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("general:User")}>
|
||||
<span style={{fontSize: 16}}>
|
||||
{order.user}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
|
||||
<div style={{marginBottom: "20px"}}>
|
||||
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("product:Product Information")}</span>} bordered column={3}>
|
||||
<Descriptions.Item label={i18next.t("general:Name")} span={3}>
|
||||
<span style={{fontSize: 20}}>
|
||||
{Setting.getLanguageText(product?.displayName)}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:Image")} span={3}>
|
||||
<img src={product?.image} alt={Setting.getLanguageText(product?.displayName)} height={90} style={{marginBottom: "20px"}} />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:Price")} span={3}>
|
||||
<span style={{fontSize: 18, fontWeight: "bold"}}>
|
||||
{this.getProductPrice(product)}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:Detail")} span={3}>
|
||||
<span style={{fontSize: 16}}>{Setting.getLanguageText(product?.detail)}</span>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
|
||||
{isSubscriptionOrder && (
|
||||
<div style={{marginBottom: "20px"}}>
|
||||
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("subscription:Subscription Information")}</span>} bordered column={3}>
|
||||
<Descriptions.Item label={i18next.t("general:Plan")} span={3}>
|
||||
<span style={{fontSize: 16}}>{order.planName}</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("general:Pricing")} span={3}>
|
||||
<span style={{fontSize: 16}}>{order.pricingName}</span>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("payment:Payment Information")}</span>} bordered column={3}>
|
||||
<Descriptions.Item label={i18next.t("product:Price")} span={3}>
|
||||
<span style={{fontSize: 28, color: "red", fontWeight: "bold"}}>
|
||||
{this.getPrice(order)}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:Pay")} span={3}>
|
||||
{this.renderPaymentMethods()}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OrderPayPage;
|
||||
@@ -559,6 +559,30 @@ class OrganizationEditPage extends React.Component {
|
||||
<InputNumber value={this.state.organization.userBalance ?? 0} disabled />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Balance credit"), i18next.t("organization:Balance credit - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={4} >
|
||||
<InputNumber value={this.state.organization.balanceCredit ?? 0} onChange={value => {
|
||||
this.updateOrganizationField("balanceCredit", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Balance currency"), i18next.t("organization:Balance currency - Tooltip"))} :
|
||||
</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 +635,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 +648,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"))} :
|
||||
@@ -779,7 +818,7 @@ class OrganizationEditPage extends React.Component {
|
||||
}
|
||||
{this.state.mode !== "add" && this.state.transactions.length > 0 ? (
|
||||
<Card size="small" title={i18next.t("transaction:Transactions")} style={{marginTop: "20px"}} type="inner">
|
||||
<TransactionTable transactions={this.state.transactions} />
|
||||
<TransactionTable transactions={this.state.transactions} includeUser={true} />
|
||||
</Card>
|
||||
) : null}
|
||||
<div style={{marginTop: "20px", marginLeft: "40px"}}>
|
||||
|
||||
@@ -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,8 @@ class OrganizationListPage extends BaseListPage {
|
||||
{name: "Karma", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Ranking", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Balance", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Balance credit", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Balance currency", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Register type", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Register source", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
@@ -258,6 +261,26 @@ class OrganizationListPage extends BaseListPage {
|
||||
return text ?? 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("organization:Balance credit"),
|
||||
dataIndex: "balanceCredit",
|
||||
key: "balanceCredit",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return text ?? 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("organization:Balance currency"),
|
||||
dataIndex: "balanceCurrency",
|
||||
key: "balanceCurrency",
|
||||
width: "140px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return text || "USD";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("organization:Soft deletion"),
|
||||
dataIndex: "enableSoftDeletion",
|
||||
@@ -266,7 +289,7 @@ class OrganizationListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -233,9 +233,13 @@ class PaymentEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.payment.currency} onChange={e => {
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.payment.currency} disabled={true} onChange={(value => {
|
||||
// this.updatePaymentField('currency', e.target.value);
|
||||
}} />
|
||||
})}>
|
||||
{
|
||||
Setting.CurrencyOptions.map((item, index) => <Option key={index} value={item.id}>{Setting.getCurrencyWithFlag(item.id)}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
|
||||
@@ -204,6 +204,9 @@ class PaymentListPage extends BaseListPage {
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("currency"),
|
||||
render: (text, record, index) => {
|
||||
return Setting.getCurrencyWithFlag(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:State"),
|
||||
|
||||
@@ -306,7 +306,7 @@ class PermissionListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -150,6 +150,7 @@ class PlanEditPage extends React.Component {
|
||||
this.updatePlanField("owner", owner);
|
||||
this.getUsers(owner);
|
||||
this.getRoles(owner);
|
||||
this.getPaymentProviders(owner);
|
||||
})}
|
||||
options={this.state.organizations.map((organization) => Setting.getOption(organization.name, organization.name))
|
||||
} />
|
||||
@@ -229,34 +230,7 @@ class PlanEditPage extends React.Component {
|
||||
this.updatePlanField("currency", value);
|
||||
})}>
|
||||
{
|
||||
[
|
||||
{id: "USD", name: "USD"},
|
||||
{id: "CNY", name: "CNY"},
|
||||
{id: "EUR", name: "EUR"},
|
||||
{id: "JPY", name: "JPY"},
|
||||
{id: "GBP", name: "GBP"},
|
||||
{id: "AUD", name: "AUD"},
|
||||
{id: "CAD", name: "CAD"},
|
||||
{id: "CHF", name: "CHF"},
|
||||
{id: "HKD", name: "HKD"},
|
||||
{id: "SGD", name: "SGD"},
|
||||
{id: "BRL", name: "BRL"},
|
||||
{id: "PLN", name: "PLN"},
|
||||
{id: "KRW", name: "KRW"},
|
||||
{id: "INR", name: "INR"},
|
||||
{id: "RUB", name: "RUB"},
|
||||
{id: "MXN", name: "MXN"},
|
||||
{id: "ZAR", name: "ZAR"},
|
||||
{id: "TRY", name: "TRY"},
|
||||
{id: "SEK", name: "SEK"},
|
||||
{id: "NOK", name: "NOK"},
|
||||
{id: "DKK", name: "DKK"},
|
||||
{id: "THB", name: "THB"},
|
||||
{id: "MYR", name: "MYR"},
|
||||
{id: "TWD", name: "TWD"},
|
||||
{id: "CZK", name: "CZK"},
|
||||
{id: "HUF", name: "HUF"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
Setting.CurrencyOptions.map((item, index) => <Option key={index} value={item.id}>{Setting.getCurrencyWithFlag(item.id)}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
|
||||
@@ -136,6 +136,9 @@ class PlanListPage extends BaseListPage {
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("currency"),
|
||||
render: (text, record, index) => {
|
||||
return Setting.getCurrencyWithFlag(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("plan:Price"),
|
||||
@@ -187,7 +190,7 @@ class PlanListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -181,7 +181,7 @@ class PricingListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@ import i18next from "i18next";
|
||||
import * as ProductBackend from "./backend/ProductBackend";
|
||||
import * as PlanBackend from "./backend/PlanBackend";
|
||||
import * as PricingBackend from "./backend/PricingBackend";
|
||||
import * as OrderBackend from "./backend/OrderBackend";
|
||||
import * as Setting from "./Setting";
|
||||
|
||||
class ProductBuyPage extends React.Component {
|
||||
@@ -118,136 +119,27 @@ 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
|
||||
onBridgeReady(attachInfo) {
|
||||
const {WeixinJSBridge} = window;
|
||||
// Setting.showMessage("success", "attachInfo is " + JSON.stringify(attachInfo));
|
||||
this.setState({
|
||||
isPlacingOrder: false,
|
||||
});
|
||||
WeixinJSBridge.invoke(
|
||||
"getBrandWCPayRequest", {
|
||||
"appId": attachInfo.appId,
|
||||
"timeStamp": attachInfo.timeStamp,
|
||||
"nonceStr": attachInfo.nonceStr,
|
||||
"package": attachInfo.package,
|
||||
"signType": attachInfo.signType,
|
||||
"paySign": attachInfo.paySign,
|
||||
},
|
||||
function(res) {
|
||||
if (res.err_msg === "get_brand_wcpay_request:ok") {
|
||||
Setting.goToLink(attachInfo.payment.successUrl);
|
||||
return ;
|
||||
} else {
|
||||
if (res.err_msg === "get_brand_wcpay_request:cancel") {
|
||||
Setting.showMessage("error", i18next.t("product:Payment cancelled"));
|
||||
} else {
|
||||
Setting.showMessage("error", i18next.t("product:Payment failed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// In Wechat browser, call this function to pay via jsapi
|
||||
callWechatPay(attachInfo) {
|
||||
const {WeixinJSBridge} = window;
|
||||
if (typeof WeixinJSBridge === "undefined") {
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener("WeixinJSBridgeReady", () => this.onBridgeReady(attachInfo), false);
|
||||
} else if (document.attachEvent) {
|
||||
document.attachEvent("WeixinJSBridgeReady", () => this.onBridgeReady(attachInfo));
|
||||
document.attachEvent("onWeixinJSBridgeReady", () => this.onBridgeReady(attachInfo));
|
||||
}
|
||||
} else {
|
||||
this.onBridgeReady(attachInfo);
|
||||
}
|
||||
}
|
||||
|
||||
buyProduct(product, provider) {
|
||||
placeOrder(product) {
|
||||
this.setState({
|
||||
isPlacingOrder: true,
|
||||
});
|
||||
|
||||
ProductBackend.buyProduct(product.owner, product.name, provider.name, this.state.pricingName ?? "", this.state.planName ?? "", this.state.userName ?? "", this.state.paymentEnv, this.state.customPrice)
|
||||
const productId = `${product.owner}/${product.name}`;
|
||||
const pricingName = this.state.pricingName || "";
|
||||
const planName = this.state.planName || "";
|
||||
const customPrice = this.state.customPrice || 0;
|
||||
OrderBackend.placeOrder(productId, pricingName, planName, this.state.userName ?? "", customPrice)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const payment = res.data;
|
||||
const attachInfo = res.data2;
|
||||
let payUrl = payment.payUrl;
|
||||
if (provider.type === "WeChat Pay") {
|
||||
if (this.state.paymentEnv === "WechatBrowser") {
|
||||
attachInfo.payment = payment;
|
||||
this.callWechatPay(attachInfo);
|
||||
return ;
|
||||
}
|
||||
payUrl = `/qrcode/${payment.owner}/${payment.name}?providerName=${provider.name}&payUrl=${encodeURIComponent(payment.payUrl)}&successUrl=${encodeURIComponent(payment.successUrl)}`;
|
||||
}
|
||||
Setting.goToLink(payUrl);
|
||||
const order = res.data;
|
||||
// Redirect to order pay page
|
||||
Setting.goToLink(`/orders/${order.owner}/${order.name}/pay?created=1`);
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
|
||||
Setting.showMessage("error", `${i18next.t("product:Payment failed")}: ${res.msg}`);
|
||||
this.setState({
|
||||
isPlacingOrder: false,
|
||||
});
|
||||
@@ -255,49 +147,13 @@ class ProductBuyPage extends React.Component {
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
this.setState({
|
||||
isPlacingOrder: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getPayButton(provider) {
|
||||
let text = provider.type;
|
||||
if (provider.type === "Dummy") {
|
||||
text = i18next.t("product:Dummy");
|
||||
} else if (provider.type === "Alipay") {
|
||||
text = i18next.t("product:Alipay");
|
||||
} else if (provider.type === "WeChat Pay") {
|
||||
text = i18next.t("product:WeChat Pay");
|
||||
} else if (provider.type === "PayPal") {
|
||||
text = i18next.t("product:PayPal");
|
||||
} else if (provider.type === "Stripe") {
|
||||
text = i18next.t("product:Stripe");
|
||||
} else if (provider.type === "AirWallex") {
|
||||
text = i18next.t("product:AirWallex");
|
||||
}
|
||||
|
||||
return (
|
||||
<Button style={{height: "50px", borderWidth: "2px"}} shape="round" icon={
|
||||
<img style={{marginRight: "10px"}} width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
|
||||
} size={"large"} >
|
||||
{
|
||||
text
|
||||
}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
renderProviderButton(provider, product) {
|
||||
return (
|
||||
<span key={provider.name} style={{width: "200px", marginRight: "20px", marginBottom: "10px"}}>
|
||||
<span style={{width: "200px", cursor: "pointer"}} onClick={() => this.buyProduct(product, provider)}>
|
||||
{
|
||||
this.getPayButton(provider)
|
||||
}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
renderPay(product) {
|
||||
renderPlaceOrderButton(product) {
|
||||
if (product === undefined || product === null) {
|
||||
return null;
|
||||
}
|
||||
@@ -305,17 +161,32 @@ class ProductBuyPage extends React.Component {
|
||||
if (product.state !== "Published") {
|
||||
return i18next.t("product:This product is currently not in sale.");
|
||||
}
|
||||
if (product.providerObjs.length === 0) {
|
||||
return i18next.t("product:There is no payment channel for this product.");
|
||||
}
|
||||
|
||||
return product.providerObjs.map(provider => {
|
||||
return this.renderProviderButton(provider, product);
|
||||
});
|
||||
return (
|
||||
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
style={{
|
||||
height: "50px",
|
||||
fontSize: "18px",
|
||||
borderRadius: "30px",
|
||||
paddingLeft: "60px",
|
||||
paddingRight: "60px",
|
||||
}}
|
||||
onClick={() => this.placeOrder(product)}
|
||||
disabled={this.state.isPlacingOrder}
|
||||
loading={this.state.isPlacingOrder}
|
||||
>
|
||||
{i18next.t("order:Place Order")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const product = this.getProductObj();
|
||||
const placeOrderButton = this.renderPlaceOrderButton(product);
|
||||
|
||||
if (product === null) {
|
||||
return null;
|
||||
@@ -357,10 +228,10 @@ class ProductBuyPage extends React.Component {
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
<Descriptions.Item label={i18next.t("product:Pay")} span={3}>
|
||||
{
|
||||
this.renderPay(product)
|
||||
}
|
||||
<Descriptions.Item label={i18next.t("order:Place Order")} span={3}>
|
||||
<div style={{display: "flex", justifyContent: "center", alignItems: "center", minHeight: "80px"}}>
|
||||
{placeOrderButton}
|
||||
</div>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Spin>
|
||||
|
||||
@@ -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>
|
||||
|
||||
146
web/src/ProductStorePage.js
Normal file
146
web/src/ProductStorePage.js
Normal file
@@ -0,0 +1,146 @@
|
||||
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Row, Tag, Typography} from "antd";
|
||||
import * as Setting from "./Setting";
|
||||
import * as ProductBackend from "./backend/ProductBackend";
|
||||
import i18next from "i18next";
|
||||
|
||||
const {Text, Title} = Typography;
|
||||
|
||||
class ProductStorePage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
products: [],
|
||||
loading: true,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getProducts();
|
||||
}
|
||||
|
||||
getProducts() {
|
||||
const pageSize = 100; // Max products to display in the store
|
||||
const owner = Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account);
|
||||
this.setState({loading: true});
|
||||
ProductBackend.getProducts(owner, 1, pageSize, "state", "Published", "", "")
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
products: res.data,
|
||||
loading: false,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
this.setState({loading: false});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
this.setState({loading: false});
|
||||
});
|
||||
}
|
||||
|
||||
handleBuyProduct(product) {
|
||||
this.props.history.push(`/products/${product.owner}/${product.name}/buy`);
|
||||
}
|
||||
|
||||
renderProductCard(product) {
|
||||
return (
|
||||
<Col xs={24} sm={12} md={8} lg={6} key={`${product.owner}/${product.name}`} style={{marginBottom: "20px"}}>
|
||||
<Card
|
||||
hoverable
|
||||
onClick={() => this.handleBuyProduct(product)}
|
||||
style={{cursor: "pointer", height: "100%", display: "flex", flexDirection: "column"}}
|
||||
cover={
|
||||
<div style={{height: "200px", overflow: "hidden", display: "flex", alignItems: "center", justifyContent: "center", backgroundColor: "#f0f0f0"}}>
|
||||
<img
|
||||
alt={product.displayName}
|
||||
src={product.image}
|
||||
style={{width: "100%", height: "100%", objectFit: "contain"}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
actions={[
|
||||
<Button
|
||||
key="buy"
|
||||
type="primary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
this.handleBuyProduct(product);
|
||||
}}
|
||||
>
|
||||
{i18next.t("product:Buy")}
|
||||
</Button>,
|
||||
]}
|
||||
bodyStyle={{flex: 1, display: "flex", flexDirection: "column"}}
|
||||
>
|
||||
<div style={{flex: 1, display: "flex", flexDirection: "column"}}>
|
||||
<Title level={5} ellipsis={{rows: 2}} style={{margin: "0 0 4px 0", minHeight: "44px"}}>
|
||||
{Setting.getLanguageText(product.displayName)}
|
||||
</Title>
|
||||
<Text style={{display: "block", marginBottom: 4, minHeight: "40px"}} ellipsis={{rows: 2}}>
|
||||
{Setting.getLanguageText(product.detail)}
|
||||
</Text>
|
||||
{product.tag && (
|
||||
<Tag color="blue" style={{marginBottom: 4, display: "inline-block"}}>{product.tag}</Tag>
|
||||
)}
|
||||
<div style={{marginTop: "auto"}}>
|
||||
<div style={{marginBottom: 4}}>
|
||||
<Text strong style={{fontSize: "24px", color: "#ff4d4f"}}>
|
||||
{Setting.getCurrencySymbol(product.currency)}{product.price}
|
||||
</Text>
|
||||
<Text type="secondary" style={{fontSize: "12px", marginLeft: 8}}>
|
||||
{Setting.getCurrencyWithFlag(product.currency)}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text type="secondary" style={{fontSize: "12px"}}>
|
||||
{i18next.t("product:Sold")}: {product.sold}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Row gutter={[16, 16]}>
|
||||
{this.state.loading ? (
|
||||
<Col span={24}>
|
||||
<Card loading={true} />
|
||||
</Col>
|
||||
) : this.state.products.length === 0 ? (
|
||||
<Col span={24}>
|
||||
<Card>
|
||||
<Text type="secondary">{i18next.t("general:No products available")}</Text>
|
||||
</Card>
|
||||
</Col>
|
||||
) : (
|
||||
this.state.products.map(product => this.renderProductCard(product))
|
||||
)}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductStorePage;
|
||||
@@ -1811,6 +1811,20 @@ class ProviderEditPage extends React.Component {
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
(this.state.provider.type === "GC" || this.state.provider.type === "FastSpring") ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.provider.host} onChange={e => {
|
||||
this.updateProviderField("host", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Provider URL"), i18next.t("provider:Provider URL - Tooltip"))} :
|
||||
|
||||
@@ -192,7 +192,7 @@ class RecordListPage extends BaseListPage {
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -214,7 +214,7 @@ class RoleListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -288,6 +288,22 @@ export const OtherProviderInfo = {
|
||||
logo: `${StaticBaseUrl}/img/payment_gc.png`,
|
||||
url: "https://gc.org",
|
||||
},
|
||||
"Polar": {
|
||||
logo: `${StaticBaseUrl}/img/payment_polar.png`,
|
||||
url: "https://polar.sh/",
|
||||
},
|
||||
"Paddle": {
|
||||
logo: `${StaticBaseUrl}/img/payment_paddle.png`,
|
||||
url: "https://www.paddle.com/",
|
||||
},
|
||||
"FastSpring": {
|
||||
logo: `${StaticBaseUrl}/img/payment_fastspring.png`,
|
||||
url: "https://fastspring.com/",
|
||||
},
|
||||
"Lemon Squeezy": {
|
||||
logo: `${StaticBaseUrl}/img/payment_lemonsqueezy.png`,
|
||||
url: "https://www.lemonsqueezy.com/",
|
||||
},
|
||||
},
|
||||
Captcha: {
|
||||
"Default": {
|
||||
@@ -435,6 +451,120 @@ export const OtherProviderInfo = {
|
||||
},
|
||||
};
|
||||
|
||||
export const UserFields = ["owner", "name", "password", "display_name", "id", "type", "email", "phone", "country_code",
|
||||
"is_admin", "homepage", "birthday", "gender", "password_type", "password_salt", "external_id", "avatar", "first_name", "last_name",
|
||||
"avatar_type", "permanent_avatar", "email_verified", "region", "location", "address",
|
||||
"affiliation", "title", "id_card_type", "id_card", "bio", "tag", "language",
|
||||
"education", "score", "karma", "ranking", "balance", "currency", "is_default_avatar", "is_online",
|
||||
"is_forbidden", "is_deleted", "signup_application", "hash", "pre_hash", "access_key", "access_secret", "access_token",
|
||||
"created_ip", "last_signin_time", "last_signin_ip", "github", "google", "qq", "wechat", "facebook", "dingtalk",
|
||||
"weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs", "baidu", "alipay", "casdoor", "infoflow", "apple",
|
||||
"azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "kwai", "line", "amazon", "auth0",
|
||||
"battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
|
||||
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru",
|
||||
"meetup", "microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify",
|
||||
"soundcloud", "spotify", "strava", "stripe", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk",
|
||||
"wepay", "xero", "yahoo", "yammer", "yandex", "zoom", "metamask", "web3onboard", "custom", "webauthnCredentials",
|
||||
"preferred_mfa_type", "recovery_codes", "totp_secret", "mfa_phone_enabled", "mfa_email_enabled", "invitation",
|
||||
"invitation_code", "face_ids", "ldap", "properties", "roles", "permissions", "groups", "last_change_password_time",
|
||||
"last_signin_wrong_time", "signin_wrong_times", "managedAccounts", "mfaAccounts", "need_update_password",
|
||||
"created_time", "updated_time", "deleted_time",
|
||||
"ip_whitelist"];
|
||||
|
||||
export const GetTranslatedUserItems = () => {
|
||||
return [
|
||||
{name: "Organization", label: i18next.t("general:Organization")},
|
||||
{name: "ID", label: i18next.t("general:ID")},
|
||||
{name: "Name", label: i18next.t("general:Name")},
|
||||
{name: "Display name", label: i18next.t("general:Display name")},
|
||||
{name: "First name", label: i18next.t("general:First name")},
|
||||
{name: "Last name", label: i18next.t("general:Last name")},
|
||||
{name: "Avatar", label: i18next.t("general:Avatar")},
|
||||
{name: "User type", label: i18next.t("general:User type")},
|
||||
{name: "Password", label: i18next.t("general:Password")},
|
||||
{name: "Email", label: i18next.t("general:Email")},
|
||||
{name: "Phone", label: i18next.t("general:Phone")},
|
||||
{name: "Country code", label: i18next.t("user:Country code")},
|
||||
{name: "Country/Region", label: i18next.t("user:Country/Region")},
|
||||
{name: "Location", label: i18next.t("user:Location")},
|
||||
{name: "Address", label: i18next.t("user:Address")},
|
||||
{name: "Affiliation", label: i18next.t("user:Affiliation")},
|
||||
{name: "Title", label: i18next.t("user:Title")},
|
||||
{name: "ID card type", label: i18next.t("user:ID card type")},
|
||||
{name: "ID card", label: i18next.t("user:ID card")},
|
||||
{name: "ID card info", label: i18next.t("user:ID card info")},
|
||||
{name: "Homepage", label: i18next.t("user:Homepage")},
|
||||
{name: "Bio", label: i18next.t("user:Bio")},
|
||||
{name: "Tag", label: i18next.t("user:Tag")},
|
||||
{name: "Language", label: i18next.t("user:Language")},
|
||||
{name: "Gender", label: i18next.t("user:Gender")},
|
||||
{name: "Birthday", label: i18next.t("user:Birthday")},
|
||||
{name: "Education", label: i18next.t("user:Education")},
|
||||
{name: "Balance", label: i18next.t("user:Balance")},
|
||||
{name: "Balance currency", label: i18next.t("organization:Balance currency")},
|
||||
{name: "Balance credit", label: i18next.t("organization:Balance credit")},
|
||||
{name: "Transactions", label: i18next.t("transaction:Transactions")},
|
||||
{name: "Score", label: i18next.t("user:Score")},
|
||||
{name: "Karma", label: i18next.t("user:Karma")},
|
||||
{name: "Ranking", label: i18next.t("user:Ranking")},
|
||||
{name: "Signup application", label: i18next.t("general:Signup application")},
|
||||
{name: "API key", label: i18next.t("general:API key")},
|
||||
{name: "Groups", label: i18next.t("general:Groups")},
|
||||
{name: "Roles", label: i18next.t("general:Roles")},
|
||||
{name: "Permissions", label: i18next.t("general:Permissions")},
|
||||
{name: "3rd-party logins", label: i18next.t("user:3rd-party logins")},
|
||||
{name: "Properties", label: i18next.t("user:Properties")},
|
||||
{name: "Is online", label: i18next.t("user:Is online")},
|
||||
{name: "Is admin", label: i18next.t("user:Is admin")},
|
||||
{name: "Is forbidden", label: i18next.t("user:Is forbidden")},
|
||||
{name: "Is deleted", label: i18next.t("user:Is deleted")},
|
||||
{name: "Need update password", label: i18next.t("user:Need update password")},
|
||||
{name: "IP whitelist", label: i18next.t("general:IP whitelist")},
|
||||
{name: "Multi-factor authentication", label: i18next.t("user:Multi-factor authentication")},
|
||||
{name: "WebAuthn credentials", label: i18next.t("user:WebAuthn credentials")},
|
||||
{name: "Managed accounts", label: i18next.t("user:Managed accounts")},
|
||||
{name: "Face ID", label: i18next.t("user:Face ID")},
|
||||
{name: "MFA accounts", label: i18next.t("user:MFA accounts")},
|
||||
{name: "MFA items", label: i18next.t("general:MFA items")},
|
||||
];
|
||||
};
|
||||
|
||||
export function getUserColumns() {
|
||||
const items = GetTranslatedUserItems();
|
||||
return UserFields.map(field => {
|
||||
let transField = "";
|
||||
if (field === "webauthnCredentials") {
|
||||
transField = "WebAuthn credentials";
|
||||
} else if (field === "region") {
|
||||
transField = "Country/Region";
|
||||
} else if (field === "mfaAccounts") {
|
||||
transField = "MFA accounts";
|
||||
} else if (field === "face_ids") {
|
||||
transField = "Face ID";
|
||||
} else if (field === "managedAccounts") {
|
||||
transField = "Managed accounts";
|
||||
} else {
|
||||
transField = field.toLowerCase().split("_").join(" ");
|
||||
transField = transField.charAt(0).toUpperCase() + transField.slice(1);
|
||||
transField = transField.replace("ip", "IP")
|
||||
.replace("Ip", "IP")
|
||||
.replace("Id", "ID")
|
||||
.replace("id", "ID");
|
||||
}
|
||||
if (transField === "Owner") {
|
||||
transField = "Organization";
|
||||
}
|
||||
const transFieldItem = items.find(item => item.name === transField);
|
||||
if (transFieldItem === undefined) {
|
||||
const toTranslateList = ["general", "user", "organization"].map(ns => `${ns}:${transField}`);
|
||||
const transResult = toTranslateList.map(item => i18next.t(item) === transField ? null : i18next.t(item))
|
||||
.find(item => item !== null);
|
||||
transField = transResult ? transResult : transField;
|
||||
}
|
||||
return `${transFieldItem ? transFieldItem.label : transField}#${field}`;
|
||||
});
|
||||
}
|
||||
|
||||
export function initCountries() {
|
||||
const countries = require("i18n-iso-countries");
|
||||
countries.registerLocale(require("i18n-iso-countries/langs/" + getLanguage() + ".json"));
|
||||
@@ -1148,6 +1278,10 @@ export function getProviderTypeOptions(category) {
|
||||
{id: "Stripe", name: "Stripe"},
|
||||
{id: "AirWallex", name: "AirWallex"},
|
||||
{id: "GC", name: "GC"},
|
||||
{id: "Polar", name: "Polar"},
|
||||
{id: "Paddle", name: "Paddle"},
|
||||
{id: "FastSpring", name: "FastSpring"},
|
||||
{id: "Lemon Squeezy", name: "Lemon Squeezy"},
|
||||
]);
|
||||
} else if (category === "Captcha") {
|
||||
return ([
|
||||
@@ -1544,6 +1678,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 +1799,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 +1828,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",
|
||||
|
||||
@@ -143,6 +143,9 @@ class SubscriptionListPage extends BaseListPage {
|
||||
key: "startTime",
|
||||
width: "140px",
|
||||
...this.getColumnSearchProps("startTime"),
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("subscription:End time"),
|
||||
@@ -150,6 +153,9 @@ class SubscriptionListPage extends BaseListPage {
|
||||
key: "endTime",
|
||||
width: "140px",
|
||||
...this.getColumnSearchProps("endTime"),
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Plan"),
|
||||
|
||||
@@ -61,10 +61,15 @@ class SyncerEditPage extends React.Component {
|
||||
this.setState({
|
||||
syncer: res.data,
|
||||
});
|
||||
|
||||
if (res.data && res.data.organization) {
|
||||
this.getCerts(res.data.organization);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCerts(owner) {
|
||||
// Load certificates for the given organization
|
||||
CertBackend.getCerts(owner)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
@@ -79,9 +84,6 @@ class SyncerEditPage extends React.Component {
|
||||
this.setState({
|
||||
organizations: res.data || [],
|
||||
});
|
||||
if (res.data) {
|
||||
this.getCerts(`${res.data.owner}/${res.data.name}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -96,6 +98,12 @@ class SyncerEditPage extends React.Component {
|
||||
value = this.parseSyncerField(key, value);
|
||||
|
||||
const syncer = this.state.syncer;
|
||||
if (key === "organization" && syncer["organization"] !== value) {
|
||||
// the syncer changed the organization, reset the cert and reload certs
|
||||
syncer["cert"] = "";
|
||||
this.getCerts(value);
|
||||
}
|
||||
|
||||
syncer[key] = value;
|
||||
this.setState({
|
||||
syncer: syncer,
|
||||
@@ -320,11 +328,140 @@ class SyncerEditPage extends React.Component {
|
||||
"values": [],
|
||||
},
|
||||
];
|
||||
case "Google Workspace":
|
||||
return [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"casdoorName": "Id",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "primaryEmail",
|
||||
"type": "string",
|
||||
"casdoorName": "Name",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "name.fullName",
|
||||
"type": "string",
|
||||
"casdoorName": "DisplayName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "name.givenName",
|
||||
"type": "string",
|
||||
"casdoorName": "FirstName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "name.familyName",
|
||||
"type": "string",
|
||||
"casdoorName": "LastName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "suspended",
|
||||
"type": "boolean",
|
||||
"casdoorName": "IsForbidden",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "isAdmin",
|
||||
"type": "boolean",
|
||||
"casdoorName": "IsAdmin",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
];
|
||||
case "Active Directory":
|
||||
return [
|
||||
{
|
||||
"name": "objectGUID",
|
||||
"type": "string",
|
||||
"casdoorName": "Id",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "sAMAccountName",
|
||||
"type": "string",
|
||||
"casdoorName": "Name",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"casdoorName": "DisplayName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "givenName",
|
||||
"type": "string",
|
||||
"casdoorName": "FirstName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "sn",
|
||||
"type": "string",
|
||||
"casdoorName": "LastName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "mail",
|
||||
"type": "string",
|
||||
"casdoorName": "Email",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "mobile",
|
||||
"type": "string",
|
||||
"casdoorName": "Phone",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"casdoorName": "Title",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "department",
|
||||
"type": "string",
|
||||
"casdoorName": "Affiliation",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "userAccountControl",
|
||||
"type": "string",
|
||||
"casdoorName": "IsForbidden",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
needSshfields() {
|
||||
return this.state.syncer.type === "Database" && (this.state.syncer.databaseType === "mysql" || this.state.syncer.databaseType === "mssql" || this.state.syncer.databaseType === "postgres");
|
||||
}
|
||||
|
||||
renderSyncer() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
@@ -372,14 +509,14 @@ class SyncerEditPage extends React.Component {
|
||||
});
|
||||
})}>
|
||||
{
|
||||
["Database", "Keycloak", "WeCom", "Azure AD"]
|
||||
["Database", "Keycloak", "WeCom", "Azure AD", "Active Directory", "Google Workspace"]
|
||||
.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" ? null : (
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Active Directory" || this.state.syncer.type === "Google Workspace" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("syncer:Database type"), i18next.t("syncer:Database type - Tooltip"))} :
|
||||
@@ -434,7 +571,7 @@ class SyncerEditPage extends React.Component {
|
||||
this.state.syncer.type === "WeCom" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(this.state.syncer.type === "Azure AD" ? i18next.t("provider:Tenant ID") : i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
|
||||
{Setting.getLabel(this.state.syncer.type === "Azure AD" ? i18next.t("provider:Tenant ID") : this.state.syncer.type === "Google Workspace" ? i18next.t("syncer:Admin Email") : this.state.syncer.type === "Active Directory" ? i18next.t("ldap:Server") : i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.syncer.host} onChange={e => {
|
||||
@@ -445,10 +582,10 @@ class SyncerEditPage extends React.Component {
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" ? null : (
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
|
||||
{Setting.getLabel(this.state.syncer.type === "Active Directory" ? i18next.t("provider:LDAP port") : i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber value={this.state.syncer.port} onChange={value => {
|
||||
@@ -458,41 +595,55 @@ class SyncerEditPage extends React.Component {
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.syncer.type === "Google Workspace" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(
|
||||
this.state.syncer.type === "WeCom" ? i18next.t("syncer:Corp ID") :
|
||||
this.state.syncer.type === "Azure AD" ? i18next.t("provider:Client ID") :
|
||||
this.state.syncer.type === "Active Directory" ? i18next.t("syncer:Bind DN") :
|
||||
i18next.t("general:User"),
|
||||
i18next.t("general:User - Tooltip")
|
||||
)} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.syncer.user} onChange={e => {
|
||||
this.updateSyncerField("user", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(
|
||||
this.state.syncer.type === "WeCom" ? i18next.t("provider:Corp ID") :
|
||||
this.state.syncer.type === "Azure AD" ? i18next.t("provider:Client ID") :
|
||||
i18next.t("general:User"),
|
||||
i18next.t("general:User - Tooltip")
|
||||
)} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.syncer.user} onChange={e => {
|
||||
this.updateSyncerField("user", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(
|
||||
this.state.syncer.type === "WeCom" ? i18next.t("provider:Corp Secret") :
|
||||
this.state.syncer.type === "Azure AD" ? i18next.t("provider:Client Secret") :
|
||||
i18next.t("general:Password"),
|
||||
this.state.syncer.type === "WeCom" ? i18next.t("syncer:Corp secret") :
|
||||
this.state.syncer.type === "Azure AD" ? i18next.t("provider:Client secret") :
|
||||
this.state.syncer.type === "Google Workspace" ? i18next.t("syncer:Service account key") :
|
||||
i18next.t("general:Password"),
|
||||
i18next.t("general:Password - Tooltip")
|
||||
)} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input.Password value={this.state.syncer.password} onChange={e => {
|
||||
this.updateSyncerField("password", e.target.value);
|
||||
}} />
|
||||
{
|
||||
this.state.syncer.type === "Google Workspace" ? (
|
||||
<Input.TextArea rows={4} value={this.state.syncer.password} onChange={e => {
|
||||
this.updateSyncerField("password", e.target.value);
|
||||
}} placeholder={i18next.t("syncer:Paste your Google Workspace service account JSON key here")} />
|
||||
) : (
|
||||
<Input.Password value={this.state.syncer.password} onChange={e => {
|
||||
this.updateSyncerField("password", e.target.value);
|
||||
}} />
|
||||
)
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" ? null : (
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :
|
||||
{Setting.getLabel(this.state.syncer.type === "Active Directory" ? i18next.t("syncer:Base DN") : i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.syncer.database} onChange={e => {
|
||||
@@ -503,7 +654,7 @@ class SyncerEditPage extends React.Component {
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.syncer.databaseType === "mysql" || this.state.syncer.databaseType === "mssql" || this.state.syncer.databaseType === "postgres" ? (
|
||||
this.needSshfields() ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:SSH type"), i18next.t("general:SSH type - Tooltip"))} :
|
||||
@@ -521,7 +672,7 @@ class SyncerEditPage extends React.Component {
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.syncer.sshType && this.state.syncer.databaseType === "mysql" || this.state.syncer.databaseType === "mssql" || this.state.syncer.databaseType === "postgres" ? (
|
||||
this.state.syncer.sshType && this.needSshfields() ? (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
@@ -554,7 +705,7 @@ class SyncerEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.syncer.sshType === "password" && (this.state.syncer.databaseType === "mysql" || this.state.syncer.databaseType === "mssql" || this.state.syncer.databaseType === "postgres") ?
|
||||
this.state.syncer.sshType === "password" && this.needSshfields() ?
|
||||
(
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
@@ -585,7 +736,7 @@ class SyncerEditPage extends React.Component {
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" ? null : (
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("syncer:Table"), i18next.t("syncer:Table - Tooltip"))} :
|
||||
|
||||
@@ -227,7 +227,7 @@ class SyncerListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -14,11 +14,15 @@
|
||||
|
||||
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 UserBackend from "./backend/UserBackend";
|
||||
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 +30,21 @@ 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: [],
|
||||
users: [],
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getTransaction();
|
||||
if (this.state.mode === "recharge") {
|
||||
this.getOrganizations();
|
||||
this.getApplications(this.state.organizationName);
|
||||
this.getUsers(this.state.organizationName);
|
||||
}
|
||||
}
|
||||
|
||||
getTransaction() {
|
||||
@@ -45,15 +55,71 @@ 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}`);
|
||||
});
|
||||
}
|
||||
|
||||
getUsers(organizationName) {
|
||||
const targetOrganizationName = organizationName || this.state.organizationName;
|
||||
UserBackend.getUsers(targetOrganizationName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
users: res.data || [],
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
submitTransactionEdit(exitAfterSave) {
|
||||
if (this.state.transaction === null) {
|
||||
return;
|
||||
}
|
||||
const transaction = Setting.deepCopy(this.state.transaction);
|
||||
TransactionBackend.updateTransaction(this.state.transaction.owner, this.state.transactionName, transaction)
|
||||
.then((res) => {
|
||||
@@ -70,7 +136,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 +145,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 +162,69 @@ class TransactionEditPage extends React.Component {
|
||||
}
|
||||
|
||||
parseTransactionField(key, value) {
|
||||
if ([""].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
if (["amount"].includes(key)) {
|
||||
value = parseFloat(value);
|
||||
if (isNaN(value)) {
|
||||
value = 0;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
getApplication() {
|
||||
ApplicationBackend.getApplication("admin", this.state.applicationName)
|
||||
.then((res) => {
|
||||
if (res.data === null) {
|
||||
this.props.history.push("/404");
|
||||
return;
|
||||
}
|
||||
updateTransactionField(key, value) {
|
||||
value = this.parseTransactionField(key, value);
|
||||
|
||||
if (res.status === "error") {
|
||||
Setting.showMessage("error", res.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
const application = res.data;
|
||||
if (application.grantTypes === null || application.grantTypes === undefined || application.grantTypes.length === 0) {
|
||||
application.grantTypes = ["authorization_code"];
|
||||
}
|
||||
|
||||
if (application.tags === null || application.tags === undefined) {
|
||||
application.tags = [];
|
||||
}
|
||||
|
||||
this.setState({
|
||||
application: application,
|
||||
});
|
||||
|
||||
this.getCerts(application);
|
||||
|
||||
this.getSamlMetadata(application.enableSamlPostBinding);
|
||||
});
|
||||
const transaction = this.state.transaction;
|
||||
transaction[key] = value;
|
||||
this.setState({
|
||||
transaction: transaction,
|
||||
});
|
||||
}
|
||||
|
||||
renderTransaction() {
|
||||
const isRechargeMode = this.state.mode === "recharge";
|
||||
const title = isRechargeMode ? i18next.t("transaction:Recharge") : (this.state.mode === "add" ? i18next.t("transaction:New Transaction") : i18next.t("transaction:Edit Transaction"));
|
||||
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{this.state.mode === "add" ? i18next.t("transaction:New Transaction") : i18next.t("transaction:Edit Transaction")}
|
||||
{title}
|
||||
<Button onClick={() => this.submitTransactionEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitTransactionEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteTransaction()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
{(this.state.mode === "add" || isRechargeMode) ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteTransaction()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.owner} onChange={e => {
|
||||
// this.updatePaymentField('organization', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
{isRechargeMode ? (
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.transaction.owner}
|
||||
onChange={(value) => {
|
||||
this.updateTransactionField("owner", value);
|
||||
this.updateTransactionField("application", "");
|
||||
this.getApplications(value);
|
||||
this.getUsers(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 +235,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>
|
||||
@@ -181,84 +311,27 @@ class TransactionEditPage extends React.Component {
|
||||
}} />
|
||||
</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} 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);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("transaction:Tag - Tooltip"))} :
|
||||
</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);
|
||||
}} />
|
||||
</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>
|
||||
</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"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.transaction.user} onChange={e => {
|
||||
// this.updatePaymentField('amount', e.target.value);
|
||||
}} />
|
||||
{isRechargeMode ? (
|
||||
<Select virtual={false} style={{width: "100%"}}
|
||||
value={this.state.transaction.tag}
|
||||
onChange={(value) => {
|
||||
this.updateTransactionField("tag", value);
|
||||
if (value === "Organization") {
|
||||
this.updateTransactionField("user", "");
|
||||
}
|
||||
}}>
|
||||
<Option value="User">User</Option>
|
||||
<Option value="Organization">Organization</Option>
|
||||
</Select>
|
||||
) : (
|
||||
<Input disabled={true} value={this.state.transaction.tag} onChange={e => {
|
||||
}} />
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
@@ -266,19 +339,46 @@ class TransactionEditPage extends React.Component {
|
||||
{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);
|
||||
{isRechargeMode ? (
|
||||
<Select virtual={false} style={{width: "100%"}}
|
||||
value={this.state.transaction.user}
|
||||
disabled={this.state.transaction.tag === "Organization"}
|
||||
allowClear
|
||||
onChange={(value) => {
|
||||
this.updateTransactionField("user", value || "");
|
||||
}}>
|
||||
{
|
||||
this.state.users.map((user, index) => <Option key={index} value={user.name}>{user.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
) : (
|
||||
<Input disabled={true} value={this.state.transaction.user} onChange={e => {
|
||||
}} />
|
||||
)}
|
||||
</Col>
|
||||
</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={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("general:Application"), i18next.t("general:Application - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("currency:Currency"), i18next.t("currency:Currency - 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 +409,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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,37 +14,30 @@
|
||||
|
||||
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";
|
||||
import * as TransactionBackend from "./backend/TransactionBackend";
|
||||
import moment from "moment/moment";
|
||||
import {getTransactionTableColumns} from "./table/TransactionTableColumns";
|
||||
|
||||
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 +67,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,192 +80,54 @@ class TransactionListPage extends BaseListPage {
|
||||
});
|
||||
}
|
||||
|
||||
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",
|
||||
key: "owner",
|
||||
width: "120px",
|
||||
fixed: "left",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("owner"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/organizations/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Provider"),
|
||||
dataIndex: "provider",
|
||||
key: "provider",
|
||||
width: "150px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("provider"),
|
||||
render: (text, record, index) => {
|
||||
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) => {
|
||||
return (
|
||||
<Link to={`/users/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
rechargeTransaction() {
|
||||
const organizationName = Setting.getRequestOrganization(this.props.account);
|
||||
const newTransaction = {
|
||||
owner: organizationName,
|
||||
createdTime: moment().format(),
|
||||
application: this.props.account.signupApplication || "",
|
||||
domain: "",
|
||||
category: "Recharge",
|
||||
type: "",
|
||||
subtype: "",
|
||||
provider: "",
|
||||
user: this.props.account.name || "",
|
||||
tag: "User",
|
||||
amount: 100,
|
||||
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}`);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
title: i18next.t("general:Created time"),
|
||||
dataIndex: "createdTime",
|
||||
key: "createdTime",
|
||||
width: "160px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
renderTable(transactions) {
|
||||
const columns = getTransactionTableColumns({
|
||||
includeOrganization: true,
|
||||
includeUser: true,
|
||||
includeTag: true,
|
||||
includeActions: true,
|
||||
getColumnSearchProps: this.getColumnSearchProps,
|
||||
account: this.props.account,
|
||||
onEdit: (record, isAdmin) => {
|
||||
this.props.history.push({pathname: `/transactions/${record.owner}/${record.name}`, mode: isAdmin ? "edit" : "view"});
|
||||
},
|
||||
{
|
||||
title: i18next.t("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);
|
||||
},
|
||||
onDelete: (index) => {
|
||||
this.deleteTransaction(index);
|
||||
},
|
||||
{
|
||||
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",
|
||||
key: "application",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("application"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/applications/${record.owner}/${record.application}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Payment"),
|
||||
dataIndex: "payment",
|
||||
key: "payment",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("payment"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/payments/${record.owner}/${record.payment}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:State"),
|
||||
dataIndex: "state",
|
||||
key: "state",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("state"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
key: "op",
|
||||
width: "240px",
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
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>
|
||||
<PopconfirmModal
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteTransaction(index)}
|
||||
>
|
||||
</PopconfirmModal>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const paginationProps = {
|
||||
total: this.state.pagination.total,
|
||||
@@ -283,12 +139,17 @@ class TransactionListPage extends BaseListPage {
|
||||
return (
|
||||
<div>
|
||||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={transactions} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Transactions")}
|
||||
<Button type="primary" size="small" onClick={this.addTransaction.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
title={() => {
|
||||
const isAdmin = Setting.isLocalAdminUser(this.props.account);
|
||||
return (
|
||||
<div>
|
||||
{i18next.t("general:Transactions")}
|
||||
<Button size="small" disabled={!isAdmin} onClick={this.addTransaction.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||
|
||||
<Button type="primary" size="small" disabled={!isAdmin} onClick={this.rechargeTransaction.bind(this)}>{i18next.t("transaction:Recharge")}</Button>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
/>
|
||||
|
||||
@@ -316,11 +316,6 @@ class UserEditPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
let isKeysGenerated = false;
|
||||
if (this.state.user.accessKey !== "" && this.state.user.accessKey !== "") {
|
||||
isKeysGenerated = true;
|
||||
}
|
||||
|
||||
if (accountItem.name === "Organization") {
|
||||
return (
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
@@ -465,7 +460,7 @@ class UserEditPage extends React.Component {
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Email"), i18next.t("general:Email - Tooltip"))} :
|
||||
</Col>
|
||||
<Col style={{paddingRight: "20px"}} span={11} >
|
||||
<Col style={{paddingRight: "20px"}} span={5} >
|
||||
<Input
|
||||
value={this.state.user.email}
|
||||
style={{width: "280Px"}}
|
||||
@@ -475,7 +470,7 @@ class UserEditPage extends React.Component {
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={Setting.isMobile() ? 22 : 11} >
|
||||
<Col span={Setting.isMobile() ? 22 : 5} >
|
||||
{/* backend auto get the current user, so admin can not edit. Just self can reset*/}
|
||||
{this.isSelf() ? <ResetModal application={this.state.application} disabled={disabled} buttonText={i18next.t("user:Reset Email...")} destType={"email"} /> : null}
|
||||
</Col>
|
||||
@@ -487,7 +482,7 @@ class UserEditPage extends React.Component {
|
||||
<Col style={{marginTop: "5px"}} span={Setting.isMobile() ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Phone"), i18next.t("general:Phone - Tooltip"))} :
|
||||
</Col>
|
||||
<Col style={{paddingRight: "20px"}} span={11} >
|
||||
<Col style={{paddingRight: "20px"}} span={5} >
|
||||
<Input.Group compact style={{width: "280Px"}}>
|
||||
<CountryCodeSelect
|
||||
style={{width: "30%"}}
|
||||
@@ -506,7 +501,7 @@ class UserEditPage extends React.Component {
|
||||
}} />
|
||||
</Input.Group>
|
||||
</Col>
|
||||
<Col span={Setting.isMobile() ? 24 : 11} >
|
||||
<Col span={Setting.isMobile() ? 24 : 5} >
|
||||
{this.isSelf() ? (<ResetModal application={this.state.application} countryCode={this.getCountryCode()} disabled={disabled} buttonText={i18next.t("user:Reset Phone...")} destType={"phone"} />) : null}
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -750,6 +745,47 @@ class UserEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "Balance credit") {
|
||||
return (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("user:Balance credit"), i18next.t("user:Balance credit - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<InputNumber value={this.state.user.balanceCredit ?? 0} onChange={value => {
|
||||
this.updateUserField("balanceCredit", value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "Balance currency") {
|
||||
return (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<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 +871,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 +879,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 +1227,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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Space, Switch, Table, Upload} from "antd";
|
||||
import {Button, Modal, Space, Switch, Table, Upload} from "antd";
|
||||
import {UploadOutlined} from "@ant-design/icons";
|
||||
import moment from "moment";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
@@ -24,6 +24,7 @@ import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
import AccountAvatar from "./account/AccountAvatar";
|
||||
import * as XLSX from "xlsx";
|
||||
|
||||
class UserListPage extends BaseListPage {
|
||||
constructor(props) {
|
||||
@@ -87,6 +88,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",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -148,19 +150,27 @@ class UserListPage extends BaseListPage {
|
||||
}
|
||||
|
||||
uploadFile(info) {
|
||||
const {status, response: res} = info.file;
|
||||
if (status === "done") {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", "Users uploaded successfully, refreshing the page");
|
||||
|
||||
const {pagination} = this.state;
|
||||
this.fetch({pagination});
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to upload")}: ${res.msg}`);
|
||||
}
|
||||
const {status, msg} = info;
|
||||
if (status === "ok") {
|
||||
Setting.showMessage("success", "Users uploaded successfully, refreshing the page");
|
||||
const {pagination} = this.state;
|
||||
this.fetch({pagination});
|
||||
} else if (status === "error") {
|
||||
Setting.showMessage("error", i18next.t("general:Failed to upload"));
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to upload")}: ${msg}`);
|
||||
}
|
||||
this.setState({uploadJsonData: [], uploadColumns: [], showUploadModal: false});
|
||||
}
|
||||
|
||||
generateDownloadTemplate() {
|
||||
const userObj = {};
|
||||
const items = Setting.getUserColumns();
|
||||
items.forEach((item) => {
|
||||
userObj[item] = null;
|
||||
});
|
||||
const worksheet = XLSX.utils.json_to_sheet([userObj]);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1");
|
||||
XLSX.writeFile(workbook, "import-user.xlsx", {compression: true});
|
||||
}
|
||||
|
||||
getOrganization(organizationName) {
|
||||
@@ -177,23 +187,82 @@ class UserListPage extends BaseListPage {
|
||||
}
|
||||
|
||||
renderUpload() {
|
||||
const uploadThis = this;
|
||||
const props = {
|
||||
name: "file",
|
||||
accept: ".xlsx",
|
||||
method: "post",
|
||||
action: `${Setting.ServerUrl}/api/upload-users`,
|
||||
withCredentials: true,
|
||||
onChange: (info) => {
|
||||
this.uploadFile(info);
|
||||
showUploadList: false,
|
||||
beforeUpload: (file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const binary = e.target.result;
|
||||
|
||||
try {
|
||||
const workbook = XLSX.read(binary, {type: "array"});
|
||||
if (!workbook.SheetNames || workbook.SheetNames.length === 0) {
|
||||
Setting.showMessage("error", i18next.t("general:No sheets found in file"));
|
||||
return;
|
||||
}
|
||||
|
||||
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet);
|
||||
this.setState({uploadJsonData: jsonData, file: file});
|
||||
|
||||
const columns = Setting.getUserColumns().map(el => {
|
||||
return {title: el.split("#")[0], dataIndex: el, key: el};
|
||||
});
|
||||
this.setState({uploadColumns: columns}, () => {this.setState({showUploadModal: true});});
|
||||
} catch (err) {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to upload")}: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = (error) => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to upload")}: ${error?.message || error}`);
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Upload {...props}>
|
||||
<Button icon={<UploadOutlined />} id="upload-button" size="small">
|
||||
{i18next.t("user:Upload (.xlsx)")}
|
||||
</Button>
|
||||
</Upload>
|
||||
<>
|
||||
<Upload {...props}>
|
||||
<Button icon={<UploadOutlined />} id="upload-button" size="small">
|
||||
{i18next.t("user:Upload (.xlsx)")}
|
||||
</Button>
|
||||
</Upload>
|
||||
<Modal title={i18next.t("user:Upload (.xlsx)")}
|
||||
width={"100%"}
|
||||
closable={true}
|
||||
open={this.state.showUploadModal}
|
||||
okText={i18next.t("general:Click to Upload")}
|
||||
onOk = {() => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", this.state.file);
|
||||
fetch(`${Setting.ServerUrl}/api/upload-users`, {
|
||||
method: "post",
|
||||
body: formData,
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {uploadThis.uploadFile(res);})
|
||||
.catch((error) => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to upload")}: ${error.message}`);
|
||||
});
|
||||
}}
|
||||
cancelText={i18next.t("general:Cancel")}
|
||||
onCancel={() => {this.setState({showUploadModal: false, uploadJsonData: [], uploadColumns: []});}}
|
||||
>
|
||||
<div style={{marginRight: "34px"}}>
|
||||
<Table scroll={{x: "max-content"}} dataSource={this.state.uploadJsonData} columns={this.state.uploadColumns} />
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -375,6 +444,26 @@ class UserListPage extends BaseListPage {
|
||||
return text ?? 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("user:Balance credit"),
|
||||
dataIndex: "balanceCredit",
|
||||
key: "balanceCredit",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return text ?? 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("user:Balance currency"),
|
||||
dataIndex: "balanceCurrency",
|
||||
key: "balanceCurrency",
|
||||
width: "140px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return text || "USD";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("user:Is admin"),
|
||||
dataIndex: "isAdmin",
|
||||
@@ -383,7 +472,7 @@ class UserListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -395,7 +484,7 @@ class UserListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -407,7 +496,7 @@ class UserListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -462,6 +551,7 @@ class UserListPage extends BaseListPage {
|
||||
<div>
|
||||
{i18next.t("general:Users")}
|
||||
<Button style={{marginRight: "15px"}} type="primary" size="small" onClick={this.addUser.bind(this)}>{i18next.t("general:Add")} </Button>
|
||||
<Button style={{marginRight: "15px"}} type="primary" size="small" onClick={this.generateDownloadTemplate}>{i18next.t("general:Download template")} </Button>
|
||||
{
|
||||
this.renderUpload()
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ class VerificationListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -175,7 +175,7 @@ class WebhookListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -187,7 +187,7 @@ class WebhookListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -200,7 +200,7 @@ class WebhookListPage extends BaseListPage {
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
|
||||
<Switch disabled checkedChildren={i18next.t("general:ON")} unCheckedChildren={i18next.t("general:OFF")} checked={text} />
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -290,6 +290,10 @@ const authInfo = {
|
||||
scope: "users.read%20tweet.read",
|
||||
endpoint: "https://twitter.com/i/oauth2/authorize",
|
||||
},
|
||||
Telegram: {
|
||||
scope: "",
|
||||
endpoint: "https://core.telegram.org/widgets/login",
|
||||
},
|
||||
Typetalk: {
|
||||
scope: "my",
|
||||
endpoint: "https://typetalk.com/oauth2/authorize",
|
||||
|
||||
@@ -57,7 +57,7 @@ const renderFormItem = (signupItem) => {
|
||||
rules: [
|
||||
{
|
||||
required: signupItem.required,
|
||||
message: i18next.t(`signup:Please input your ${signupItem.label || signupItem.name}!`),
|
||||
message: i18next.t("signup:Please input your {label}!").replace("{label}", signupItem.label || signupItem.name),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -655,7 +655,7 @@ class SignupPage extends React.Component {
|
||||
}
|
||||
} else if (signupItem.name === "Password") {
|
||||
return (
|
||||
<Popover placement={window.innerWidth >= 960 ? "right" : "top"} content={this.state.passwordPopover} open={this.state.passwordPopoverOpen}>
|
||||
<Popover placement={"top"} content={this.state.passwordPopover} open={this.state.passwordPopoverOpen}>
|
||||
<Form.Item
|
||||
name="password"
|
||||
className="signup-password"
|
||||
|
||||
111
web/src/backend/OrderBackend.js
Normal file
111
web/src/backend/OrderBackend.js
Normal file
@@ -0,0 +1,111 @@
|
||||
// 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 payOrder(owner, name, providerName, paymentEnv = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/pay-order?id=${owner}/${encodeURIComponent(name)}&providerName=${encodeURIComponent(providerName)}&paymentEnv=${paymentEnv}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getUserOrders(owner, user) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-user-orders?owner=${owner}&user=${user}`, {
|
||||
method: "GET",
|
||||
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());
|
||||
}
|
||||
|
||||
export function placeOrder(productId, pricingName = "", planName = "", userName = "", customPrice = 0) {
|
||||
return fetch(`${Setting.ServerUrl}/api/place-order?productId=${encodeURIComponent(productId)}&pricingName=${encodeURIComponent(pricingName)}&planName=${encodeURIComponent(planName)}&userName=${encodeURIComponent(userName)}&customPrice=${customPrice}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function cancelOrder(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/cancel-order?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
@@ -69,13 +69,3 @@ export function deleteProduct(product) {
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function buyProduct(owner, name, providerName, pricingName = "", planName = "", userName = "", paymentEnv = "", customPrice = 0) {
|
||||
return fetch(`${Setting.ServerUrl}/api/buy-product?id=${owner}/${encodeURIComponent(name)}&providerName=${providerName}&pricingName=${pricingName}&planName=${planName}&userName=${userName}&paymentEnv=${paymentEnv}&customPrice=${customPrice}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user