forked from casdoor/casdoor
Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ddb87cdf8 | ||
|
|
fac45f5ac7 | ||
|
|
266d361244 | ||
|
|
b454ab1931 | ||
|
|
ff39b6f186 | ||
|
|
0597dbbe20 | ||
|
|
49c417c70e | ||
|
|
8b30e12915 | ||
|
|
2e18c65429 | ||
|
|
27c98bb056 | ||
|
|
4400b66862 | ||
|
|
e7e7d18ee7 | ||
|
|
66d1e28300 | ||
|
|
53782a6706 | ||
|
|
30bb0ce92f | ||
|
|
29f7dda858 | ||
|
|
68b82ed524 | ||
|
|
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 | ||
|
|
7e6929b900 | ||
|
|
46ae1a9580 | ||
|
|
37e22f3e2c | ||
|
|
68cde65d84 | ||
|
|
1c7f5fdfe4 | ||
|
|
1a5be46325 |
@@ -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()
|
||||
|
||||
@@ -276,7 +276,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
Application: application.Name,
|
||||
SessionId: []string{c.Ctx.Input.CruSession.SessionID()},
|
||||
|
||||
ExclusiveSignin: true,
|
||||
ExclusiveSignin: application.EnableExclusiveSignin,
|
||||
})
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -234,10 +234,19 @@ func (c *ApiController) SendInvitation() {
|
||||
return
|
||||
}
|
||||
|
||||
application, err := object.GetApplicationByOrganizationName(invitation.Owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
var application *object.Application
|
||||
if invitation.Application != "" {
|
||||
application, err = object.GetApplication(fmt.Sprintf("admin/%s-org-%s", invitation.Application, invitation.Owner))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
application, err = object.GetApplicationByOrganizationName(invitation.Owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
@@ -245,6 +254,10 @@ func (c *ApiController) SendInvitation() {
|
||||
return
|
||||
}
|
||||
|
||||
if application.IsShared {
|
||||
application.Name = fmt.Sprintf("%s-org-%s", application.Name, invitation.Owner)
|
||||
}
|
||||
|
||||
provider, err := application.GetEmailProvider("Invitation")
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,26 @@ func (c *ApiController) GetTransactions() {
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
transactions, err := object.GetTransactions(owner)
|
||||
var transactions []*object.Transaction
|
||||
var err error
|
||||
|
||||
if c.IsAdmin() {
|
||||
// If field is "user", filter by that user even for admins
|
||||
if field == "user" && value != "" {
|
||||
transactions, err = object.GetUserTransactions(owner, value)
|
||||
} else {
|
||||
transactions, err = object.GetTransactions(owner)
|
||||
}
|
||||
} else {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
transactions, err = object.GetUserTransactions(owner, userName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -48,6 +67,19 @@ func (c *ApiController) GetTransactions() {
|
||||
c.ResponseOk(transactions)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
|
||||
// Apply user filter for non-admin users
|
||||
if !c.IsAdmin() {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
field = "user"
|
||||
value = userName
|
||||
}
|
||||
|
||||
count, err := object.GetTransactionCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@@ -81,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)
|
||||
}
|
||||
|
||||
@@ -111,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() {
|
||||
@@ -121,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
|
||||
|
||||
@@ -603,7 +603,7 @@ func (c *ApiController) SetPassword() {
|
||||
}
|
||||
}
|
||||
|
||||
msg := object.CheckPasswordComplexity(targetUser, newPassword)
|
||||
msg := object.CheckPasswordComplexity(targetUser, newPassword, c.GetAcceptLanguage())
|
||||
if msg != "" {
|
||||
c.ResponseError(msg)
|
||||
return
|
||||
@@ -777,3 +777,133 @@ func (c *ApiController) RemoveUserFromGroup() {
|
||||
|
||||
c.ResponseOk(affected)
|
||||
}
|
||||
|
||||
// VerifyIdentification
|
||||
// @Title VerifyIdentification
|
||||
// @Tag User API
|
||||
// @Description verify user's real identity using ID Verification provider
|
||||
// @Param owner query string false "The owner of the user (optional, defaults to logged-in user)"
|
||||
// @Param name query string false "The name of the user (optional, defaults to logged-in user)"
|
||||
// @Param provider query string false "The name of the ID Verification provider (optional, auto-selected if not provided)"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /verify-identification [post]
|
||||
func (c *ApiController) VerifyIdentification() {
|
||||
owner := c.Input().Get("owner")
|
||||
name := c.Input().Get("name")
|
||||
providerName := c.Input().Get("provider")
|
||||
|
||||
// If user not specified, use logged-in user
|
||||
if owner == "" || name == "" {
|
||||
loggedInUser := c.GetSessionUsername()
|
||||
if loggedInUser == "" {
|
||||
c.ResponseError(c.T("general:Please login first"))
|
||||
return
|
||||
}
|
||||
var err error
|
||||
owner, name, err = util.GetOwnerAndNameFromIdWithError(loggedInUser)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// If user is specified, check if current user has permission to verify other users
|
||||
// Only admins can verify other users
|
||||
loggedInUser := c.GetSessionUsername()
|
||||
if loggedInUser != util.GetId(owner, name) && !c.IsAdmin() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
user, err := object.GetUser(util.GetId(owner, name))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(owner, name)))
|
||||
return
|
||||
}
|
||||
|
||||
if user.IdCard == "" || user.IdCardType == "" || user.RealName == "" {
|
||||
c.ResponseError(c.T("user:ID card information and real name are required"))
|
||||
return
|
||||
}
|
||||
|
||||
if user.IsVerified {
|
||||
c.ResponseError(c.T("user:User is already verified"))
|
||||
return
|
||||
}
|
||||
|
||||
var provider *object.Provider
|
||||
// If provider not specified, find suitable IDV provider from user's application
|
||||
if providerName == "" {
|
||||
application, err := object.GetApplicationByUser(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
c.ResponseError(c.T("user:No application found for user"))
|
||||
return
|
||||
}
|
||||
|
||||
// Find IDV provider from application
|
||||
idvProvider, err := object.GetIdvProviderByApplication(util.GetId(application.Owner, application.Name), "false", c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if idvProvider == nil {
|
||||
c.ResponseError(c.T("provider:No ID Verification provider configured"))
|
||||
return
|
||||
}
|
||||
provider = idvProvider
|
||||
} else {
|
||||
provider, err = object.GetProvider(providerName)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if provider == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("provider:The provider: %s does not exist"), providerName))
|
||||
return
|
||||
}
|
||||
|
||||
if provider.Category != "ID Verification" {
|
||||
c.ResponseError(c.T("provider:Provider is not an ID Verification provider"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
idvProvider := object.GetIdvProviderFromProvider(provider)
|
||||
if idvProvider == nil {
|
||||
c.ResponseError(c.T("provider:Failed to initialize ID Verification provider"))
|
||||
return
|
||||
}
|
||||
|
||||
verified, err := idvProvider.VerifyIdentity(user.IdCardType, user.IdCard, user.RealName)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !verified {
|
||||
c.ResponseError(c.T("user:Identity verification failed"))
|
||||
return
|
||||
}
|
||||
|
||||
// Set IsVerified to true upon successful verification
|
||||
user.IsVerified = true
|
||||
_, err = object.UpdateUser(user.GetId(), user, []string{"is_verified"}, false)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(user.RealName)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !skipCi
|
||||
// +build !skipCi
|
||||
|
||||
package deployment
|
||||
|
||||
|
||||
16
go.mod
16
go.mod
@@ -4,7 +4,10 @@ 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/cloudauth-20190307/v3 v3.9.2
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.4
|
||||
github.com/alibabacloud-go/facebody-20191230/v5 v5.1.2
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0
|
||||
@@ -15,7 +18,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 +46,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 +57,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 +129,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 +158,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 +175,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 +204,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
|
||||
|
||||
40
go.sum
40
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=
|
||||
@@ -106,6 +110,8 @@ github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do2
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
|
||||
github.com/alibabacloud-go/cloudauth-20190307/v3 v3.9.2 h1:y4s0WQ1jrBtOJfXGgsv/83brJvkkHbFdORp0WDyVAuw=
|
||||
github.com/alibabacloud-go/cloudauth-20190307/v3 v3.9.2/go.mod h1:kD75qqMQyjCiz6lssjRzYGTumcli8STLXQstVe6ytxk=
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
|
||||
@@ -116,6 +122,7 @@ github.com/alibabacloud-go/darabonba-number v1.0.4 h1:aTY1TanasI0A1AYT3Co+PLttFS
|
||||
github.com/alibabacloud-go/darabonba-number v1.0.4/go.mod h1:9NJbJwLCPxHzFwYqnr27G2X8pSTAz0uSQEJsrjr/kqw=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.0/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.6/go.mod h1:CzQnh+94WDnJOnKZH5YRyouL+OOcdBnXY5VWAf0McgI=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10/go.mod h1:26a14FGhZVELuz2cc2AolvW4RHmIO3/HRwsdHhaIPDE=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.4 h1:IGSZHlOnWwBbLtX5xDplQvZOH0nkrV7Wmq+Fto7JK5w=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.4/go.mod h1:Wxis0IBFusdbo44HO6KYYCJR1rRkoh47QQOYWvaheSU=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
|
||||
@@ -159,6 +166,7 @@ github.com/alibabacloud-go/tea-utils v1.3.6 h1:bVjrxHztM8hAs6nOfLWCgxQfAtKb9RgFF
|
||||
github.com/alibabacloud-go/tea-utils v1.3.6/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.0/go.mod h1:U5MTY10WwlquGPS34DOeomUGBB0gXbLueiq5Trwu0C4=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.1/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
||||
@@ -233,8 +241,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 +358,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 +546,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 +565,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 +611,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 +632,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 +700,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 +711,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 +807,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 +915,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 +939,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 +1299,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
|
||||
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "يرجى التسجيل باستخدام اسم المستخدم المطابق لرمز الدعوة",
|
||||
"Session outdated, please login again": "الجلسة منتهية الصلاحية، يرجى تسجيل الدخول مرة أخرى",
|
||||
"The invitation code has already been used": "رمز الدعوة تم استخدامه بالفعل",
|
||||
"The password must contain at least one special character": "يجب أن تحتوي كلمة المرور على حرف خاص واحد على الأقل",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "يجب أن تحتوي كلمة المرور على حرف كبير واحد على الأقل وحرف صغير ورقم",
|
||||
"The password must have at least 6 characters": "يجب أن تحتوي كلمة المرور على 6 أحرف على الأقل",
|
||||
"The password must have at least 8 characters": "يجب أن تحتوي كلمة المرور على 8 أحرف على الأقل",
|
||||
"The password must not contain any repeated characters": "يجب ألا تحتوي كلمة المرور على أي أحرف متكررة",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "تم حذف المستخدم ولا يمكن استخدامه لتسجيل الدخول، يرجى الاتصال بالمسؤول",
|
||||
"The user is forbidden to sign in, please contact the administrator": "المستخدم ممنوع من تسجيل الدخول، يرجى الاتصال بالمسؤول",
|
||||
"The user: %s doesn't exist in LDAP server": "المستخدم: %s غير موجود في خادم LDAP",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Xahiş edirik dəvət koduna uyğun istifadəçi adı istifadə edərək qeydiyyatdan keçin",
|
||||
"Session outdated, please login again": "Sessiyanın vaxtı keçib, xahiş edirik yenidən daxil olun",
|
||||
"The invitation code has already been used": "Dəvət kodu artıq istifadə edilib",
|
||||
"The password must contain at least one special character": "Parol ən azı bir xüsusi simvol ehtiva etməlidir",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Parol ən azı bir böyük hərf, bir kiçik hərf və bir rəqəm ehtiva etməlidir",
|
||||
"The password must have at least 6 characters": "Parol ən azı 6 simvoldan ibarət olmalıdır",
|
||||
"The password must have at least 8 characters": "Parol ən azı 8 simvoldan ibarət olmalıdır",
|
||||
"The password must not contain any repeated characters": "Parol təkrarlanan simvollar ehtiva etməməlidir",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "İstifadəçi silinib və daxil olmaq üçün istifadə edilə bilməz, zəhmət olmasa administratorla əlaqə saxlayın",
|
||||
"The user is forbidden to sign in, please contact the administrator": "İstifadəçinin girişi qadağandır, xahiş edirik administratorla əlaqə saxlayın",
|
||||
"The user: %s doesn't exist in LDAP server": "İstifadəçi: %s LDAP serverində mövcud deyil",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Prosím registrujte se pomocí uživatelského jména odpovídajícího pozvánkovému kódu",
|
||||
"Session outdated, please login again": "Relace je zastaralá, prosím přihlaste se znovu",
|
||||
"The invitation code has already been used": "Pozvánkový kód již byl použit",
|
||||
"The password must contain at least one special character": "Heslo musí obsahovat alespoň jeden speciální znak",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Heslo musí obsahovat alespoň jedno velké písmeno, jedno malé písmeno a jednu číslici",
|
||||
"The password must have at least 6 characters": "Heslo musí mít alespoň 6 znaků",
|
||||
"The password must have at least 8 characters": "Heslo musí mít alespoň 8 znaků",
|
||||
"The password must not contain any repeated characters": "Heslo nesmí obsahovat opakující se znaky",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "Uživatel byl odstraněn a nelze jej použít k přihlášení, kontaktujte prosím správce",
|
||||
"The user is forbidden to sign in, please contact the administrator": "Uživatel má zakázáno se přihlásit, prosím kontaktujte administrátora",
|
||||
"The user: %s doesn't exist in LDAP server": "Uživatel: %s neexistuje na LDAP serveru",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Bitte registrieren Sie sich mit dem Benutzernamen, der zum Einladungscode gehört",
|
||||
"Session outdated, please login again": "Sitzung abgelaufen, bitte erneut anmelden",
|
||||
"The invitation code has already been used": "Der Einladungscode wurde bereits verwendet",
|
||||
"The password must contain at least one special character": "Das Passwort muss mindestens ein Sonderzeichen enthalten",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Das Passwort muss mindestens einen Großbuchstaben, einen Kleinbuchstaben und eine Ziffer enthalten",
|
||||
"The password must have at least 6 characters": "Das Passwort muss mindestens 6 Zeichen haben",
|
||||
"The password must have at least 8 characters": "Das Passwort muss mindestens 8 Zeichen haben",
|
||||
"The password must not contain any repeated characters": "Das Passwort darf keine wiederholten Zeichen enthalten",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "Der Benutzer wurde gelöscht und kann nicht zur Anmeldung verwendet werden. Bitte wenden Sie sich an den Administrator",
|
||||
"The user is forbidden to sign in, please contact the administrator": "Dem Benutzer ist der Zugang verboten, bitte kontaktieren Sie den Administrator",
|
||||
"The user: %s doesn't exist in LDAP server": "Der Benutzer: %s existiert nicht im LDAP-Server",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Please register using the username corresponding to the invitation code",
|
||||
"Session outdated, please login again": "Session outdated, please login again",
|
||||
"The invitation code has already been used": "The invitation code has already been used",
|
||||
"The password must contain at least one special character": "The password must contain at least one special character",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
|
||||
"The password must have at least 6 characters": "The password must have at least 6 characters",
|
||||
"The password must have at least 8 characters": "The password must have at least 8 characters",
|
||||
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "The user has been deleted and cannot be used to sign in, please contact the administrator",
|
||||
"The user is forbidden to sign in, please contact the administrator": "The user is forbidden to sign in, please contact the administrator",
|
||||
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Regístrese usando el nombre de usuario correspondiente al código de invitación",
|
||||
"Session outdated, please login again": "Sesión expirada, por favor vuelva a iniciar sesión",
|
||||
"The invitation code has already been used": "El código de invitación ya ha sido utilizado",
|
||||
"The password must contain at least one special character": "La contraseña debe contener al menos un carácter especial",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "La contraseña debe contener al menos una letra mayúscula, una letra minúscula y un dígito",
|
||||
"The password must have at least 6 characters": "La contraseña debe tener al menos 6 caracteres",
|
||||
"The password must have at least 8 characters": "La contraseña debe tener al menos 8 caracteres",
|
||||
"The password must not contain any repeated characters": "La contraseña no debe contener caracteres repetidos",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "El usuario ha sido eliminado y no se puede usar para iniciar sesión, póngase en contacto con el administrador",
|
||||
"The user is forbidden to sign in, please contact the administrator": "El usuario no está autorizado a iniciar sesión, por favor contacte al administrador",
|
||||
"The user: %s doesn't exist in LDAP server": "El usuario: %s no existe en el servidor LDAP",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "لطفاً با استفاده از نام کاربری مربوط به کد دعوت ثبتنام کنید",
|
||||
"Session outdated, please login again": "جلسه منقضی شده است، لطفاً دوباره وارد شوید",
|
||||
"The invitation code has already been used": "کد دعوت قبلاً استفاده شده است",
|
||||
"The password must contain at least one special character": "رمز عبور باید حداقل یک کاراکتر خاص داشته باشد",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "رمز عبور باید حداقل یک حرف بزرگ، یک حرف کوچک و یک رقم داشته باشد",
|
||||
"The password must have at least 6 characters": "رمز عبور باید حداقل 6 کاراکتر داشته باشد",
|
||||
"The password must have at least 8 characters": "رمز عبور باید حداقل 8 کاراکتر داشته باشد",
|
||||
"The password must not contain any repeated characters": "رمز عبور نباید شامل کاراکترهای تکراری باشد",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "کاربر حذف شده است و نمی توان از آن برای ورود استفاده کرد، لطفا با مدیر تماس بگیرید",
|
||||
"The user is forbidden to sign in, please contact the administrator": "ورود کاربر ممنوع است، لطفاً با مدیر تماس بگیرید",
|
||||
"The user: %s doesn't exist in LDAP server": "کاربر: %s در سرور LDAP وجود ندارد",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Rekisteröidy käyttämällä kutsukoodiin vastaavaa käyttäjänimeä",
|
||||
"Session outdated, please login again": "Istunto vanhentunut, kirjaudu uudelleen",
|
||||
"The invitation code has already been used": "Kutsukoodi on jo käytetty",
|
||||
"The password must contain at least one special character": "Salasanan on sisällettävä vähintään yksi erikoismerkki",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Salasanan on sisällettävä vähintään yksi iso kirjain, yksi pieni kirjain ja yksi numero",
|
||||
"The password must have at least 6 characters": "Salasanassa on oltava vähintään 6 merkkiä",
|
||||
"The password must have at least 8 characters": "Salasanassa on oltava vähintään 8 merkkiä",
|
||||
"The password must not contain any repeated characters": "Salasana ei saa sisältää toistuvia merkkejä",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "Käyttäjä on poistettu eikä sitä voi käyttää kirjautumiseen, ota yhteyttä järjestelmänvalvojaan",
|
||||
"The user is forbidden to sign in, please contact the administrator": "Käyttäjän kirjautuminen on estetty, ota yhteyttä ylläpitäjään",
|
||||
"The user: %s doesn't exist in LDAP server": "Käyttäjä: %s ei ole olemassa LDAP-palvelimessa",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Veuillez vous inscrire avec le nom d'utilisateur correspondant au code d'invitation",
|
||||
"Session outdated, please login again": "Session expirée, veuillez vous connecter à nouveau",
|
||||
"The invitation code has already been used": "Le code d'invitation a déjà été utilisé",
|
||||
"The password must contain at least one special character": "Le mot de passe doit contenir au moins un caractère spécial",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Le mot de passe doit contenir au moins une lettre majuscule, une lettre minuscule et un chiffre",
|
||||
"The password must have at least 6 characters": "Le mot de passe doit contenir au moins 6 caractères",
|
||||
"The password must have at least 8 characters": "Le mot de passe doit contenir au moins 8 caractères",
|
||||
"The password must not contain any repeated characters": "Le mot de passe ne doit pas contenir de caractères répétés",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "L'utilisateur a été supprimé et ne peut pas être utilisé pour se connecter, veuillez contacter l'administrateur",
|
||||
"The user is forbidden to sign in, please contact the administrator": "L'utilisateur est interdit de se connecter, veuillez contacter l'administrateur",
|
||||
"The user: %s doesn't exist in LDAP server": "L'utilisateur : %s n'existe pas sur le serveur LDAP",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "אנא הרשם באמצעות שם המשתמש התואם לקוד ההזמנה",
|
||||
"Session outdated, please login again": "הסשן פג תוקף, אנא התחבר שוב",
|
||||
"The invitation code has already been used": "קוד ההזמנה כבר נוצל",
|
||||
"The password must contain at least one special character": "הסיסמה חייבת להכיל לפחות תו מיוחד אחד",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "הסיסמה חייבת להכיל לפחות אות גדולה אחת, אות קטנה אחת וספרה אחת",
|
||||
"The password must have at least 6 characters": "הסיסמה חייבת להכיל לפחות 6 תווים",
|
||||
"The password must have at least 8 characters": "הסיסמה חייבת להכיל לפחות 8 תווים",
|
||||
"The password must not contain any repeated characters": "הסיסמה אינה יכולה להכיל תווים חוזרים",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "המשתמש נמחק ולא ניתן להשתמש בו לכניסה, אנא צור קשר עם המנהל",
|
||||
"The user is forbidden to sign in, please contact the administrator": "המשתמש אסור להיכנס, אנא צור קשר עם המנהל",
|
||||
"The user: %s doesn't exist in LDAP server": "המשתמש: %s אינו קיים בשרת ה-LDAP",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Silakan daftar menggunakan nama pengguna yang sesuai dengan kode undangan",
|
||||
"Session outdated, please login again": "Sesi kadaluwarsa, silakan masuk lagi",
|
||||
"The invitation code has already been used": "Kode undangan sudah digunakan",
|
||||
"The password must contain at least one special character": "Kata sandi harus berisi setidaknya satu karakter khusus",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Kata sandi harus berisi setidaknya satu huruf besar, satu huruf kecil dan satu angka",
|
||||
"The password must have at least 6 characters": "Kata sandi harus memiliki setidaknya 6 karakter",
|
||||
"The password must have at least 8 characters": "Kata sandi harus memiliki setidaknya 8 karakter",
|
||||
"The password must not contain any repeated characters": "Kata sandi tidak boleh berisi karakter yang berulang",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "Pengguna telah dihapus dan tidak dapat digunakan untuk masuk, silakan hubungi administrator",
|
||||
"The user is forbidden to sign in, please contact the administrator": "Pengguna dilarang masuk, silakan hubungi administrator",
|
||||
"The user: %s doesn't exist in LDAP server": "Pengguna: %s tidak ada di server LDAP",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Registrati con il nome utente corrispondente al codice di invito",
|
||||
"Session outdated, please login again": "Sessione scaduta, rieffettua il login",
|
||||
"The invitation code has already been used": "Il codice di invito è già stato utilizzato",
|
||||
"The password must contain at least one special character": "La password deve contenere almeno un carattere speciale",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "La password deve contenere almeno una lettera maiuscola, una lettera minuscola e una cifra",
|
||||
"The password must have at least 6 characters": "La password deve avere almeno 6 caratteri",
|
||||
"The password must have at least 8 characters": "La password deve avere almeno 8 caratteri",
|
||||
"The password must not contain any repeated characters": "La password non deve contenere caratteri ripetuti",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "L'utente è stato eliminato e non può essere utilizzato per accedere, contattare l'amministratore",
|
||||
"The user is forbidden to sign in, please contact the administrator": "Utente bloccato, contatta l'amministratore",
|
||||
"The user: %s doesn't exist in LDAP server": "L'utente: %s non esiste nel server LDAP",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "招待コードに対応するユーザー名で登録してください",
|
||||
"Session outdated, please login again": "セッションが期限切れになりました。再度ログインしてください",
|
||||
"The invitation code has already been used": "この招待コードは既に使用されています",
|
||||
"The password must contain at least one special character": "パスワードには少なくとも1つの特殊文字が必要です",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "パスワードには少なくとも1つの大文字、1つの小文字、1つの数字が必要です",
|
||||
"The password must have at least 6 characters": "パスワードは少なくとも6文字必要です",
|
||||
"The password must have at least 8 characters": "パスワードは少なくとも8文字必要です",
|
||||
"The password must not contain any repeated characters": "パスワードに繰り返し文字を含めることはできません",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "ユーザーは削除されており、サインインに使用できません。管理者にお問い合わせください",
|
||||
"The user is forbidden to sign in, please contact the administrator": "ユーザーはサインインできません。管理者に連絡してください",
|
||||
"The user: %s doesn't exist in LDAP server": "ユーザー「%s」は LDAP サーバーに存在しません",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Registreer met de gebruikersnaam die hoort bij de uitnodigingscode",
|
||||
"Session outdated, please login again": "Sessie verlopen, gelieve opnieuw in te loggen",
|
||||
"The invitation code has already been used": "Uitnodigingscode is al gebruikt",
|
||||
"The password must contain at least one special character": "Құпия сөз кемінде бір арнайы таңбаны қамтуы керек",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Құпия сөз кемінде бір бас әріпті, бір кіші әріпті және бір санды қамтуы керек",
|
||||
"The password must have at least 6 characters": "Құпия сөз кемінде 6 таңбадан тұруы керек",
|
||||
"The password must have at least 8 characters": "Құпия сөз кемінде 8 таңбадан тұруы керек",
|
||||
"The password must not contain any repeated characters": "Құпия сөз қайталанатын таңбаларды қамтымауы керек",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "Пайдаланушы жойылған және кіру үшін пайдалануға болмайды, әкімшіге хабарласыңыз",
|
||||
"The user is forbidden to sign in, please contact the administrator": "Gebruiker mag niet inloggen, contacteer beheerder",
|
||||
"The user: %s doesn't exist in LDAP server": "Gebruiker: %s bestaat niet in LDAP-server",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "초대 코드에 해당하는 사용자 이름으로 가입해 주세요",
|
||||
"Session outdated, please login again": "세션이 만료되었습니다. 다시 로그인해주세요",
|
||||
"The invitation code has already been used": "초대 코드는 이미 사용되었습니다",
|
||||
"The password must contain at least one special character": "비밀번호에는 하나 이상의 특수 문자가 포함되어야 합니다",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "비밀번호에는 하나 이상의 대문자, 소문자 및 숫자가 포함되어야 합니다",
|
||||
"The password must have at least 6 characters": "비밀번호는 최소 6자 이상이어야 합니다",
|
||||
"The password must have at least 8 characters": "비밀번호는 최소 8자 이상이어야 합니다",
|
||||
"The password must not contain any repeated characters": "비밀번호에는 반복되는 문자가 포함될 수 없습니다",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "사용자가 삭제되어 로그인에 사용할 수 없습니다. 관리자에게 문의하세요",
|
||||
"The user is forbidden to sign in, please contact the administrator": "사용자는 로그인이 금지되어 있습니다. 관리자에게 문의하십시오",
|
||||
"The user: %s doesn't exist in LDAP server": "LDAP 서버에 사용자 %s이(가) 존재하지 않습니다",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Sila daftar dengan nama pengguna yang sepadan dengan kod jemputan",
|
||||
"Session outdated, please login again": "Sesi tamat, sila log masuk semula",
|
||||
"The invitation code has already been used": "Kod jemputan sudah digunakan",
|
||||
"The password must contain at least one special character": "Kata laluan mesti mengandungi sekurang-kurangnya satu aksara khas",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Kata laluan mesti mengandungi sekurang-kurangnya satu huruf besar, satu huruf kecil dan satu digit",
|
||||
"The password must have at least 6 characters": "Kata laluan mesti mempunyai sekurang-kurangnya 6 aksara",
|
||||
"The password must have at least 8 characters": "Kata laluan mesti mempunyai sekurang-kurangnya 8 aksara",
|
||||
"The password must not contain any repeated characters": "Kata laluan tidak boleh mengandungi aksara berulang",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "Pengguna telah dipadamkan dan tidak boleh digunakan untuk log masuk, sila hubungi pentadbir",
|
||||
"The user is forbidden to sign in, please contact the administrator": "Pengguna dilarang log masuk, sila hubungi pentadbir",
|
||||
"The user: %s doesn't exist in LDAP server": "Pengguna: %s tidak wujud dalam pelayan LDAP",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Registreer met de gebruikersnaam die bij de code hoort",
|
||||
"Session outdated, please login again": "Sessie verlopen, log opnieuw in",
|
||||
"The invitation code has already been used": "Code al gebruikt",
|
||||
"The password must contain at least one special character": "Het wachtwoord moet minstens één speciaal teken bevatten",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Het wachtwoord moet minstens één hoofdletter, één kleine letter en één cijfer bevatten",
|
||||
"The password must have at least 6 characters": "Het wachtwoord moet minstens 6 tekens bevatten",
|
||||
"The password must have at least 8 characters": "Het wachtwoord moet minstens 8 tekens bevatten",
|
||||
"The password must not contain any repeated characters": "Het wachtwoord mag geen herhaalde tekens bevatten",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "De gebruiker is verwijderd en kan niet worden gebruikt om in te loggen, neem contact op met de beheerder",
|
||||
"The user is forbidden to sign in, please contact the administrator": "Inloggen verboden, neem contact op met beheerder",
|
||||
"The user: %s doesn't exist in LDAP server": "Gebruiker %s ontbreekt in LDAP",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Zarejestruj się używając nazwy użytkownika odpowiadającej kodowi zaproszenia",
|
||||
"Session outdated, please login again": "Sesja wygasła, zaloguj się ponownie",
|
||||
"The invitation code has already been used": "Kod zaproszenia został już wykorzystany",
|
||||
"The password must contain at least one special character": "Hasło musi zawierać co najmniej jeden znak specjalny",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Hasło musi zawierać co najmniej jedną wielką literę, jedną małą literę i jedną cyfrę",
|
||||
"The password must have at least 6 characters": "Hasło musi zawierać co najmniej 6 znaków",
|
||||
"The password must have at least 8 characters": "Hasło musi zawierać co najmniej 8 znaków",
|
||||
"The password must not contain any repeated characters": "Hasło nie może zawierać powtarzających się znaków",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "Użytkownik został usunięty i nie może być używany do logowania, skontaktuj się z administratorem",
|
||||
"The user is forbidden to sign in, please contact the administrator": "Użytkownikowi zabroniono logowania, skontaktuj się z administratorem",
|
||||
"The user: %s doesn't exist in LDAP server": "Użytkownik: %s nie istnieje w serwerze LDAP",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Por favor, registre-se usando o nome de usuário correspondente ao código de convite",
|
||||
"Session outdated, please login again": "Sessão expirada, faça login novamente",
|
||||
"The invitation code has already been used": "O código de convite já foi utilizado",
|
||||
"The password must contain at least one special character": "A senha deve conter pelo menos um caractere especial",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "A senha deve conter pelo menos uma letra maiúscula, uma letra minúscula e um dígito",
|
||||
"The password must have at least 6 characters": "A senha deve ter pelo menos 6 caracteres",
|
||||
"The password must have at least 8 characters": "A senha deve ter pelo menos 8 caracteres",
|
||||
"The password must not contain any repeated characters": "A senha não deve conter caracteres repetidos",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "O usuário foi excluído e não pode ser usado para fazer login, entre em contato com o administrador",
|
||||
"The user is forbidden to sign in, please contact the administrator": "O usuário está proibido de entrar, entre em contato com o administrador",
|
||||
"The user: %s doesn't exist in LDAP server": "O usuário: %s não existe no servidor LDAP",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Пожалуйста, зарегистрируйтесь, используя имя пользователя, соответствующее коду приглашения",
|
||||
"Session outdated, please login again": "Сессия устарела, пожалуйста, войдите снова",
|
||||
"The invitation code has already been used": "Код приглашения уже использован",
|
||||
"The password must contain at least one special character": "Пароль должен содержать хотя бы один специальный символ",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Пароль должен содержать хотя бы одну заглавную букву, одну строчную букву и одну цифру",
|
||||
"The password must have at least 6 characters": "Пароль должен содержать не менее 6 символов",
|
||||
"The password must have at least 8 characters": "Пароль должен содержать не менее 8 символов",
|
||||
"The password must not contain any repeated characters": "Пароль не должен содержать повторяющихся символов",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "Пользователь был удален и не может быть использован для входа, пожалуйста, свяжитесь с администратором",
|
||||
"The user is forbidden to sign in, please contact the administrator": "Пользователю запрещен вход, пожалуйста, обратитесь к администратору",
|
||||
"The user: %s doesn't exist in LDAP server": "Пользователь: %s не существует на сервере LDAP",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Prosím, zaregistrujte sa pomocou používateľského mena zodpovedajúceho kódu pozvania",
|
||||
"Session outdated, please login again": "Relácia je zastaraná, prosím, prihláste sa znova",
|
||||
"The invitation code has already been used": "Kód pozvania už bol použitý",
|
||||
"The password must contain at least one special character": "Heslo musí obsahovať aspoň jeden špeciálny znak",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Heslo musí obsahovať aspoň jedno veľké písmeno, jedno malé písmeno a jednu číslicu",
|
||||
"The password must have at least 6 characters": "Heslo musí mať aspoň 6 znakov",
|
||||
"The password must have at least 8 characters": "Heslo musí mať aspoň 8 znakov",
|
||||
"The password must not contain any repeated characters": "Heslo nesmie obsahovať opakujúce sa znaky",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "Používateľ bol odstránený a nie je možné ho použiť na prihlásenie, kontaktujte prosím správcu",
|
||||
"The user is forbidden to sign in, please contact the administrator": "Používateľovi je zakázané prihlásenie, prosím, kontaktujte administrátora",
|
||||
"The user: %s doesn't exist in LDAP server": "Používateľ: %s neexistuje na LDAP serveri",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Registrera dig med det användarnamn som motsvarar inbjudningskoden",
|
||||
"Session outdated, please login again": "Sessionen har gått ut, logga in igen",
|
||||
"The invitation code has already been used": "Inbjudningskoden har redan använts",
|
||||
"The password must contain at least one special character": "Lösenordet måste innehålla minst ett specialtecken",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Lösenordet måste innehålla minst en stor bokstav, en liten bokstav och en siffra",
|
||||
"The password must have at least 6 characters": "Lösenordet måste ha minst 6 tecken",
|
||||
"The password must have at least 8 characters": "Lösenordet måste ha minst 8 tecken",
|
||||
"The password must not contain any repeated characters": "Lösenordet får inte innehålla upprepade tecken",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "Användaren har tagits bort och kan inte användas för att logga in, kontakta administratören",
|
||||
"The user is forbidden to sign in, please contact the administrator": "Användaren är förbjuden att logga in, kontakta administratören",
|
||||
"The user: %s doesn't exist in LDAP server": "Användaren: %s finns inte i LDAP-servern",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Lütfen davet koduna karşılık gelen kullanıcı adıyla kayıt olun",
|
||||
"Session outdated, please login again": "Oturum süresi doldu, lütfen tekrar giriş yapın",
|
||||
"The invitation code has already been used": "Davet kodu zaten kullanılmış",
|
||||
"The password must contain at least one special character": "Şifre en az bir özel karakter içermelidir",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Şifre en az bir büyük harf, bir küçük harf ve bir rakam içermelidir",
|
||||
"The password must have at least 6 characters": "Şifre en az 6 karakter içermelidir",
|
||||
"The password must have at least 8 characters": "Şifre en az 8 karakter içermelidir",
|
||||
"The password must not contain any repeated characters": "Şifre tekrarlanan karakterler içermemelidir",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "Kullanıcı silinmiş ve oturum açmak için kullanılamaz, lütfen yöneticiyle iletişime geçin",
|
||||
"The user is forbidden to sign in, please contact the administrator": "Kullanıcı giriş yapmaktan men edildi, lütfen yönetici ile iletişime geçin",
|
||||
"The user: %s doesn't exist in LDAP server": "Kullanıcı: %s LDAP sunucusunda mevcut değil",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Будь ласка, зареєструйтесь, використовуючи ім’я користувача, що відповідає коду запрошення",
|
||||
"Session outdated, please login again": "Сесію застаро, будь ласка, увійдіть знову",
|
||||
"The invitation code has already been used": "Код запрошення вже використано",
|
||||
"The password must contain at least one special character": "Пароль повинен містити принаймні один спеціальний символ",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Пароль повинен містити принаймні одну велику літеру, одну малу літеру та одну цифру",
|
||||
"The password must have at least 6 characters": "Пароль повинен містити принаймні 6 символів",
|
||||
"The password must have at least 8 characters": "Пароль повинен містити принаймні 8 символів",
|
||||
"The password must not contain any repeated characters": "Пароль не повинен містити повторюваних символів",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "Користувача було видалено і не можна використовувати для входу, будь ласка, зверніться до адміністратора",
|
||||
"The user is forbidden to sign in, please contact the administrator": "Користувачу заборонено вхід, зверніться до адміністратора",
|
||||
"The user: %s doesn't exist in LDAP server": "Користувач: %s не існує на сервері LDAP",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "Vui lòng đăng ký bằng tên người dùng tương ứng với mã mời",
|
||||
"Session outdated, please login again": "Phiên làm việc hết hạn, vui lòng đăng nhập lại",
|
||||
"The invitation code has already been used": "Mã mời đã được sử dụng",
|
||||
"The password must contain at least one special character": "Mật khẩu phải chứa ít nhất một ký tự đặc biệt",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "Mật khẩu phải chứa ít nhất một chữ hoa, một chữ thường và một chữ số",
|
||||
"The password must have at least 6 characters": "Mật khẩu phải có ít nhất 6 ký tự",
|
||||
"The password must have at least 8 characters": "Mật khẩu phải có ít nhất 8 ký tự",
|
||||
"The password must not contain any repeated characters": "Mật khẩu không được chứa ký tự lặp lại",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "Người dùng đã bị xóa và không thể được sử dụng để đăng nhập, vui lòng liên hệ với quản trị viên",
|
||||
"The user is forbidden to sign in, please contact the administrator": "Người dùng bị cấm đăng nhập, vui lòng liên hệ với quản trị viên",
|
||||
"The user: %s doesn't exist in LDAP server": "Người dùng: %s không tồn tại trên máy chủ LDAP",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"Please register using the username corresponding to the invitation code": "请使用邀请码关联的用户名注册",
|
||||
"Session outdated, please login again": "会话已过期,请重新登录",
|
||||
"The invitation code has already been used": "邀请码已被使用",
|
||||
"The password must contain at least one special character": "密码必须包含至少一个特殊字符",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "密码必须包含至少一个大写字母、一个小写字母和一个数字",
|
||||
"The password must have at least 6 characters": "密码必须至少包含6个字符",
|
||||
"The password must have at least 8 characters": "密码必须至少包含8个字符",
|
||||
"The password must not contain any repeated characters": "密码不能包含任何重复字符",
|
||||
"The user has been deleted and cannot be used to sign in, please contact the administrator": "该用户已被删除, 无法用于登录, 请联系管理员",
|
||||
"The user is forbidden to sign in, please contact the administrator": "该用户被禁止登录,请联系管理员",
|
||||
"The user: %s doesn't exist in LDAP server": "用户: %s 在LDAP服务器中未找到",
|
||||
|
||||
@@ -157,6 +157,10 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if dtUserInfo.OpenId == "" || dtUserInfo.UnionId == "" {
|
||||
return nil, fmt.Errorf(string(data))
|
||||
}
|
||||
|
||||
countryCode, err := util.GetCountryCode(dtUserInfo.StateCode, dtUserInfo.Mobile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -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
|
||||
}
|
||||
111
idv/aliyun.go
Normal file
111
idv/aliyun.go
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.
|
||||
|
||||
package idv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
cloudauth "github.com/alibabacloud-go/cloudauth-20190307/v3/client"
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultAlibabaCloudEndpoint is the default endpoint for Alibaba Cloud ID verification service
|
||||
DefaultAlibabaCloudEndpoint = "cloudauth.cn-shanghai.aliyuncs.com"
|
||||
)
|
||||
|
||||
type AlibabaCloudIdvProvider struct {
|
||||
ClientId string
|
||||
ClientSecret string
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
func NewAlibabaCloudIdvProvider(clientId string, clientSecret string, endpoint string) *AlibabaCloudIdvProvider {
|
||||
return &AlibabaCloudIdvProvider{
|
||||
ClientId: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
Endpoint: endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *AlibabaCloudIdvProvider) VerifyIdentity(idCardType string, idCard string, realName string) (bool, error) {
|
||||
if provider.ClientId == "" || provider.ClientSecret == "" {
|
||||
return false, fmt.Errorf("Alibaba Cloud credentials not configured")
|
||||
}
|
||||
|
||||
if idCard == "" || realName == "" {
|
||||
return false, fmt.Errorf("ID card and real name are required")
|
||||
}
|
||||
|
||||
// Default endpoint if not configured
|
||||
endpoint := provider.Endpoint
|
||||
if endpoint == "" {
|
||||
endpoint = DefaultAlibabaCloudEndpoint
|
||||
}
|
||||
|
||||
// Create client configuration
|
||||
config := &openapi.Config{
|
||||
AccessKeyId: tea.String(provider.ClientId),
|
||||
AccessKeySecret: tea.String(provider.ClientSecret),
|
||||
Endpoint: tea.String(endpoint),
|
||||
}
|
||||
|
||||
// Create Alibaba Cloud Auth client
|
||||
client, err := cloudauth.NewClient(config)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create Alibaba Cloud client: %v", err)
|
||||
}
|
||||
|
||||
// Prepare verification request using Id2MetaVerify API
|
||||
// This API verifies Chinese ID card number and real name
|
||||
// Reference: https://help.aliyun.com/zh/id-verification/financial-grade-id-verification/server-side-integration-2
|
||||
request := &cloudauth.Id2MetaVerifyRequest{
|
||||
IdentifyNum: tea.String(idCard),
|
||||
UserName: tea.String(realName),
|
||||
ParamType: tea.String("normal"),
|
||||
}
|
||||
|
||||
// Send verification request
|
||||
response, err := client.Id2MetaVerify(request)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to verify identity with Alibaba Cloud: %v", err)
|
||||
}
|
||||
|
||||
// Check response
|
||||
if response == nil || response.Body == nil {
|
||||
return false, fmt.Errorf("empty response from Alibaba Cloud")
|
||||
}
|
||||
|
||||
// Check if the API call was successful
|
||||
if response.Body.Code == nil || *response.Body.Code != "200" {
|
||||
message := "unknown error"
|
||||
if response.Body.Message != nil {
|
||||
message = *response.Body.Message
|
||||
}
|
||||
return false, fmt.Errorf("Alibaba Cloud API error: %s", message)
|
||||
}
|
||||
|
||||
// Check verification result
|
||||
// BizCode "1" means verification passed
|
||||
if response.Body.ResultObject != nil && response.Body.ResultObject.BizCode != nil {
|
||||
if *response.Body.ResultObject.BizCode == "1" {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("identity verification failed: BizCode=%s", *response.Body.ResultObject.BizCode)
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("identity verification failed: missing result")
|
||||
}
|
||||
143
idv/jumio.go
Normal file
143
idv/jumio.go
Normal file
@@ -0,0 +1,143 @@
|
||||
// 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 idv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type JumioIdvProvider struct {
|
||||
ClientId string
|
||||
ClientSecret string
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
type JumioInitiateRequest struct {
|
||||
CustomerInternalReference string `json:"customerInternalReference"`
|
||||
UserReference string `json:"userReference"`
|
||||
WorkflowId string `json:"workflowId,omitempty"`
|
||||
}
|
||||
|
||||
type JumioInitiateResponse struct {
|
||||
TransactionReference string `json:"transactionReference"`
|
||||
RedirectUrl string `json:"redirectUrl"`
|
||||
}
|
||||
|
||||
type JumioVerificationData struct {
|
||||
IdCard string `json:"idNumber"`
|
||||
RealName string `json:"firstName"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func NewJumioIdvProvider(clientId string, clientSecret string, endpoint string) *JumioIdvProvider {
|
||||
return &JumioIdvProvider{
|
||||
ClientId: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
Endpoint: endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *JumioIdvProvider) VerifyIdentity(idCardType string, idCard string, realName string) (bool, error) {
|
||||
if provider.ClientId == "" || provider.ClientSecret == "" {
|
||||
return false, fmt.Errorf("Jumio credentials not configured")
|
||||
}
|
||||
|
||||
if provider.Endpoint == "" {
|
||||
return false, fmt.Errorf("Jumio endpoint not configured")
|
||||
}
|
||||
|
||||
if idCard == "" || realName == "" {
|
||||
return false, fmt.Errorf("ID card and real name are required")
|
||||
}
|
||||
|
||||
// Jumio ID Verification implementation
|
||||
// This implementation follows Jumio's API workflow:
|
||||
// 1. Initiate a verification session
|
||||
// 2. User would normally go through verification flow (redirected to Jumio)
|
||||
// 3. Check verification status
|
||||
// For automated verification, we simulate the process
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
// Prepare the initiation request
|
||||
initiateReq := JumioInitiateRequest{
|
||||
CustomerInternalReference: fmt.Sprintf("user_%s", idCard),
|
||||
UserReference: realName,
|
||||
}
|
||||
|
||||
reqBody, err := json.Marshal(initiateReq)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to marshal request: %v", err)
|
||||
}
|
||||
|
||||
// Create HTTP request to Jumio API
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/v4/initiate", provider.Endpoint), bytes.NewBuffer(reqBody))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
// Set authentication headers
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "Casdoor/1.0")
|
||||
req.SetBasicAuth(provider.ClientId, provider.ClientSecret)
|
||||
|
||||
// Send request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to send request to Jumio: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read response
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to read response: %v", err)
|
||||
}
|
||||
|
||||
// Check response status
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
return false, fmt.Errorf("Jumio API returned error status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// Parse response
|
||||
var initiateResp JumioInitiateResponse
|
||||
if err := json.Unmarshal(body, &initiateResp); err != nil {
|
||||
return false, fmt.Errorf("failed to parse Jumio response: %v", err)
|
||||
}
|
||||
|
||||
// In a real implementation, the user would be redirected to initiateResp.RedirectUrl
|
||||
// to complete the verification process. Here we simulate successful verification.
|
||||
// For production, you would need to:
|
||||
// 1. Store the transaction reference
|
||||
// 2. Redirect user to RedirectUrl or provide it to them
|
||||
// 3. Implement a webhook to receive verification results
|
||||
// 4. Query the transaction status using the transaction reference
|
||||
|
||||
// Simulate verification check (in production, this would be a webhook callback or status query)
|
||||
if initiateResp.TransactionReference != "" {
|
||||
// Successfully initiated verification session
|
||||
// In a real scenario, return would depend on actual verification completion
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("verification could not be initiated")
|
||||
}
|
||||
29
idv/provider.go
Normal file
29
idv/provider.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// 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 idv
|
||||
|
||||
type IdvProvider interface {
|
||||
VerifyIdentity(idCardType string, idCard string, realName string) (bool, error)
|
||||
}
|
||||
|
||||
func GetIdvProvider(typ string, clientId string, clientSecret string, endpoint string) IdvProvider {
|
||||
if typ == "Jumio" {
|
||||
return NewJumioIdvProvider(clientId, clientSecret, endpoint)
|
||||
} else if typ == "Alibaba Cloud" {
|
||||
return NewAlibabaCloudIdvProvider(clientId, clientSecret, endpoint)
|
||||
}
|
||||
// Default to Jumio for backward compatibility
|
||||
return NewJumioIdvProvider(clientId, clientSecret, endpoint)
|
||||
}
|
||||
@@ -212,6 +212,10 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
|
||||
e.AddAttribute("homeDirectory", message.AttributeValue("/home/"+user.Name))
|
||||
e.AddAttribute("cn", message.AttributeValue(user.Name))
|
||||
e.AddAttribute("uid", message.AttributeValue(user.Id))
|
||||
e.AddAttribute("mail", message.AttributeValue(user.Email))
|
||||
e.AddAttribute("mobile", message.AttributeValue(user.Phone))
|
||||
e.AddAttribute("sn", message.AttributeValue(user.LastName))
|
||||
e.AddAttribute("givenName", message.AttributeValue(user.FirstName))
|
||||
for _, group := range user.Groups {
|
||||
e.AddAttribute(ldapMemberOfAttr, message.AttributeValue(group))
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ type SamlItem struct {
|
||||
type JwtItem struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
@@ -91,6 +92,7 @@ type Application struct {
|
||||
EnableSamlCompress bool `json:"enableSamlCompress"`
|
||||
EnableSamlC14n10 bool `json:"enableSamlC14n10"`
|
||||
EnableSamlPostBinding bool `json:"enableSamlPostBinding"`
|
||||
DisableSamlAttributes bool `json:"disableSamlAttributes"`
|
||||
UseEmailAsSamlNameId bool `json:"useEmailAsSamlNameId"`
|
||||
EnableWebAuthn bool `json:"enableWebAuthn"`
|
||||
EnableLinkWithEmail bool `json:"enableLinkWithEmail"`
|
||||
@@ -560,6 +562,7 @@ func GetMaskedApplication(application *Application, userId string) *Application
|
||||
application.EnableSamlCompress = false
|
||||
application.EnableSamlC14n10 = false
|
||||
application.EnableSamlPostBinding = false
|
||||
application.DisableSamlAttributes = false
|
||||
application.EnableWebAuthn = false
|
||||
application.EnableLinkWithEmail = false
|
||||
application.SamlReplyUrl = "***"
|
||||
|
||||
@@ -71,7 +71,7 @@ func CheckUserSignup(application *Application, organization *Organization, authF
|
||||
}
|
||||
|
||||
if application.IsSignupItemVisible("Password") {
|
||||
msg := CheckPasswordComplexityByOrg(organization, authForm.Password)
|
||||
msg := CheckPasswordComplexityByOrg(organization, authForm.Password, lang)
|
||||
if msg != "" {
|
||||
return msg
|
||||
}
|
||||
@@ -282,14 +282,14 @@ func CheckPassword(user *User, password string, lang string, options ...bool) er
|
||||
return resetUserSigninErrorTimes(user)
|
||||
}
|
||||
|
||||
func CheckPasswordComplexityByOrg(organization *Organization, password string) string {
|
||||
errorMsg := checkPasswordComplexity(password, organization.PasswordOptions)
|
||||
func CheckPasswordComplexityByOrg(organization *Organization, password string, lang string) string {
|
||||
errorMsg := checkPasswordComplexity(password, organization.PasswordOptions, lang)
|
||||
return errorMsg
|
||||
}
|
||||
|
||||
func CheckPasswordComplexity(user *User, password string) string {
|
||||
func CheckPasswordComplexity(user *User, password string, lang string) string {
|
||||
organization, _ := GetOrganizationByUser(user)
|
||||
return CheckPasswordComplexityByOrg(organization, password)
|
||||
return CheckPasswordComplexityByOrg(organization, password, lang)
|
||||
}
|
||||
|
||||
func CheckLdapUserPassword(user *User, password string, lang string) error {
|
||||
|
||||
@@ -18,9 +18,10 @@ import (
|
||||
"regexp"
|
||||
|
||||
"github.com/casdoor/casdoor/cred"
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
)
|
||||
|
||||
type ValidatorFunc func(password string) string
|
||||
type ValidatorFunc func(password string, lang string) string
|
||||
|
||||
var (
|
||||
regexLowerCase = regexp.MustCompile(`[a-z]`)
|
||||
@@ -29,50 +30,50 @@ var (
|
||||
regexSpecial = regexp.MustCompile("[!-/:-@[-`{-~]")
|
||||
)
|
||||
|
||||
func isValidOption_AtLeast6(password string) string {
|
||||
func isValidOption_AtLeast6(password string, lang string) string {
|
||||
if len(password) < 6 {
|
||||
return "The password must have at least 6 characters"
|
||||
return i18n.Translate(lang, "check:The password must have at least 6 characters")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isValidOption_AtLeast8(password string) string {
|
||||
func isValidOption_AtLeast8(password string, lang string) string {
|
||||
if len(password) < 8 {
|
||||
return "The password must have at least 8 characters"
|
||||
return i18n.Translate(lang, "check:The password must have at least 8 characters")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isValidOption_Aa123(password string) string {
|
||||
func isValidOption_Aa123(password string, lang string) string {
|
||||
hasLowerCase := regexLowerCase.MatchString(password)
|
||||
hasUpperCase := regexUpperCase.MatchString(password)
|
||||
hasDigit := regexDigit.MatchString(password)
|
||||
|
||||
if !hasLowerCase || !hasUpperCase || !hasDigit {
|
||||
return "The password must contain at least one uppercase letter, one lowercase letter and one digit"
|
||||
return i18n.Translate(lang, "check:The password must contain at least one uppercase letter, one lowercase letter and one digit")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isValidOption_SpecialChar(password string) string {
|
||||
func isValidOption_SpecialChar(password string, lang string) string {
|
||||
if !regexSpecial.MatchString(password) {
|
||||
return "The password must contain at least one special character"
|
||||
return i18n.Translate(lang, "check:The password must contain at least one special character")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isValidOption_NoRepeat(password string) string {
|
||||
func isValidOption_NoRepeat(password string, lang string) string {
|
||||
for i := 0; i < len(password)-1; i++ {
|
||||
if password[i] == password[i+1] {
|
||||
return "The password must not contain any repeated characters"
|
||||
return i18n.Translate(lang, "check:The password must not contain any repeated characters")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func checkPasswordComplexity(password string, options []string) string {
|
||||
func checkPasswordComplexity(password string, options []string, lang string) string {
|
||||
if len(password) == 0 {
|
||||
return "Please input your password!"
|
||||
return i18n.Translate(lang, "check:Password cannot be empty")
|
||||
}
|
||||
|
||||
if len(options) == 0 {
|
||||
@@ -90,7 +91,7 @@ func checkPasswordComplexity(password string, options []string) string {
|
||||
for _, option := range options {
|
||||
checkerFunc, ok := checkers[option]
|
||||
if ok {
|
||||
errorMsg := checkerFunc(password)
|
||||
errorMsg := checkerFunc(password, lang)
|
||||
if errorMsg != "" {
|
||||
return errorMsg
|
||||
}
|
||||
|
||||
@@ -63,6 +63,10 @@ func getBuiltInAccountItems() []*AccountItem {
|
||||
{Name: "Location", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "Affiliation", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "Title", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "ID card type", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "ID card", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "Real name", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "ID verification", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Homepage", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "Bio", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
|
||||
{Name: "Tag", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
156
object/order.go
Normal file
156
object/order.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"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)
|
||||
}
|
||||
332
object/order_pay.go
Normal file
332
object/order_pay.go
Normal file
@@ -0,0 +1,332 @@
|
||||
// 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)
|
||||
}
|
||||
|
||||
if !product.IsRecharge && product.Quantity <= 0 {
|
||||
return nil, fmt.Errorf("the product: %s is out of stock", product.Name)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if !product.IsRecharge && product.Quantity <= 0 {
|
||||
return nil, nil, fmt.Errorf("the product: %s is out of stock", product.Name)
|
||||
}
|
||||
|
||||
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,
|
||||
IsRecharge: product.IsRecharge,
|
||||
|
||||
User: user.Name,
|
||||
Order: order.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()
|
||||
}
|
||||
|
||||
// Update order state first to avoid inconsistency
|
||||
_, err = UpdateOrder(order.GetId(), order)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Update product stock after order state is persisted (for instant payment methods)
|
||||
if provider.Type == "Dummy" || provider.Type == "Balance" {
|
||||
err = UpdateProductStock(product)
|
||||
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)
|
||||
|
||||
@@ -38,7 +38,6 @@ type Payment struct {
|
||||
Tag string `xorm:"varchar(100)" json:"tag"`
|
||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||
Price float64 `json:"price"`
|
||||
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
|
||||
IsRecharge bool `xorm:"bool" json:"isRecharge"`
|
||||
|
||||
// Payer Info
|
||||
@@ -54,7 +53,8 @@ type Payment struct {
|
||||
InvoiceRemark string `xorm:"varchar(100)" json:"invoiceRemark"`
|
||||
InvoiceUrl string `xorm:"varchar(255)" json:"invoiceUrl"`
|
||||
// Order Info
|
||||
OutOrderId string `xorm:"varchar(100)" json:"outOrderId"`
|
||||
Order string `xorm:"varchar(100)" json:"order"` // Internal order name
|
||||
OutOrderId string `xorm:"varchar(100)" json:"outOrderId"` // External payment provider's order ID
|
||||
PayUrl string `xorm:"varchar(2000)" json:"payUrl"`
|
||||
SuccessUrl string `xorm:"varchar(2000)" json:"successUrl"` // `successUrl` is redirected from `payUrl` after pay success
|
||||
State pp.PaymentState `xorm:"varchar(100)" json:"state"`
|
||||
@@ -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,52 @@ func NotifyPayment(body []byte, owner string, paymentName string) (*Payment, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Update order state based on payment status
|
||||
if payment.Order != "" {
|
||||
order, err := getOrder(payment.Owner, payment.Order)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if order == nil {
|
||||
return nil, fmt.Errorf("the order: %s does not exist", payment.Order)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Update product stock after order state is persisted
|
||||
if payment.State == pp.PaymentStatePaid {
|
||||
product, err := getProduct(payment.Owner, payment.ProductName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if product == nil {
|
||||
return nil, fmt.Errorf("the product: %s does not exist", payment.ProductName)
|
||||
}
|
||||
|
||||
err = UpdateProductStock(product)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return payment, nil
|
||||
|
||||
@@ -90,9 +90,13 @@ func (p *Permission) setEnforcerAdapter(enforcer *casbin.Enforcer) error {
|
||||
}
|
||||
|
||||
func (p *Permission) setEnforcerModel(enforcer *casbin.Enforcer) error {
|
||||
permissionModel, err := GetModel(p.Model)
|
||||
if err != nil {
|
||||
return err
|
||||
var permissionModel *Model
|
||||
var err error
|
||||
if p.Model != "" {
|
||||
permissionModel, err = GetModel(p.Model)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: return error if permissionModel is nil.
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/xlsx"
|
||||
)
|
||||
|
||||
@@ -36,45 +39,30 @@ func getPermissionMap(owner string) (map[string]*Permission, error) {
|
||||
func UploadPermissions(owner string, path string) (bool, error) {
|
||||
table := xlsx.ReadXlsxFile(path)
|
||||
|
||||
oldUserMap, err := getPermissionMap(owner)
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
uploadedPermissions, err := StringArrayToStruct[Permission](table)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
oldPermissionMap, err := getPermissionMap(owner)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
newPermissions := []*Permission{}
|
||||
for index, line := range table {
|
||||
line := line
|
||||
if index == 0 || parseLineItem(&line, 0) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
permission := &Permission{
|
||||
Owner: parseLineItem(&line, 0),
|
||||
Name: parseLineItem(&line, 1),
|
||||
CreatedTime: parseLineItem(&line, 2),
|
||||
DisplayName: parseLineItem(&line, 3),
|
||||
|
||||
Users: parseListItem(&line, 4),
|
||||
Roles: parseListItem(&line, 5),
|
||||
Domains: parseListItem(&line, 6),
|
||||
|
||||
Model: parseLineItem(&line, 7),
|
||||
Adapter: parseLineItem(&line, 8),
|
||||
ResourceType: parseLineItem(&line, 9),
|
||||
|
||||
Resources: parseListItem(&line, 10),
|
||||
Actions: parseListItem(&line, 11),
|
||||
|
||||
Effect: parseLineItem(&line, 12),
|
||||
IsEnabled: parseLineItemBool(&line, 13),
|
||||
|
||||
Submitter: parseLineItem(&line, 14),
|
||||
Approver: parseLineItem(&line, 15),
|
||||
ApproveTime: parseLineItem(&line, 16),
|
||||
State: parseLineItem(&line, 17),
|
||||
}
|
||||
|
||||
if _, ok := oldUserMap[permission.GetId()]; !ok {
|
||||
for _, permission := range uploadedPermissions {
|
||||
if _, ok := oldPermissionMap[permission.GetId()]; !ok {
|
||||
newPermissions = append(newPermissions, permission)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -31,18 +27,19 @@ type Product struct {
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
|
||||
Image string `xorm:"varchar(100)" json:"image"`
|
||||
Detail string `xorm:"varchar(1000)" json:"detail"`
|
||||
Description string `xorm:"varchar(200)" json:"description"`
|
||||
Tag string `xorm:"varchar(100)" json:"tag"`
|
||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||
Price float64 `json:"price"`
|
||||
Quantity int `json:"quantity"`
|
||||
Sold int `json:"sold"`
|
||||
IsRecharge bool `json:"isRecharge"`
|
||||
Providers []string `xorm:"varchar(255)" json:"providers"`
|
||||
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
|
||||
SuccessUrl string `xorm:"varchar(1000)" json:"successUrl"`
|
||||
Image string `xorm:"varchar(100)" json:"image"`
|
||||
Detail string `xorm:"varchar(1000)" json:"detail"`
|
||||
Description string `xorm:"varchar(200)" json:"description"`
|
||||
Tag string `xorm:"varchar(100)" json:"tag"`
|
||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||
Price float64 `json:"price"`
|
||||
Quantity int `json:"quantity"`
|
||||
Sold int `json:"sold"`
|
||||
IsRecharge bool `json:"isRecharge"`
|
||||
RechargeOptions []float64 `xorm:"varchar(500)" json:"rechargeOptions"`
|
||||
DisableCustomRecharge bool `json:"disableCustomRecharge"`
|
||||
Providers []string `xorm:"varchar(255)" json:"providers"`
|
||||
SuccessUrl string `xorm:"varchar(1000)" json:"successUrl"`
|
||||
|
||||
State string `xorm:"varchar(100)" json:"state"`
|
||||
|
||||
@@ -101,6 +98,35 @@ func GetProduct(id string) (*Product, error) {
|
||||
return getProduct(owner, name)
|
||||
}
|
||||
|
||||
func UpdateProductStock(product *Product) error {
|
||||
var (
|
||||
affected int64
|
||||
err error
|
||||
)
|
||||
if product.IsRecharge {
|
||||
affected, err = ormer.Engine.ID(core.PK{product.Owner, product.Name}).
|
||||
Incr("sold", 1).
|
||||
Update(&Product{})
|
||||
} else {
|
||||
affected, err = ormer.Engine.ID(core.PK{product.Owner, product.Name}).
|
||||
Where("quantity > 0").
|
||||
Decr("quantity", 1).
|
||||
Incr("sold", 1).
|
||||
Update(&Product{})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if affected == 0 {
|
||||
if product.IsRecharge {
|
||||
return fmt.Errorf("failed to update stock for product: %s", product.Name)
|
||||
}
|
||||
return fmt.Errorf("insufficient stock for product: %s", product.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateProduct(id string, product *Product) (bool, error) {
|
||||
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
@@ -111,7 +137,6 @@ func UpdateProduct(id string, product *Product) (bool, error) {
|
||||
} else if p == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(product)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -168,190 +193,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
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/beego/beego/context"
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/idp"
|
||||
"github.com/casdoor/casdoor/idv"
|
||||
"github.com/casdoor/casdoor/pp"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
@@ -345,6 +346,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)
|
||||
}
|
||||
@@ -436,6 +461,47 @@ func GetFaceIdProviderByApplication(applicationId, isCurrentProvider, lang strin
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func GetIdvProviderByOwnerName(applicationId, lang string) (*Provider, error) {
|
||||
owner, name, err := util.GetOwnerAndNameFromIdWithError(applicationId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
provider := Provider{Owner: owner, Name: name, Category: "ID Verification"}
|
||||
existed, err := ormer.Engine.Get(&provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !existed {
|
||||
return nil, fmt.Errorf(i18n.Translate(lang, "provider:the provider: %s does not exist"), applicationId)
|
||||
}
|
||||
|
||||
return &provider, nil
|
||||
}
|
||||
|
||||
func GetIdvProviderByApplication(applicationId, isCurrentProvider, lang string) (*Provider, error) {
|
||||
if isCurrentProvider == "true" {
|
||||
return GetIdvProviderByOwnerName(applicationId, lang)
|
||||
}
|
||||
application, err := GetApplication(applicationId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if application == nil || len(application.Providers) == 0 {
|
||||
return nil, fmt.Errorf(i18n.Translate(lang, "provider:Invalid application id"))
|
||||
}
|
||||
for _, provider := range application.Providers {
|
||||
if provider.Provider == nil {
|
||||
continue
|
||||
}
|
||||
if provider.Provider.Category == "ID Verification" {
|
||||
return GetIdvProviderByOwnerName(util.GetId(provider.Provider.Owner, provider.Provider.Name), lang)
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func providerChangeTrigger(oldName string, newName string) error {
|
||||
session := ormer.Engine.NewSession()
|
||||
defer session.Close()
|
||||
@@ -502,3 +568,10 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.Provid
|
||||
|
||||
return providerInfo
|
||||
}
|
||||
|
||||
func GetIdvProviderFromProvider(provider *Provider) idv.IdvProvider {
|
||||
if provider.Category != "ID Verification" {
|
||||
return nil
|
||||
}
|
||||
return idv.GetIdvProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Endpoint)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/xlsx"
|
||||
)
|
||||
|
||||
@@ -36,31 +39,30 @@ func getRoleMap(owner string) (map[string]*Role, error) {
|
||||
func UploadRoles(owner string, path string) (bool, error) {
|
||||
table := xlsx.ReadXlsxFile(path)
|
||||
|
||||
oldUserMap, err := getRoleMap(owner)
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
uploadedRoles, err := StringArrayToStruct[Role](table)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
oldRoleMap, err := getRoleMap(owner)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
newRoles := []*Role{}
|
||||
for index, line := range table {
|
||||
line := line
|
||||
if index == 0 || parseLineItem(&line, 0) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
role := &Role{
|
||||
Owner: parseLineItem(&line, 0),
|
||||
Name: parseLineItem(&line, 1),
|
||||
CreatedTime: parseLineItem(&line, 2),
|
||||
DisplayName: parseLineItem(&line, 3),
|
||||
|
||||
Users: parseListItem(&line, 4),
|
||||
Roles: parseListItem(&line, 5),
|
||||
Domains: parseListItem(&line, 6),
|
||||
IsEnabled: parseLineItemBool(&line, 7),
|
||||
}
|
||||
|
||||
if _, ok := oldUserMap[role.GetId()]; !ok {
|
||||
for _, role := range uploadedRoles {
|
||||
if _, ok := oldRoleMap[role.GetId()]; !ok {
|
||||
newRoles = append(newRoles, role)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,85 +104,53 @@ func NewSamlResponse(application *Application, user *User, host string, certific
|
||||
authnStatement.CreateAttr("SessionNotOnOrAfter", expireTime)
|
||||
authnStatement.CreateElement("saml:AuthnContext").CreateElement("saml:AuthnContextClassRef").SetText("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport")
|
||||
|
||||
attributes := assertion.CreateElement("saml:AttributeStatement")
|
||||
if !application.DisableSamlAttributes {
|
||||
attributes := assertion.CreateElement("saml:AttributeStatement")
|
||||
|
||||
email := attributes.CreateElement("saml:Attribute")
|
||||
email.CreateAttr("Name", "Email")
|
||||
email.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic")
|
||||
email.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(user.Email)
|
||||
email := attributes.CreateElement("saml:Attribute")
|
||||
email.CreateAttr("Name", "Email")
|
||||
email.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic")
|
||||
email.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(user.Email)
|
||||
|
||||
name := attributes.CreateElement("saml:Attribute")
|
||||
name.CreateAttr("Name", "Name")
|
||||
name.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic")
|
||||
name.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(user.Name)
|
||||
name := attributes.CreateElement("saml:Attribute")
|
||||
name.CreateAttr("Name", "Name")
|
||||
name.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic")
|
||||
name.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(user.Name)
|
||||
|
||||
displayName := attributes.CreateElement("saml:Attribute")
|
||||
displayName.CreateAttr("Name", "DisplayName")
|
||||
displayName.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic")
|
||||
displayName.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(user.DisplayName)
|
||||
displayName := attributes.CreateElement("saml:Attribute")
|
||||
displayName.CreateAttr("Name", "DisplayName")
|
||||
displayName.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic")
|
||||
displayName.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(user.DisplayName)
|
||||
|
||||
err := ExtendUserWithRolesAndPermissions(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, item := range application.SamlAttributes {
|
||||
role := attributes.CreateElement("saml:Attribute")
|
||||
role.CreateAttr("Name", item.Name)
|
||||
role.CreateAttr("NameFormat", item.NameFormat)
|
||||
|
||||
valueList := replaceAttributeValue(user, item.Value)
|
||||
for _, value := range valueList {
|
||||
av := role.CreateElement("saml:AttributeValue")
|
||||
av.CreateAttr("xsi:type", "xs:string").Element().SetText(value)
|
||||
err := ExtendUserWithRolesAndPermissions(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
roles := attributes.CreateElement("saml:Attribute")
|
||||
roles.CreateAttr("Name", "Roles")
|
||||
roles.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic")
|
||||
for _, item := range application.SamlAttributes {
|
||||
role := attributes.CreateElement("saml:Attribute")
|
||||
role.CreateAttr("Name", item.Name)
|
||||
role.CreateAttr("NameFormat", item.NameFormat)
|
||||
|
||||
for _, role := range user.Roles {
|
||||
roles.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(role.Name)
|
||||
valueList := replaceAttributeValue(user, item.Value)
|
||||
for _, value := range valueList {
|
||||
av := role.CreateElement("saml:AttributeValue")
|
||||
av.CreateAttr("xsi:type", "xs:string").Element().SetText(value)
|
||||
}
|
||||
}
|
||||
|
||||
roles := attributes.CreateElement("saml:Attribute")
|
||||
roles.CreateAttr("Name", "Roles")
|
||||
roles.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic")
|
||||
|
||||
for _, role := range user.Roles {
|
||||
roles.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(role.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return samlResponse, nil
|
||||
}
|
||||
|
||||
// ensureNamespaces ensures that xsi and xs namespaces are present on Response and Assertion elements
|
||||
// This is needed because C14N10 Exclusive Canonicalization may remove namespace declarations
|
||||
// during the canonicalization process, even if they are used in attributes like xsi:type="xs:string"
|
||||
func ensureNamespaces(samlResponse *etree.Element) {
|
||||
xsiNS := "http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsNS := "http://www.w3.org/2001/XMLSchema"
|
||||
|
||||
// Ensure namespaces on Response element
|
||||
// Check if namespaces exist and update/add them
|
||||
setNamespaceAttr(samlResponse, "xmlns:xsi", xsiNS)
|
||||
setNamespaceAttr(samlResponse, "xmlns:xs", xsNS)
|
||||
|
||||
// Find and ensure namespaces on Assertion element
|
||||
assertion := samlResponse.FindElement("./Assertion")
|
||||
if assertion != nil {
|
||||
setNamespaceAttr(assertion, "xmlns:xsi", xsiNS)
|
||||
setNamespaceAttr(assertion, "xmlns:xs", xsNS)
|
||||
}
|
||||
}
|
||||
|
||||
// setNamespaceAttr sets a namespace attribute on an element, removing any existing one first
|
||||
func setNamespaceAttr(elem *etree.Element, key, value string) {
|
||||
// Remove existing attribute if present by filtering the Attr slice
|
||||
newAttrs := []etree.Attr{}
|
||||
for _, attr := range elem.Attr {
|
||||
if attr.Key != key {
|
||||
newAttrs = append(newAttrs, attr)
|
||||
}
|
||||
}
|
||||
elem.Attr = newAttrs
|
||||
// Add the new attribute
|
||||
elem.CreateAttr(key, value)
|
||||
}
|
||||
|
||||
type X509Key struct {
|
||||
X509Certificate string
|
||||
PrivateKey string
|
||||
@@ -418,10 +386,6 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
|
||||
|
||||
if application.EnableSamlC14n10 {
|
||||
ctx.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList("")
|
||||
// Ensure xsi and xs namespaces are present on Response and Assertion elements BEFORE signing
|
||||
// This is critical for C14N10 which may remove namespace declarations during canonicalization
|
||||
// If we add namespaces after signing, the XML won't match the signature
|
||||
ensureNamespaces(samlResponse)
|
||||
}
|
||||
|
||||
// signedXML, err := ctx.SignEnvelopedLimix(samlResponse)
|
||||
@@ -451,7 +415,7 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
|
||||
doc := etree.NewDocument()
|
||||
doc.SetRoot(samlResponse)
|
||||
|
||||
// Write to bytes and ensure namespaces are preserved in the final XML
|
||||
// Write to bytes
|
||||
xmlBytes, err := doc.WriteToBytes()
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("err: Failed to serializes the SAML request into bytes, %s", err.Error())
|
||||
@@ -490,8 +454,6 @@ func NewSamlResponse11(application *Application, user *User, requestID string, h
|
||||
}
|
||||
|
||||
samlResponse.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:1.0:protocol")
|
||||
samlResponse.CreateAttr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
samlResponse.CreateAttr("xmlns:xs", "http://www.w3.org/2001/XMLSchema")
|
||||
samlResponse.CreateAttr("MajorVersion", "1")
|
||||
samlResponse.CreateAttr("MinorVersion", "1")
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -356,19 +356,17 @@ func getClaimsCustom(claims Claims, tokenField []string, tokenAttributes []*JwtI
|
||||
res["azp"] = claims.Azp
|
||||
}
|
||||
|
||||
// Always include nonce and scope as they are built-in OAuth/OIDC fields (even if empty)
|
||||
res["nonce"] = claims.Nonce
|
||||
res["scope"] = claims.Scope
|
||||
|
||||
// Create a map for quick lookup of selected token fields
|
||||
selectedFields := make(map[string]bool)
|
||||
for _, field := range tokenField {
|
||||
selectedFields[field] = true
|
||||
}
|
||||
|
||||
// Only include optional fields if they are explicitly selected in tokenFields
|
||||
if selectedFields["nonce"] {
|
||||
res["nonce"] = claims.Nonce
|
||||
}
|
||||
if selectedFields["scope"] {
|
||||
res["scope"] = claims.Scope
|
||||
}
|
||||
// Only include signinMethod and provider if they are explicitly selected in tokenFields
|
||||
if selectedFields["signinMethod"] {
|
||||
res["signinMethod"] = claims.SigninMethod
|
||||
}
|
||||
@@ -413,8 +411,11 @@ func getClaimsCustom(claims Claims, tokenField []string, tokenAttributes []*JwtI
|
||||
|
||||
for _, item := range tokenAttributes {
|
||||
valueList := replaceAttributeValue(claims.User, item.Value)
|
||||
if len(valueList) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(valueList) == 1 {
|
||||
if item.Type == "String" {
|
||||
res[item.Name] = valueList[0]
|
||||
} else {
|
||||
res[item.Name] = valueList
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -63,6 +62,16 @@ func GetTransactions(owner string) ([]*Transaction, error) {
|
||||
return transactions, nil
|
||||
}
|
||||
|
||||
func GetUserTransactions(owner, user string) ([]*Transaction, error) {
|
||||
transactions := []*Transaction{}
|
||||
err := ormer.Engine.Desc("created_time").Find(&transactions, &Transaction{Owner: owner, User: user})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transactions, nil
|
||||
}
|
||||
|
||||
func GetPaginationTransactions(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Transaction, error) {
|
||||
transactions := []*Transaction{}
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
@@ -132,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) {
|
||||
@@ -166,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
|
||||
}
|
||||
@@ -81,6 +81,8 @@ type User struct {
|
||||
Title string `xorm:"varchar(100)" json:"title"`
|
||||
IdCardType string `xorm:"varchar(100)" json:"idCardType"`
|
||||
IdCard string `xorm:"varchar(100) index" json:"idCard"`
|
||||
RealName string `xorm:"varchar(100)" json:"realName"`
|
||||
IsVerified bool `json:"isVerified"`
|
||||
Homepage string `xorm:"varchar(100)" json:"homepage"`
|
||||
Bio string `xorm:"varchar(100)" json:"bio"`
|
||||
Tag string `xorm:"varchar(100)" json:"tag"`
|
||||
@@ -92,7 +94,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 +110,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"`
|
||||
@@ -246,6 +251,8 @@ type Userinfo struct {
|
||||
Avatar string `json:"picture,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
RealName string `json:"real_name,omitempty"`
|
||||
IsVerified bool `json:"is_verified,omitempty"`
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
Roles []string `json:"roles,omitempty"`
|
||||
Permissions []string `json:"permissions,omitempty"`
|
||||
@@ -663,6 +670,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 +837,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 +848,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 +880,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 +981,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 +1001,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 +1065,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
|
||||
@@ -1179,6 +1214,11 @@ func GetUserInfo(user *User, scope string, aud string, host string) (*Userinfo,
|
||||
resp.Phone = user.Phone
|
||||
}
|
||||
|
||||
if strings.Contains(scope, "profile") {
|
||||
resp.RealName = user.RealName
|
||||
resp.IsVerified = user.IsVerified
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
@@ -1453,7 +1493,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 +1501,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
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
|
||||
package object
|
||||
|
||||
import "github.com/casdoor/casdoor/cred"
|
||||
import (
|
||||
"github.com/casdoor/casdoor/cred"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func calculateHash(user *User) (string, error) {
|
||||
syncer, err := getDbSyncerForUser(user)
|
||||
@@ -40,11 +43,21 @@ 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 {
|
||||
hashedPassword := credManager.GetHashedPassword(user.Password, organization.PasswordSalt)
|
||||
// Use organization salt if available, otherwise generate a random salt for the user
|
||||
salt := organization.PasswordSalt
|
||||
if salt == "" {
|
||||
salt = util.GeneratePasswordSalt()
|
||||
}
|
||||
hashedPassword := credManager.GetHashedPassword(user.Password, salt)
|
||||
user.Password = hashedPassword
|
||||
user.PasswordType = organization.PasswordType
|
||||
user.PasswordSalt = organization.PasswordSalt
|
||||
user.PasswordSalt = salt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -73,6 +73,11 @@ func CorsFilter(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Request.Method == "POST" && ctx.Request.RequestURI == "/api/acs" {
|
||||
setCorsHeaders(ctx, origin)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Request.RequestURI == "/api/userinfo" {
|
||||
setCorsHeaders(ctx, origin)
|
||||
return
|
||||
|
||||
@@ -94,6 +94,7 @@ func initAPI() {
|
||||
beego.Router("/api/delete-user", &controllers.ApiController{}, "POST:DeleteUser")
|
||||
beego.Router("/api/upload-users", &controllers.ApiController{}, "POST:UploadUsers")
|
||||
beego.Router("/api/remove-user-from-group", &controllers.ApiController{}, "POST:RemoveUserFromGroup")
|
||||
beego.Router("/api/verify-identification", &controllers.ApiController{}, "POST:VerifyIdentification")
|
||||
|
||||
beego.Router("/api/get-invitations", &controllers.ApiController{}, "GET:GetInvitations")
|
||||
beego.Router("/api/get-invitation", &controllers.ApiController{}, "GET:GetInvitation")
|
||||
@@ -199,7 +200,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
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user