Compare commits

..

25 Commits

Author SHA1 Message Date
XDTD
cc456f265f feat: fix LDAP user password checking logic in GetOAuthToken() (#1975) 2023-06-15 21:04:09 +08:00
Yaodong Yu
7058a34f87 feat: complete group tree (#1967)
* feat: complete group tree

* feat: ui

* fix: i18n

* refactor code

* fix: support remove user from group

* fix: format code

* Update organization.go

* Update organization.go

* Update user_group.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-06-14 23:27:46 +08:00
UsherFall
8e6755845f ci: fix bug in PaypalPaymentProvider (#1972) 2023-06-13 23:33:03 +08:00
XDTD
967fa4be68 feat: add access key and secret key for user (#1971) 2023-06-13 22:18:17 +08:00
Yaodong Yu
805cf20d04 feat: fix incorrect VerifyTypePhone value (#1968) 2023-06-13 17:26:37 +08:00
907997375
2a8001f490 fix: clean timeout when componentWillUnmount in PaymentResult page (#1962) 2023-06-13 02:00:52 +08:00
UsherFall
451fc9034f fix: fix bug in PayPal payment provider (#1959) 2023-06-12 13:43:37 +08:00
Yaodong Yu
0e14a2597e feat: Add tree structure to organization page (#1910)
* rebase master

* feat: add group in userEditPage

* feat: use id as the pk

* feat: add groups item in user

* feat: add tree component

* rebase

* feat: ui

* fix: fix some bug

* fix: route

* fix: ui

* fix: improve ui
2023-06-12 09:27:16 +08:00
Yang Luo
ff87c4ea33 feat: fix createDatabase arg not recognized bug 2023-06-12 01:57:58 +08:00
Yang Luo
4f5396c70e Check error for CreateDatabase() 2023-06-12 01:47:26 +08:00
Yang Luo
3c30222fce Fix payment owner issue 2023-06-12 00:34:41 +08:00
Yang Luo
2d04731622 Provide default value for logConfig 2023-06-10 15:59:56 +08:00
Yang Luo
e0d2bc3dc9 Return error in GetProviderFromContext() 2023-06-10 15:51:26 +08:00
Jiawei Chen
0bda29f143 feat: show 404 error for non-existent objects in edit pages 2023-06-10 01:56:15 +08:00
Yang Luo
05703720c5 Add Custom to resourceType 2023-06-09 21:52:30 +08:00
hsluoyz
cc566bf31f Move DoMigration() after CreateTables() 2023-06-09 09:36:20 +08:00
XDTD
e93d8c19d9 feat: resolve user pages malfunction after using tableNamePrefix (#1945) 2023-06-08 00:43:05 +08:00
Yang Luo
f2e3182a69 Fix null value in backend Translate() 2023-06-07 02:17:48 +08:00
Yang Luo
f934531083 Fix organization search in some pages 2023-06-06 20:53:45 +08:00
XDTD
e1c0af345f feat: resolve casdoor as SAML SP with keycloak login not work bug (#1937)
* fix: resolve casdoor as SAML SP with keycloak login not work

* Update provider.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-06-06 15:19:00 +08:00
Yang Luo
3b3bfe39f9 Fix user field bug 2023-06-06 14:59:50 +08:00
Alex OvsInc
18cc952f8e feat: Customization of the initialization file (#1936) 2023-06-05 21:00:28 +08:00
Yang Luo
43439bc8c6 Apply tableNamePrefix before migration 2023-06-05 00:30:48 +08:00
Yang Luo
9a2800e3b3 Add error to Enforce() 2023-06-04 17:29:34 +08:00
Yaodong Yu
fdaad2b608 chore: refactor enforce() handler and update Swagger docs (#1931)
* chore: add swaggerof enforce api

* Update enforcer.go

* Update string.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-06-04 17:19:58 +08:00
93 changed files with 2931 additions and 337 deletions

View File

@@ -21,4 +21,5 @@ isDemoMode = false
batchSize = 100
ldapServerPort = 389
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
logConfig = {"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
logConfig = {"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
initDataFile = "./init_data.json"

View File

@@ -66,6 +66,8 @@ func GetConfigString(key string) string {
if res == "" {
if key == "staticBaseUrl" {
res = "https://cdn.casbin.org"
} else if key == "logConfig" {
res = "{\"filename\": \"logs/casdoor.log\", \"maxdays\":99999, \"perm\":\"0770\"}"
}
}
@@ -108,10 +110,10 @@ func GetLanguage(language string) string {
return "en"
}
if len(language) < 2 {
if len(language) != 2 {
return "en"
} else {
return language[0:2]
return language
}
}

View File

@@ -528,7 +528,7 @@ func (c *ApiController) Login() {
}
properties := map[string]string{}
count, err := object.GetUserCount(application.Organization, "", "")
count, err := object.GetUserCount(application.Organization, "", "", "")
if err != nil {
c.ResponseError(err.Error())
return

View File

@@ -21,6 +21,16 @@ import (
"github.com/casdoor/casdoor/util"
)
// Enforce
// @Title Enforce
// @Tag Enforce API
// @Description Call Casbin Enforce API
// @Param body body object.CasbinRequest true "Casbin request"
// @Param permissionId query string false "permission id"
// @Param modelId query string false "model id"
// @Param resourceId query string false "resource id"
// @Success 200 {object} controllers.Response The Response object
// @router /enforce [post]
func (c *ApiController) Enforce() {
permissionId := c.Input().Get("permissionId")
modelId := c.Input().Get("modelId")
@@ -34,33 +44,59 @@ func (c *ApiController) Enforce() {
}
if permissionId != "" {
c.ResponseOk(object.Enforce(permissionId, &request))
enforceResult, err := object.Enforce(permissionId, &request)
if err != nil {
c.ResponseError(err.Error())
return
}
res := []bool{}
res = append(res, enforceResult)
c.ResponseOk(res)
return
}
permissions := make([]*object.Permission, 0)
res := []bool{}
permissions := []*object.Permission{}
if modelId != "" {
owner, modelName := util.GetOwnerAndNameFromId(modelId)
permissions, err = object.GetPermissionsByModel(owner, modelName)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
} else {
} else if resourceId != "" {
permissions, err = object.GetPermissionsByResource(resourceId)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
} else {
c.ResponseError(c.T("general:Missing parameter"))
return
}
res := []bool{}
for _, permission := range permissions {
res = append(res, object.Enforce(permission.GetId(), &request))
enforceResult, err := object.Enforce(permission.GetId(), &request)
if err != nil {
c.ResponseError(err.Error())
return
}
res = append(res, enforceResult)
}
c.Data["json"] = res
c.ServeJSON()
c.ResponseOk(res)
}
// BatchEnforce
// @Title BatchEnforce
// @Tag Enforce API
// @Description Call Casbin BatchEnforce API
// @Param body body object.CasbinRequest true "array of casbin requests"
// @Param permissionId query string false "permission id"
// @Param modelId query string false "model id"
// @Success 200 {object} controllers.Response The Response object
// @router /batch-enforce [post]
func (c *ApiController) BatchEnforce() {
permissionId := c.Input().Get("permissionId")
modelId := c.Input().Get("modelId")
@@ -68,26 +104,47 @@ func (c *ApiController) BatchEnforce() {
var requests []object.CasbinRequest
err := json.Unmarshal(c.Ctx.Input.RequestBody, &requests)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
if permissionId != "" {
c.Data["json"] = object.BatchEnforce(permissionId, &requests)
c.ServeJSON()
} else {
owner, modelName := util.GetOwnerAndNameFromId(modelId)
permissions, err := object.GetPermissionsByModel(owner, modelName)
enforceResult, err := object.BatchEnforce(permissionId, &requests)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
res := [][]bool{}
for _, permission := range permissions {
res = append(res, object.BatchEnforce(permission.GetId(), &requests))
res = append(res, enforceResult)
c.ResponseOk(res)
return
}
permissions := []*object.Permission{}
if modelId != "" {
owner, modelName := util.GetOwnerAndNameFromId(modelId)
permissions, err = object.GetPermissionsByModel(owner, modelName)
if err != nil {
c.ResponseError(err.Error())
return
}
} else {
c.ResponseError(c.T("general:Missing parameter"))
return
}
res := [][]bool{}
for _, permission := range permissions {
enforceResult, err := object.BatchEnforce(permission.GetId(), &requests)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(res)
res = append(res, enforceResult)
}
c.ResponseOk(res)
}
func (c *ApiController) GetAllObjects() {

148
controllers/group.go Normal file
View File

@@ -0,0 +1,148 @@
// Copyright 2023 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
package controllers
import (
"encoding/json"
"github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetGroups
// @Title GetGroups
// @Tag Group API
// @Description get groups
// @Param owner query string true "The owner of groups"
// @Success 200 {array} object.Group The Response object
// @router /get-groups [get]
func (c *ApiController) GetGroups() {
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")
withTree := c.Input().Get("withTree")
if limit == "" || page == "" {
groups, err := object.GetGroups(owner)
if err != nil {
c.ResponseError(err.Error())
return
} else {
if withTree == "true" {
c.ResponseOk(object.ConvertToTreeData(groups, owner))
return
}
c.ResponseOk(groups)
}
} else {
limit := util.ParseInt(limit)
count, err := object.GetGroupCount(owner, field, value)
if err != nil {
c.ResponseError(err.Error())
return
}
paginator := pagination.SetPaginator(c.Ctx, limit, count)
groups, err := object.GetPaginationGroups(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil {
c.ResponseError(err.Error())
return
} else {
c.ResponseOk(groups, paginator.Nums())
}
}
}
// GetGroup
// @Title GetGroup
// @Tag Group API
// @Description get group
// @Param id query string true "The id ( owner/name ) of the group"
// @Success 200 {object} object.Group The Response object
// @router /get-group [get]
func (c *ApiController) GetGroup() {
id := c.Input().Get("id")
group, err := object.GetGroup(id)
if err != nil {
c.ResponseError(err.Error())
} else {
c.ResponseOk(group)
}
}
// UpdateGroup
// @Title UpdateGroup
// @Tag Group API
// @Description update group
// @Param id query string true "The id ( owner/name ) of the group"
// @Param body body object.Group true "The details of the group"
// @Success 200 {object} controllers.Response The Response object
// @router /update-group [post]
func (c *ApiController) UpdateGroup() {
id := c.Input().Get("id")
var group object.Group
err := json.Unmarshal(c.Ctx.Input.RequestBody, &group)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateGroup(id, &group))
c.ServeJSON()
}
// AddGroup
// @Title AddGroup
// @Tag Group API
// @Description add group
// @Param body body object.Group true "The details of the group"
// @Success 200 {object} controllers.Response The Response object
// @router /add-group [post]
func (c *ApiController) AddGroup() {
var group object.Group
err := json.Unmarshal(c.Ctx.Input.RequestBody, &group)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddGroup(&group))
c.ServeJSON()
}
// DeleteGroup
// @Title DeleteGroup
// @Tag Group API
// @Description delete group
// @Param body body object.Group true "The details of the group"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-group [post]
func (c *ApiController) DeleteGroup() {
var group object.Group
err := json.Unmarshal(c.Ctx.Input.RequestBody, &group)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteGroup(&group))
c.ServeJSON()
}

View File

@@ -34,6 +34,7 @@ import (
// @router /get-messages [get]
func (c *ApiController) GetMessages() {
owner := c.Input().Get("owner")
organization := c.Input().Get("organization")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
@@ -41,7 +42,7 @@ func (c *ApiController) GetMessages() {
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
chat := c.Input().Get("chat")
organization := c.Input().Get("organization")
if limit == "" || page == "" {
var messages []*object.Message
var err error

View File

@@ -47,21 +47,31 @@ func (c *ApiController) GetOrganizations() {
c.Data["json"] = maskedOrganizations
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
count, err := object.GetOrganizationCount(owner, field, value)
if err != nil {
c.ResponseError(err.Error())
return
}
isGlobalAdmin := c.IsGlobalAdmin()
if !isGlobalAdmin {
maskedOrganizations, err := object.GetMaskedOrganizations(object.GetOrganizations(owner, c.getCurrentUser().Owner))
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedOrganizations)
} else {
limit := util.ParseInt(limit)
count, err := object.GetOrganizationCount(owner, field, value)
if err != nil {
c.ResponseError(err.Error())
return
}
paginator := pagination.SetPaginator(c.Ctx, limit, count)
organizations, err := object.GetMaskedOrganizations(object.GetPaginationOrganizations(owner, paginator.Offset(), limit, field, value, sortField, sortOrder))
if err != nil {
c.ResponseError(err.Error())
return
}
paginator := pagination.SetPaginator(c.Ctx, limit, count)
organizations, err := object.GetMaskedOrganizations(object.GetPaginationOrganizations(owner, paginator.Offset(), limit, field, value, sortField, sortOrder))
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(organizations, paginator.Nums())
c.ResponseOk(organizations, paginator.Nums())
}
}
}
@@ -74,14 +84,13 @@ func (c *ApiController) GetOrganizations() {
// @router /get-organization [get]
func (c *ApiController) GetOrganization() {
id := c.Input().Get("id")
maskedOrganization, err := object.GetMaskedOrganization(object.GetOrganization(id))
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = maskedOrganization
c.ServeJSON()
c.ResponseOk(maskedOrganization)
}
// UpdateOrganization ...
@@ -180,12 +189,12 @@ func (c *ApiController) GetDefaultApplication() {
// @Title GetOrganizationNames
// @Tag Organization API
// @Param owner query string true "owner"
// @Description get all organization names
// @Description get all organization name and displayName
// @Success 200 {array} object.Organization The Response object
// @router /get-organization-names [get]
func (c *ApiController) GetOrganizationNames() {
owner := c.Input().Get("owner")
organizationNames, err := object.GetOrganizationsByFields(owner, "name")
organizationNames, err := object.GetOrganizationsByFields(owner, []string{"name", "display_name"}...)
if err != nil {
c.ResponseError(err.Error())
return

View File

@@ -31,6 +31,7 @@ import (
// @router /get-payments [get]
func (c *ApiController) GetPayments() {
owner := c.Input().Get("owner")
organization := c.Input().Get("organization")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
@@ -48,13 +49,13 @@ func (c *ApiController) GetPayments() {
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
count, err := object.GetPaymentCount(owner, field, value)
count, err := object.GetPaymentCount(owner, organization, field, value)
if err != nil {
panic(err)
}
paginator := pagination.SetPaginator(c.Ctx, limit, count)
payments, err := object.GetPaginationPayments(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
payments, err := object.GetPaginationPayments(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil {
panic(err)
}
@@ -177,10 +178,11 @@ func (c *ApiController) NotifyPayment() {
providerName := c.Ctx.Input.Param(":provider")
productName := c.Ctx.Input.Param(":product")
paymentName := c.Ctx.Input.Param(":payment")
orderId := c.Ctx.Input.Param("order")
body := c.Ctx.Input.RequestBody
err, errorResponse := object.NotifyPayment(c.Ctx.Request, body, owner, providerName, productName, paymentName)
err, errorResponse := object.NotifyPayment(c.Ctx.Request, body, owner, providerName, productName, paymentName, orderId)
_, err2 := c.Ctx.ResponseWriter.Write([]byte(errorResponse))
if err2 != nil {

View File

@@ -180,11 +180,11 @@ func (c *ApiController) BuyProduct() {
return
}
payUrl, err := object.BuyProduct(id, providerName, user, host)
payUrl, orderId, err := object.BuyProduct(id, providerName, user, host)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(payUrl)
c.ResponseOk(payUrl, orderId)
}

View File

@@ -139,8 +139,9 @@ func (c *ApiController) DeleteResource() {
return
}
provider, _, ok := c.GetProviderFromContext("Storage")
if !ok {
provider, err := c.GetProviderFromContext("Storage")
if err != nil {
c.ResponseError(err.Error())
return
}
@@ -187,8 +188,9 @@ func (c *ApiController) UploadResource() {
return
}
provider, _, ok := c.GetProviderFromContext("Storage")
if !ok {
provider, err := c.GetProviderFromContext("Storage")
if err != nil {
c.ResponseError(err.Error())
return
}

View File

@@ -69,9 +69,9 @@ func (c *ApiController) SendEmail() {
} else {
// called by Casdoor SDK via Client ID & Client Secret, so the used Email provider will be the application' Email provider or the default Email provider
var ok bool
provider, _, ok = c.GetProviderFromContext("Email")
if !ok {
provider, err = c.GetProviderFromContext("Email")
if err != nil {
c.ResponseError(err.Error())
return
}
}
@@ -127,13 +127,14 @@ func (c *ApiController) SendEmail() {
// @Success 200 {object} Response object
// @router /api/send-sms [post]
func (c *ApiController) SendSms() {
provider, _, ok := c.GetProviderFromContext("SMS")
if !ok {
provider, err := c.GetProviderFromContext("SMS")
if err != nil {
c.ResponseError(err.Error())
return
}
var smsForm SmsForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &smsForm)
err = json.Unmarshal(c.Ctx.Input.RequestBody, &smsForm)
if err != nil {
c.ResponseError(err.Error())
return

View File

@@ -80,6 +80,7 @@ func (c *ApiController) GetGlobalUsers() {
// @router /get-users [get]
func (c *ApiController) GetUsers() {
owner := c.Input().Get("owner")
groupId := c.Input().Get("groupId")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
@@ -88,6 +89,16 @@ func (c *ApiController) GetUsers() {
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
if groupId != "" {
maskedUsers, err := object.GetMaskedUsers(object.GetGroupUsers(groupId))
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedUsers)
return
}
maskedUsers, err := object.GetMaskedUsers(object.GetUsers(owner))
if err != nil {
panic(err)
@@ -97,14 +108,14 @@ func (c *ApiController) GetUsers() {
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
count, err := object.GetUserCount(owner, field, value)
count, err := object.GetUserCount(owner, field, value, groupId)
if err != nil {
c.ResponseError(err.Error())
return
}
paginator := pagination.SetPaginator(c.Ctx, limit, count)
users, err := object.GetPaginationUsers(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
users, err := object.GetPaginationUsers(owner, paginator.Offset(), limit, field, value, sortField, sortOrder, groupId)
if err != nil {
c.ResponseError(err.Error())
return
@@ -287,7 +298,7 @@ func (c *ApiController) AddUser() {
return
}
count, err := object.GetUserCount("", "", "")
count, err := object.GetUserCount("", "", "", "")
if err != nil {
c.ResponseError(err.Error())
return
@@ -505,7 +516,7 @@ func (c *ApiController) GetUserCount() {
var count int64
var err error
if isOnline == "" {
count, err = object.GetUserCount(owner, "", "")
count, err = object.GetUserCount(owner, "", "", "")
} else {
count, err = object.GetOnlineUserCount(owner, util.ParseInt(isOnline))
}
@@ -517,3 +528,34 @@ func (c *ApiController) GetUserCount() {
c.Data["json"] = count
c.ServeJSON()
}
// AddUserkeys
// @Title AddUserkeys
// @router /add-user-keys [post]
// @Tag User API
func (c *ApiController) AddUserkeys() {
var user object.User
err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
if err != nil {
c.ResponseError(err.Error())
return
}
isAdmin := c.IsAdmin()
affected, err := object.AddUserkeys(&user, isAdmin)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(affected)
}
func (c *ApiController) RemoveUserFromGroup() {
owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name")
groupId := c.Ctx.Request.Form.Get("groupId")
c.Data["json"] = wrapActionResponse(object.RemoveUserFromGroup(owner, name, groupId))
c.ServeJSON()
}

View File

@@ -139,47 +139,46 @@ func (c *ApiController) IsMaskedEnabled() (bool, bool) {
return true, isMaskEnabled
}
func (c *ApiController) GetProviderFromContext(category string) (*object.Provider, *object.User, bool) {
func (c *ApiController) GetProviderFromContext(category string) (*object.Provider, error) {
providerName := c.Input().Get("provider")
if providerName != "" {
provider, err := object.GetProvider(util.GetId("admin", providerName))
if err != nil {
panic(err)
return nil, err
}
if provider == nil {
c.ResponseError(fmt.Sprintf(c.T("util:The provider: %s is not found"), providerName))
return nil, nil, false
err = fmt.Errorf(c.T("util:The provider: %s is not found"), providerName)
return nil, err
}
return provider, nil, true
return provider, nil
}
userId, ok := c.RequireSignedIn()
if !ok {
return nil, nil, false
return nil, fmt.Errorf(c.T("general:Please login first"))
}
application, user, err := object.GetApplicationByUserId(userId)
application, err := object.GetApplicationByUserId(userId)
if err != nil {
panic(err)
return nil, err
}
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("util:No application is found for userId: %s"), userId))
return nil, nil, false
return nil, fmt.Errorf(c.T("util:No application is found for userId: %s"), userId)
}
provider, err := application.GetProviderByCategory(category)
if err != nil {
panic(err)
return nil, err
}
if provider == nil {
c.ResponseError(fmt.Sprintf(c.T("util:No provider for category: %s is found for application: %s"), category, application.Name))
return nil, nil, false
return nil, fmt.Errorf(c.T("util:No provider for category: %s is found for application: %s"), category, application.Name)
}
return provider, user, true
return provider, nil
}
func checkQuotaForApplication(count int) error {

1
go.mod
View File

@@ -59,6 +59,7 @@ require (
github.com/tealeg/xlsx v1.0.5
github.com/thanhpk/randstr v1.0.4
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/xorm-io/builder v0.3.13 // indirect
github.com/xorm-io/core v0.7.4
github.com/xorm-io/xorm v1.1.6
github.com/yusufpapurcu/wmi v1.2.2 // indirect

11
main.go
View File

@@ -30,13 +30,18 @@ import (
"github.com/casdoor/casdoor/util"
)
func main() {
createDatabase := flag.Bool("createDatabase", false, "true if you need Casdoor to create database")
func getCreateDatabaseFlag() bool {
res := flag.Bool("createDatabase", false, "true if you need Casdoor to create database")
flag.Parse()
return *res
}
func main() {
createDatabase := getCreateDatabaseFlag()
object.InitAdapter()
object.CreateTables(createDatabase)
object.DoMigration()
object.CreateTables(*createDatabase)
object.InitDb()
object.InitFromFile()

View File

@@ -41,18 +41,26 @@ func InitConfig() {
beego.BConfig.WebConfig.Session.SessionOn = true
InitAdapter()
DoMigration()
CreateTables(true)
DoMigration()
}
func InitAdapter() {
adapter = NewAdapter(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName(), conf.GetConfigString("dbName"))
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
tbMapper := core.NewPrefixMapper(core.SnakeMapper{}, tableNamePrefix)
adapter.Engine.SetTableMapper(tbMapper)
}
func CreateTables(createDatabase bool) {
if createDatabase {
adapter.CreateDatabase()
err := adapter.CreateDatabase()
if err != nil {
panic(err)
}
}
adapter.createTable()
}
@@ -122,10 +130,6 @@ func (a *Adapter) createTable() {
showSql := conf.GetConfigBool("showSql")
a.Engine.ShowSQL(showSql)
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
tbMapper := core.NewPrefixMapper(core.SnakeMapper{}, tableNamePrefix)
a.Engine.SetTableMapper(tbMapper)
err := a.Engine.Sync2(new(Organization))
if err != nil {
panic(err)
@@ -136,6 +140,16 @@ func (a *Adapter) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(Group))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(UserGroupRelation))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Role))
if err != nil {
panic(err)
@@ -295,6 +309,9 @@ func GetSessionForUser(owner string, offset, limit int, field, value, sortField,
}
if field != "" && value != "" {
if filterField(field) {
if offset != -1 {
field = fmt.Sprintf("a.%s", field)
}
session = session.And(fmt.Sprintf("%s like ?", util.SnakeString(field)), fmt.Sprintf("%%%s%%", value))
}
}
@@ -302,7 +319,8 @@ func GetSessionForUser(owner string, offset, limit int, field, value, sortField,
sortField = "created_time"
}
tableName := "user"
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
tableName := tableNamePrefix + "user"
if offset == -1 {
if sortOrder == "ascend" {
session = session.Asc(util.SnakeString(sortField))

View File

@@ -235,16 +235,16 @@ func GetApplicationByUser(user *User) (*Application, error) {
}
}
func GetApplicationByUserId(userId string) (application *Application, user *User, err error) {
func GetApplicationByUserId(userId string) (application *Application, err error) {
owner, name := util.GetOwnerAndNameFromId(userId)
if owner == "app" {
application, err = getApplication("admin", name)
return
}
user, err = GetUser(userId)
user, err := GetUser(userId)
if err != nil {
return nil, nil, err
return nil, err
}
application, err = GetApplicationByUser(user)
return

228
object/group.go Normal file
View File

@@ -0,0 +1,228 @@
// Copyright 2023 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 (
"errors"
"fmt"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
)
type Group struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk unique" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
Id string `xorm:"varchar(100) not null index" json:"id"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Manager string `xorm:"varchar(100)" json:"manager"`
ContactEmail string `xorm:"varchar(100)" json:"contactEmail"`
Type string `xorm:"varchar(100)" json:"type"`
ParentId string `xorm:"varchar(100)" json:"parentId"`
IsTopGroup bool `xorm:"bool" json:"isTopGroup"`
Users *[]string `xorm:"-" json:"users"`
Title string `json:"title,omitempty"`
Key string `json:"key,omitempty"`
Children []*Group `json:"children,omitempty"`
IsEnabled bool `json:"isEnabled"`
}
type GroupNode struct{}
func GetGroupCount(owner, field, value string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Group{})
if err != nil {
return 0, err
}
return count, nil
}
func GetGroups(owner string) ([]*Group, error) {
groups := []*Group{}
err := adapter.Engine.Desc("created_time").Find(&groups, &Group{Owner: owner})
if err != nil {
return nil, err
}
return groups, nil
}
func GetPaginationGroups(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Group, error) {
groups := []*Group{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&groups)
if err != nil {
return nil, err
}
return groups, nil
}
func getGroup(owner string, name string) (*Group, error) {
if owner == "" || name == "" {
return nil, nil
}
group := Group{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&group)
if err != nil {
return nil, err
}
if existed {
return &group, nil
} else {
return nil, nil
}
}
func getGroupById(id string) (*Group, error) {
if id == "" {
return nil, nil
}
group := Group{Id: id}
existed, err := adapter.Engine.Get(&group)
if err != nil {
return nil, err
}
if existed {
return &group, nil
} else {
return nil, nil
}
}
func GetGroup(id string) (*Group, error) {
owner, name := util.GetOwnerAndNameFromId(id)
return getGroup(owner, name)
}
func UpdateGroup(id string, group *Group) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(id)
oldGroup, err := getGroup(owner, name)
if oldGroup == nil {
return false, err
}
group.UpdatedTime = util.GetCurrentTime()
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(group)
if err != nil {
return false, err
}
return affected != 0, nil
}
func AddGroup(group *Group) (bool, error) {
if group.Id == "" {
group.Id = util.GenerateId()
}
affected, err := adapter.Engine.Insert(group)
if err != nil {
return false, err
}
return affected != 0, nil
}
func AddGroups(groups []*Group) (bool, error) {
if len(groups) == 0 {
return false, nil
}
affected, err := adapter.Engine.Insert(groups)
if err != nil {
return false, err
}
return affected != 0, nil
}
func DeleteGroup(group *Group) (bool, error) {
_, err := adapter.Engine.Get(group)
if err != nil {
return false, err
}
if count, err := adapter.Engine.Where("parent_id = ?", group.Id).Count(&Group{}); err != nil {
return false, err
} else if count > 0 {
return false, errors.New("group has children group")
}
if count, err := GetGroupUserCount(group.GetId(), "", ""); err != nil {
return false, err
} else if count > 0 {
return false, errors.New("group has users")
}
session := adapter.Engine.NewSession()
defer session.Close()
if err := session.Begin(); err != nil {
return false, err
}
if _, err := session.Delete(&UserGroupRelation{GroupId: group.Id}); err != nil {
session.Rollback()
return false, err
}
affected, err := session.ID(core.PK{group.Owner, group.Name}).Delete(&Group{})
if err != nil {
session.Rollback()
return false, err
}
if err := session.Commit(); err != nil {
return false, err
}
return affected != 0, nil
}
func (group *Group) GetId() string {
return fmt.Sprintf("%s/%s", group.Owner, group.Name)
}
func ConvertToTreeData(groups []*Group, parentId string) []*Group {
treeData := []*Group{}
for _, group := range groups {
if group.ParentId == parentId {
node := &Group{
Title: group.DisplayName,
Key: group.Name,
Type: group.Type,
Owner: group.Owner,
Id: group.Id,
}
children := ConvertToTreeData(groups, group.Id)
if len(children) > 0 {
node.Children = children
}
treeData = append(treeData, node)
}
}
return treeData
}

View File

@@ -61,6 +61,7 @@ func getBuiltInAccountItems() []*AccountItem {
{Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Roles", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Permissions", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Groups", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Properties", Visible: false, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},

View File

@@ -14,7 +14,10 @@
package object
import "github.com/casdoor/casdoor/util"
import (
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
)
type InitData struct {
Organizations []*Organization `json:"organizations"`
@@ -35,7 +38,12 @@ type InitData struct {
}
func InitFromFile() {
initData, err := readInitDataFromFile("./init_data.json")
initDataFile := conf.GetConfigString("initDataFile")
if initDataFile == "" {
return
}
initData, err := readInitDataFromFile(initDataFile)
if err != nil {
panic(err)
}

View File

@@ -22,6 +22,7 @@ import (
"github.com/casdoor/casdoor/cred"
"github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/builder"
"github.com/xorm-io/core"
)
@@ -75,11 +76,18 @@ func GetOrganizationCount(owner, field, value string) (int64, error) {
return session.Count(&Organization{})
}
func GetOrganizations(owner string) ([]*Organization, error) {
func GetOrganizations(owner string, name ...string) ([]*Organization, error) {
organizations := []*Organization{}
err := adapter.Engine.Desc("created_time").Find(&organizations, &Organization{Owner: owner})
if err != nil {
return nil, err
if name != nil && len(name) > 0 {
err := adapter.Engine.Desc("created_time").Where(builder.In("name", name)).Find(&organizations)
if err != nil {
return nil, err
}
} else {
err := adapter.Engine.Desc("created_time").Find(&organizations, &Organization{Owner: owner})
if err != nil {
return nil, err
}
}
return organizations, nil
@@ -334,6 +342,13 @@ func organizationChangeTrigger(oldName string, newName string) error {
return err
}
group := new(Group)
group.Owner = newName
_, err = session.Where("owner=?", oldName).Update(group)
if err != nil {
return err
}
role := new(Role)
_, err = adapter.Engine.Where("owner=?", oldName).Get(role)
if err != nil {

View File

@@ -56,9 +56,9 @@ type Payment struct {
InvoiceUrl string `xorm:"varchar(255)" json:"invoiceUrl"`
}
func GetPaymentCount(owner, field, value string) (int64, error) {
func GetPaymentCount(owner, organization, field, value string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "")
return session.Count(&Payment{})
return session.Count(&Payment{Organization: organization})
}
func GetPayments(owner string) ([]*Payment, error) {
@@ -81,10 +81,10 @@ func GetUserPayments(owner string, organization string, user string) ([]*Payment
return payments, nil
}
func GetPaginationPayments(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Payment, error) {
func GetPaginationPayments(owner, organization string, offset, limit int, field, value, sortField, sortOrder string) ([]*Payment, error) {
payments := []*Payment{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&payments)
err := session.Find(&payments, &Payment{Organization: organization})
if err != nil {
return nil, err
}
@@ -149,7 +149,7 @@ func DeletePayment(payment *Payment) (bool, error) {
return affected != 0, nil
}
func notifyPayment(request *http.Request, body []byte, owner string, providerName string, productName string, paymentName string) (*Payment, error, string) {
func notifyPayment(request *http.Request, body []byte, owner string, providerName string, productName string, paymentName string, orderId string) (*Payment, error, string) {
provider, err := getProvider(owner, providerName)
if err != nil {
panic(err)
@@ -180,7 +180,7 @@ func notifyPayment(request *http.Request, body []byte, owner string, providerNam
return payment, err, pProvider.GetResponseError(err)
}
productDisplayName, paymentName, price, productName, providerName, err := pProvider.Notify(request, body, cert.AuthorityPublicKey)
productDisplayName, paymentName, price, productName, providerName, err := pProvider.Notify(request, body, cert.AuthorityPublicKey, orderId)
if err != nil {
return payment, err, pProvider.GetResponseError(err)
}
@@ -199,8 +199,8 @@ func notifyPayment(request *http.Request, body []byte, owner string, providerNam
return payment, err, pProvider.GetResponseError(err)
}
func NotifyPayment(request *http.Request, body []byte, owner string, providerName string, productName string, paymentName string) (error, string) {
payment, err, errorResponse := notifyPayment(request, body, owner, providerName, productName, paymentName)
func NotifyPayment(request *http.Request, body []byte, owner string, providerName string, productName string, paymentName string, orderId string) (error, string) {
payment, err, errorResponse := notifyPayment(request, body, owner, providerName, productName, paymentName, orderId)
if payment != nil {
if err != nil {
payment.State = "Error"

View File

@@ -241,33 +241,29 @@ func removePolicies(permission *Permission) {
type CasbinRequest = []interface{}
func Enforce(permissionId string, request *CasbinRequest) bool {
func Enforce(permissionId string, request *CasbinRequest) (bool, error) {
permission, err := GetPermission(permissionId)
if err != nil {
panic(err)
return false, err
}
enforcer := getEnforcer(permission)
allow, err := enforcer.Enforce(*request...)
if err != nil {
panic(err)
}
return allow
return enforcer.Enforce(*request...)
}
func BatchEnforce(permissionId string, requests *[]CasbinRequest) []bool {
func BatchEnforce(permissionId string, requests *[]CasbinRequest) ([]bool, error) {
permission, err := GetPermission(permissionId)
if err != nil {
panic(err)
res := []bool{}
for i := 0; i < len(*requests); i++ {
res = append(res, false)
}
return res, err
}
enforcer := getEnforcer(permission)
allow, err := enforcer.BatchEnforce(*requests)
if err != nil {
panic(err)
}
return allow
return enforcer.BatchEnforce(*requests)
}
func getAllValues(userId string, fn func(enforcer *casbin.Enforcer) []string) []string {

View File

@@ -156,24 +156,24 @@ func (product *Product) getProvider(providerId string) (*Provider, error) {
return provider, nil
}
func BuyProduct(id string, providerName string, user *User, host string) (string, error) {
func BuyProduct(id string, providerName string, user *User, host string) (string, string, error) {
product, err := GetProduct(id)
if err != nil {
return "", err
return "", "", err
}
if product == nil {
return "", fmt.Errorf("the product: %s does not exist", id)
return "", "", fmt.Errorf("the product: %s does not exist", id)
}
provider, err := product.getProvider(providerName)
if err != nil {
return "", err
return "", "", err
}
pProvider, _, err := provider.getPaymentProvider()
if err != nil {
return "", err
return "", "", err
}
owner := product.Owner
@@ -186,9 +186,9 @@ func BuyProduct(id string, providerName string, user *User, host string) (string
returnUrl := fmt.Sprintf("%s/payments/%s/result", originFrontend, paymentName)
notifyUrl := fmt.Sprintf("%s/api/notify-payment/%s/%s/%s/%s", originBackend, owner, providerName, productName, paymentName)
payUrl, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, returnUrl, notifyUrl)
payUrl, orderId, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, product.Currency, returnUrl, notifyUrl)
if err != nil {
return "", err
return "", "", err
}
payment := Payment{
@@ -217,14 +217,14 @@ func BuyProduct(id string, providerName string, user *User, host string) (string
affected, err := AddPayment(&payment)
if err != nil {
return "", err
return "", "", err
}
if !affected {
return "", fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
return "", "", fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
}
return payUrl, err
return payUrl, orderId, err
}
func ExtendProductWithProviders(product *Product) error {

View File

@@ -38,7 +38,7 @@ func TestProduct(t *testing.T) {
paymentName := util.GenerateTimeId()
returnUrl := ""
notifyUrl := ""
payUrl, err := pProvider.Pay(provider.Name, product.Name, "alice", paymentName, product.DisplayName, product.Price, returnUrl, notifyUrl)
payUrl, _, err := pProvider.Pay(provider.Name, product.Name, "alice", paymentName, product.DisplayName, product.Price, product.Currency, returnUrl, notifyUrl)
if err != nil {
panic(err)
}

View File

@@ -225,8 +225,10 @@ func UpdateProvider(id string, provider *Provider) (bool, error) {
session = session.Omit("client_secret2")
}
provider.Endpoint = util.GetEndPoint(provider.Endpoint)
provider.IntranetEndpoint = util.GetEndPoint(provider.IntranetEndpoint)
if provider.Type != "Keycloak" {
provider.Endpoint = util.GetEndPoint(provider.Endpoint)
provider.IntranetEndpoint = util.GetEndPoint(provider.IntranetEndpoint)
}
affected, err := session.Update(provider)
if err != nil {
@@ -237,8 +239,10 @@ func UpdateProvider(id string, provider *Provider) (bool, error) {
}
func AddProvider(provider *Provider) (bool, error) {
provider.Endpoint = util.GetEndPoint(provider.Endpoint)
provider.IntranetEndpoint = util.GetEndPoint(provider.IntranetEndpoint)
if provider.Type != "Keycloak" {
provider.Endpoint = util.GetEndPoint(provider.Endpoint)
provider.IntranetEndpoint = util.GetEndPoint(provider.IntranetEndpoint)
}
affected, err := adapter.Engine.Insert(provider)
if err != nil {

View File

@@ -628,7 +628,12 @@ func GetPasswordToken(application *Application, username string, password string
ErrorDescription: "the user does not exist",
}, nil
}
msg := CheckPassword(user, password, "en")
var msg string
if user.Ldap != "" {
msg = checkLdapUserPassword(user, password, "en")
} else {
msg = CheckPassword(user, password, "en")
}
if msg != "" {
return nil, &TokenError{
Error: InvalidGrant,

View File

@@ -77,6 +77,9 @@ type User struct {
SignupApplication string `xorm:"varchar(100)" json:"signupApplication"`
Hash string `xorm:"varchar(100)" json:"hash"`
PreHash string `xorm:"varchar(100)" json:"preHash"`
Groups []string `xorm:"varchar(1000)" json:"groups"`
AccessKey string `xorm:"varchar(100)" json:"accessKey"`
SecretKey string `xorm:"varchar(100)" json:"secretKey"`
CreatedIp string `xorm:"varchar(100)" json:"createdIp"`
LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"`
@@ -218,8 +221,13 @@ func GetPaginationGlobalUsers(offset, limit int, field, value, sortField, sortOr
return users, nil
}
func GetUserCount(owner, field, value string) (int64, error) {
func GetUserCount(owner, field, value string, groupId string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "")
if groupId != "" {
return GetGroupUserCount(groupId, field, value)
}
return session.Count(&User{})
}
@@ -257,14 +265,18 @@ func GetSortedUsers(owner string, sorter string, limit int) ([]*User, error) {
return users, nil
}
func GetPaginationUsers(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*User, error) {
func GetPaginationUsers(owner string, offset, limit int, field, value, sortField, sortOrder string, groupId string) ([]*User, error) {
users := []*User{}
if groupId != "" {
return GetPaginationGroupUsers(groupId, offset, limit, field, value, sortField, sortOrder)
}
session := GetSessionForUser(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&users)
if err != nil {
return nil, err
}
return users, nil
}
@@ -375,6 +387,23 @@ func GetUserByUserId(owner string, userId string) (*User, error) {
}
}
func GetUserByAccessKey(accessKey string) (*User, error) {
if accessKey == "" {
return nil, nil
}
user := User{AccessKey: accessKey}
existed, err := adapter.Engine.Get(&user)
if err != nil {
return nil, err
}
if existed {
return &user, nil
} else {
return nil, nil
}
}
func GetUser(id string) (*User, error) {
owner, name := util.GetOwnerAndNameFromId(id)
return getUser(owner, name)
@@ -479,7 +508,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
"owner", "display_name", "avatar",
"location", "address", "country_code", "region", "language", "affiliation", "title", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts",
"signin_wrong_times", "last_signin_wrong_time",
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "secret_key",
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
@@ -493,7 +522,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
columns = append(columns, "name", "email", "phone", "country_code")
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).Cols(columns...).Update(user)
affected, err := updateUser(oldUser, user, columns)
if err != nil {
return false, err
}
@@ -501,6 +530,35 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
return affected != 0, nil
}
func updateUser(oldUser, user *User, columns []string) (int64, error) {
session := adapter.Engine.NewSession()
defer session.Close()
session.Begin()
if util.ContainsString(columns, "groups") {
affected, err := updateUserGroupRelation(session, user)
if err != nil {
session.Rollback()
return affected, err
}
}
affected, err := session.ID(core.PK{oldUser.Owner, oldUser.Name}).Cols(columns...).Update(user)
if err != nil {
session.Rollback()
return affected, err
}
err = session.Commit()
if err != nil {
session.Rollback()
return 0, err
}
return affected, nil
}
func UpdateUserForAllFields(id string, user *User) (bool, error) {
var err error
owner, name := util.GetOwnerAndNameFromId(id)
@@ -580,7 +638,7 @@ func AddUser(user *User) (bool, error) {
}
}
count, err := GetUserCount(user.Owner, "", "")
count, err := GetUserCount(user.Owner, "", "", "")
if err != nil {
return false, err
}
@@ -668,6 +726,11 @@ func DeleteUser(user *User) (bool, error) {
return false, err
}
affected, err = deleteRelationByUser(user.Id)
if err != nil {
return false, err
}
return affected != 0, nil
}
@@ -852,3 +915,14 @@ func (user *User) GetPreferMfa(masked bool) *MfaProps {
return user.MultiFactorAuths[0]
}
}
func AddUserkeys(user *User, isAdmin bool) (bool, error) {
if user == nil {
return false, nil
}
user.AccessKey = util.GenerateId()
user.SecretKey = util.GenerateId()
return UpdateUser(user.GetId(), user, []string{}, isAdmin)
}

157
object/user_group.go Normal file
View File

@@ -0,0 +1,157 @@
package object
import (
"errors"
"fmt"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
"github.com/xorm-io/xorm"
)
type UserGroupRelation struct {
UserId string `xorm:"varchar(100) notnull pk" json:"userId"`
GroupId string `xorm:"varchar(100) notnull pk" json:"groupId"`
CreatedTime string `xorm:"created" json:"createdTime"`
UpdatedTime string `xorm:"updated" json:"updatedTime"`
}
func updateUserGroupRelation(session *xorm.Session, user *User) (int64, error) {
physicalGroupCount, err := session.Where("type = ?", "Physical").In("id", user.Groups).Count(Group{})
if err != nil {
return 0, err
}
if physicalGroupCount > 1 {
return 0, errors.New("user can only be in one physical group")
}
groups := []*Group{}
err = session.In("id", user.Groups).Find(&groups)
if err != nil {
return 0, err
}
if len(groups) != len(user.Groups) {
return 0, errors.New("group not found")
}
_, err = session.Delete(&UserGroupRelation{UserId: user.Id})
if err != nil {
return 0, err
}
relations := []*UserGroupRelation{}
for _, group := range groups {
relations = append(relations, &UserGroupRelation{UserId: user.Id, GroupId: group.Id})
}
if len(relations) == 0 {
return 1, nil
}
_, err = session.Insert(relations)
if err != nil {
return 0, err
}
return 1, nil
}
func RemoveUserFromGroup(owner, name, groupId string) (bool, error) {
user, err := getUser(owner, name)
if err != nil {
return false, err
}
groups := []string{}
for _, group := range user.Groups {
if group != groupId {
groups = append(groups, group)
}
}
user.Groups = groups
_, err = UpdateUser(util.GetId(owner, name), user, []string{"groups"}, false)
if err != nil {
return false, err
}
return true, nil
}
func deleteUserGroupRelation(session *xorm.Session, userId, groupId string) (int64, error) {
affected, err := session.ID(core.PK{userId, groupId}).Delete(&UserGroupRelation{})
return affected, err
}
func deleteRelationByUser(id string) (int64, error) {
affected, err := adapter.Engine.Delete(&UserGroupRelation{UserId: id})
return affected, err
}
func GetGroupUserCount(id string, field, value string) (int64, error) {
group, err := GetGroup(id)
if group == nil || err != nil {
return 0, err
}
if field == "" && value == "" {
return adapter.Engine.Count(UserGroupRelation{GroupId: group.Id})
} else {
return adapter.Engine.Table("user").
Join("INNER", []string{"user_group_relation", "r"}, "user.id = r.user_id").
Where("r.group_id = ?", group.Id).
And(fmt.Sprintf("user.%s LIKE ?", util.CamelToSnakeCase(field)), "%"+value+"%").
Count()
}
}
func GetPaginationGroupUsers(id string, offset, limit int, field, value, sortField, sortOrder string) ([]*User, error) {
group, err := GetGroup(id)
if group == nil || err != nil {
return nil, err
}
users := []*User{}
session := adapter.Engine.Table("user").
Join("INNER", []string{"user_group_relation", "r"}, "user.id = r.user_id").
Where("r.group_id = ?", group.Id)
if offset != -1 && limit != -1 {
session.Limit(limit, offset)
}
if field != "" && value != "" {
session = session.And(fmt.Sprintf("user.%s LIKE ?", util.CamelToSnakeCase(field)), "%"+value+"%")
}
if sortField == "" || sortOrder == "" {
sortField = "created_time"
}
if sortOrder == "ascend" {
session = session.Asc(fmt.Sprintf("user.%s", util.SnakeString(sortField)))
} else {
session = session.Desc(fmt.Sprintf("user.%s", util.SnakeString(sortField)))
}
err = session.Find(&users)
if err != nil {
return nil, err
}
return users, nil
}
func GetGroupUsers(id string) ([]*User, error) {
group, err := GetGroup(id)
if group == nil || err != nil {
return []*User{}, err
}
users := []*User{}
err = adapter.Engine.Table("user_group_relation").Join("INNER", []string{"user", "u"}, "user_group_relation.user_id = u.id").
Where("user_group_relation.group_id = ?", group.Id).
Find(&users)
if err != nil {
return nil, err
}
return users, nil
}

View File

@@ -295,6 +295,13 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
itemsChanged = append(itemsChanged, item)
}
oldUserGroupsJson, _ := json.Marshal(oldUser.Groups)
newUserGroupsJson, _ := json.Marshal(newUser.Groups)
if string(oldUserGroupsJson) != string(newUserGroupsJson) {
item := GetAccountItemByName("Groups", organization)
itemsChanged = append(itemsChanged, item)
}
if oldUser.IsAdmin != newUser.IsAdmin {
item := GetAccountItemByName("Is admin", organization)
itemsChanged = append(itemsChanged, item)

View File

@@ -224,7 +224,7 @@ func GetVerifyType(username string) (verificationCodeType string) {
if strings.Contains(username, "@") {
return VerifyTypeEmail
} else {
return VerifyTypeEmail
return VerifyTypePhone
}
}

View File

@@ -45,7 +45,7 @@ func NewAlipayPaymentProvider(appId string, appCertificate string, appPrivateKey
return pp, nil
}
func (pp *AlipayPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error) {
func (pp *AlipayPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
// pp.Client.DebugSwitch = gopay.DebugOn
bm := gopay.BodyMap{}
@@ -62,12 +62,12 @@ func (pp *AlipayPaymentProvider) Pay(providerName string, productName string, pa
payUrl, err := pp.Client.TradePagePay(context.Background(), bm)
if err != nil {
return "", err
return "", "", err
}
return payUrl, nil
return payUrl, "", nil
}
func (pp *AlipayPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string) (string, string, float64, string, string, error) {
func (pp *AlipayPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (string, string, float64, string, string, error) {
bm, err := alipay.ParseNotifyToBodyMap(request)
if err != nil {
return "", "", 0, "", "", err

View File

@@ -26,12 +26,12 @@ func NewDummyPaymentProvider() (*DummyPaymentProvider, error) {
return pp, nil
}
func (pp *DummyPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error) {
func (pp *DummyPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
payUrl := fmt.Sprintf("/payments/%s/result", paymentName)
return payUrl, nil
return payUrl, "", nil
}
func (pp *DummyPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string) (string, string, float64, string, string, error) {
func (pp *DummyPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (string, string, float64, string, string, error) {
return "", "", 0, "", "", nil
}

View File

@@ -153,7 +153,7 @@ func (pp *GcPaymentProvider) doPost(postBytes []byte) ([]byte, error) {
return respBytes, nil
}
func (pp *GcPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error) {
func (pp *GcPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
payReqInfo := GcPayReqInfo{
OrderDate: util.GenerateSimpleTimeId(),
OrderNo: paymentName,
@@ -168,7 +168,7 @@ func (pp *GcPaymentProvider) Pay(providerName string, productName string, payerN
b, err := json.Marshal(payReqInfo)
if err != nil {
return "", err
return "", "", err
}
body := GcRequestBody{
@@ -184,39 +184,39 @@ func (pp *GcPaymentProvider) Pay(providerName string, productName string, payerN
bodyBytes, err := json.Marshal(body)
if err != nil {
return "", err
return "", "", err
}
respBytes, err := pp.doPost(bodyBytes)
if err != nil {
return "", err
return "", "", err
}
var respBody GcResponseBody
err = json.Unmarshal(respBytes, &respBody)
if err != nil {
return "", err
return "", "", err
}
if respBody.ReturnCode != "SUCCESS" {
return "", fmt.Errorf("%s: %s", respBody.ReturnCode, respBody.ReturnMsg)
return "", "", fmt.Errorf("%s: %s", respBody.ReturnCode, respBody.ReturnMsg)
}
payRespInfoBytes, err := base64.StdEncoding.DecodeString(respBody.Data)
if err != nil {
return "", err
return "", "", err
}
var payRespInfo GcPayRespInfo
err = json.Unmarshal(payRespInfoBytes, &payRespInfo)
if err != nil {
return "", err
return "", "", err
}
return payRespInfo.PayUrl, nil
return payRespInfo.PayUrl, "", nil
}
func (pp *GcPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string) (string, string, float64, string, string, error) {
func (pp *GcPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (string, string, float64, string, string, error) {
reqBody := GcRequestBody{}
m, err := url.ParseQuery(string(body))
if err != nil {

View File

@@ -16,10 +16,13 @@ package pp
import (
"context"
"fmt"
"errors"
"net/http"
"strconv"
"github.com/plutov/paypal/v4"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/paypal"
"github.com/go-pay/gopay/pkg/util"
)
type PaypalPaymentProvider struct {
@@ -29,7 +32,7 @@ type PaypalPaymentProvider struct {
func NewPaypalPaymentProvider(clientID string, secret string) (*PaypalPaymentProvider, error) {
pp := &PaypalPaymentProvider{}
client, err := paypal.NewClient(clientID, secret, paypal.APIBaseSandBox)
client, err := paypal.NewClient(clientID, secret, false)
if err != nil {
return nil, err
}
@@ -38,51 +41,62 @@ func NewPaypalPaymentProvider(clientID string, secret string) (*PaypalPaymentPro
return pp, nil
}
func (pp *PaypalPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error) {
// pp.Client.SetLog(os.Stdout) // Set log to terminal stdout
func (pp *PaypalPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
// pp.Client.DebugSwitch = gopay.DebugOn // Set log to terminal stdout
receiverEmail := "sb-tmsqa26118644@business.example.com"
amount := paypal.AmountPayout{
Value: fmt.Sprintf("%.2f", price),
Currency: "USD",
}
description := fmt.Sprintf("%s-%s", providerName, productName)
payout := paypal.Payout{
SenderBatchHeader: &paypal.SenderBatchHeader{
EmailSubject: description,
},
Items: []paypal.PayoutItem{
{
RecipientType: "EMAIL",
Receiver: receiverEmail,
Amount: &amount,
Note: description,
SenderItemID: description,
},
priceStr := strconv.FormatFloat(price, 'f', 2, 64)
var pus []*paypal.PurchaseUnit
item := &paypal.PurchaseUnit{
ReferenceId: util.GetRandomString(16),
Amount: &paypal.Amount{
CurrencyCode: currency,
Value: priceStr,
},
Description: joinAttachString([]string{productDisplayName, productName, providerName}),
}
pus = append(pus, item)
_, err := pp.Client.GetAccessToken(context.Background())
bm := make(gopay.BodyMap)
bm.Set("intent", "CAPTURE")
bm.Set("purchase_units", pus)
bm.SetBodyMap("application_context", func(b gopay.BodyMap) {
b.Set("brand_name", "Casdoor")
b.Set("locale", "en-PT")
b.Set("return_url", returnUrl)
})
ppRsp, err := pp.Client.CreateOrder(context.Background(), bm)
if err != nil {
return "", err
return "", "", err
}
if ppRsp.Code != paypal.Success {
return "", "", errors.New(ppRsp.Error)
}
payoutResponse, err := pp.Client.CreatePayout(context.Background(), payout)
if err != nil {
return "", err
}
payUrl := payoutResponse.Links[0].Href
return payUrl, nil
return ppRsp.Response.Links[1].Href, ppRsp.Response.Id, nil
}
func (pp *PaypalPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string) (string, string, float64, string, string, error) {
// The PayPal SDK does not directly support IPN verification.
// So, you need to implement this part according to PayPal's IPN guide.
return "", "", 0, "", "", nil
func (pp *PaypalPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (string, string, float64, string, string, error) {
ppRsp, err := pp.Client.OrderCapture(context.Background(), orderId, nil)
if err != nil {
return "", "", 0, "", "", err
}
if ppRsp.Code != paypal.Success {
return "", "", 0, "", "", errors.New(ppRsp.Error)
}
paymentName := ppRsp.Response.Id
price, err := strconv.ParseFloat(ppRsp.Response.PurchaseUnits[0].Amount.Value, 64)
if err != nil {
return "", "", 0, "", "", err
}
productDisplayName, productName, providerName, err := parseAttachString(ppRsp.Response.PurchaseUnits[0].Description)
if err != nil {
return "", "", 0, "", "", err
}
return productDisplayName, paymentName, price, productName, providerName, nil
}
func (pp *PaypalPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {

View File

@@ -17,8 +17,8 @@ package pp
import "net/http"
type PaymentProvider interface {
Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error)
Notify(request *http.Request, body []byte, authorityPublicKey string) (string, string, float64, string, string, error)
Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error)
Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (string, string, float64, string, string, error)
GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error)
GetResponseError(err error) string
}

View File

@@ -56,7 +56,7 @@ func NewWechatPaymentProvider(mchId string, apiV3Key string, appId string, mchCe
return pp, nil
}
func (pp *WechatPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error) {
func (pp *WechatPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
// pp.Client.DebugSwitch = gopay.DebugOn
bm := gopay.BodyMap{}
@@ -73,17 +73,17 @@ func (pp *WechatPaymentProvider) Pay(providerName string, productName string, pa
wxRsp, err := pp.Client.V3TransactionNative(context.Background(), bm)
if err != nil {
return "", err
return "", "", err
}
if wxRsp.Code != wechat.Success {
return "", errors.New(wxRsp.Error)
return "", "", errors.New(wxRsp.Error)
}
return wxRsp.Response.CodeUrl, nil
return wxRsp.Response.CodeUrl, "", nil
}
func (pp *WechatPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string) (string, string, float64, string, string, error) {
func (pp *WechatPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (string, string, float64, string, string, error) {
notifyReq, err := wechat.V3ParseNotify(request)
if err != nil {
panic(err)

View File

@@ -26,8 +26,10 @@ import (
)
type Object struct {
Owner string `json:"owner"`
Name string `json:"name"`
Owner string `json:"owner"`
Name string `json:"name"`
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
}
func getUsername(ctx *context.Context) (username string) {
@@ -43,6 +45,9 @@ func getUsername(ctx *context.Context) (username string) {
username = getUsernameByClientIdSecret(ctx)
}
if username == "" {
username = getUsernameByKeys(ctx)
}
return
}
@@ -98,6 +103,30 @@ func getObject(ctx *context.Context) (string, string) {
}
}
func getKeys(ctx *context.Context) (string, string) {
method := ctx.Request.Method
if method == http.MethodGet {
accessKey := ctx.Input.Query("accesskey")
secretKey := ctx.Input.Query("secretkey")
return accessKey, secretKey
} else {
body := ctx.Input.RequestBody
if len(body) == 0 {
return ctx.Request.Form.Get("accesskey"), ctx.Request.Form.Get("secretkey")
}
var obj Object
err := json.Unmarshal(body, &obj)
if err != nil {
return "", ""
}
return obj.AccessKey, obj.SecretKey
}
}
func willLog(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
if subOwner == "anonymous" && subName == "anonymous" && method == "GET" && (urlPath == "/api/get-account" || urlPath == "/api/get-app-login") && objOwner == "" && objName == "" {
return false

View File

@@ -49,7 +49,7 @@ func AutoSigninFilter(ctx *context.Context) {
}
userId := util.GetId(token.Organization, token.User)
application, _, err := object.GetApplicationByUserId(fmt.Sprintf("app/%s", token.Application))
application, err := object.GetApplicationByUserId(fmt.Sprintf("app/%s", token.Application))
if err != nil {
panic(err)
}

View File

@@ -84,6 +84,18 @@ func getUsernameByClientIdSecret(ctx *context.Context) string {
return fmt.Sprintf("app/%s", application.Name)
}
func getUsernameByKeys(ctx *context.Context) string {
accessKey, secretKey := getKeys(ctx)
user, err := object.GetUserByAccessKey(accessKey)
if err != nil {
panic(err)
}
if user != nil && secretKey == user.SecretKey {
return user.GetId()
}
return ""
}
func getSessionUser(ctx *context.Context) string {
user := ctx.Input.CruSession.Get("username")
if user == nil {

View File

@@ -73,9 +73,17 @@ func initAPI() {
beego.Router("/api/get-user-count", &controllers.ApiController{}, "GET:GetUserCount")
beego.Router("/api/get-user", &controllers.ApiController{}, "GET:GetUser")
beego.Router("/api/update-user", &controllers.ApiController{}, "POST:UpdateUser")
beego.Router("/api/add-user-keys", &controllers.ApiController{}, "POST:AddUserkeys")
beego.Router("/api/add-user", &controllers.ApiController{}, "POST:AddUser")
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/get-groups", &controllers.ApiController{}, "GET:GetGroups")
beego.Router("/api/get-group", &controllers.ApiController{}, "GET:GetGroup")
beego.Router("/api/update-group", &controllers.ApiController{}, "POST:UpdateGroup")
beego.Router("/api/add-group", &controllers.ApiController{}, "POST:AddGroup")
beego.Router("/api/delete-group", &controllers.ApiController{}, "POST:DeleteGroup")
beego.Router("/api/get-roles", &controllers.ApiController{}, "GET:GetRoles")
beego.Router("/api/get-role", &controllers.ApiController{}, "GET:GetRole")

View File

@@ -777,6 +777,46 @@
"operationId": "ApiController.HandleOfficialAccountEvent"
}
},
"/api/batch-enforce": {
"post": {
"tags": [
"Enforce API"
],
"description": "perform enforce",
"operationId": "ApiController.BatchEnforce",
"parameters": [
{
"in": "body",
"name": "body",
"description": "casbin request array",
"required": true,
"schema": {
"$ref": "#/definitions/object.CasbinRequest"
}
},
{
"in": "query",
"name": "permissionId",
"description": "permission id",
"type": "string"
},
{
"in": "query",
"name": "modelId",
"description": "model id",
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/buy-product": {
"post": {
"tags": [
@@ -1384,6 +1424,52 @@
}
}
},
"/api/enforce": {
"post": {
"tags": [
"Enforce API"
],
"description": "perform enforce",
"operationId": "ApiController.Enforce",
"parameters": [
{
"in": "body",
"name": "body",
"description": "casbin request",
"required": true,
"schema": {
"$ref": "#/definitions/object.CasbinRequest"
}
},
{
"in": "query",
"name": "permissionId",
"description": "permission id",
"type": "string"
},
{
"in": "query",
"name": "modelId",
"description": "model id",
"type": "string"
},
{
"in": "query",
"name": "resourceId",
"description": "resource id",
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/get-account": {
"get": {
"tags": [
@@ -1954,6 +2040,35 @@
}
}
},
"/api/get-organization-names": {
"get": {
"tags": [
"Organization API"
],
"description": "get all organization names",
"operationId": "ApiController.GetOrganizationNames",
"parameters": [
{
"in": "query",
"name": "owner",
"description": "owner",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/object.Organization"
}
}
}
}
}
},
"/api/get-organizations": {
"get": {
"tags": [
@@ -2826,7 +2941,6 @@
"in": "query",
"name": "id",
"description": "The id ( owner/name ) of the user",
"required": true,
"type": "string"
},
{
@@ -3062,6 +3176,23 @@
}
}
},
"/api/health": {
"get": {
"tags": [
"System API"
],
"description": "check if the system is live",
"operationId": "ApiController.Health",
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/invoice-payment": {
"post": {
"tags": [
@@ -4501,11 +4632,11 @@
}
},
"definitions": {
"1183.0x1400042eb70.false": {
"1225.0xc0002e2ae0.false": {
"title": "false",
"type": "object"
},
"1217.0x1400042eba0.false": {
"1260.0xc0002e2b10.false": {
"title": "false",
"type": "object"
},
@@ -4554,10 +4685,10 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/1183.0x1400042eb70.false"
"$ref": "#/definitions/1225.0xc0002e2ae0.false"
},
"data2": {
"$ref": "#/definitions/1217.0x1400042eba0.false"
"$ref": "#/definitions/1260.0xc0002e2b10.false"
},
"msg": {
"type": "string"
@@ -4595,6 +4726,10 @@
"title": "JSONWebKey",
"type": "object"
},
"object.\u0026{179844 0xc000a02f90 false}": {
"title": "\u0026{179844 0xc000a02f90 false}",
"type": "object"
},
"object.AccountItem": {
"title": "AccountItem",
"type": "object",
@@ -4693,6 +4828,9 @@
"formCss": {
"type": "string"
},
"formCssMobile": {
"type": "string"
},
"formOffset": {
"type": "integer",
"format": "int64"
@@ -4715,6 +4853,9 @@
"name": {
"type": "string"
},
"orgChoiceMode": {
"type": "string"
},
"organization": {
"type": "string"
},
@@ -4772,6 +4913,13 @@
}
}
},
"object.CasbinRequest": {
"title": "CasbinRequest",
"type": "array",
"items": {
"$ref": "#/definitions/object.\u0026{179844 0xc000a02f90 false}"
}
},
"object.Cert": {
"title": "Cert",
"type": "object",
@@ -5008,6 +5156,18 @@
}
}
},
"object.MfaItem": {
"title": "MfaItem",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"rule": {
"type": "string"
}
}
},
"object.MfaProps": {
"title": "MfaProps",
"type": "object",
@@ -5190,6 +5350,12 @@
"masterPassword": {
"type": "string"
},
"mfaItems": {
"type": "array",
"items": {
"$ref": "#/definitions/object.MfaItem"
}
},
"name": {
"type": "string"
},
@@ -5395,9 +5561,18 @@
"displayName": {
"type": "string"
},
"isEnabled": {
"type": "boolean"
},
"name": {
"type": "string"
},
"options": {
"type": "array",
"items": {
"type": "string"
}
},
"owner": {
"type": "string"
},
@@ -5411,9 +5586,6 @@
},
"role": {
"type": "string"
},
"options": {
"type": "array"
}
}
},
@@ -5737,6 +5909,9 @@
"name": {
"type": "string"
},
"object": {
"type": "string"
},
"organization": {
"type": "string"
},
@@ -6341,6 +6516,9 @@
"passwordSalt": {
"type": "string"
},
"passwordType": {
"type": "string"
},
"patreon": {
"type": "string"
},
@@ -6505,6 +6683,9 @@
"name": {
"type": "string"
},
"organization": {
"type": "string"
},
"phone": {
"type": "string"
},
@@ -6634,4 +6815,4 @@
"type": "object"
}
}
}
}

View File

@@ -502,6 +502,32 @@ paths:
tags:
- HandleOfficialAccountEvent API
operationId: ApiController.HandleOfficialAccountEvent
/api/batch-enforce:
post:
tags:
- Enforce API
description: perform enforce
operationId: ApiController.BatchEnforce
parameters:
- in: body
name: body
description: casbin request array
required: true
schema:
$ref: '#/definitions/object.CasbinRequest'
- in: query
name: permissionId
description: permission id
type: string
- in: query
name: modelId
description: model id
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/buy-product:
post:
tags:
@@ -893,6 +919,36 @@ paths:
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/enforce:
post:
tags:
- Enforce API
description: perform enforce
operationId: ApiController.Enforce
parameters:
- in: body
name: body
description: casbin request
required: true
schema:
$ref: '#/definitions/object.CasbinRequest'
- in: query
name: permissionId
description: permission id
type: string
- in: query
name: modelId
description: model id
type: string
- in: query
name: resourceId
description: resource id
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/get-account:
get:
tags:
@@ -1267,6 +1323,25 @@ paths:
type: array
items:
$ref: '#/definitions/object.Application'
/api/get-organization-names:
get:
tags:
- Organization API
description: get all organization names
operationId: ApiController.GetOrganizationNames
parameters:
- in: query
name: owner
description: owner
required: true
type: string
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Organization'
/api/get-organizations:
get:
tags:
@@ -1841,7 +1916,6 @@ paths:
- in: query
name: id
description: The id ( owner/name ) of the user
required: true
type: string
- in: query
name: owner
@@ -1994,6 +2068,17 @@ paths:
type: array
items:
$ref: '#/definitions/object.Webhook'
/api/health:
get:
tags:
- System API
description: check if the system is live
operationId: ApiController.Health
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/invoice-payment:
post:
tags:
@@ -2940,10 +3025,10 @@ paths:
schema:
$ref: '#/definitions/Response'
definitions:
1183.0x1400042eb70.false:
1225.0xc0002e2ae0.false:
title: "false"
type: object
1217.0x1400042eba0.false:
1260.0xc0002e2b10.false:
title: "false"
type: object
LaravelResponse:
@@ -2979,9 +3064,9 @@ definitions:
type: object
properties:
data:
$ref: '#/definitions/1183.0x1400042eb70.false'
$ref: '#/definitions/1225.0xc0002e2ae0.false'
data2:
$ref: '#/definitions/1217.0x1400042eba0.false'
$ref: '#/definitions/1260.0xc0002e2b10.false'
msg:
type: string
name:
@@ -3005,6 +3090,9 @@ definitions:
jose.JSONWebKey:
title: JSONWebKey
type: object
object.&{179844 0xc000a02f90 false}:
title: '&{179844 0xc000a02f90 false}'
type: object
object.AccountItem:
title: AccountItem
type: object
@@ -3072,6 +3160,8 @@ definitions:
type: string
formCss:
type: string
formCssMobile:
type: string
formOffset:
type: integer
format: int64
@@ -3087,6 +3177,8 @@ definitions:
type: string
name:
type: string
orgChoiceMode:
type: string
organization:
type: string
organizationObj:
@@ -3124,6 +3216,11 @@ definitions:
$ref: '#/definitions/object.ThemeData'
tokenFormat:
type: string
object.CasbinRequest:
title: CasbinRequest
type: array
items:
$ref: '#/definitions/object.&{179844 0xc000a02f90 false}'
object.Cert:
title: Cert
type: object
@@ -3284,6 +3381,14 @@ definitions:
type: string
text:
type: string
object.MfaItem:
title: MfaItem
type: object
properties:
name:
type: string
rule:
type: string
object.MfaProps:
title: MfaProps
type: object
@@ -3407,6 +3512,10 @@ definitions:
type: string
masterPassword:
type: string
mfaItems:
type: array
items:
$ref: '#/definitions/object.MfaItem'
name:
type: string
owner:
@@ -3544,8 +3653,14 @@ definitions:
type: string
displayName:
type: string
isEnabled:
type: boolean
name:
type: string
options:
type: array
items:
type: string
owner:
type: string
pricePerMonth:
@@ -3556,8 +3671,6 @@ definitions:
format: double
role:
type: string
options:
type: array
object.Pricing:
title: Pricing
type: object
@@ -3775,6 +3888,8 @@ definitions:
type: string
name:
type: string
object:
type: string
organization:
type: string
owner:
@@ -4181,6 +4296,8 @@ definitions:
type: string
passwordSalt:
type: string
passwordType:
type: string
patreon:
type: string
paypal:
@@ -4291,6 +4408,8 @@ definitions:
type: string
name:
type: string
organization:
type: string
phone:
type: string
picture:

View File

@@ -50,6 +50,11 @@ class AdapterEditPage extends React.Component {
AdapterBackend.getAdapter("admin", this.state.adapterName)
.then((res) => {
if (res.status === "ok") {
if (res.data === null) {
this.props.history.push("/404");
return;
}
this.setState({
adapter: res.data,
});

View File

@@ -15,6 +15,9 @@
import React, {Component} from "react";
import "./App.less";
import {Helmet} from "react-helmet";
import GroupTreePage from "./GroupTreePage";
import GroupEditPage from "./GroupEdit";
import GroupListPage from "./GroupList";
import * as Setting from "./Setting";
import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs";
import {BarsOutlined, CommentOutlined, DownOutlined, InfoCircleFilled, LogoutOutlined, SettingOutlined} from "@ant-design/icons";
@@ -128,10 +131,12 @@ class App extends Component {
});
if (uri === "/") {
this.setState({selectedMenuKey: "/"});
} else if (uri.includes("/organizations")) {
} else if (uri.includes("/organizations") || uri.includes("/trees")) {
this.setState({selectedMenuKey: "/organizations"});
} else if (uri.includes("/users")) {
this.setState({selectedMenuKey: "/users"});
} else if (uri.includes("/groups")) {
this.setState({selectedMenuKey: "/groups"});
} else if (uri.includes("/roles")) {
this.setState({selectedMenuKey: "/roles"});
} else if (uri.includes("/permissions")) {
@@ -405,12 +410,13 @@ class App extends Component {
res.push(Setting.getItem(<Link to="/">{i18next.t("general:Home")}</Link>, "/"));
if (Setting.isAdminUser(this.state.account)) {
if (Setting.isLocalAdminUser(this.state.account)) {
res.push(Setting.getItem(<Link to="/organizations">{i18next.t("general:Organizations")}</Link>,
"/organizations"));
}
if (Setting.isLocalAdminUser(this.state.account)) {
res.push(Setting.getItem(<Link to="/groups">{i18next.t("general:Groups")}</Link>,
"/groups"));
res.push(Setting.getItem(<Link to="/users">{i18next.t("general:Users")}</Link>,
"/users"
));
@@ -552,6 +558,10 @@ class App extends Component {
<Route exact path="/organizations" render={(props) => this.renderLoginIfNotLoggedIn(<OrganizationListPage account={this.state.account} {...props} />)} />
<Route exact path="/organizations/:organizationName" render={(props) => this.renderLoginIfNotLoggedIn(<OrganizationEditPage account={this.state.account} onChangeTheme={this.setTheme} {...props} />)} />
<Route exact path="/organizations/:organizationName/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)} />
<Route exact path="/trees/:organizationName" render={(props) => this.renderLoginIfNotLoggedIn(<GroupTreePage account={this.state.account} {...props} />)} />
<Route exact path="/trees/:organizationName/:groupName" render={(props) => this.renderLoginIfNotLoggedIn(<GroupTreePage account={this.state.account} {...props} />)} />
<Route exact path="/groups" render={(props) => this.renderLoginIfNotLoggedIn(<GroupListPage account={this.state.account} {...props} />)} />
<Route exact path="/groups/:organizationName/:groupName" render={(props) => this.renderLoginIfNotLoggedIn(<GroupEditPage account={this.state.account} {...props} />)} />
<Route exact path="/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)} />
<Route exact path="/users/:organizationName/:userName" render={(props) => <UserEditPage account={this.state.account} {...props} />} />
<Route exact path="/roles" render={(props) => this.renderLoginIfNotLoggedIn(<RoleListPage account={this.state.account} {...props} />)} />
@@ -618,6 +628,11 @@ class App extends Component {
});
};
isWithoutCard() {
return Setting.isMobile() || window.location.pathname === "/chat" ||
window.location.pathname.startsWith("/trees");
}
renderContent() {
const onClick = ({key}) => {
if (key === "/swagger") {
@@ -628,7 +643,6 @@ class App extends Component {
};
return (
<Layout id="parent-area">
{/* https://github.com/ant-design/ant-design/issues/40394 ant design bug. If it will be fixed, we can delete the code for control the color of Header*/}
<Header style={{padding: "0", marginBottom: "3px", backgroundColor: this.state.themeAlgorithm.includes("dark") ? "black" : "white"}}>
{Setting.isMobile() ? null : (
<Link to={"/"}>
@@ -664,7 +678,7 @@ class App extends Component {
}
</Header>
<Content style={{display: "flex", flexDirection: "column"}} >
{(Setting.isMobile() || window.location.pathname === "/chat") ?
{this.isWithoutCard() ?
this.renderRouter() :
<Card className="content-warp-card">
{this.renderRouter()}

View File

@@ -74,7 +74,6 @@ img {
.content-warp-card {
box-shadow: 0 1px 5px 0 rgb(51 51 51 / 14%);
margin: 5px;
flex: 1;
align-items: stretch;
}

View File

@@ -119,6 +119,11 @@ class ApplicationEditPage extends React.Component {
getApplication() {
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => {
if (application === null) {
this.props.history.push("/404");
return;
}
if (application.grantTypes === null || application.grantTypes === undefined || application.grantTypes.length === 0) {
application.grantTypes = ["authorization_code"];
}

View File

@@ -45,6 +45,11 @@ class CertEditPage extends React.Component {
getCert() {
CertBackend.getCert(this.state.owner, this.state.certName)
.then((cert) => {
if (cert === null) {
this.props.history.push("/404");
return;
}
this.setState({
cert: cert,
});

View File

@@ -108,7 +108,7 @@ class CertListPage extends BaseListPage {
key: "owner",
width: "150px",
sorter: true,
...this.getColumnSearchProps("organization"),
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (text !== "admin") ? text : i18next.t("provider:admin (Shared)");
},

View File

@@ -41,6 +41,11 @@ class ChatEditPage extends React.Component {
getChat() {
ChatBackend.getChat("admin", this.state.chatName)
.then((chat) => {
if (chat === null) {
this.props.history.push("/404");
return;
}
this.setState({
chat: chat,
});

268
web/src/GroupEdit.js Normal file
View File

@@ -0,0 +1,268 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, Row, Select, Switch} from "antd";
import * as GroupBackend from "./backend/GroupBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
class GroupEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
groupName: props.match.params.groupName,
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
group: null,
users: [],
groups: [],
organizations: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
UNSAFE_componentWillMount() {
this.getGroup();
this.getGroups(this.state.organizationName);
this.getOrganizations();
}
getGroup() {
GroupBackend.getGroup(this.state.organizationName, this.state.groupName)
.then((res) => {
if (res.status === "ok") {
if (res.data === null) {
this.props.history.push("/404");
return;
}
this.setState({
group: res.data,
});
}
});
}
getGroups(organizationName) {
GroupBackend.getGroups(organizationName)
.then((res) => {
if (res.status === "ok") {
this.setState({
groups: res.data,
});
}
});
}
getOrganizations() {
OrganizationBackend.getOrganizationNames("admin")
.then((res) => {
if (res.status === "ok") {
this.setState({
organizations: res.data,
});
}
});
}
parseGroupField(key, value) {
if ([""].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateGroupField(key, value) {
value = this.parseGroupField(key, value);
const group = this.state.group;
group[key] = value;
this.setState({
group: group,
});
}
getParentIdOptions() {
const groups = this.state.groups.filter((group) => group.id !== this.state.group.id);
const organization = this.state.organizations.find((organization) => organization.name === this.state.group.owner);
if (organization !== undefined) {
groups.push({id: organization.name, displayName: organization.displayName});
}
return groups.map((group) => ({label: group.displayName, value: group.id}));
}
renderGroup() {
return (
<Card size="small" title={
<div>
{this.state.mode === "add" ? i18next.t("group:New Group") : i18next.t("group:Edit Group")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitGroupEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitGroupEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteGroup()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
}
style={(Setting.isMobile()) ? {margin: "5px"} : {}}
type="inner"
>
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.group.owner}
onChange={(value => {
this.updateGroupField("owner", value);
this.getGroups(value);
})}
options={this.state.organizations.map((organization) => Setting.getOption(organization.displayName, organization.name))
} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.group.name} onChange={e => {
this.updateGroupField("name", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.group.displayName} onChange={e => {
this.updateGroupField("displayName", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select style={{width: "100%"}}
options={
[
{label: i18next.t("group:Virtual"), value: "Virtual"},
{label: i18next.t("group:Physical"), value: "Physical"},
].map((item) => ({label: item.label, value: item.value}))
}
value={this.state.group.type} onChange={(value => {
this.updateGroupField("type", value);
}
)} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("group:Parent group"), i18next.t("group:Parent group - Tooltip"))} :
</Col>
<Col span={22} >
<Select style={{width: "100%"}}
options={this.getParentIdOptions()}
value={this.state.group.parentId} onChange={(value => {
this.updateGroupField("parentId", value);
}
)} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.group.isEnabled} onChange={checked => {
this.updateGroupField("isEnabled", checked);
}} />
</Col>
</Row>
</Card>
);
}
submitGroupEdit(willExist) {
const group = Setting.deepCopy(this.state.group);
group["isTopGroup"] = this.state.organizations.some((organization) => organization.name === group.parentId);
GroupBackend.updateGroup(this.state.organizationName, this.state.groupName, group)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
groupName: this.state.group.name,
});
if (willExist) {
const groupTreeUrl = sessionStorage.getItem("groupTreeUrl");
if (groupTreeUrl !== null) {
sessionStorage.removeItem("groupTreeUrl");
this.props.history.push(groupTreeUrl);
} else {
this.props.history.push("/groups");
}
} else {
this.props.history.push(`/groups/${this.state.group.owner}/${this.state.group.name}`);
}
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateGroupField("name", this.state.groupName);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteGroup() {
GroupBackend.deleteGroup(this.state.group)
.then((res) => {
if (res.status === "ok") {
const groupTreeUrl = sessionStorage.getItem("groupTreeUrl");
if (groupTreeUrl !== null) {
sessionStorage.removeItem("groupTreeUrl");
this.props.history.push(groupTreeUrl);
} else {
this.props.history.push("/groups");
}
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
render() {
return (
<div>
{
this.state.group !== null ? this.renderGroup() : null
}
<div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitGroupEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitGroupEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteGroup()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);
}
}
export default GroupEditPage;

286
web/src/GroupList.js Normal file
View File

@@ -0,0 +1,286 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Table} from "antd";
import moment from "moment";
import * as Setting from "./Setting";
import * as GroupBackend from "./backend/GroupBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class GroupListPage extends BaseListPage {
constructor(props) {
super(props);
this.state = {
...this.state,
owner: Setting.isAdminUser(this.props.account) ? "" : this.props.account.owner,
groups: [],
};
}
UNSAFE_componentWillMount() {
super.UNSAFE_componentWillMount();
this.getGroups(this.state.owner);
}
getGroups(organizationName) {
GroupBackend.getGroups(organizationName)
.then((res) => {
if (res.status === "ok") {
this.setState({
groups: res.data,
});
}
});
}
newGroup() {
const randomName = Setting.getRandomName();
return {
owner: this.props.account.owner,
name: `group_${randomName}`,
createdTime: moment().format(),
updatedTime: moment().format(),
displayName: `New Group - ${randomName}`,
type: "Virtual",
parentId: this.props.account.owner,
isTopGroup: true,
isEnabled: true,
};
}
addGroup() {
const newGroup = this.newGroup();
GroupBackend.addGroup(newGroup)
.then((res) => {
if (res.status === "ok") {
this.props.history.push({pathname: `/groups/${newGroup.owner}/${newGroup.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteGroup(i) {
GroupBackend.deleteGroup(this.state.data[i])
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
renderTable(data) {
const columns = [
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "120px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
return (
<Link to={`/groups/${record.owner}/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
key: "createdTime",
width: "150px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("general:Updated time"),
dataIndex: "updatedTime",
key: "updatedTime",
width: "150px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("general:Display name"),
dataIndex: "displayName",
key: "displayName",
width: "100px",
sorter: true,
...this.getColumnSearchProps("displayName"),
},
{
title: i18next.t("general:Type"),
dataIndex: "type",
key: "type",
width: "110px",
sorter: true,
filterMultiple: false,
filters: [
{text: i18next.t("group:Virtual"), value: "Virtual"},
{text: i18next.t("group:Physical"), value: "Physical"},
],
render: (text, record, index) => {
return i18next.t("group:" + text);
},
},
{
title: i18next.t("group:Parent group"),
dataIndex: "parentId",
key: "parentId",
width: "110px",
sorter: true,
...this.getColumnSearchProps("parentId"),
render: (text, record, index) => {
if (record.isTopGroup) {
return <Link to={`/organizations/${record.parentId}`}>
{record.parentId}
</Link>;
}
const parentGroup = this.state.groups.find((group) => group.id === text);
if (parentGroup === undefined) {
return "";
}
return <Link to={`/groups/${parentGroup.owner}/${parentGroup.name}`}>
{parentGroup?.displayName}
</Link>;
},
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "170px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
const haveChildren = this.state.groups.find((group) => group.parentId === record.id) !== undefined;
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/groups/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal
disabled={haveChildren}
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteGroup(index)}
>
</PopconfirmModal>
</div>
);
},
},
];
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={data} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Groups")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addGroup.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
const sortField = params.sortField, sortOrder = params.sortOrder;
if (params.category !== undefined && params.category !== null) {
field = "category";
value = params.category;
} else if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({loading: true});
GroupBackend.getGroups(this.state.owner, false, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
this.setState({
loading: false,
});
if (res.status === "ok") {
this.setState({
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
} else {
if (Setting.isResponseDenied(res)) {
this.setState({
isAuthorized: false,
});
}
}
})
.catch(error => {
this.setState({
loading: false,
});
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
};
}
export default GroupListPage;

336
web/src/GroupTreePage.js Normal file
View File

@@ -0,0 +1,336 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {DeleteOutlined, EditOutlined, HolderOutlined, PlusOutlined, UsergroupAddOutlined} from "@ant-design/icons";
import {Button, Col, Empty, Row, Space, Tree} from "antd";
import i18next from "i18next";
import moment from "moment/moment";
import React from "react";
import * as GroupBackend from "./backend/GroupBackend";
import * as Setting from "./Setting";
import OrganizationSelect from "./common/select/OrganizationSelect";
import UserListPage from "./UserListPage";
class GroupTreePage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
owner: Setting.isAdminUser(this.props.account) ? "" : this.props.account.owner,
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
groupName: this.props.match?.params.groupName,
groupId: undefined,
treeData: [],
selectedKeys: [this.props.match?.params.groupName],
};
}
UNSAFE_componentWillMount() {
this.getTreeData();
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.organizationName !== prevState.organizationName) {
this.getTreeData();
}
if (prevState.treeData !== this.state.treeData) {
this.setTreeExpandedKeys();
}
}
getTreeData() {
GroupBackend.getGroups(this.state.organizationName, true).then((res) => {
if (res.status === "ok") {
this.setState({
treeData: res.data,
groupId: this.findNodeId({children: res.data}, this.state.groupName),
});
} else {
Setting.showMessage("error", res.msg);
}
});
}
findNodeId(node, targetName) {
if (node.key === targetName) {
return node.id;
}
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
const result = this.findNodeId(node.children[i], targetName);
if (result) {
return result;
}
}
}
return null;
}
setTreeTitle(treeData) {
const haveChildren = Array.isArray(treeData.children) && treeData.children.length > 0;
const isSelected = this.state.groupName === treeData.key;
return {
id: treeData.id,
key: treeData.key,
title: <Space>
{treeData.type === "Physical" ? <UsergroupAddOutlined /> : <HolderOutlined />}
<span>{treeData.title}</span>
{isSelected && (
<React.Fragment>
<PlusOutlined
style={{
visibility: "visible",
color: "inherit",
transition: "color 0.3s",
}}
onMouseEnter={(e) => {
e.currentTarget.style.color = "rgba(89,54,213,0.6)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = "inherit";
}}
onMouseDown={(e) => {
e.currentTarget.style.color = "rgba(89,54,213,0.4)";
}}
onMouseUp={(e) => {
e.currentTarget.style.color = "rgba(89,54,213,0.6)";
}}
onClick={(e) => {
e.stopPropagation();
sessionStorage.setItem("groupTreeUrl", window.location.pathname);
this.addGroup();
}}
/>
<EditOutlined
style={{
visibility: "visible",
color: "inherit",
transition: "color 0.3s",
}}
onMouseEnter={(e) => {
e.currentTarget.style.color = "rgba(89,54,213,0.6)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = "inherit";
}}
onMouseDown={(e) => {
e.currentTarget.style.color = "rgba(89,54,213,0.4)";
}}
onMouseUp={(e) => {
e.currentTarget.style.color = "rgba(89,54,213,0.6)";
}}
onClick={(e) => {
e.stopPropagation();
sessionStorage.setItem("groupTreeUrl", window.location.pathname);
this.props.history.push(`/groups/${this.state.organizationName}/${treeData.key}`);
}}
/>
{!haveChildren &&
<DeleteOutlined
style={{
visibility: "visible",
color: "inherit",
transition: "color 0.3s",
}}
onMouseEnter={(e) => {
e.currentTarget.style.color = "rgba(89,54,213,0.6)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = "inherit";
}}
onMouseDown={(e) => {
e.currentTarget.style.color = "rgba(89,54,213,0.4)";
}}
onMouseUp={(e) => {
e.currentTarget.style.color = "rgba(89,54,213,0.6)";
}}
onClick={(e) => {
e.stopPropagation();
GroupBackend.deleteGroup({owner: treeData.owner, name: treeData.key})
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.getTreeData();
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}}
/>
}
</React.Fragment>
)}
</Space>,
children: haveChildren ? treeData.children.map(i => this.setTreeTitle(i)) : [],
};
}
setTreeExpandedKeys = () => {
const expandedKeys = [];
const setExpandedKeys = (nodes) => {
for (const node of nodes) {
expandedKeys.push(node.key);
if (node.children) {
setExpandedKeys(node.children);
}
}
};
setExpandedKeys(this.state.treeData);
this.setState({
expandedKeys: expandedKeys,
});
};
renderTree() {
const onSelect = (selectedKeys, info) => {
this.setState({
selectedKeys: selectedKeys,
groupName: info.node.key,
groupId: info.node.id,
});
this.props.history.push(`/trees/${this.state.organizationName}/${info.node.key}`);
};
const onExpand = (expandedKeysValue) => {
this.setState({
expandedKeys: expandedKeysValue,
});
};
if (this.state.treeData.length === 0) {
return <Empty />;
}
const treeData = this.state.treeData.map(i => this.setTreeTitle(i));
return (
<Tree
blockNode={true}
defaultSelectedKeys={[this.state.groupName]}
defaultExpandAll={true}
selectedKeys={this.state.selectedKeys}
expandedKeys={this.state.expandedKeys}
onSelect={onSelect}
onExpand={onExpand}
showIcon={true}
treeData={treeData}
/>
);
}
renderOrganizationSelect() {
if (Setting.isAdminUser(this.props.account)) {
return (
<OrganizationSelect
initValue={this.state.organizationName}
style={{width: "100%"}}
onChange={(value) => {
this.setState({
organizationName: value,
});
this.props.history.push(`/trees/${value}`);
}}
/>
);
}
}
newGroup(isRoot) {
const randomName = Setting.getRandomName();
return {
owner: this.state.organizationName,
name: `group_${randomName}`,
createdTime: moment().format(),
updatedTime: moment().format(),
displayName: `New Group - ${randomName}`,
type: "Virtual",
parentId: isRoot ? this.state.organizationName : this.state.groupId,
isTopGroup: isRoot,
isEnabled: true,
};
}
addGroup(isRoot = false) {
const newGroup = this.newGroup(isRoot);
GroupBackend.addGroup(newGroup)
.then((res) => {
if (res.status === "ok") {
sessionStorage.setItem("groupTreeUrl", window.location.pathname);
this.props.history.push({pathname: `/groups/${newGroup.owner}/${newGroup.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
render() {
return (
<div style={{
flex: 1,
backgroundColor: "white",
padding: "5px 5px 2px 5px",
}}>
<Row>
<Col span={5}>
<Row>
<Col span={24} style={{textAlign: "center"}}>
{this.renderOrganizationSelect()}
</Col>
</Row>
<Row>
<Col span={24} style={{marginTop: "10px"}}>
<Button size={"small"}
onClick={() => {
this.setState({
selectedKeys: [],
groupName: null,
groupId: undefined,
});
this.props.history.push(`/trees/${this.state.organizationName}`);
}}
>
{i18next.t("group:Show all")}
</Button>
<Button size={"small"} type={"primary"} style={{marginLeft: "10px"}} onClick={() => this.addGroup(true)}>
{i18next.t("general:Add")}
</Button>
</Col>
</Row>
<Row style={{marginTop: 10}}>
<Col span={24} style={{textAlign: "left"}}>
{this.renderTree()}
</Col>
</Row>
</Col>
<Col span={19}>
<UserListPage
organizationName={this.state.organizationName}
groupName={this.state.groupName}
groupId={this.state.groupId}
{...this.props}
/>
</Col>
</Row>
</div>
);
}
}
export default GroupTreePage;

View File

@@ -46,6 +46,11 @@ class MessageEditPage extends React.Component {
getMessage() {
MessageBackend.getMessage("admin", this.state.messageName)
.then((message) => {
if (message === null) {
this.props.history.push("/404");
return;
}
this.setState({
message: message,
});

View File

@@ -48,6 +48,11 @@ class ModelEditPage extends React.Component {
getModel() {
ModelBackend.getModel(this.state.organizationName, this.state.modelName)
.then((model) => {
if (model === null) {
this.props.history.push("/404");
return;
}
this.setState({
model: model,
});

View File

@@ -49,10 +49,20 @@ class OrganizationEditPage extends React.Component {
getOrganization() {
OrganizationBackend.getOrganization("admin", this.state.organizationName)
.then((organization) => {
this.setState({
organization: organization,
});
.then((res) => {
if (res.status === "ok") {
const organization = res.data;
if (organization === null) {
this.props.history.push("/404");
return;
}
this.setState({
organization: organization,
});
} else {
Setting.showMessage("error", res.msg);
}
});
}

View File

@@ -60,8 +60,10 @@ class OrganizationListPage extends BaseListPage {
{name: "Bio", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Tag", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "API key", label: i18next.t("general:API key")},
{name: "Roles", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "Permissions", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "Groups", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "3rd-party logins", visible: true, viewRule: "Self", modifyRule: "Self"},
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{name: "Properties", visible: false, viewRule: "Admin", modifyRule: "Admin"},
@@ -221,11 +223,12 @@ class OrganizationListPage extends BaseListPage {
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "240px",
width: "320px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/trees/${record.name}`)}>{i18next.t("general:Groups")}</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/organizations/${record.name}/users`)}>{i18next.t("general:Users")}</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/organizations/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal
@@ -253,7 +256,7 @@ class OrganizationListPage extends BaseListPage {
title={() => (
<div>
{i18next.t("general:Organizations")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addOrganization.bind(this)}>{i18next.t("general:Add")}</Button>
<Button type="primary" size="small" disabled={!Setting.isAdminUser(this.props.account)} onClick={this.addOrganization.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={this.state.loading}

View File

@@ -40,8 +40,13 @@ class PaymentEditPage extends React.Component {
}
getPayment() {
PaymentBackend.getPayment(this.props.account.owner, this.state.paymentName)
PaymentBackend.getPayment("admin", this.state.paymentName)
.then((payment) => {
if (payment === null) {
this.props.history.push("/404");
return;
}
this.setState({
payment: payment,
});

View File

@@ -27,7 +27,7 @@ class PaymentListPage extends BaseListPage {
newPayment() {
const randomName = Setting.getRandomName();
return {
owner: this.props.account.owner,
owner: "admin",
name: `payment_${randomName}`,
createdTime: moment().format(),
displayName: `New Payment - ${randomName}`,
@@ -265,7 +265,7 @@ class PaymentListPage extends BaseListPage {
value = params.type;
}
this.setState({loading: true});
PaymentBackend.getPayments(Setting.isAdminUser(this.props.account) ? "" : this.props.account.owner, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
PaymentBackend.getPayments("admin", Setting.isAdminUser(this.props.account) ? "" : this.props.account.owner, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
this.setState({
loading: false,

View File

@@ -25,6 +25,7 @@ class PaymentResultPage extends React.Component {
classes: props,
paymentName: props.match.params.paymentName,
payment: null,
timeout: null,
};
}
@@ -32,15 +33,21 @@ class PaymentResultPage extends React.Component {
this.getPayment();
}
componentWillUnmount() {
if (this.state.timeout !== null) {
clearTimeout(this.state.timeout);
}
}
getPayment() {
PaymentBackend.getPayment(this.props.account.owner, this.state.paymentName)
PaymentBackend.getPayment("admin", this.state.paymentName)
.then((payment) => {
this.setState({
payment: payment,
});
if (payment.state === "Created") {
setTimeout(() => this.getPayment(), 1000);
this.setState({timeout: setTimeout(() => this.getPayment(), 1000)});
}
});
}

View File

@@ -50,6 +50,11 @@ class PermissionEditPage extends React.Component {
getPermission() {
PermissionBackend.getPermission(this.state.organizationName, this.state.permissionName)
.then((permission) => {
if (permission === null) {
this.props.history.push("/404");
return;
}
this.setState({
permission: permission,
});
@@ -264,10 +269,12 @@ class PermissionEditPage extends React.Component {
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.permission.resourceType} onChange={(value => {
this.updatePermissionField("resourceType", value);
this.updatePermissionField("resources", []);
})}
options={[
{value: "Application", name: i18next.t("general:Application")},
{value: "TreeNode", name: i18next.t("permission:TreeNode")},
{value: "Custom", name: i18next.t("general:Custom")},
].map((item) => Setting.getOption(item.name, item.value))}
/>
</Col>
@@ -277,7 +284,7 @@ class PermissionEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Resources"), i18next.t("permission:Resources - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.resources}
<Select virtual={false} mode={(this.state.permission.resourceType === "Custom") ? "tags" : "multiple"} style={{width: "100%"}} value={this.state.permission.resources}
onChange={(value => {this.updatePermissionField("resources", value);})}
options={this.state.resources.map((resource) => Setting.getOption(`${resource.name}`, `${resource.name}`))
} />

View File

@@ -47,6 +47,11 @@ class PlanEditPage extends React.Component {
getPlan() {
PlanBackend.getPlan(this.state.organizationName, this.state.planName)
.then((plan) => {
if (plan === null) {
this.props.history.push("/404");
return;
}
this.setState({
plan: plan,
});

View File

@@ -50,6 +50,11 @@ class PricingEditPage extends React.Component {
getPricing() {
PricingBackend.getPricing(this.state.organizationName, this.state.pricingName)
.then((pricing) => {
if (pricing === null) {
this.props.history.push("/404");
return;
}
this.setState({
pricing: pricing,
});

View File

@@ -45,6 +45,11 @@ class ProductEditPage extends React.Component {
getProduct() {
ProductBackend.getProduct(this.props.account.owner, this.state.productName)
.then((product) => {
if (product === null) {
this.props.history.push("/404");
return;
}
this.setState({
product: product,
});

View File

@@ -100,7 +100,7 @@ class ProductListPage extends BaseListPage {
key: "owner",
width: "150px",
sorter: true,
...this.getColumnSearchProps("organization"),
...this.getColumnSearchProps("owner"),
},
{
title: i18next.t("general:Created time"),

View File

@@ -50,6 +50,11 @@ class ProviderEditPage extends React.Component {
getProvider() {
ProviderBackend.getProvider(this.state.owner, this.state.providerName)
.then((res) => {
if (res === null) {
this.props.history.push("/404");
return;
}
if (res.status === "ok") {
this.setState({
provider: res.data,

View File

@@ -111,7 +111,7 @@ class ProviderListPage extends BaseListPage {
key: "owner",
width: "150px",
sorter: true,
...this.getColumnSearchProps("organization"),
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (text !== "admin") ? text : i18next.t("provider:admin (Shared)");
},

View File

@@ -43,6 +43,11 @@ class RoleEditPage extends React.Component {
getRole() {
RoleBackend.getRole(this.state.organizationName, this.state.roleName)
.then((role) => {
if (role === null) {
this.props.history.push("/404");
return;
}
this.setState({
role: role,
});

View File

@@ -55,10 +55,10 @@ class SessionListPage extends BaseListPage {
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "organization",
key: "owner",
width: "110px",
sorter: true,
...this.getColumnSearchProps("organization"),
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>

View File

@@ -47,6 +47,11 @@ class SubscriptionEditPage extends React.Component {
getSubscription() {
SubscriptionBackend.getSubscription(this.state.organizationName, this.state.subscriptionName)
.then((subscription) => {
if (subscription === null) {
this.props.history.push("/404");
return;
}
this.setState({
subscription: subscription,
});

View File

@@ -48,6 +48,11 @@ class SyncerEditPage extends React.Component {
getSyncer() {
SyncerBackend.getSyncer("admin", this.state.syncerName)
.then((syncer) => {
if (syncer === null) {
this.props.history.push("/404");
return;
}
this.setState({
syncer: syncer,
});

View File

@@ -36,6 +36,11 @@ class TokenEditPage extends React.Component {
getToken() {
TokenBackend.getToken("admin", this.state.tokenName)
.then((token) => {
if (token === null) {
this.props.history.push("/404");
return;
}
this.setState({
token: token,
});

View File

@@ -13,7 +13,8 @@
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, InputNumber, List, Result, Row, Select, Spin, Switch, Tag} from "antd";
import {Button, Card, Col, Input, InputNumber, List, Result, Row, Select, Space, Spin, Switch, Tag} from "antd";
import * as GroupBackend from "./backend/GroupBackend";
import * as UserBackend from "./backend/UserBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting";
@@ -32,7 +33,7 @@ import PropertyTable from "./table/propertyTable";
import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
import PopconfirmModal from "./common/modal/PopconfirmModal";
import {DeleteMfa} from "./backend/MfaBackend";
import {CheckCircleOutlined} from "@ant-design/icons";
import {CheckCircleOutlined, HolderOutlined, UsergroupAddOutlined} from "@ant-design/icons";
import {SmsMfaType} from "./auth/MfaSetupPage";
import * as MfaBackend from "./backend/MfaBackend";
@@ -47,6 +48,7 @@ class UserEditPage extends React.Component {
userName: props.userName !== undefined ? props.userName : props.match.params.userName,
user: null,
application: null,
groups: null,
organizations: [],
applications: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
@@ -63,9 +65,20 @@ class UserEditPage extends React.Component {
this.setReturnUrl();
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (prevState.application !== this.state.application) {
this.getGroups(this.state.organizationName);
}
}
getUser() {
UserBackend.getUser(this.state.organizationName, this.state.userName)
.then((data) => {
if (data === null) {
this.props.history.push("/404");
return;
}
if (data.status === null || data.status !== "error") {
this.setState({
user: data,
@@ -78,6 +91,17 @@ class UserEditPage extends React.Component {
});
}
addUserKeys() {
UserBackend.addUserKeys(this.state.user)
.then((res) => {
if (res.status === "ok") {
this.getUser();
} else {
Setting.showMessage("error", res.msg);
}
});
}
getOrganizations() {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
@@ -102,9 +126,26 @@ class UserEditPage extends React.Component {
this.setState({
application: application,
});
this.setState({
isGroupsVisible: application.organizationObj.accountItems?.some((item) => item.name === "Groups" && item.visible),
});
});
}
getGroups(organizationName) {
if (this.state.isGroupsVisible) {
GroupBackend.getGroups(organizationName)
.then((res) => {
if (res.status === "ok") {
this.setState({
groups: res.data,
});
}
});
}
}
setReturnUrl() {
const searchParams = new URLSearchParams(this.props.location.search);
const returnUrl = searchParams.get("returnUrl");
@@ -207,14 +248,6 @@ class UserEditPage extends React.Component {
const isAdmin = Setting.isAdminUser(this.props.account);
// return (
// <div>
// {
// JSON.stringify({accountItem: accountItem, isSelf: isSelf, isAdmin: isAdmin})
// }
// </div>
// )
if (accountItem.viewRule === "Self") {
if (!this.isSelfOrAdmin()) {
return null;
@@ -244,6 +277,11 @@ class UserEditPage extends React.Component {
}
}
let isKeysGenerated = false;
if (this.state.user.accessKey !== "" && this.state.user.accessKey !== "") {
isKeysGenerated = true;
}
if (accountItem.name === "Organization") {
return (
<Row style={{marginTop: "10px"}} >
@@ -254,6 +292,7 @@ class UserEditPage extends React.Component {
<Select virtual={false} style={{width: "100%"}} disabled={disabled} value={this.state.user.owner} onChange={(value => {
this.getApplicationsByOrganization(value);
this.updateUserField("owner", value);
this.getGroups(value);
})}>
{
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
@@ -262,6 +301,35 @@ class UserEditPage extends React.Component {
</Col>
</Row>
);
} else if (accountItem.name === "Groups") {
return (
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Groups"), i18next.t("general:Groups - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="multiple" style={{width: "100%"}} disabled={disabled} value={this.state.user.groups ?? []} onChange={(value => {
if (this.state.groups?.filter(group => value.includes(group.id))
.filter(group => group.type === "Physical").length > 1) {
Setting.showMessage("error", i18next.t("general:You can only select one physical group"));
return;
}
this.updateUserField("groups", value);
})}
>
{
this.state.groups?.map((group) => <Option key={group.id} value={group.id}>
<Space>
{group.type === "Physical" ? <UsergroupAddOutlined /> : <HolderOutlined />}
{group.displayName}
</Space>
</Option>)
}
</Select>
</Col>
</Row>
);
} else if (accountItem.name === "ID") {
return (
<Row style={{marginTop: "20px"}} >
@@ -639,6 +707,37 @@ class UserEditPage extends React.Component {
</Col>
</Row>
);
} else if (accountItem.name === "API key") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:API key"), i18next.t("general:API key - Tooltip"))} :
</Col>
<Col span={22} >
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:Access key"), i18next.t("general:Access key - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.accessKey} disabled={true} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:Secret key"), i18next.t("general:Secret key - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.secretKey} disabled={true} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col span={22} >
<Button onClick={() => this.addUserKeys()}>{i18next.t(isKeysGenerated ? "general:update" : "general:generate")}</Button>
</Col>
</Row>
</Col>
</Row>
);
} else if (accountItem.name === "Roles") {
return (
<Row style={{marginTop: "20px", alignItems: "center"}} >
@@ -920,7 +1019,12 @@ class UserEditPage extends React.Component {
UserBackend.deleteUser(this.state.user)
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/users");
const userListUrl = sessionStorage.getItem("userListUrl");
if (userListUrl !== null) {
this.props.history.push(userListUrl);
} else {
this.props.history.push("/users");
}
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}

View File

@@ -14,7 +14,7 @@
import React from "react";
import {Link} from "react-router-dom";
import {Button, Switch, Table, Upload} from "antd";
import {Button, Space, Switch, Table, Upload} from "antd";
import {UploadOutlined} from "@ant-design/icons";
import moment from "moment";
import * as OrganizationBackend from "./backend/OrganizationBackend";
@@ -27,18 +27,41 @@ import PopconfirmModal from "./common/modal/PopconfirmModal";
class UserListPage extends BaseListPage {
constructor(props) {
super(props);
this.state = {
...this.state,
organizationName: this.props.organizationName ?? this.props.match?.params.organizationName ?? this.props.account.owner,
organization: null,
};
}
componentDidMount() {
this.setState({
organizationName: this.props.match.params.organizationName,
organization: null,
});
UNSAFE_componentWillMount() {
super.UNSAFE_componentWillMount();
this.getOrganization(this.state.organizationName);
}
componentDidUpdate(prevProps, prevState) {
if (this.props.match.path !== prevProps.match.path || this.props.organizationName !== prevProps.organizationName) {
this.setState({
organizationName: this.props.organizationName ?? this.props.match?.params.organizationName,
});
}
if (this.state.organizationName !== prevState.organizationName) {
this.getOrganization(this.state.organizationName);
}
if (prevProps.groupName !== this.props.groupName || this.state.organizationName !== prevState.organizationName) {
this.fetch({
pagination: this.state.pagination,
searchText: this.state.searchText,
searchedColumn: this.state.searchedColumn,
});
}
}
newUser() {
const randomName = Setting.getRandomName();
const owner = (this.state.organizationName !== undefined) ? this.state.organizationName : this.props.account.owner;
const owner = this.state.organizationName;
return {
owner: owner,
name: `user_${randomName}`,
@@ -52,6 +75,7 @@ class UserListPage extends BaseListPage {
phone: Setting.getRandomNumber(),
countryCode: this.state.organization.countryCodes?.length > 0 ? this.state.organization.countryCodes[0] : "",
address: [],
groups: this.props.groupId ? [this.props.groupId] : [],
affiliation: "Example Inc.",
tag: "staff",
region: "",
@@ -100,6 +124,26 @@ class UserListPage extends BaseListPage {
});
}
removeUserFromGroup(i) {
const user = this.state.data[i];
const group = this.props.groupId;
UserBackend.removeUserFromGroup({groupId: group, owner: user.owner, name: user.name})
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully removed"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to remove")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
uploadFile(info) {
const {status, response: res} = info.file;
if (status === "done") {
@@ -116,6 +160,19 @@ class UserListPage extends BaseListPage {
}
}
getOrganization(organizationName) {
OrganizationBackend.getOrganization("admin", organizationName)
.then((res) => {
if (res.status === "ok") {
this.setState({
organization: res.data,
});
} else {
Setting.showMessage("error", `Failed to get organization: ${res.msg}`);
}
});
}
renderUpload() {
const props = {
name: "file",
@@ -339,20 +396,30 @@ class UserListPage extends BaseListPage {
width: "190px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
const isTreePage = this.props.groupId !== undefined;
const disabled = (record.owner === this.props.account.owner && record.name === this.props.account.name) || (record.owner === "built-in" && record.name === "admin");
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => {
<Space>
<Button size={isTreePage ? "small" : "middle"} type="primary" onClick={() => {
sessionStorage.setItem("userListUrl", window.location.pathname);
this.props.history.push(`/users/${record.owner}/${record.name}`);
}}>{i18next.t("general:Edit")}</Button>
}}>{i18next.t("general:Edit")}
</Button>
{isTreePage ?
<PopconfirmModal
text={i18next.t("general:remove")}
title={i18next.t("general:Sure to remove") + `: ${record.name} ?`}
onConfirm={() => this.removeUserFromGroup(index)}
disabled={disabled}
size="small"
/> : null}
<PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteUser(index)}
disabled={disabled}
>
</PopconfirmModal>
</div>
size={isTreePage ? "small" : "default"}
/>
</Space>
);
},
},
@@ -388,7 +455,7 @@ class UserListPage extends BaseListPage {
const field = params.searchedColumn, value = params.searchText;
const sortField = params.sortField, sortOrder = params.sortOrder;
this.setState({loading: true});
if (this.props.match.params.organizationName === undefined) {
if (this.props.match?.path === "/users") {
(Setting.isAdminUser(this.props.account) ? UserBackend.getGlobalUsers(params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder) : UserBackend.getUsers(this.props.account.owner, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder))
.then((res) => {
this.setState({
@@ -404,15 +471,7 @@ class UserListPage extends BaseListPage {
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
const users = res.data;
if (users.length > 0) {
this.getOrganization(users[0].owner);
} else {
this.getOrganization(this.state.organizationName);
}
} else {
if (Setting.isResponseDenied(res)) {
this.setState({
isAuthorized: false,
@@ -423,7 +482,9 @@ class UserListPage extends BaseListPage {
}
});
} else {
UserBackend.getUsers(this.props.match.params.organizationName, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
(this.props.groupName ?
UserBackend.getUsers(this.state.organizationName, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder, `${this.state.organizationName}/${this.props.groupName}`) :
UserBackend.getUsers(this.state.organizationName, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder))
.then((res) => {
this.setState({
loading: false,
@@ -438,13 +499,6 @@ class UserListPage extends BaseListPage {
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
const users = res.data;
if (users.length > 0) {
this.getOrganization(users[0].owner);
} else {
this.getOrganization(this.state.organizationName);
}
} else {
if (Setting.isResponseDenied(res)) {
this.setState({
@@ -457,15 +511,6 @@ class UserListPage extends BaseListPage {
});
}
};
getOrganization(organizationName) {
OrganizationBackend.getOrganization("admin", organizationName)
.then((organization) => {
this.setState({
organization: organization,
});
});
}
}
export default UserListPage;

View File

@@ -123,6 +123,11 @@ class WebhookEditPage extends React.Component {
getWebhook() {
WebhookBackend.getWebhook("admin", this.state.webhookName)
.then((webhook) => {
if (webhook === null) {
this.props.history.push("/404");
return;
}
this.setState({
webhook: webhook,
});

View File

@@ -0,0 +1,71 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as Setting from "../Setting";
export function getGroups(owner = "", withTree = false, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-groups?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}&withTree=${withTree}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function getGroup(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-group?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function updateGroup(owner, name, group) {
const newGroup = Setting.deepCopy(group);
return fetch(`${Setting.ServerUrl}/api/update-group?id=${owner}/${encodeURIComponent(name)}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newGroup),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function addGroup(group) {
const newGroup = Setting.deepCopy(group);
return fetch(`${Setting.ServerUrl}/api/add-group`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newGroup),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function deleteGroup(group) {
const newGroup = Setting.deepCopy(group);
return fetch(`${Setting.ServerUrl}/api/delete-group`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newGroup),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}

View File

@@ -14,8 +14,8 @@
import * as Setting from "../Setting";
export function getPayments(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-payments?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
export function getPayments(owner, organization, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-payments?owner=${owner}&organization=${organization}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include",
headers: {

View File

@@ -25,8 +25,8 @@ export function getGlobalUsers(page, pageSize, field = "", value = "", sortField
}).then(res => res.json());
}
export function getUsers(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-users?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
export function getUsers(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "", groupId = "") {
return fetch(`${Setting.ServerUrl}/api/get-users?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}&groupId=${groupId}`, {
method: "GET",
credentials: "include",
headers: {
@@ -45,6 +45,17 @@ export function getUser(owner, name) {
}).then(res => res.json());
}
export function addUserKeys(user) {
return fetch(`${Setting.ServerUrl}/api/add-user-keys`, {
method: "POST",
credentials: "include",
body: JSON.stringify(user),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function updateUser(owner, name, user) {
const newUser = Setting.deepCopy(user);
return fetch(`${Setting.ServerUrl}/api/update-user?id=${owner}/${encodeURIComponent(name)}`, {
@@ -211,3 +222,18 @@ export function checkUserPassword(values) {
body: JSON.stringify(values),
}).then(res => res.json());
}
export function removeUserFromGroup({owner, name, groupId}) {
const formData = new FormData();
formData.append("owner", owner);
formData.append("name", name);
formData.append("groupId", groupId);
return fetch(`${Setting.ServerUrl}/api/remove-user-from-group`, {
method: "POST",
credentials: "include",
body: formData,
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}

View File

@@ -17,6 +17,8 @@ import i18next from "i18next";
import React from "react";
export const PopconfirmModal = (props) => {
const text = props.text ? props.text : i18next.t("general:Delete");
const size = props.size ? props.size : "middle";
return (
<Popconfirm
title={props.title}
@@ -25,7 +27,7 @@ export const PopconfirmModal = (props) => {
okText={i18next.t("general:OK")}
cancelText={i18next.t("general:Cancel")}
>
<Button style={{marginBottom: "10px"}} disabled={props.disabled} type="primary" danger>{i18next.t("general:Delete")}</Button>
<Button style={{...props.style}} size={size} disabled={props.disabled} type="primary" danger>{text}</Button>
</Popconfirm>
);
};

View File

@@ -19,16 +19,14 @@ import * as OrganizationBackend from "../../backend/OrganizationBackend";
import * as Setting from "../../Setting";
function OrganizationSelect(props) {
const {onChange} = props;
const {onChange, initValue, style, onSelect} = props;
const [organizations, setOrganizations] = React.useState([]);
const [value, setValue] = React.useState("");
const [value, setValue] = React.useState(initValue);
React.useEffect(() => {
if (props.organizations === undefined) {
getOrganizations();
}
const initValue = organizations.length > 0 ? organizations[0] : "";
handleOnChange(initValue);
}, []);
const getOrganizations = () => {
@@ -36,6 +34,9 @@ function OrganizationSelect(props) {
.then((res) => {
if (res.status === "ok") {
setOrganizations(res.data);
if (initValue === undefined) {
setValue(organizations.length > 0 ? organizations[0] : "");
}
}
});
};
@@ -47,14 +48,14 @@ function OrganizationSelect(props) {
return (
<Select
options={organizations.map((organization) => Setting.getOption(organization.name, organization.name))}
options={organizations.map((organization) => Setting.getOption(organization.displayName, organization.name))}
virtual={false}
showSearch
placeholder={i18next.t("login:Please select an organization")}
value={value}
onChange={handleOnChange}
filterOption={(input, option) => (option?.text ?? "").toLowerCase().includes(input.toLowerCase())}
{...props}
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
style={style}
onSelect={onSelect}
>
</Select>
);

View File

@@ -14,7 +14,7 @@
import React from "react";
import * as Setting from "../../Setting";
import {Dropdown} from "antd";
import {Dropdown, Space} from "antd";
import "../../App.less";
import i18next from "i18next";
import {CheckOutlined} from "@ant-design/icons";
@@ -43,10 +43,10 @@ class ThemeSelect extends React.Component {
getThemeItems() {
return Themes.map((theme) => Setting.getItem(
<div style={{display: "flex", justifyContent: "space-between"}}>
<div>{i18next.t(`theme:${theme.label}`)}</div>
<Space>
{i18next.t(`theme:${theme.label}`)}
{this.props.themeAlgorithm.includes(theme.key) ? <CheckOutlined style={{marginLeft: "5px"}} /> : null}
</div>,
</Space>,
theme.key, theme.icon));
}

View File

@@ -188,6 +188,7 @@
"Close": "Schließen",
"Confirm": "Confirm",
"Created time": "Erstellte Zeit",
"Custom": "Custom",
"Default application": "Standard Anwendung",
"Default application - Tooltip": "Standard-Anwendung für Benutzer, die direkt von der Organisationsseite registriert wurden",
"Default avatar": "Standard-Avatar",
@@ -209,6 +210,7 @@
"Failed to delete": "Konnte nicht gelöscht werden",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "Konnte nicht gespeichert werden",
"Failed to verify": "Failed to verify",
"Favicon": "Favicon",
@@ -218,6 +220,8 @@
"Forget URL - Tooltip": "Benutzerdefinierte URL für die \"Passwort vergessen\" Seite. Wenn nicht festgelegt, wird die standardmäßige Casdoor \"Passwort vergessen\" Seite verwendet. Wenn sie festgelegt ist, wird der \"Passwort vergessen\" Link auf der Login-Seite zu dieser URL umgeleitet",
"Found some texts still not translated? Please help us translate at": "Haben Sie noch Texte gefunden, die nicht übersetzt wurden? Bitte helfen Sie uns beim Übersetzen",
"Go to writable demo site?": "Gehe zur beschreibbaren Demo-Website?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
"Home": "Zuhause",
"Home - Tooltip": "Homepage der Anwendung",
"ID": "ID",
@@ -299,10 +303,12 @@
"Subscriptions": "Abonnements",
"Successfully added": "Erfolgreich hinzugefügt",
"Successfully deleted": "Erfolgreich gelöscht",
"Successfully removed": "Successfully removed",
"Successfully saved": "Erfolgreich gespeichert",
"Supported country codes": "Unterstützte Ländercodes",
"Supported country codes - Tooltip": "Ländercodes, die von der Organisation unterstützt werden. Diese Codes können als Präfix ausgewählt werden, wenn SMS-Verifizierungscodes gesendet werden",
"Sure to delete": "Sicher zu löschen",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "Synchronisieren",
"Syncers": "Syncers",
@@ -311,6 +317,8 @@
"This is a read-only demo site!": "Dies ist eine schreibgeschützte Demo-Seite!",
"Timestamp": "Zeitstempel",
"Tokens": "Token",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "URL-Link",
"Up": "Oben",
@@ -323,9 +331,20 @@
"Users": "Benutzer",
"Users under all organizations": "Benutzer unter allen Organisationen",
"Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group",
"empty": "leere",
"remove": "remove",
"{total} in total": "Insgesamt {total}"
},
"group": {
"Edit Group": "Edit Group",
"New Group": "New Group",
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
"Admin": "Admin",
"Admin - Tooltip": "CN oder ID des LDAP-Serveradministrators",
@@ -539,8 +558,6 @@
"Edit Pricing": "Edit Pricing",
"Free": "Kostenlos",
"Getting started": "Loslegen",
"Has trial": "Testphase verfügbar",
"Has trial - Tooltip": "Verfügbarkeit der Testphase nach Auswahl eines Plans",
"New Pricing": "New Pricing",
"Trial duration": "Testphase Dauer",
"Trial duration - Tooltip": "Dauer der Testphase",

View File

@@ -188,6 +188,7 @@
"Close": "Close",
"Confirm": "Confirm",
"Created time": "Created time",
"Custom": "Custom",
"Default application": "Default application",
"Default application - Tooltip": "Default application for users registered directly from the organization page",
"Default avatar": "Default avatar",
@@ -209,6 +210,7 @@
"Failed to delete": "Failed to delete",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "Failed to save",
"Failed to verify": "Failed to verify",
"Favicon": "Favicon",
@@ -218,6 +220,8 @@
"Forget URL - Tooltip": "Custom URL for the \"Forget password\" page. If not set, the default Casdoor \"Forget password\" page will be used. When set, the \"Forget password\" link on the login page will redirect to this URL",
"Found some texts still not translated? Please help us translate at": "Found some texts still not translated? Please help us translate at",
"Go to writable demo site?": "Go to writable demo site?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
"Home": "Home",
"Home - Tooltip": "Home page of the application",
"ID": "ID",
@@ -299,10 +303,12 @@
"Subscriptions": "Subscriptions",
"Successfully added": "Successfully added",
"Successfully deleted": "Successfully deleted",
"Successfully removed": "Successfully removed",
"Successfully saved": "Successfully saved",
"Supported country codes": "Supported country codes",
"Supported country codes - Tooltip": "Country codes supported by the organization. These codes can be selected as a prefix when sending SMS verification codes",
"Sure to delete": "Sure to delete",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "Sync",
"Syncers": "Syncers",
@@ -311,6 +317,8 @@
"This is a read-only demo site!": "This is a read-only demo site!",
"Timestamp": "Timestamp",
"Tokens": "Tokens",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "URL link",
"Up": "Up",
@@ -323,9 +331,20 @@
"Users": "Users",
"Users under all organizations": "Users under all organizations",
"Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group",
"empty": "empty",
"remove": "remove",
"{total} in total": "{total} in total"
},
"group": {
"Edit Group": "Edit Group",
"New Group": "New Group",
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
"Admin": "Admin",
"Admin - Tooltip": "CN or ID of the LDAP server administrator",
@@ -539,8 +558,6 @@
"Edit Pricing": "Edit Pricing",
"Free": "Free",
"Getting started": "Getting started",
"Has trial": "Has trial",
"Has trial - Tooltip": "Availability of the trial period after choosing a plan",
"New Pricing": "New Pricing",
"Trial duration": "Trial duration",
"Trial duration - Tooltip": "Trial duration period",

View File

@@ -188,6 +188,7 @@
"Close": "Cerca",
"Confirm": "Confirm",
"Created time": "Tiempo creado",
"Custom": "Custom",
"Default application": "Aplicación predeterminada",
"Default application - Tooltip": "Aplicación predeterminada para usuarios registrados directamente desde la página de la organización",
"Default avatar": "Avatar predeterminado",
@@ -209,6 +210,7 @@
"Failed to delete": "No se pudo eliminar",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "No se pudo guardar",
"Failed to verify": "Failed to verify",
"Favicon": "Favicon",
@@ -218,6 +220,8 @@
"Forget URL - Tooltip": "URL personalizada para la página \"Olvidé mi contraseña\". Si no se establece, se utilizará la página \"Olvidé mi contraseña\" predeterminada de Casdoor. Cuando se establezca, el enlace \"Olvidé mi contraseña\" en la página de inicio de sesión redireccionará a esta URL",
"Found some texts still not translated? Please help us translate at": "¿Encontraste algunos textos que aún no están traducidos? Por favor, ayúdanos a traducirlos en",
"Go to writable demo site?": "¿Ir al sitio demo editable?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
"Home": "Hogar",
"Home - Tooltip": "Página de inicio de la aplicación",
"ID": "identificación",
@@ -299,10 +303,12 @@
"Subscriptions": "Suscripciones",
"Successfully added": "Éxito al agregar",
"Successfully deleted": "Éxito en la eliminación",
"Successfully removed": "Successfully removed",
"Successfully saved": "Guardado exitosamente",
"Supported country codes": "Códigos de país admitidos",
"Supported country codes - Tooltip": "Códigos de país compatibles con la organización. Estos códigos se pueden seleccionar como prefijo al enviar códigos de verificación SMS",
"Sure to delete": "Seguro que eliminar",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "Sincronización",
"Syncers": "Sincronizadores",
@@ -311,6 +317,8 @@
"This is a read-only demo site!": "¡Este es un sitio de demostración solo de lectura!",
"Timestamp": "Marca de tiempo",
"Tokens": "Tokens",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "Dirección URL",
"URL - Tooltip": "Enlace de URL",
"Up": "Arriba",
@@ -323,9 +331,20 @@
"Users": "Usuarios",
"Users under all organizations": "Usuarios bajo todas las organizaciones",
"Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group",
"empty": "vacío",
"remove": "remove",
"{total} in total": "{total} en total"
},
"group": {
"Edit Group": "Edit Group",
"New Group": "New Group",
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
"Admin": "Administrador",
"Admin - Tooltip": "CN o ID del administrador del servidor LDAP",
@@ -539,8 +558,6 @@
"Edit Pricing": "Edit Pricing",
"Free": "Gratis",
"Getting started": "Empezar",
"Has trial": "Tiene período de prueba",
"Has trial - Tooltip": "Disponibilidad del período de prueba después de elegir un plan",
"New Pricing": "New Pricing",
"Trial duration": "Duración del período de prueba",
"Trial duration - Tooltip": "Duración del período de prueba",

View File

@@ -188,6 +188,7 @@
"Close": "Fermer",
"Confirm": "Confirm",
"Created time": "Temps créé",
"Custom": "Custom",
"Default application": "Application par défaut",
"Default application - Tooltip": "Application par défaut pour les utilisateurs enregistrés directement depuis la page de l'organisation",
"Default avatar": "Avatar par défaut",
@@ -209,6 +210,7 @@
"Failed to delete": "Échec de la suppression",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "Échec de sauvegarde",
"Failed to verify": "Failed to verify",
"Favicon": "Favicon",
@@ -218,6 +220,8 @@
"Forget URL - Tooltip": "URL personnalisée pour la page \"Mot de passe oublié\". Si elle n'est pas définie, la page par défaut \"Mot de passe oublié\" de Casdoor sera utilisée. Lorsqu'elle est définie, le lien \"Mot de passe oublié\" sur la page de connexion sera redirigé vers cette URL",
"Found some texts still not translated? Please help us translate at": "Trouvé des textes encore non traduits ? Veuillez nous aider à les traduire",
"Go to writable demo site?": "Allez sur le site de démonstration modifiable ?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
"Home": "Maison",
"Home - Tooltip": "Page d'accueil de l'application",
"ID": "Identité",
@@ -299,10 +303,12 @@
"Subscriptions": "Abonnements",
"Successfully added": "Ajouté avec succès",
"Successfully deleted": "Supprimé avec succès",
"Successfully removed": "Successfully removed",
"Successfully saved": "Succès enregistré",
"Supported country codes": "Codes de pays pris en charge",
"Supported country codes - Tooltip": "Codes de pays pris en charge par l'organisation. Ces codes peuvent être sélectionnés comme préfixe lors de l'envoi de codes de vérification SMS",
"Sure to delete": "Sûr de supprimer",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "Synchronisation",
"Syncers": "Synchroniseurs",
@@ -311,6 +317,8 @@
"This is a read-only demo site!": "Ceci est un site de démonstration en lecture seule !",
"Timestamp": "Horodatage",
"Tokens": "Les jetons",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "Lien d'URL",
"Up": "Haut",
@@ -323,9 +331,20 @@
"Users": "Utilisateurs",
"Users under all organizations": "Utilisateurs sous toutes les organisations",
"Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group",
"empty": "vide",
"remove": "remove",
"{total} in total": "{total} au total"
},
"group": {
"Edit Group": "Edit Group",
"New Group": "New Group",
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
"Admin": "Admin",
"Admin - Tooltip": "CN ou ID de l'administrateur du serveur LDAP",
@@ -539,8 +558,6 @@
"Edit Pricing": "Edit Pricing",
"Free": "Gratuit",
"Getting started": "Commencer",
"Has trial": "Essai gratuit disponible",
"Has trial - Tooltip": "Disponibilité de la période d'essai après avoir choisi un forfait",
"New Pricing": "New Pricing",
"Trial duration": "Durée de l'essai",
"Trial duration - Tooltip": "Durée de la période d'essai",

View File

@@ -188,6 +188,7 @@
"Close": "Tutup",
"Confirm": "Confirm",
"Created time": "Waktu dibuat",
"Custom": "Custom",
"Default application": "Aplikasi default",
"Default application - Tooltip": "Aplikasi default untuk pengguna yang terdaftar langsung dari halaman organisasi",
"Default avatar": "Avatar default",
@@ -209,6 +210,7 @@
"Failed to delete": "Gagal menghapus",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "Gagal menyimpan",
"Failed to verify": "Failed to verify",
"Favicon": "Favicon",
@@ -218,6 +220,8 @@
"Forget URL - Tooltip": "URL kustom untuk halaman \"Lupa kata sandi\". Jika tidak diatur, halaman \"Lupa kata sandi\" default Casdoor akan digunakan. Ketika diatur, tautan \"Lupa kata sandi\" pada halaman masuk akan diarahkan ke URL ini",
"Found some texts still not translated? Please help us translate at": "Menemukan beberapa teks yang masih belum diterjemahkan? Tolong bantu kami menerjemahkan di",
"Go to writable demo site?": "Pergi ke situs demo yang dapat ditulis?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
"Home": "Rumah",
"Home - Tooltip": "Halaman utama aplikasi",
"ID": "ID",
@@ -299,10 +303,12 @@
"Subscriptions": "Langganan",
"Successfully added": "Berhasil ditambahkan",
"Successfully deleted": "Berhasil dihapus",
"Successfully removed": "Successfully removed",
"Successfully saved": "Berhasil disimpan",
"Supported country codes": "Kode negara yang didukung",
"Supported country codes - Tooltip": "Kode negara yang didukung oleh organisasi. Kode-kode ini dapat dipilih sebagai awalan saat mengirim kode verifikasi SMS",
"Sure to delete": "Pasti untuk menghapus",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "Sinkronisasi",
"Syncers": "Sinkronisasi",
@@ -311,6 +317,8 @@
"This is a read-only demo site!": "Ini adalah situs demo hanya untuk dibaca saja!",
"Timestamp": "Waktu penanda waktu",
"Tokens": "Token-token",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "Tautan URL",
"Up": "Ke atas",
@@ -323,9 +331,20 @@
"Users": "Pengguna-pengguna",
"Users under all organizations": "Pengguna di bawah semua organisasi",
"Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group",
"empty": "kosong",
"remove": "remove",
"{total} in total": "{total} secara keseluruhan"
},
"group": {
"Edit Group": "Edit Group",
"New Group": "New Group",
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
"Admin": "Admin",
"Admin - Tooltip": "CN atau ID dari administrator server LDAP",
@@ -539,8 +558,6 @@
"Edit Pricing": "Edit Pricing",
"Free": "Gratis",
"Getting started": "Mulai",
"Has trial": "Mempunyai periode percobaan",
"Has trial - Tooltip": "Ketersediaan periode percobaan setelah memilih rencana",
"New Pricing": "New Pricing",
"Trial duration": "Durasi percobaan",
"Trial duration - Tooltip": "Durasi periode percobaan",

View File

@@ -188,6 +188,7 @@
"Close": "閉じる",
"Confirm": "Confirm",
"Created time": "作成された時間",
"Custom": "Custom",
"Default application": "デフォルトアプリケーション",
"Default application - Tooltip": "組織ページから直接登録されたユーザーのデフォルトアプリケーション",
"Default avatar": "デフォルトのアバター",
@@ -209,6 +210,7 @@
"Failed to delete": "削除に失敗しました",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "保存に失敗しました",
"Failed to verify": "Failed to verify",
"Favicon": "ファビコン",
@@ -218,6 +220,8 @@
"Forget URL - Tooltip": "「パスワードをお忘れの場合」ページのカスタムURL。未設定の場合、デフォルトのCasdoor「パスワードをお忘れの場合」ページが使用されます。設定された場合、ログインページの「パスワードをお忘れの場合」リンクはこのURLにリダイレクトされます",
"Found some texts still not translated? Please help us translate at": "まだ翻訳されていない文章が見つかりましたか?是非とも翻訳のお手伝いをお願いします",
"Go to writable demo site?": "書き込み可能なデモサイトに移動しますか?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
"Home": "ホーム",
"Home - Tooltip": "アプリケーションのホームページ",
"ID": "ID",
@@ -299,10 +303,12 @@
"Subscriptions": "サブスクリプション",
"Successfully added": "正常に追加されました",
"Successfully deleted": "正常に削除されました",
"Successfully removed": "Successfully removed",
"Successfully saved": "成功的に保存されました",
"Supported country codes": "サポートされている国コード",
"Supported country codes - Tooltip": "組織でサポートされている国コード。これらのコードは、SMS認証コードのプレフィックスとして選択できます",
"Sure to delete": "削除することが確実です",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "同期",
"Syncers": "シンカーズ",
@@ -311,6 +317,8 @@
"This is a read-only demo site!": "これは読み取り専用のデモサイトです!",
"Timestamp": "タイムスタンプ",
"Tokens": "トークン",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "URLリンク",
"Up": "アップ",
@@ -323,9 +331,20 @@
"Users": "ユーザー",
"Users under all organizations": "すべての組織のユーザー",
"Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group",
"empty": "空",
"remove": "remove",
"{total} in total": "総計{total}"
},
"group": {
"Edit Group": "Edit Group",
"New Group": "New Group",
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
"Admin": "Admin",
"Admin - Tooltip": "LDAPサーバー管理者のCNまたはID",
@@ -539,8 +558,6 @@
"Edit Pricing": "Edit Pricing",
"Free": "無料",
"Getting started": "はじめる",
"Has trial": "トライアル期間あり",
"Has trial - Tooltip": "プラン選択後のトライアル期間の有無",
"New Pricing": "New Pricing",
"Trial duration": "トライアル期間の長さ",
"Trial duration - Tooltip": "トライアル期間の長さ",

View File

@@ -188,6 +188,7 @@
"Close": "닫다",
"Confirm": "Confirm",
"Created time": "작성한 시간",
"Custom": "Custom",
"Default application": "기본 애플리케이션",
"Default application - Tooltip": "조직 페이지에서 직접 등록한 사용자의 기본 응용 프로그램",
"Default avatar": "기본 아바타",
@@ -209,6 +210,7 @@
"Failed to delete": "삭제에 실패했습니다",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "저장에 실패했습니다",
"Failed to verify": "Failed to verify",
"Favicon": "파비콘",
@@ -218,6 +220,8 @@
"Forget URL - Tooltip": "\"비밀번호를 잊어버렸을 경우\" 페이지에 대한 사용자 정의 URL. 설정되지 않은 경우 기본 Casdoor \"비밀번호를 잊어버렸을 경우\" 페이지가 사용됩니다. 설정된 경우 로그인 페이지의 \"비밀번호를 잊으셨나요?\" 링크는 이 URL로 리디렉션됩니다",
"Found some texts still not translated? Please help us translate at": "아직 번역되지 않은 텍스트가 있나요? 번역에 도움을 주실 수 있나요?",
"Go to writable demo site?": "쓰기 가능한 데모 사이트로 이동하시겠습니까?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
"Home": "집",
"Home - Tooltip": "어플리케이션 홈 페이지",
"ID": "ID",
@@ -299,10 +303,12 @@
"Subscriptions": "구독",
"Successfully added": "성공적으로 추가되었습니다",
"Successfully deleted": "성공적으로 삭제되었습니다",
"Successfully removed": "Successfully removed",
"Successfully saved": "성공적으로 저장되었습니다",
"Supported country codes": "지원되는 국가 코드들",
"Supported country codes - Tooltip": "조직에서 지원하는 국가 코드입니다. 이 코드들은 SMS 인증 코드를 보낼 때 접두사로 선택할 수 있습니다",
"Sure to delete": "삭제하시겠습니까?",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "싱크",
"Syncers": "싱크어스",
@@ -311,6 +317,8 @@
"This is a read-only demo site!": "이것은 읽기 전용 데모 사이트입니다!",
"Timestamp": "타임스탬프",
"Tokens": "토큰",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "URL 링크",
"Up": "위로",
@@ -323,9 +331,20 @@
"Users": "사용자들",
"Users under all organizations": "모든 조직의 사용자",
"Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group",
"empty": "빈",
"remove": "remove",
"{total} in total": "총 {total}개"
},
"group": {
"Edit Group": "Edit Group",
"New Group": "New Group",
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
"Admin": "Admin",
"Admin - Tooltip": "LDAP 서버 관리자의 CN 또는 ID",
@@ -539,8 +558,6 @@
"Edit Pricing": "Edit Pricing",
"Free": "무료",
"Getting started": "시작하기",
"Has trial": "무료 체험 가능",
"Has trial - Tooltip": "플랜 선택 후 체험 기간의 가용 여부",
"New Pricing": "New Pricing",
"Trial duration": "체험 기간",
"Trial duration - Tooltip": "체험 기간의 기간",

View File

@@ -188,6 +188,7 @@
"Close": "Fechar",
"Confirm": "Confirm",
"Created time": "Hora de Criação",
"Custom": "Custom",
"Default application": "Aplicação padrão",
"Default application - Tooltip": "Aplicação padrão para usuários registrados diretamente na página da organização",
"Default avatar": "Avatar padrão",
@@ -209,6 +210,7 @@
"Failed to delete": "Falha ao excluir",
"Failed to enable": "Falha ao habilitar",
"Failed to get answer": "Falha ao obter resposta",
"Failed to remove": "Failed to remove",
"Failed to save": "Falha ao salvar",
"Failed to verify": "Falha ao verificar",
"Favicon": "Favicon",
@@ -218,6 +220,8 @@
"Forget URL - Tooltip": "URL personalizada para a página de \"Esqueci a senha\". Se não definido, será usada a página padrão de \"Esqueci a senha\" do Casdoor. Quando definido, o link de \"Esqueci a senha\" na página de login será redirecionado para esta URL",
"Found some texts still not translated? Please help us translate at": "Encontrou algum texto ainda não traduzido? Ajude-nos a traduzir em",
"Go to writable demo site?": "Acessar o site de demonstração gravável?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
"Home": "Página Inicial",
"Home - Tooltip": "Página inicial do aplicativo",
"ID": "ID",
@@ -299,10 +303,12 @@
"Subscriptions": "Đăng ký",
"Successfully added": "Adicionado com sucesso",
"Successfully deleted": "Excluído com sucesso",
"Successfully removed": "Successfully removed",
"Successfully saved": "Salvo com sucesso",
"Supported country codes": "Códigos de país suportados",
"Supported country codes - Tooltip": "Códigos de país suportados pela organização. Esses códigos podem ser selecionados como prefixo ao enviar códigos de verificação SMS",
"Sure to delete": "Tem certeza que deseja excluir",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "Sincronizar",
"Syncers": "Sincronizadores",
@@ -311,6 +317,8 @@
"This is a read-only demo site!": "Este é um site de demonstração apenas para leitura!",
"Timestamp": "Carimbo de Data/Hora",
"Tokens": "Tokens",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "Link da URL",
"Up": "Acima",
@@ -323,9 +331,20 @@
"Users": "Usuários",
"Users under all organizations": "Usuários em todas as organizações",
"Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group",
"empty": "vazio",
"remove": "remove",
"{total} in total": "{total} no total"
},
"group": {
"Edit Group": "Edit Group",
"New Group": "New Group",
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
"Admin": "Administrador",
"Admin - Tooltip": "CN ou ID do administrador do servidor LDAP",
@@ -539,8 +558,6 @@
"Edit Pricing": "Edit Pricing",
"Free": "Miễn phí",
"Getting started": "Bắt đầu",
"Has trial": "Có thời gian thử nghiệm",
"Has trial - Tooltip": "Khả dụng thời gian thử nghiệm sau khi chọn kế hoạch",
"New Pricing": "New Pricing",
"Trial duration": "Thời gian thử nghiệm",
"Trial duration - Tooltip": "Thời gian thử nghiệm",

View File

@@ -188,6 +188,7 @@
"Close": "Близко",
"Confirm": "Confirm",
"Created time": "Созданное время",
"Custom": "Custom",
"Default application": "Приложение по умолчанию",
"Default application - Tooltip": "По умолчанию приложение для пользователей, зарегистрированных непосредственно со страницы организации",
"Default avatar": "Стандартный аватар",
@@ -209,6 +210,7 @@
"Failed to delete": "Не удалось удалить",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "Не удалось сохранить",
"Failed to verify": "Failed to verify",
"Favicon": "Фавикон",
@@ -218,6 +220,8 @@
"Forget URL - Tooltip": "Настроенный URL для страницы \"Забыли пароль\". Если не установлено, будет использоваться стандартная страница \"Забыли пароль\" Casdoor. При установке, ссылка \"Забыли пароль\" на странице входа будет перенаправляться на этот URL",
"Found some texts still not translated? Please help us translate at": "Нашли некоторые тексты, которые еще не переведены? Пожалуйста, помогите нам перевести на",
"Go to writable demo site?": "Перейти на демонстрационный сайт для записи данных?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
"Home": "Дом",
"Home - Tooltip": "Главная страница приложения",
"ID": "ID",
@@ -299,10 +303,12 @@
"Subscriptions": "Подписки",
"Successfully added": "Успешно добавлено",
"Successfully deleted": "Успешно удалено",
"Successfully removed": "Successfully removed",
"Successfully saved": "Успешно сохранено",
"Supported country codes": "Поддерживаемые коды стран",
"Supported country codes - Tooltip": "Коды стран, поддерживаемые организацией. Эти коды могут быть выбраны в качестве префикса при отправке SMS-кодов подтверждения",
"Sure to delete": "Обязательное удаление",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "Синхронизация",
"Syncers": "Синкеры",
@@ -311,6 +317,8 @@
"This is a read-only demo site!": "Это демонстрационный сайт только для чтения!",
"Timestamp": "Отметка времени",
"Tokens": "Токены",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "Ссылка URL",
"Up": "Вверх",
@@ -323,9 +331,20 @@
"Users": "Пользователи",
"Users under all organizations": "Пользователи всех организаций",
"Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group",
"empty": "пустые",
"remove": "remove",
"{total} in total": "{total} в общей сложности"
},
"group": {
"Edit Group": "Edit Group",
"New Group": "New Group",
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
"Admin": "Admin",
"Admin - Tooltip": "CN или ID администратора сервера LDAP",
@@ -539,8 +558,6 @@
"Edit Pricing": "Edit Pricing",
"Free": "Бесплатно",
"Getting started": "Выьрать план",
"Has trial": "Есть пробный период",
"Has trial - Tooltip": "Наличие пробного периода после выбора плана",
"New Pricing": "New Pricing",
"Trial duration": "Продолжительность пробного периода",
"Trial duration - Tooltip": "Продолжительность пробного периода",

View File

@@ -188,6 +188,7 @@
"Close": "Đóng lại",
"Confirm": "Confirm",
"Created time": "Thời gian tạo",
"Custom": "Custom",
"Default application": "Ứng dụng mặc định",
"Default application - Tooltip": "Ứng dụng mặc định cho người dùng đăng ký trực tiếp từ trang tổ chức",
"Default avatar": "Hình đại diện mặc định",
@@ -209,6 +210,7 @@
"Failed to delete": "Không thể xoá",
"Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "Không thể lưu được",
"Failed to verify": "Failed to verify",
"Favicon": "Favicon",
@@ -218,6 +220,8 @@
"Forget URL - Tooltip": "Đường dẫn tùy chỉnh cho trang \"Quên mật khẩu\". Nếu không được thiết lập, trang \"Quên mật khẩu\" mặc định của Casdoor sẽ được sử dụng. Khi cài đặt, liên kết \"Quên mật khẩu\" trên trang đăng nhập sẽ chuyển hướng đến URL này",
"Found some texts still not translated? Please help us translate at": "Tìm thấy một số văn bản vẫn chưa được dịch? Vui lòng giúp chúng tôi dịch tại",
"Go to writable demo site?": "Bạn có muốn đi đến trang demo có thể viết được không?",
"Groups": "Groups",
"Groups - Tooltip": "Groups - Tooltip",
"Home": "Nhà",
"Home - Tooltip": "Trang chủ của ứng dụng",
"ID": "ID",
@@ -299,10 +303,12 @@
"Subscriptions": "Đăng ký",
"Successfully added": "Đã thêm thành công",
"Successfully deleted": "Đã xóa thành công",
"Successfully removed": "Successfully removed",
"Successfully saved": "Thành công đã được lưu lại",
"Supported country codes": "Các mã quốc gia được hỗ trợ",
"Supported country codes - Tooltip": "Mã quốc gia được hỗ trợ bởi tổ chức. Những mã này có thể được chọn làm tiền tố khi gửi mã xác nhận SMS",
"Sure to delete": "Chắc chắn muốn xóa",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "Đồng bộ hoá",
"Syncers": "Đồng bộ hóa",
@@ -311,6 +317,8 @@
"This is a read-only demo site!": "Đây là trang web giới thiệu chỉ có chức năng đọc!",
"Timestamp": "Đánh dấu thời gian",
"Tokens": "Mã thông báo",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "Đường dẫn URL",
"Up": "Lên",
@@ -323,9 +331,20 @@
"Users": "Người dùng",
"Users under all organizations": "Người dùng trong tất cả các tổ chức",
"Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group",
"empty": "trống",
"remove": "remove",
"{total} in total": "Trong tổng số {total}"
},
"group": {
"Edit Group": "Edit Group",
"New Group": "New Group",
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
"Admin": "Admin",
"Admin - Tooltip": "CN hoặc ID của quản trị viên máy chủ LDAP",
@@ -539,8 +558,6 @@
"Edit Pricing": "Edit Pricing",
"Free": "Miễn phí",
"Getting started": "Bắt đầu",
"Has trial": "Có thời gian thử nghiệm",
"Has trial - Tooltip": "Khả dụng thời gian thử nghiệm sau khi chọn kế hoạch",
"New Pricing": "New Pricing",
"Trial duration": "Thời gian thử nghiệm",
"Trial duration - Tooltip": "Thời gian thử nghiệm",

View File

@@ -188,6 +188,7 @@
"Close": "关闭",
"Confirm": "确认",
"Created time": "创建时间",
"Custom": "自定义",
"Default application": "默认应用",
"Default application - Tooltip": "直接从组织页面注册的用户默认所属的应用",
"Default avatar": "默认头像",
@@ -209,6 +210,7 @@
"Failed to delete": "删除失败",
"Failed to enable": "启用失败",
"Failed to get answer": "获取回答失败",
"Failed to remove": "移除失败",
"Failed to save": "保存失败",
"Failed to verify": "验证失败",
"Favicon": "Favicon",
@@ -218,6 +220,8 @@
"Forget URL - Tooltip": "自定义忘记密码页面的URL不设置时采用Casdoor默认的忘记密码页面设置后Casdoor各类页面的忘记密码链接会跳转到该URL",
"Found some texts still not translated? Please help us translate at": "发现有些文字尚未翻译?请移步这里帮我们翻译:",
"Go to writable demo site?": "跳转至可写演示站点?",
"Groups": "用户组",
"Groups - Tooltip": "Groups - Tooltip",
"Home": "首页",
"Home - Tooltip": "应用的首页",
"ID": "ID",
@@ -299,10 +303,12 @@
"Subscriptions": "订阅",
"Successfully added": "添加成功",
"Successfully deleted": "删除成功",
"Successfully removed": "移除成功",
"Successfully saved": "保存成功",
"Supported country codes": "支持的国家代码",
"Supported country codes - Tooltip": "该组织所支持的国家代码,发送短信验证码时可以选择这些国家的代码前缀",
"Sure to delete": "确定删除",
"Sure to remove": "确定移除",
"Swagger": "API文档",
"Sync": "同步",
"Syncers": "同步器",
@@ -311,6 +317,8 @@
"This is a read-only demo site!": "这是一个只读演示站点!",
"Timestamp": "时间戳",
"Tokens": "令牌",
"Type": "类型",
"Type - Tooltip": "类型",
"URL": "链接",
"URL - Tooltip": "URL链接",
"Up": "上移",
@@ -323,9 +331,20 @@
"Users": "用户",
"Users under all organizations": "所有组织里的用户",
"Webhooks": "Webhooks",
"You can only select one physical group": "只能选择一个实体组",
"empty": "无",
"remove": "移除",
"{total} in total": "{total} 总计"
},
"group": {
"Edit Group": "编辑用户组",
"New Group": "新建用户组",
"Parent group": "上级组",
"Parent group - Tooltip": "上级组",
"Physical": "物理组",
"Show all": "显示全部",
"Virtual": "虚拟组"
},
"ldap": {
"Admin": "管理员",
"Admin - Tooltip": "LDAP服务器管理员的CN或ID",
@@ -539,8 +558,6 @@
"Edit Pricing": "编辑定价",
"Free": "免费",
"Getting started": "开始使用",
"Has trial": "有试用期",
"Has trial - Tooltip": "选择计划后是否有试用期",
"New Pricing": "添加定价",
"Trial duration": "试用期时长",
"Trial duration - Tooltip": "试用期时长",

View File

@@ -91,8 +91,10 @@ class AccountTable extends React.Component {
{name: "Karma", label: i18next.t("user:Karma")},
{name: "Ranking", label: i18next.t("user:Ranking")},
{name: "Signup application", label: i18next.t("general:Signup application")},
{name: "API key", label: i18next.t("general:API key")},
{name: "Roles", label: i18next.t("general:Roles")},
{name: "Permissions", label: i18next.t("general:Permissions")},
{name: "Groups", label: i18next.t("general:Groups")},
{name: "3rd-party logins", label: i18next.t("user:3rd-party logins")},
{name: "Properties", label: i18next.t("user:Properties")},
{name: "Is online", label: i18next.t("user:Is online")},