Compare commits

...

98 Commits

Author SHA1 Message Date
Yang Luo
7ddb87cdf8 fix: Fix JWT-Custom token format: always include nonce/scope, add signinMethod and provider to dropdown (#4649) 2025-12-08 17:55:31 +08:00
Yang Luo
fac45f5ac7 feat: add Alibaba Cloud ID verification provider (#4645) 2025-12-08 17:48:52 +08:00
Yang Luo
266d361244 feat: fix "only the last session is displayed" bug by respecting application.EnableExclusiveSignin when adding sessions (#4643) 2025-12-08 17:14:11 +08:00
DacongDA
b454ab1931 feat: fix generated link has no org info bug while using shared application (#4647) 2025-12-08 16:35:17 +08:00
Yang Luo
ff39b6f186 feat: add Jumio ID Verification provider (#4641) 2025-12-08 00:39:34 +08:00
DacongDA
0597dbbe20 feat: always return array if item contains roles, groups or permissions in JWT (#4640) 2025-12-08 00:11:39 +08:00
Yang Luo
49c417c70e fix: add excel import support for groups, permissions, and roles (#4585) 2025-12-07 22:24:12 +08:00
IsAurora6
8b30e12915 feat: improve inventory logic: check stock before order and update stock/sales after payment. (#4633) 2025-12-07 19:38:41 +08:00
Jacob
2e18c65429 feat: add Application.DisableSamlAttributes field and fix C14N namespace issue (#4634) 2025-12-06 21:45:02 +08:00
IsAurora6
27c98bb056 feat: improve payment flow with order navigation and remove returnUrl field (#4632) 2025-12-06 17:57:59 +08:00
DacongDA
4400b66862 feat: fix silentSignin not working bug (#4629) 2025-12-06 11:10:10 +08:00
IsAurora6
e7e7d18ee7 fix: add permission control and view mode for product/order/payment/plan/pricing/subscription pages. (#4628) 2025-12-04 23:08:41 +08:00
IsAurora6
66d1e28300 feat: Add payment column to order list and refine product store card layout. (#4625) 2025-12-04 18:18:10 +08:00
IsAurora6
53782a6706 feat: support recharge products with preset amounts and disable custom amount option. (#4619) 2025-12-03 13:50:33 +08:00
Yang Luo
30bb0ce92f feat: fix signupItem.regex validation not working in signup page frontend (#4614) 2025-12-03 08:56:45 +08:00
Yang Luo
29f7dda858 feat: fix 403 error on /api/acs endpoint for SAML IdP responses (#4620) 2025-12-02 21:19:00 +08:00
Yang Luo
68b82ed524 fix: accept all file types in resources list page's upload button 2025-11-30 20:42:54 +08:00
Yang Luo
c4ce88198f feat: improve password popover positioning on signup page 2025-11-30 18:10:19 +08:00
Yang Luo
a11fa23add fix: fix i18n for "Please input your {field}!" validation message in signup page (#4610) 2025-11-30 17:47:25 +08:00
Yang Luo
add6ba32db fix: improve application edit page's Providers dropdown with search, icons, and display names (#4608) 2025-11-30 17:13:06 +08:00
Yang Luo
37379dee13 fix: fix get-groups API call in ApplicationEditPage to use correct owner parameter (#4606) 2025-11-30 16:23:28 +08:00
Yang Luo
2066670b76 feat: add Lemon Squeezy payment provider (#4604) 2025-11-30 13:40:48 +08:00
Yang Luo
e751148be2 feat: add FastSpring payment provider (#4601) 2025-11-30 12:02:18 +08:00
Yang Luo
c541d0bcdd feat: add Paddle payment provider (#4598) 2025-11-30 11:31:16 +08:00
Yang Luo
f0db95d006 feat: add Polar payment provider (#4595) 2025-11-30 10:45:11 +08:00
IsAurora6
e4db367eaa feat: Remove BuyProduct endpoint and legacy purchase logic. (#4591) 2025-11-28 23:51:22 +08:00
IsAurora6
9df81e3ffc feat: feat: add OrderPayPage.js, fix subscription redirect & refine list time format. (#4586) 2025-11-27 20:49:49 +08:00
IsAurora6
048d6acc83 feat: Implement the complete process of product purchase, order placement, and payment. (#4588) 2025-11-27 20:49:34 +08:00
Yang Luo
e440199977 feat: regenerate the Swagger docs 2025-11-25 22:24:32 +08:00
IsAurora6
cb4e559d51 feat: Added PlaceOrder, CancelOrder, and PayOrder methods, and added corresponding buttons to the frontend. (#4583) 2025-11-25 22:22:46 +08:00
zjumathcode
4d1d0b95d6 feat: drop legacy // +build comment (#4582) 2025-11-25 20:21:09 +08:00
Yang Luo
9cc1133a96 feat: upgrade gomail to v2.2.0 2025-11-25 01:03:45 +08:00
Yang Luo
897c28e8ad fix: fix SQL query in Keycloak syncer (#4578) 2025-11-24 23:40:30 +08:00
Yang Luo
9d37a7e38e fix: fix memory leaks in database syncer from unclosed connections (#4574) 2025-11-24 23:38:50 +08:00
Yang Luo
ea597296b4 fix: allow normal users to view their own transactions (#4572) 2025-11-24 01:47:10 +08:00
Yang Luo
427ddd215e feat: add Telegram OAuth provider (#4570) 2025-11-24 01:04:36 +08:00
Yang Luo
24de79b100 Improve getTransactionTableColumns UI 2025-11-23 22:07:33 +08:00
DacongDA
9ab9c7c8e0 fix: show error better for user upload (#4568) 2025-11-23 21:52:44 +08:00
Yang Luo
0728a9716b feat: deduplicate code between TransactionTable and TransactionListPage (#4567) 2025-11-23 21:47:58 +08:00
Yang Luo
471570f24a Improve AddTransaction API return value 2025-11-23 21:02:06 +08:00
Yang Luo
2fa520844b fix: fix product store page to pass owner parameter to API (#4565) 2025-11-23 20:48:15 +08:00
Yang Luo
2306acb416 fix: improve balanceCredit for org and user 2025-11-23 19:51:39 +08:00
Yang Luo
d3f3f76290 fix: add dry run mode to add-transaction API (#4563) 2025-11-23 17:36:51 +08:00
DacongDA
fe93128495 feat: improve user upload UX (#4542) 2025-11-23 16:05:46 +08:00
seth-shi
7fd890ff14 fix: ticket error handling in HandleOfficialAccountEvent() (#4557) 2025-11-23 14:58:23 +08:00
Yang Luo
83b56d7ceb feat: add product store page (#4544) 2025-11-23 14:54:35 +08:00
Yang Luo
503e5a75d2 feat: add User.OriginalToken field to expose OAuth provider access tokens (#4559) 2025-11-23 14:54:02 +08:00
seth-shi
5a607b4991 fix: close file handle in GetUploadXlsxPath to prevent resource leak (#4558) 2025-11-23 14:37:06 +08:00
Yang Luo
ca2dc2825d feat: add SSO logout notifications to user's signup application (#4547) 2025-11-23 00:47:29 +08:00
Yang Luo
446d0b9047 Improve TransactionTable UI 2025-11-23 00:45:47 +08:00
Yang Luo
ee708dbf48 feat: add Organization.OrgBalanceCredit and User.BalanceCredit fields for credit limit enforcement (#4552) 2025-11-23 00:37:44 +08:00
Yang Luo
221ca28488 fix: flatten top navbar to single level when ≤7 items (#4550) 2025-11-23 00:34:17 +08:00
Yang Luo
e93d3f6c13 Improve transaction list page UI 2025-11-22 23:35:04 +08:00
Yang Luo
e285396d4e fix: fix recharge transaction default values (#4546) 2025-11-22 23:27:29 +08:00
Yang Luo
10320bb49f Improve TransactionTable UI 2025-11-22 21:39:56 +08:00
seth-shi
4d27ebd82a feat: Use email as username when organization setting is enabled during login (#4539) 2025-11-22 20:58:27 +08:00
Yang Luo
6d5e6dab0a Fix account table missing item 2025-11-22 20:56:45 +08:00
Yang Luo
e600ea7efd feat: add i18n support for table column widgets (#4541) 2025-11-22 16:39:44 +08:00
Yang Luo
8002613398 feat: Add exchange rate conversion for balance calculations (#4534) 2025-11-21 22:13:26 +08:00
IsAurora6
a48b1d0c73 feat: Add recharge functionality with editable fields to transaction list page. (#4536) 2025-11-21 22:11:38 +08:00
Yang Luo
d8b5ecba36 feat: add transaction's subtype field and fix product recharge (#4531) 2025-11-21 19:27:07 +08:00
IsAurora6
e3a8a464d5 feat: Add balanceCurrency field to Organization and User models. (#4525) 2025-11-21 14:42:54 +08:00
IsAurora6
a575ba02d6 feat: Fixed a bug in addTransaction and optimized the transactionEdit page. (#4523) 2025-11-21 09:35:12 +08:00
IsAurora6
a9fcfceb8f feat: Add currency icons wherever currency appears, and optimize the display columns in the transaction table. (#4516) 2025-11-20 22:33:00 +08:00
ledigang
712482ffb9 refactor: omit unnecessary reassignment (#4509) 2025-11-20 18:47:03 +08:00
Yang Luo
84e2c760d9 feat: lazy-load Face ID models only when modal opens (#4508) 2025-11-20 18:46:31 +08:00
IsAurora6
4ab85d6781 feat: Distinguish and allow users to configure adminNavItems and userNavItems. (#4503) 2025-11-20 11:05:30 +08:00
Yang Luo
2ede56ac46 fix: refactor out Setting.CurrencyOptions (#4502) 2025-11-19 21:51:28 +08:00
Yang Luo
6a819a9a20 feat: persist hash column when updating users (#4500) 2025-11-19 21:50:32 +08:00
IsAurora6
ddaeac46e8 fix: optimize UpdateUserBalance and fix precision loss for orgBalance/userBalance. (#4499) 2025-11-19 21:13:32 +08:00
IsAurora6
f9d061d905 feat: return transaction IDs in API and disable links for anonymous user in transaction list (#4498) 2025-11-19 17:40:30 +08:00
Yang Luo
5e550e4364 feat: fix bug in createTable() 2025-11-19 17:33:51 +08:00
Yang Luo
146d54d6f6 feat: add Order pages (#4492) 2025-11-19 14:05:52 +08:00
IsAurora6
1df15a2706 fix: Transaction category & type links not navigating. (#4496) 2025-11-19 11:41:36 +08:00
Yang Luo
f7d73bbfdd Improve transaction fields 2025-11-19 09:14:49 +08:00
Yang Luo
a8b7217348 fix: add needSshfields() 2025-11-19 08:37:13 +08:00
Yang Luo
40a3b19cee feat: add Active Directory syncer support (#4495) 2025-11-19 08:30:01 +08:00
Yang Luo
98b45399a7 feat: add Google Workspace syncer (#4494) 2025-11-19 07:37:11 +08:00
Yang Luo
90edb7ab6b feat: refactor syncers into interface (#4490) 2025-11-19 01:28:37 +08:00
marun
e21b995eca feat: update payment providers when organization changes in PlanEditPage (#4462) 2025-11-19 00:14:01 +08:00
Yang Luo
81221f07f0 fix: improve isAllowedInDemoMode() for add-transaction API 2025-11-18 23:55:43 +08:00
Yang Luo
5fc2cdf637 feat: fix bug in GetEnforcer() API 2025-11-18 23:31:53 +08:00
Yang Luo
5e852e0121 feat: improve user edit page UI 2025-11-18 23:31:17 +08:00
Yang Luo
513ac6ffe9 fix: improve user edit page's transaction table UI 2025-11-18 23:31:16 +08:00
Yang Luo
821ba5673d Improve "Generate" button i18n 2025-11-18 23:31:16 +08:00
IsAurora6
d3ee73e48c feat: Add a URL field to the Transaction structure and optimize the display of the Transaction List. (#4487) 2025-11-18 21:45:57 +08:00
Yang Luo
1d719e3759 feat: fix OAuth-registered users to keep empty passwords unhashed (#4482) 2025-11-17 23:12:53 +08:00
Yang Luo
b3355a9fa6 fix: fix undefined owner in syncer edit page getCerts API call (#4471) 2025-11-17 22:51:12 +08:00
Yang Luo
ccc88cdafb feat: populate updated_time for all user creation paths (#4472) 2025-11-17 22:07:47 +08:00
Yang Luo
abf328bbe5 feat: allow setting email_verified in UpdateUser() API 2025-11-17 22:04:33 +08:00
DacongDA
5530253d38 feat: use correct org owner for UpdateOrganizationBalance (#4478) 2025-11-17 18:17:02 +08:00
Yang Luo
4cef6c5f3f feat: fix duplicate key error when re-importing users from different organization (#4473) 2025-11-17 02:13:35 +08:00
aozima
7e6929b900 feat: LDAP server adds more attributes: mail, mobile, sn, giveName (#4468) 2025-11-16 19:13:12 +08:00
aozima
46ae1a9580 fix: improve error handling for DingTalkIdProvider.GetUserInfo() (#4469) 2025-11-16 17:42:55 +08:00
Yang Luo
37e22f3e2c feat: support user custom password salt when organization salt is empty (#4465) 2025-11-15 02:35:15 +08:00
Yang Luo
68cde65d84 feat: fix bug about adding new permission in setEnforcerModel() 2025-11-12 20:39:44 +08:00
Yang Luo
1c7f5fdfe4 fix: fix transaction API to enforce user-level access control (#4447) 2025-11-12 20:31:14 +08:00
Yang Luo
1a5be46325 feat: add i18n support for password complexity error messages (#4458) 2025-11-12 19:40:21 +08:00
193 changed files with 10051 additions and 2168 deletions

View File

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

View File

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

View File

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

View File

@@ -83,15 +83,13 @@ func (c *ApiController) GetEnforcer() {
c.ResponseError(err.Error())
return
}
if enforcer == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The enforcer: %s does not exist"), id))
return
}
if loadModelCfg == "true" && enforcer.Model != "" {
err := enforcer.LoadModelCfg()
if err != nil {
return
if enforcer != nil {
if loadModelCfg == "true" && enforcer.Model != "" {
err = enforcer.LoadModelCfg()
if err != nil {
return
}
}
}

View File

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

@@ -0,0 +1,166 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetOrders
// @Title GetOrders
// @Tag Order API
// @Description get orders
// @Param owner query string true "The owner of orders"
// @Success 200 {array} object.Order The Response object
// @router /get-orders [get]
func (c *ApiController) GetOrders() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
orders, err := object.GetOrders(owner)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(orders)
} else {
limit := util.ParseInt(limit)
count, err := object.GetOrderCount(owner, field, value)
if err != nil {
c.ResponseError(err.Error())
return
}
paginator := pagination.SetPaginator(c.Ctx, limit, count)
orders, err := object.GetPaginationOrders(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(orders, paginator.Nums())
}
}
// GetUserOrders
// @Title GetUserOrders
// @Tag Order API
// @Description get orders for a user
// @Param owner query string true "The owner of orders"
// @Param user query string true "The username of the user"
// @Success 200 {array} object.Order The Response object
// @router /get-user-orders [get]
func (c *ApiController) GetUserOrders() {
owner := c.Input().Get("owner")
user := c.Input().Get("user")
orders, err := object.GetUserOrders(owner, user)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(orders)
}
// GetOrder
// @Title GetOrder
// @Tag Order API
// @Description get order
// @Param id query string true "The id ( owner/name ) of the order"
// @Success 200 {object} object.Order The Response object
// @router /get-order [get]
func (c *ApiController) GetOrder() {
id := c.Input().Get("id")
order, err := object.GetOrder(id)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(order)
}
// UpdateOrder
// @Title UpdateOrder
// @Tag Order API
// @Description update order
// @Param id query string true "The id ( owner/name ) of the order"
// @Param body body object.Order true "The details of the order"
// @Success 200 {object} controllers.Response The Response object
// @router /update-order [post]
func (c *ApiController) UpdateOrder() {
id := c.Input().Get("id")
var order object.Order
err := json.Unmarshal(c.Ctx.Input.RequestBody, &order)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateOrder(id, &order))
c.ServeJSON()
}
// AddOrder
// @Title AddOrder
// @Tag Order API
// @Description add order
// @Param body body object.Order true "The details of the order"
// @Success 200 {object} controllers.Response The Response object
// @router /add-order [post]
func (c *ApiController) AddOrder() {
var order object.Order
err := json.Unmarshal(c.Ctx.Input.RequestBody, &order)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddOrder(&order))
c.ServeJSON()
}
// DeleteOrder
// @Title DeleteOrder
// @Tag Order API
// @Description delete order
// @Param body body object.Order true "The details of the order"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-order [post]
func (c *ApiController) DeleteOrder() {
var order object.Order
err := json.Unmarshal(c.Ctx.Input.RequestBody, &order)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteOrder(&order))
c.ServeJSON()
}

169
controllers/order_pay.go Normal file
View 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()
}

View File

@@ -130,6 +130,10 @@ func (c *ApiController) UpdateOrganization() {
isGlobalAdmin, _ := c.isGlobalAdmin()
if organization.BalanceCurrency == "" {
organization.BalanceCurrency = "USD"
}
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization, isGlobalAdmin))
c.ServeJSON()
}
@@ -165,6 +169,10 @@ func (c *ApiController) AddOrganization() {
return
}
if organization.BalanceCurrency == "" {
organization.BalanceCurrency = "USD"
}
c.Data["json"] = wrapActionResponse(object.AddOrganization(&organization))
c.ServeJSON()
}

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package deployment

16
go.mod
View File

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

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

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package i18n

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 وجود ندارد",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 サーバーに存在しません",

View File

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

View File

@@ -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이(가) 존재하지 않습니다",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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服务器中未找到",

View File

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

View File

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

View File

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

View File

@@ -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 = "***"

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package object

View File

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

@@ -0,0 +1,156 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"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
View 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)
}

View File

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

View File

@@ -384,6 +384,11 @@ func (a *Ormer) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(Order))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Plan))
if err != nil {
panic(err)

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package object

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

@@ -0,0 +1,166 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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
}

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

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

@@ -0,0 +1,31 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
// KeycloakSyncerProvider implements SyncerProvider for Keycloak database syncers
// Keycloak syncer extends DatabaseSyncerProvider with special handling for Keycloak schema
type KeycloakSyncerProvider struct {
DatabaseSyncerProvider
}
// GetOriginalUsers retrieves all users from Keycloak database
// This method overrides the base implementation to handle Keycloak-specific logic
func (p *KeycloakSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
// Use the base database implementation
return p.DatabaseSyncerProvider.GetOriginalUsers()
}
// Note: Keycloak-specific user mapping is handled in syncer_util.go
// via getOriginalUsersFromMap which checks syncer.Type == "Keycloak"

View File

@@ -15,62 +15,22 @@
package object
import (
"context"
"database/sql"
"database/sql/driver"
"fmt"
"strings"
"time"
"golang.org/x/crypto/ssh"
"github.com/casdoor/casdoor/util"
"github.com/go-sql-driver/mysql"
)
type OriginalUser = User
type Credential struct {
Value string `json:"value"`
Salt string `json:"salt"`
}
func (syncer *Syncer) getOriginalUsers() ([]*OriginalUser, error) {
// Handle WeCom syncer separately
if syncer.Type == "WeCom" {
return syncer.getWecomOriginalUsers()
}
// Handle Azure AD syncer separately
if syncer.Type == "Azure AD" {
return syncer.getAzureAdOriginalUsers()
}
var results []map[string]sql.NullString
err := syncer.Ormer.Engine.Table(syncer.getTable()).Find(&results)
if err != nil {
return nil, err
}
// Memory leak problem handling
// https://github.com/casdoor/casdoor/issues/1256
users := syncer.getOriginalUsersFromMap(results)
for _, m := range results {
for k := range m {
delete(m, k)
}
}
return users, nil
provider := GetSyncerProvider(syncer)
return provider.GetOriginalUsers()
}
func (syncer *Syncer) addUser(user *OriginalUser) (bool, error) {
m := syncer.getMapFromOriginalUser(user)
affected, err := syncer.Ormer.Engine.Table(syncer.getTable()).Insert(m)
if err != nil {
return false, err
}
return affected != 0, nil
provider := GetSyncerProvider(syncer)
return provider.AddUser(user)
}
func (syncer *Syncer) getCasdoorColumns() []string {
@@ -85,16 +45,8 @@ func (syncer *Syncer) getCasdoorColumns() []string {
}
func (syncer *Syncer) updateUser(user *OriginalUser) (bool, error) {
key := syncer.getTargetTablePrimaryKey()
m := syncer.getMapFromOriginalUser(user)
pkValue := m[key]
delete(m, key)
affected, err := syncer.Ormer.Engine.Table(syncer.getTable()).Where(fmt.Sprintf("%s = ?", key), pkValue).Update(&m)
if err != nil {
return false, err
}
return affected != 0, nil
provider := GetSyncerProvider(syncer)
return provider.UpdateUser(user)
}
func (syncer *Syncer) updateUserForOriginalFields(user *User, key string) (bool, error) {
@@ -139,80 +91,9 @@ func (syncer *Syncer) calculateHash(user *OriginalUser) string {
return util.GetMd5Hash(s)
}
type dsnConnector struct {
dsn string
driver driver.Driver
}
func (t dsnConnector) Connect(ctx context.Context) (driver.Conn, error) {
return t.driver.Open(t.dsn)
}
func (t dsnConnector) Driver() driver.Driver {
return t.driver
}
func (syncer *Syncer) initAdapter() error {
if syncer.Ormer != nil {
return nil
}
// WeCom syncer doesn't need database adapter
if syncer.Type == "WeCom" {
return nil
}
// Azure AD syncer doesn't need database adapter
if syncer.Type == "Azure AD" {
return nil
}
var dataSourceName string
if syncer.DatabaseType == "mssql" {
dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database)
} else if syncer.DatabaseType == "postgres" {
sslMode := "disable"
if syncer.SslMode != "" {
sslMode = syncer.SslMode
}
dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=%s dbname=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, sslMode, syncer.Database)
} else {
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", syncer.User, syncer.Password, syncer.Host, syncer.Port)
}
var db *sql.DB
var err error
if syncer.SshType != "" && (syncer.DatabaseType == "mysql" || syncer.DatabaseType == "postgres" || syncer.DatabaseType == "mssql") {
var dial *ssh.Client
if syncer.SshType == "password" {
dial, err = DialWithPassword(syncer.SshUser, syncer.SshPassword, syncer.SshHost, syncer.SshPort)
} else {
dial, err = DialWithCert(syncer.SshUser, syncer.Owner+"/"+syncer.Cert, syncer.SshHost, syncer.SshPort)
}
if err != nil {
return err
}
if syncer.DatabaseType == "mysql" {
dataSourceName = fmt.Sprintf("%s:%s@%s(%s:%d)/", syncer.User, syncer.Password, syncer.Owner+syncer.Name, syncer.Host, syncer.Port)
mysql.RegisterDialContext(syncer.Owner+syncer.Name, (&ViaSSHDialer{Client: dial, Context: nil}).MysqlDial)
} else if syncer.DatabaseType == "postgres" || syncer.DatabaseType == "mssql" {
db = sql.OpenDB(dsnConnector{dsn: dataSourceName, driver: &ViaSSHDialer{Client: dial, Context: nil, DatabaseType: syncer.DatabaseType}})
}
}
if !isCloudIntranet {
dataSourceName = strings.ReplaceAll(dataSourceName, "dbi.", "db.")
}
if db != nil {
syncer.Ormer, err = NewAdapterFromDb(syncer.DatabaseType, dataSourceName, syncer.Database, db)
} else {
syncer.Ormer, err = NewAdapter(syncer.DatabaseType, dataSourceName, syncer.Database)
}
return err
provider := GetSyncerProvider(syncer)
return provider.InitAdapter()
}
func RunSyncUsersJob() {

View File

@@ -12,7 +12,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package object

View File

@@ -26,6 +26,11 @@ import (
"github.com/casdoor/casdoor/util"
)
type Credential struct {
Value string `json:"value"`
Salt string `json:"salt"`
}
func (syncer *Syncer) getFullAvatarUrl(avatar string) string {
if syncer.AvatarBaseUrl == "" {
return avatar
@@ -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 {

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ package object
import (
"fmt"
"strings"
"github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/pp"
@@ -28,22 +29,20 @@ type Transaction struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
// Transaction Provider Info
Provider string `xorm:"varchar(100)" json:"provider"`
Category string `xorm:"varchar(100)" json:"category"`
Type string `xorm:"varchar(100)" json:"type"`
// Product Info
ProductName string `xorm:"varchar(100)" json:"productName"`
ProductDisplayName string `xorm:"varchar(100)" json:"productDisplayName"`
Detail string `xorm:"varchar(255)" json:"detail"`
Tag string `xorm:"varchar(100)" json:"tag"`
Currency string `xorm:"varchar(100)" json:"currency"`
Amount float64 `json:"amount"`
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
// User Info
User string `xorm:"varchar(100)" json:"user"`
Application string `xorm:"varchar(100)" json:"application"`
Payment string `xorm:"varchar(100)" json:"payment"`
Domain string `xorm:"varchar(1000)" json:"domain"`
Category string `xorm:"varchar(100)" json:"category"`
Type string `xorm:"varchar(100)" json:"type"`
Subtype string `xorm:"varchar(100)" json:"subtype"`
Provider string `xorm:"varchar(100)" json:"provider"`
User string `xorm:"varchar(100)" json:"user"`
Tag string `xorm:"varchar(100)" json:"tag"`
Amount float64 `json:"amount"`
Currency string `xorm:"varchar(100)" json:"currency"`
Payment string `xorm:"varchar(100)" json:"payment"`
State pp.PaymentState `xorm:"varchar(100)" json:"state"`
}
@@ -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
}

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package object

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

View File

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

View File

@@ -124,7 +124,6 @@ func chooseFaviconLinkBySizes(links []Link) *Link {
var chosenLink *Link
for _, link := range links {
link := link
if chosenLink == nil || compareSizes(link.Sizes, chosenLink.Sizes) > 0 {
chosenLink = &link
}

View File

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

View File

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

View File

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

@@ -0,0 +1,88 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import "math"
// Fixed exchange rates (temporary implementation as per requirements)
// All rates represent how many units of the currency equal 1 USD
// Example: EUR: 0.92 means 1 USD = 0.92 EUR
var exchangeRates = map[string]float64{
"USD": 1.0,
"EUR": 0.92,
"GBP": 0.79,
"JPY": 149.50,
"CNY": 7.24,
"AUD": 1.52,
"CAD": 1.39,
"CHF": 0.88,
"HKD": 7.82,
"SGD": 1.34,
"INR": 83.12,
"KRW": 1319.50,
"BRL": 4.97,
"MXN": 17.09,
"ZAR": 18.15,
"RUB": 92.50,
"TRY": 32.15,
"NZD": 1.67,
"SEK": 10.35,
"NOK": 10.72,
"DKK": 6.87,
"PLN": 3.91,
"THB": 34.50,
"MYR": 4.47,
"IDR": 15750.00,
"PHP": 55.50,
"VND": 24500.00,
}
// GetExchangeRate returns the exchange rate from fromCurrency to toCurrency
func GetExchangeRate(fromCurrency, toCurrency string) float64 {
if fromCurrency == toCurrency {
return 1.0
}
// Default to USD if currency not found
fromRate, fromExists := exchangeRates[fromCurrency]
if !fromExists {
fromRate = 1.0
}
toRate, toExists := exchangeRates[toCurrency]
if !toExists {
toRate = 1.0
}
// Convert from source currency to USD, then from USD to target currency
// Example: EUR to JPY = (1/0.92) * 149.50 = USD/EUR * JPY/USD
return toRate / fromRate
}
// ConvertCurrency converts an amount from one currency to another using exchange rates
func ConvertCurrency(amount float64, fromCurrency, toCurrency string) float64 {
if fromCurrency == toCurrency {
return amount
}
rate := GetExchangeRate(fromCurrency, toCurrency)
converted := amount * rate
return math.Round(converted*1e8) / 1e8
}
func AddPrices(price1 float64, price2 float64) float64 {
res := price1 + price2
return math.Round(res*1e8) / 1e8
}

255
pp/fastspring.go Normal file
View 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
View 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
View 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
View 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"
}

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package radius

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,6 @@
// limitations under the License.
//go:build !skipCi
// +build !skipCi
package sync

View File

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