Compare commits

...

27 Commits

Author SHA1 Message Date
Yaodong Yu
d0ac265c91 fix: Deprecate the id field in group (#1987) 2023-06-18 23:33:13 +08:00
hsluoyz
3562c36817 feat: Revert "fix: fix URL path in MinIO storage provider" (#1988)
This reverts commit 3699177837.
2023-06-18 23:08:40 +08:00
Yang Luo
7884e10ca3 Refactor adapter's owner and organization 2023-06-18 00:22:12 +08:00
Yang Luo
12dee8afd3 Fix null options in checkPasswordComplexity() 2023-06-17 22:38:02 +08:00
Yang Luo
ac4b870309 Improve getFaviconFileBuffer() 2023-06-17 12:50:01 +08:00
Yang Luo
b9140e2d5a Refactor refreshAvatar() 2023-06-17 11:43:46 +08:00
Yang Luo
501f0dc74f Add user_avatar.go 2023-06-17 01:25:15 +08:00
Yang Luo
a932b76fba Remove useless check in SetPassword() 2023-06-17 00:58:31 +08:00
leoil
0f57ac297b ci: add password complexity options to organization edit page (#1949)
* Support uploading roles and permissions via xlsx file.

* Template xlsx file for uploading users and permissions.

* reformat according to gofumpt.

* fix typo.

* add password complexity options to organization edit page.

* add password complexity options to organization edit page.

* Fixed Typos.

* Fixed Typos.

* feat:add password complexity options to organization edit page

* Auto generate i18n fields.

* Refactor code according to instructions

* Support autocheck passwd complexity in frontend when setting passwd in user edit page.

* feat:Backend Support for password validation in signup and forget page.

* feat:Frontend Support for password validation in signup and forget page.

* Add default password complex option & Update historical empty filed with default option.

* Migrator for field `password_complex_options` in org table.

* feat: support frontend password complex option check in user_edit/forget/signup page.

* frontend update for user edit page

* update i18n file

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-06-17 00:07:36 +08:00
June
edc6aa0d50 feat: get all role/permission of an user (#1978) 2023-06-16 22:44:21 +08:00
Yang Luo
ebc0e0f2c9 Update i18n words 2023-06-16 22:06:54 +08:00
Yang Luo
63dd2e781e Update backend i18n files 2023-06-16 21:55:08 +08:00
Yang Luo
b01ba792bb Rename to accessSecret 2023-06-16 20:42:15 +08:00
Yaodong Yu
98fb9f25b0 feat: fix bug that users in role don't work for permissions (#1977)
* feat: fix check login permission

* feat: fix check login permission
2023-06-16 20:14:27 +08:00
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
101 changed files with 3712 additions and 593 deletions

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

View File

@@ -140,6 +140,13 @@ func (c *ApiController) Signup() {
username = id
}
password := authForm.Password
msg = object.CheckPasswordComplexityByOrg(organization, password)
if msg != "" {
c.ResponseError(msg)
return
}
initScore, err := organization.GetInitScore()
if err != nil {
c.ResponseError(fmt.Errorf(c.T("account:Get init score failed, error: %w"), err).Error())

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

@@ -23,6 +23,13 @@ import (
xormadapter "github.com/casdoor/xorm-adapter/v3"
)
// GetCasbinAdapters
// @Title GetCasbinAdapters
// @Tag Adapter API
// @Description get adapters
// @Param owner query string true "The owner of adapters"
// @Success 200 {array} object.Adapter The Response object
// @router /get-adapters [get]
func (c *ApiController) GetCasbinAdapters() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
@@ -31,9 +38,9 @@ func (c *ApiController) GetCasbinAdapters() {
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
organization := c.Input().Get("organization")
if limit == "" || page == "" {
adapters, err := object.GetCasbinAdapters(owner, organization)
adapters, err := object.GetCasbinAdapters(owner)
if err != nil {
c.ResponseError(err.Error())
return
@@ -42,14 +49,14 @@ func (c *ApiController) GetCasbinAdapters() {
c.ResponseOk(adapters)
} else {
limit := util.ParseInt(limit)
count, err := object.GetCasbinAdapterCount(owner, organization, field, value)
count, err := object.GetCasbinAdapterCount(owner, field, value)
if err != nil {
c.ResponseError(err.Error())
return
}
paginator := pagination.SetPaginator(c.Ctx, limit, count)
adapters, err := object.GetPaginationCasbinAdapters(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder)
adapters, err := object.GetPaginationCasbinAdapters(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil {
c.ResponseError(err.Error())
return
@@ -59,8 +66,16 @@ func (c *ApiController) GetCasbinAdapters() {
}
}
// GetCasbinAdapter
// @Title GetCasbinAdapter
// @Tag Adapter API
// @Description get adapter
// @Param id query string true "The id ( owner/name ) of the adapter"
// @Success 200 {object} object.Adapter The Response object
// @router /get-adapter [get]
func (c *ApiController) GetCasbinAdapter() {
id := c.Input().Get("id")
adapter, err := object.GetCasbinAdapter(id)
if err != nil {
c.ResponseError(err.Error())
@@ -70,6 +85,14 @@ func (c *ApiController) GetCasbinAdapter() {
c.ResponseOk(adapter)
}
// UpdateCasbinAdapter
// @Title UpdateCasbinAdapter
// @Tag Adapter API
// @Description update adapter
// @Param id query string true "The id ( owner/name ) of the adapter"
// @Param body body object.Adapter true "The details of the adapter"
// @Success 200 {object} controllers.Response The Response object
// @router /update-adapter [post]
func (c *ApiController) UpdateCasbinAdapter() {
id := c.Input().Get("id")
@@ -84,6 +107,13 @@ func (c *ApiController) UpdateCasbinAdapter() {
c.ServeJSON()
}
// AddCasbinAdapter
// @Title AddCasbinAdapter
// @Tag Adapter API
// @Description add adapter
// @Param body body object.Adapter true "The details of the adapter"
// @Success 200 {object} controllers.Response The Response object
// @router /add-adapter [post]
func (c *ApiController) AddCasbinAdapter() {
var casbinAdapter object.CasbinAdapter
err := json.Unmarshal(c.Ctx.Input.RequestBody, &casbinAdapter)
@@ -96,6 +126,13 @@ func (c *ApiController) AddCasbinAdapter() {
c.ServeJSON()
}
// DeleteCasbinAdapter
// @Title DeleteCasbinAdapter
// @Tag Adapter API
// @Description delete adapter
// @Param body body object.Adapter true "The details of the adapter"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-adapter [post]
func (c *ApiController) DeleteCasbinAdapter() {
var casbinAdapter object.CasbinAdapter
err := json.Unmarshal(c.Ctx.Input.RequestBody, &casbinAdapter)

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")
groupName := c.Input().Get("groupName")
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 groupName != "" {
maskedUsers, err := object.GetMaskedUsers(object.GetGroupUsers(groupName))
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, groupName)
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, groupName)
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
@@ -399,10 +410,6 @@ func (c *ApiController) SetPassword() {
c.ResponseError(c.T("user:New password cannot contain blank space."))
return
}
if len(newPassword) <= 5 {
c.ResponseError(c.T("user:New password must have at least 6 characters"))
return
}
userId := util.GetId(userOwner, userName)
@@ -437,6 +444,12 @@ func (c *ApiController) SetPassword() {
}
}
msg := object.CheckPasswordComplexity(targetUser, newPassword)
if msg != "" {
c.ResponseError(msg)
return
}
targetUser.Password = newPassword
_, err = object.SetUserField(targetUser, "password", targetUser.Password)
if err != nil {
@@ -505,7 +518,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 +530,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

View File

@@ -68,7 +68,8 @@
"Missing parameter": "Fehlender Parameter",
"Please login first": "Bitte zuerst einloggen",
"The user: %s doesn't exist": "Der Benutzer %s existiert nicht",
"don't support captchaProvider: ": "Unterstütze captchaProvider nicht:"
"don't support captchaProvider: ": "Unterstütze captchaProvider nicht:",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
},
"ldap": {
"Ldap server exist": "Es gibt einen LDAP-Server"
@@ -119,8 +120,7 @@
},
"user": {
"Display name cannot be empty": "Anzeigename darf nicht leer sein",
"New password cannot contain blank space.": "Das neue Passwort darf keine Leerzeichen enthalten.",
"New password must have at least 6 characters": "Das neue Passwort muss mindestens 6 Zeichen haben"
"New password cannot contain blank space.": "Das neue Passwort darf keine Leerzeichen enthalten."
},
"user_upload": {
"Failed to import users": "Fehler beim Importieren von Benutzern"

View File

@@ -68,7 +68,8 @@
"Missing parameter": "Missing parameter",
"Please login first": "Please login first",
"The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: "
"don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
},
"ldap": {
"Ldap server exist": "Ldap server exist"
@@ -119,8 +120,7 @@
},
"user": {
"Display name cannot be empty": "Display name cannot be empty",
"New password cannot contain blank space.": "New password cannot contain blank space.",
"New password must have at least 6 characters": "New password must have at least 6 characters"
"New password cannot contain blank space.": "New password cannot contain blank space."
},
"user_upload": {
"Failed to import users": "Failed to import users"

View File

@@ -68,7 +68,8 @@
"Missing parameter": "Parámetro faltante",
"Please login first": "Por favor, inicia sesión primero",
"The user: %s doesn't exist": "El usuario: %s no existe",
"don't support captchaProvider: ": "No apoyo a captchaProvider"
"don't support captchaProvider: ": "No apoyo a captchaProvider",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
},
"ldap": {
"Ldap server exist": "El servidor LDAP existe"
@@ -119,8 +120,7 @@
},
"user": {
"Display name cannot be empty": "El nombre de pantalla no puede estar vacío",
"New password cannot contain blank space.": "La nueva contraseña no puede contener espacios en blanco.",
"New password must have at least 6 characters": "La nueva contraseña debe tener al menos 6 caracteres"
"New password cannot contain blank space.": "La nueva contraseña no puede contener espacios en blanco."
},
"user_upload": {
"Failed to import users": "Error al importar usuarios"

View File

@@ -68,7 +68,8 @@
"Missing parameter": "Paramètre manquant",
"Please login first": "Veuillez d'abord vous connecter",
"The user: %s doesn't exist": "L'utilisateur : %s n'existe pas",
"don't support captchaProvider: ": "Ne pas prendre en charge la captchaProvider"
"don't support captchaProvider: ": "Ne pas prendre en charge la captchaProvider",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
},
"ldap": {
"Ldap server exist": "Le serveur LDAP existe"
@@ -119,8 +120,7 @@
},
"user": {
"Display name cannot be empty": "Le nom d'affichage ne peut pas être vide",
"New password cannot contain blank space.": "Le nouveau mot de passe ne peut pas contenir d'espace.",
"New password must have at least 6 characters": "Le nouveau mot de passe doit comporter au moins 6 caractères"
"New password cannot contain blank space.": "Le nouveau mot de passe ne peut pas contenir d'espace."
},
"user_upload": {
"Failed to import users": "Échec de l'importation des utilisateurs"

View File

@@ -68,7 +68,8 @@
"Missing parameter": "Parameter hilang",
"Please login first": "Silahkan login terlebih dahulu",
"The user: %s doesn't exist": "Pengguna: %s tidak ada",
"don't support captchaProvider: ": "Jangan mendukung captchaProvider:"
"don't support captchaProvider: ": "Jangan mendukung captchaProvider:",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
},
"ldap": {
"Ldap server exist": "Server ldap ada"
@@ -119,8 +120,7 @@
},
"user": {
"Display name cannot be empty": "Nama tampilan tidak boleh kosong",
"New password cannot contain blank space.": "Kata sandi baru tidak boleh mengandung spasi kosong.",
"New password must have at least 6 characters": "Kata sandi baru harus memiliki setidaknya 6 karakter"
"New password cannot contain blank space.": "Kata sandi baru tidak boleh mengandung spasi kosong."
},
"user_upload": {
"Failed to import users": "Gagal mengimpor pengguna"

View File

@@ -68,7 +68,8 @@
"Missing parameter": "不足しているパラメーター",
"Please login first": "最初にログインしてください",
"The user: %s doesn't exist": "そのユーザー:%sは存在しません",
"don't support captchaProvider: ": "captchaProviderをサポートしないでください"
"don't support captchaProvider: ": "captchaProviderをサポートしないでください",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
},
"ldap": {
"Ldap server exist": "LDAPサーバーは存在します"
@@ -119,8 +120,7 @@
},
"user": {
"Display name cannot be empty": "表示名は空にできません",
"New password cannot contain blank space.": "新しいパスワードにはスペースを含めることはできません。",
"New password must have at least 6 characters": "新しいパスワードは少なくとも6文字必要です"
"New password cannot contain blank space.": "新しいパスワードにはスペースを含めることはできません。"
},
"user_upload": {
"Failed to import users": "ユーザーのインポートに失敗しました"

View File

@@ -68,7 +68,8 @@
"Missing parameter": "누락된 매개변수",
"Please login first": "먼저 로그인 하십시오",
"The user: %s doesn't exist": "사용자 %s는 존재하지 않습니다",
"don't support captchaProvider: ": "CaptchaProvider를 지원하지 마세요"
"don't support captchaProvider: ": "CaptchaProvider를 지원하지 마세요",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
},
"ldap": {
"Ldap server exist": "LDAP 서버가 존재합니다"
@@ -119,8 +120,7 @@
},
"user": {
"Display name cannot be empty": "디스플레이 이름은 비어 있을 수 없습니다",
"New password cannot contain blank space.": "새 비밀번호에는 공백이 포함될 수 없습니다.",
"New password must have at least 6 characters": "새로운 비밀번호는 최소 6자 이상이어야 합니다"
"New password cannot contain blank space.": "새 비밀번호에는 공백이 포함될 수 없습니다."
},
"user_upload": {
"Failed to import users": "사용자 가져오기를 실패했습니다"

149
i18n/locales/pt/data.json Normal file
View File

@@ -0,0 +1,149 @@
{
"account": {
"Failed to add user": "Failed to add user",
"Get init score failed, error: %w": "Get init score failed, error: %w",
"Please sign out first": "Please sign out first",
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
},
"auth": {
"Challenge method should be S256": "Challenge method should be S256",
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
"Failed to login in: %s": "Failed to login in: %s",
"Invalid token": "Invalid token",
"State expected: %s, but got: %s": "State expected: %s, but got: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)",
"The application: %s does not exist": "The application: %s does not exist",
"The login method: login with password is not enabled for the application": "The login method: login with password is not enabled for the application",
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Affiliation cannot be blank",
"DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists",
"Email cannot be empty": "Email cannot be empty",
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",
"Session outdated, please login again": "Session outdated, please login again",
"The user is forbidden to sign in, please contact the administrator": "The user is forbidden to sign in, please contact the administrator",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.",
"Username already exists": "Username already exists",
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"general": {
"Missing parameter": "Missing parameter",
"Please login first": "Please login first",
"The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
},
"ldap": {
"Ldap server exist": "Ldap server exist"
},
"link": {
"Please link first": "Please link first",
"This application has no providers": "This application has no providers",
"This application has no providers of type": "This application has no providers of type",
"This provider can't be unlinked": "This provider can't be unlinked",
"You are not the global admin, you can't unlink other users": "You are not the global admin, you can't unlink other users",
"You can't unlink yourself, you are not a member of any application": "You can't unlink yourself, you are not a member of any application"
},
"organization": {
"Only admin can modify the %s.": "Only admin can modify the %s.",
"The %s is immutable.": "The %s is immutable.",
"Unknown modify rule %s.": "Unknown modify rule %s."
},
"provider": {
"Invalid application id": "Invalid application id",
"the provider: %s does not exist": "the provider: %s does not exist"
},
"resource": {
"User is nil for tag: avatar": "User is nil for tag: avatar",
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "Username or fullFilePath is empty: username = %s, fullFilePath = %s"
},
"saml": {
"Application %s not found": "Application %s not found"
},
"saml_sp": {
"provider %s's category is not SAML": "provider %s's category is not SAML"
},
"service": {
"Empty parameters for emailForm: %v": "Empty parameters for emailForm: %v",
"Invalid Email receivers: %s": "Invalid Email receivers: %s",
"Invalid phone receivers: %s": "Invalid phone receivers: %s"
},
"storage": {
"The objectKey: %s is not allowed": "The objectKey: %s is not allowed",
"The provider type: %s is not supported": "The provider type: %s is not supported"
},
"token": {
"Empty clientId or clientSecret": "Empty clientId or clientSecret",
"Grant_type: %s is not supported in this application": "Grant_type: %s is not supported in this application",
"Invalid application or wrong clientSecret": "Invalid application or wrong clientSecret",
"Invalid client_id": "Invalid client_id",
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "Redirect URI: %s doesn't exist in the allowed Redirect URI list",
"Token not found, invalid accessToken": "Token not found, invalid accessToken"
},
"user": {
"Display name cannot be empty": "Display name cannot be empty",
"New password cannot contain blank space.": "New password cannot contain blank space."
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",
"The provider: %s is not found": "The provider: %s is not found"
},
"verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",
"Unknown type": "Unknown type",
"Wrong verification code!": "Wrong verification code!",
"You should verify your code in %d min!": "You should verify your code in %d min!",
"the user does not exist, please sign up first": "the user does not exist, please sign up first"
},
"webauthn": {
"Found no credentials for this user": "Found no credentials for this user",
"Please call WebAuthnSigninBegin first": "Please call WebAuthnSigninBegin first"
}
}

View File

@@ -68,7 +68,8 @@
"Missing parameter": "Отсутствующий параметр",
"Please login first": "Пожалуйста, сначала войдите в систему",
"The user: %s doesn't exist": "Пользователь %s не существует",
"don't support captchaProvider: ": "не поддерживайте captchaProvider:"
"don't support captchaProvider: ": "не поддерживайте captchaProvider:",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
},
"ldap": {
"Ldap server exist": "LDAP-сервер существует"
@@ -119,8 +120,7 @@
},
"user": {
"Display name cannot be empty": "Отображаемое имя не может быть пустым",
"New password cannot contain blank space.": "Новый пароль не может содержать пробелы.",
"New password must have at least 6 characters": "Новый пароль должен содержать не менее 6 символов"
"New password cannot contain blank space.": "Новый пароль не может содержать пробелы."
},
"user_upload": {
"Failed to import users": "Не удалось импортировать пользователей"

View File

@@ -68,7 +68,8 @@
"Missing parameter": "Thiếu tham số",
"Please login first": "Vui lòng đăng nhập trước",
"The user: %s doesn't exist": "Người dùng: %s không tồn tại",
"don't support captchaProvider: ": "không hỗ trợ captchaProvider: "
"don't support captchaProvider: ": "không hỗ trợ captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
},
"ldap": {
"Ldap server exist": "Máy chủ LDAP tồn tại"
@@ -119,8 +120,7 @@
},
"user": {
"Display name cannot be empty": "Tên hiển thị không thể trống",
"New password cannot contain blank space.": "Mật khẩu mới không thể chứa dấu trắng.",
"New password must have at least 6 characters": "Mật khẩu mới phải có ít nhất 6 ký tự"
"New password cannot contain blank space.": "Mật khẩu mới không thể chứa dấu trắng."
},
"user_upload": {
"Failed to import users": "Không thể nhập người dùng"

View File

@@ -68,7 +68,8 @@
"Missing parameter": "缺少参数",
"Please login first": "请先登录",
"The user: %s doesn't exist": "用户: %s不存在",
"don't support captchaProvider: ": "不支持验证码提供商: "
"don't support captchaProvider: ": "不支持验证码提供商: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
},
"ldap": {
"Ldap server exist": "LDAP服务器已存在"
@@ -119,8 +120,7 @@
},
"user": {
"Display name cannot be empty": "显示名称不可为空",
"New password cannot contain blank space.": "新密码不可以包含空格",
"New password must have at least 6 characters": "新密码至少需要6位字符"
"New password cannot contain blank space.": "新密码不可以包含空格"
},
"user_upload": {
"Failed to import users": "导入用户失败"

View File

@@ -8,6 +8,7 @@
"favicon": "",
"passwordType": "plain",
"passwordSalt": "",
"passwordOptions": ["AtLeast6"],
"countryCodes": ["US", "ES", "CN", "FR", "DE", "GB", "JP", "KR", "VN", "ID", "SG", "IN"],
"defaultAvatar": "",
"defaultApplication": "",

View File

@@ -30,9 +30,14 @@ 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)

View File

@@ -55,8 +55,12 @@ func InitAdapter() {
func CreateTables(createDatabase bool) {
if createDatabase {
adapter.CreateDatabase()
err := adapter.CreateDatabase()
if err != nil {
panic(err)
}
}
adapter.createTable()
}
@@ -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)

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

View File

@@ -1,167 +0,0 @@
// 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 (
"bytes"
"crypto/md5"
"fmt"
"image"
"image/color"
"image/png"
"io"
"net/http"
"strings"
"github.com/fogleman/gg"
)
func hasGravatar(client *http.Client, email string) (bool, error) {
// Clean and lowercase the email
email = strings.TrimSpace(strings.ToLower(email))
// Generate MD5 hash of the email
hash := md5.New()
io.WriteString(hash, email)
hashedEmail := fmt.Sprintf("%x", hash.Sum(nil))
// Create Gravatar URL with d=404 parameter
gravatarURL := fmt.Sprintf("https://www.gravatar.com/avatar/%s?d=404", hashedEmail)
// Send a request to Gravatar
req, err := http.NewRequest("GET", gravatarURL, nil)
if err != nil {
return false, err
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
// Check if the user has a custom Gravatar image
if resp.StatusCode == http.StatusOK {
return true, nil
} else if resp.StatusCode == http.StatusNotFound {
return false, nil
} else {
return false, fmt.Errorf("failed to fetch gravatar image: %s", resp.Status)
}
}
func getGravatarFileBuffer(client *http.Client, email string) (*bytes.Buffer, string, error) {
// Clean and lowercase the email
email = strings.TrimSpace(strings.ToLower(email))
// Generate MD5 hash of the email
hash := md5.New()
io.WriteString(hash, email)
hashedEmail := fmt.Sprintf("%x", hash.Sum(nil))
// Create Gravatar URL
gravatarURL := fmt.Sprintf("https://www.gravatar.com/avatar/%s", hashedEmail)
// Download the image
req, err := http.NewRequest("GET", gravatarURL, nil)
if err != nil {
return nil, "", err
}
resp, err := client.Do(req)
if err != nil {
return nil, "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, "", fmt.Errorf("failed to download gravatar image: %s", resp.Status)
}
// Get the content type and determine the file extension
contentType := resp.Header.Get("Content-Type")
fileExtension := ""
switch contentType {
case "image/jpeg":
fileExtension = ".jpg"
case "image/png":
fileExtension = ".png"
case "image/gif":
fileExtension = ".gif"
default:
return nil, "", fmt.Errorf("unsupported content type: %s", contentType)
}
// Save the image to a bytes.Buffer
buffer := &bytes.Buffer{}
_, err = io.Copy(buffer, resp.Body)
if err != nil {
return nil, "", err
}
return buffer, fileExtension, nil
}
func getColor(data []byte) color.RGBA {
r := int(data[0]) % 256
g := int(data[1]) % 256
b := int(data[2]) % 256
return color.RGBA{uint8(r), uint8(g), uint8(b), 255}
}
func getIdenticonFileBuffer(username string) (*bytes.Buffer, string, error) {
username = strings.TrimSpace(strings.ToLower(username))
hash := md5.New()
io.WriteString(hash, username)
hashedUsername := hash.Sum(nil)
// Define the size of the image
const imageSize = 420
const cellSize = imageSize / 7
// Create a new image
img := image.NewRGBA(image.Rect(0, 0, imageSize, imageSize))
// Create a context
dc := gg.NewContextForRGBA(img)
// Set a background color
dc.SetColor(color.RGBA{240, 240, 240, 255})
dc.Clear()
// Get avatar color
avatarColor := getColor(hashedUsername)
// Draw cells
for i := 0; i < 7; i++ {
for j := 0; j < 7; j++ {
if (hashedUsername[i] >> uint(j) & 1) == 1 {
dc.SetColor(avatarColor)
dc.DrawRectangle(float64(j*cellSize), float64(i*cellSize), float64(cellSize), float64(cellSize))
dc.Fill()
}
}
}
// Save image to a bytes.Buffer
buffer := &bytes.Buffer{}
err := png.Encode(buffer, img)
if err != nil {
return nil, "", fmt.Errorf("failed to save image: %w", err)
}
return buffer, ".png", nil
}

View File

@@ -30,9 +30,8 @@ type CasbinAdapter struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
Organization string `xorm:"varchar(100)" json:"organization"`
Type string `xorm:"varchar(100)" json:"type"`
Model string `xorm:"varchar(100)" json:"model"`
Type string `xorm:"varchar(100)" json:"type"`
Model string `xorm:"varchar(100)" json:"model"`
Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"`
@@ -46,14 +45,14 @@ type CasbinAdapter struct {
Adapter *xormadapter.Adapter `xorm:"-" json:"-"`
}
func GetCasbinAdapterCount(owner, organization, field, value string) (int64, error) {
func GetCasbinAdapterCount(owner, field, value string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "")
return session.Count(&CasbinAdapter{Organization: organization})
return session.Count(&CasbinAdapter{})
}
func GetCasbinAdapters(owner string, organization string) ([]*CasbinAdapter, error) {
func GetCasbinAdapters(owner string) ([]*CasbinAdapter, error) {
adapters := []*CasbinAdapter{}
err := adapter.Engine.Where("owner = ? and organization = ?", owner, organization).Find(&adapters)
err := adapter.Engine.Desc("created_time").Find(&adapters, &CasbinAdapter{Owner: owner})
if err != nil {
return adapters, err
}
@@ -61,10 +60,10 @@ func GetCasbinAdapters(owner string, organization string) ([]*CasbinAdapter, err
return adapters, nil
}
func GetPaginationCasbinAdapters(owner, organization string, page, limit int, field, value, sort, order string) ([]*CasbinAdapter, error) {
session := GetSession(owner, page, limit, field, value, sort, order)
func GetPaginationCasbinAdapters(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*CasbinAdapter, error) {
adapters := []*CasbinAdapter{}
err := session.Find(&adapters, &CasbinAdapter{Organization: organization})
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&adapters)
if err != nil {
return adapters, err
}
@@ -214,6 +213,10 @@ func SyncPolicies(casbinAdapter *CasbinAdapter) ([]*xormadapter.CasbinRule, erro
return nil, err
}
if modelObj == nil {
return nil, fmt.Errorf("The model: %s does not exist", util.GetId(casbinAdapter.Owner, casbinAdapter.Model))
}
enforcer, err := initEnforcer(modelObj, casbinAdapter)
if err != nil {
return nil, err

View File

@@ -203,6 +203,16 @@ func CheckPassword(user *User, password string, lang string, options ...bool) st
}
}
func CheckPasswordComplexityByOrg(organization *Organization, password string) string {
errorMsg := checkPasswordComplexity(password, organization.PasswordOptions)
return errorMsg
}
func CheckPasswordComplexity(user *User, password string) string {
organization, _ := GetOrganizationByUser(user)
return CheckPasswordComplexityByOrg(organization, password)
}
func checkLdapUserPassword(user *User, password string, lang string) string {
ldaps, err := GetLdaps(user.Owner)
if err != nil {
@@ -353,7 +363,7 @@ func CheckAccessPermission(userId string, application *Application) (bool, error
allowed := true
for _, permission := range permissions {
if !permission.IsEnabled || len(permission.Users) == 0 {
if !permission.IsEnabled {
continue
}

View File

@@ -0,0 +1,98 @@
// 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 (
"regexp"
)
type ValidatorFunc func(password string) string
var (
regexLowerCase = regexp.MustCompile(`[a-z]`)
regexUpperCase = regexp.MustCompile(`[A-Z]`)
regexDigit = regexp.MustCompile(`\d`)
regexSpecial = regexp.MustCompile(`[!@#$%^&*]`)
)
func isValidOption_AtLeast6(password string) string {
if len(password) < 6 {
return "The password must have at least 6 characters"
}
return ""
}
func isValidOption_AtLeast8(password string) string {
if len(password) < 8 {
return "The password must have at least 8 characters"
}
return ""
}
func isValidOption_Aa123(password string) string {
hasLowerCase := regexLowerCase.MatchString(password)
hasUpperCase := regexUpperCase.MatchString(password)
hasDigit := regexDigit.MatchString(password)
if !hasLowerCase || !hasUpperCase || !hasDigit {
return "The password must contain at least one uppercase letter, one lowercase letter and one digit"
}
return ""
}
func isValidOption_SpecialChar(password string) string {
if !regexSpecial.MatchString(password) {
return "The password must contain at least one special character"
}
return ""
}
func isValidOption_NoRepeat(password string) string {
for i := 0; i < len(password)-1; i++ {
if password[i] == password[i+1] {
return "The password must not contain any repeated characters"
}
}
return ""
}
func checkPasswordComplexity(password string, options []string) string {
if len(password) == 0 {
return "Please input your password!"
}
if len(options) == 0 {
options = []string{"AtLeast6"}
}
checkers := map[string]ValidatorFunc{
"AtLeast6": isValidOption_AtLeast6,
"AtLeast8": isValidOption_AtLeast8,
"Aa123": isValidOption_Aa123,
"SpecialChar": isValidOption_SpecialChar,
"NoRepeat": isValidOption_NoRepeat,
}
for _, option := range options {
checkerFunc, ok := checkers[option]
if ok {
errorMsg := checkerFunc(password)
if errorMsg != "" {
return errorMsg
}
}
}
return ""
}

222
object/group.go Normal file
View File

@@ -0,0 +1,222 @@
// 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 index" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
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 getGroupByName(name string) (*Group, error) {
if name == "" {
return nil, nil
}
group := Group{Name: name}
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) {
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.Name).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{GroupName: group.Name}); 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,
}
children := ConvertToTreeData(groups, group.Name)
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"},
@@ -91,6 +92,7 @@ func initBuiltInOrganization() bool {
WebsiteUrl: "https://example.com",
Favicon: fmt.Sprintf("%s/img/casbin/favicon.ico", conf.GetConfigString("staticBaseUrl")),
PasswordType: "plain",
PasswordOptions: []string{"AtLeast6"},
CountryCodes: []string{"US", "ES", "CN", "FR", "DE", "GB", "JP", "KR", "VN", "ID", "SG", "IN"},
DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
Tags: []string{},

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"
)
@@ -55,6 +56,7 @@ type Organization struct {
Favicon string `xorm:"varchar(100)" json:"favicon"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"`
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
@@ -75,11 +77,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 +343,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 {
@@ -386,7 +402,6 @@ func organizationChangeTrigger(oldName string, newName string) error {
casbinAdapter := new(CasbinAdapter)
casbinAdapter.Owner = newName
casbinAdapter.Organization = newName
_, err = session.Where("owner=?", oldName).Update(casbinAdapter)
if err != nil {
return err

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

@@ -264,18 +264,48 @@ func DeletePermission(permission *Permission) (bool, error) {
return affected != 0, nil
}
func GetPermissionsByUser(userId string) ([]*Permission, error) {
func GetPermissionsAndRolesByUser(userId string) ([]*Permission, []*Role, error) {
permissions := []*Permission{}
err := adapter.Engine.Where("users like ?", "%"+userId+"\"%").Find(&permissions)
if err != nil {
return permissions, err
return nil, nil, err
}
for i := range permissions {
permissions[i].Users = nil
existedPerms := map[string]struct{}{}
for _, perm := range permissions {
perm.Users = nil
if _, ok := existedPerms[perm.Name]; !ok {
existedPerms[perm.Name] = struct{}{}
}
}
return permissions, nil
permFromRoles := []*Permission{}
roles, err := GetRolesByUser(userId)
if err != nil {
return nil, nil, err
}
for _, role := range roles {
perms := []*Permission{}
err := adapter.Engine.Where("roles like ?", "%"+role.Name+"\"%").Find(&perms)
if err != nil {
return nil, nil, err
}
permFromRoles = append(permFromRoles, perms...)
}
for _, perm := range permFromRoles {
perm.Users = nil
if _, ok := existedPerms[perm.Name]; !ok {
existedPerms[perm.Name] = struct{}{}
permissions = append(permissions, perm)
}
}
return permissions, roles, nil
}
func GetPermissionsByRole(roleId string) ([]*Permission, error) {

View File

@@ -267,7 +267,7 @@ func BatchEnforce(permissionId string, requests *[]CasbinRequest) ([]bool, error
}
func getAllValues(userId string, fn func(enforcer *casbin.Enforcer) []string) []string {
permissions, err := GetPermissionsByUser(userId)
permissions, _, err := GetPermissionsAndRolesByUser(userId)
if err != nil {
panic(err)
}

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

@@ -259,11 +259,22 @@ func GetRolesByUser(userId string) ([]*Role, error) {
return roles, err
}
for i := range roles {
roles[i].Users = nil
allRolesIds := make([]string, 0, len(roles))
for _, role := range roles {
allRolesIds = append(allRolesIds, role.GetId())
}
return roles, nil
allRoles, err := GetAncestorRoles(allRolesIds...)
if err != nil {
return nil, err
}
for i := range allRoles {
allRoles[i].Users = nil
}
return allRoles, nil
}
func roleChangeTrigger(oldName string, newName string) error {
@@ -335,14 +346,22 @@ func GetRolesByNamePrefix(owner string, prefix string) ([]*Role, error) {
return roles, nil
}
func GetAncestorRoles(roleId string) ([]*Role, error) {
// GetAncestorRoles returns a list of roles that contain the given roleIds
func GetAncestorRoles(roleIds ...string) ([]*Role, error) {
var (
result []*Role
result = []*Role{}
roleMap = make(map[string]*Role)
visited = make(map[string]bool)
)
if len(roleIds) == 0 {
return result, nil
}
owner, _ := util.GetOwnerAndNameFromIdNoCheck(roleId)
for _, roleId := range roleIds {
visited[roleId] = true
}
owner, _ := util.GetOwnerAndNameFromIdNoCheck(roleIds[0])
allRoles, err := GetRoles(owner)
if err != nil {
@@ -360,7 +379,7 @@ func GetAncestorRoles(roleId string) ([]*Role, error) {
result = append(result, r)
} else if !ok {
rId := r.GetId()
visited[rId] = containsRole(r, roleId, roleMap, visited)
visited[rId] = containsRole(r, roleMap, visited, roleIds...)
if visited[rId] {
result = append(result, r)
}
@@ -370,19 +389,19 @@ func GetAncestorRoles(roleId string) ([]*Role, error) {
return result, nil
}
// containsRole is a helper function to check if a slice of roles contains a specific roleId
func containsRole(role *Role, roleId string, roleMap map[string]*Role, visited map[string]bool) bool {
// containsRole is a helper function to check if a roles is related to any role in the given list roles
func containsRole(role *Role, roleMap map[string]*Role, visited map[string]bool, roleIds ...string) bool {
if isContain, ok := visited[role.GetId()]; ok {
return isContain
}
for _, subRole := range role.Roles {
if subRole == roleId {
if util.HasString(roleIds, subRole) {
return true
}
r, ok := roleMap[subRole]
if ok && containsRole(r, roleId, roleMap, visited) {
if ok && containsRole(r, roleMap, visited, roleIds...) {
return true
}
}

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

@@ -15,12 +15,10 @@
package object
import (
"bytes"
"fmt"
"strings"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/util"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/xorm-io/core"
@@ -46,6 +44,7 @@ type User struct {
FirstName string `xorm:"varchar(100)" json:"firstName"`
LastName string `xorm:"varchar(100)" json:"lastName"`
Avatar string `xorm:"varchar(500)" json:"avatar"`
AvatarType string `xorm:"varchar(100)" json:"avatarType"`
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
Email string `xorm:"varchar(100) index" json:"email"`
EmailVerified bool `json:"emailVerified"`
@@ -77,6 +76,8 @@ type User struct {
SignupApplication string `xorm:"varchar(100)" json:"signupApplication"`
Hash string `xorm:"varchar(100)" json:"hash"`
PreHash string `xorm:"varchar(100)" json:"preHash"`
AccessKey string `xorm:"varchar(100)" json:"accessKey"`
AccessSecret string `xorm:"varchar(100)" json:"accessSecret"`
CreatedIp string `xorm:"varchar(100)" json:"createdIp"`
LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"`
@@ -165,6 +166,7 @@ type User struct {
Roles []*Role `json:"roles"`
Permissions []*Permission `json:"permissions"`
Groups []string `xorm:"groups varchar(1000)" json:"groups"`
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
SigninWrongTimes int `json:"signinWrongTimes"`
@@ -218,8 +220,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 +264,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 +386,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 +507,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", "access_secret",
"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 +521,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 +529,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 +637,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 +725,11 @@ func DeleteUser(user *User) (bool, error) {
return false, err
}
affected, err = DeleteRelationByUserId(user.Id)
if err != nil {
return false, err
}
return affected != 0, nil
}
@@ -714,12 +776,11 @@ func ExtendUserWithRolesAndPermissions(user *User) (err error) {
return
}
user.Roles, err = GetRolesByUser(user.GetId())
user.Permissions, user.Roles, err = GetPermissionsAndRolesByUser(user.GetId())
if err != nil {
return
return err
}
user.Permissions, err = GetPermissionsByUser(user.GetId())
return
}
@@ -781,46 +842,6 @@ func userChangeTrigger(oldName string, newName string) error {
return session.Commit()
}
func (user *User) refreshAvatar() (bool, error) {
var err error
var fileBuffer *bytes.Buffer
var ext string
// Gravatar + Identicon
if strings.Contains(user.Avatar, "Gravatar") && user.Email != "" {
client := proxy.ProxyHttpClient
has, err := hasGravatar(client, user.Email)
if err != nil {
return false, err
}
if has {
fileBuffer, ext, err = getGravatarFileBuffer(client, user.Email)
if err != nil {
return false, err
}
}
}
if fileBuffer == nil && strings.Contains(user.Avatar, "Identicon") {
fileBuffer, ext, err = getIdenticonFileBuffer(user.Name)
if err != nil {
return false, err
}
}
if fileBuffer != nil {
avatarUrl, err := getPermanentAvatarUrlFromBuffer(user.Owner, user.Name, fileBuffer, ext, true)
if err != nil {
return false, err
}
user.Avatar = avatarUrl
return true, nil
}
return false, nil
}
func (user *User) IsMfaEnabled() bool {
return len(user.MultiFactorAuths) > 0
}
@@ -852,3 +873,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.AccessSecret = util.GenerateId()
return UpdateUser(user.GetId(), user, []string{}, isAdmin)
}

149
object/user_avatar.go Normal file
View File

@@ -0,0 +1,149 @@
// 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 (
"bytes"
"fmt"
"io"
"net/http"
"strings"
"github.com/casdoor/casdoor/proxy"
)
func downloadImage(client *http.Client, url string) (*bytes.Buffer, string, error) {
// Download the image
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, "", err
}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("downloadImage() error for url [%s]: %s\n", url, err.Error())
if strings.Contains(err.Error(), "EOF") {
return nil, "", nil
} else {
return nil, "", err
}
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("downloadImage() error for url [%s]: %s\n", url, resp.Status)
if resp.StatusCode == 404 {
return nil, "", nil
} else {
return nil, "", fmt.Errorf("failed to download gravatar image: %s", resp.Status)
}
}
// Get the content type and determine the file extension
contentType := resp.Header.Get("Content-Type")
fileExtension := ""
if strings.Contains(contentType, "text/html") {
fileExtension = ".html"
} else {
switch contentType {
case "image/jpeg":
fileExtension = ".jpg"
case "image/png":
fileExtension = ".png"
case "image/gif":
fileExtension = ".gif"
case "image/vnd.microsoft.icon":
fileExtension = ".ico"
case "image/x-icon":
fileExtension = ".ico"
default:
return nil, "", fmt.Errorf("unsupported content type: %s", contentType)
}
}
// Save the image to a bytes.Buffer
buffer := &bytes.Buffer{}
_, err = io.Copy(buffer, resp.Body)
if err != nil {
return nil, "", err
}
return buffer, fileExtension, nil
}
func (user *User) refreshAvatar() (bool, error) {
var err error
var fileBuffer *bytes.Buffer
var ext string
// Gravatar
if (user.AvatarType == "Auto" || user.AvatarType == "Gravatar") && user.Email != "" {
client := proxy.ProxyHttpClient
has, err := hasGravatar(client, user.Email)
if err != nil {
return false, err
}
if has {
fileBuffer, ext, err = getGravatarFileBuffer(client, user.Email)
if err != nil {
return false, err
}
if fileBuffer != nil {
user.AvatarType = "Gravatar"
}
}
}
// Favicon
if fileBuffer == nil && (user.AvatarType == "Auto" || user.AvatarType == "Favicon") {
client := proxy.ProxyHttpClient
fileBuffer, ext, err = getFaviconFileBuffer(client, user.Email)
if err != nil {
return false, err
}
if fileBuffer != nil {
user.AvatarType = "Favicon"
}
}
// Identicon
if fileBuffer == nil && (user.AvatarType == "Auto" || user.AvatarType == "Identicon") {
fileBuffer, ext, err = getIdenticonFileBuffer(user.Name)
if err != nil {
return false, err
}
if fileBuffer != nil {
user.AvatarType = "Identicon"
}
}
if fileBuffer != nil {
avatarUrl, err := getPermanentAvatarUrlFromBuffer(user.Owner, user.Name, fileBuffer, ext, true)
if err != nil {
return false, err
}
user.Avatar = avatarUrl
return true, nil
}
return false, nil
}

View File

@@ -0,0 +1,220 @@
// 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 (
"bytes"
"fmt"
"net/http"
"net/url"
"strings"
"golang.org/x/net/html"
)
type Link struct {
Rel string
Sizes string
Href string
}
func GetFaviconUrl(htmlStr string) (string, error) {
doc, err := html.Parse(strings.NewReader(htmlStr))
if err != nil {
return "", err
}
var links []Link
findLinks(doc, &links)
if len(links) == 0 {
return "", fmt.Errorf("no Favicon links found")
}
chosenLink := chooseFaviconLink(links)
if chosenLink == nil {
return "", fmt.Errorf("unable to determine favicon URL")
}
return chosenLink.Href, nil
}
func findLinks(n *html.Node, links *[]Link) {
if n.Type == html.ElementNode && n.Data == "link" {
link := parseLink(n)
if link != nil {
*links = append(*links, *link)
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
findLinks(c, links)
}
}
func parseLink(n *html.Node) *Link {
var link Link
for _, attr := range n.Attr {
switch attr.Key {
case "rel":
link.Rel = attr.Val
case "sizes":
link.Sizes = attr.Val
case "href":
link.Href = attr.Val
}
}
if link.Href != "" {
return &link
}
return nil
}
func chooseFaviconLink(links []Link) *Link {
var appleTouchLinks []Link
var shortcutLinks []Link
var iconLinks []Link
for _, link := range links {
switch link.Rel {
case "apple-touch-icon":
appleTouchLinks = append(appleTouchLinks, link)
case "shortcut icon":
shortcutLinks = append(shortcutLinks, link)
case "icon":
iconLinks = append(iconLinks, link)
}
}
if len(appleTouchLinks) > 0 {
return chooseFaviconLinkBySizes(appleTouchLinks)
}
if len(shortcutLinks) > 0 {
return chooseFaviconLinkBySizes(shortcutLinks)
}
if len(iconLinks) > 0 {
return chooseFaviconLinkBySizes(iconLinks)
}
return nil
}
func chooseFaviconLinkBySizes(links []Link) *Link {
if len(links) == 1 {
return &links[0]
}
var chosenLink *Link
for _, link := range links {
if chosenLink == nil || compareSizes(link.Sizes, chosenLink.Sizes) > 0 {
chosenLink = &link
}
}
return chosenLink
}
func compareSizes(sizes1, sizes2 string) int {
if sizes1 == sizes2 {
return 0
}
size1 := parseSize(sizes1)
size2 := parseSize(sizes2)
if size1 == nil {
return -1
}
if size2 == nil {
return 1
}
if size1[0] == size2[0] {
return size1[1] - size2[1]
}
return size1[0] - size2[0]
}
func parseSize(sizes string) []int {
size := strings.Split(sizes, "x")
if len(size) != 2 {
return nil
}
var result []int
for _, s := range size {
val := strings.TrimSpace(s)
if len(val) > 0 {
num := 0
for i := 0; i < len(val); i++ {
if val[i] >= '0' && val[i] <= '9' {
num = num*10 + int(val[i]-'0')
} else {
break
}
}
result = append(result, num)
}
}
if len(result) == 2 {
return result
}
return nil
}
func getFaviconFileBuffer(client *http.Client, email string) (*bytes.Buffer, string, error) {
tokens := strings.Split(email, "@")
domain := tokens[1]
if domain == "gmail.com" || domain == "163.com" || domain == "qq.com" {
return nil, "", nil
}
htmlUrl := fmt.Sprintf("https://%s", domain)
buffer, _, err := downloadImage(client, htmlUrl)
if err != nil {
return nil, "", err
}
faviconUrl := ""
if buffer != nil {
faviconUrl, err = GetFaviconUrl(buffer.String())
if err != nil {
return nil, "", err
}
if !strings.HasPrefix(faviconUrl, "http") {
faviconUrl, err = url.JoinPath(htmlUrl, faviconUrl)
if err != nil {
return nil, "", err
}
}
}
if faviconUrl == "" {
faviconUrl = fmt.Sprintf("https://%s/favicon.ico", domain)
}
return downloadImage(client, faviconUrl)
}

View File

@@ -0,0 +1,76 @@
// 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 (
"bytes"
"crypto/md5"
"fmt"
"io"
"net/http"
"strings"
)
func hasGravatar(client *http.Client, email string) (bool, error) {
// Clean and lowercase the email
email = strings.TrimSpace(strings.ToLower(email))
// Generate MD5 hash of the email
hash := md5.New()
io.WriteString(hash, email)
hashedEmail := fmt.Sprintf("%x", hash.Sum(nil))
// Create Gravatar URL with d=404 parameter
gravatarURL := fmt.Sprintf("https://www.gravatar.com/avatar/%s?d=404", hashedEmail)
// Send a request to Gravatar
req, err := http.NewRequest("GET", gravatarURL, nil)
if err != nil {
return false, err
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
// Check if the user has a custom Gravatar image
if resp.StatusCode == http.StatusOK {
return true, nil
} else if resp.StatusCode == http.StatusNotFound {
return false, nil
} else {
return false, fmt.Errorf("failed to fetch gravatar image: %s", resp.Status)
}
}
func getGravatarFileBuffer(client *http.Client, email string) (*bytes.Buffer, string, error) {
// Clean and lowercase the email
email = strings.TrimSpace(strings.ToLower(email))
// Generate MD5 hash of the email
hash := md5.New()
_, err := io.WriteString(hash, email)
if err != nil {
return nil, "", err
}
hashedEmail := fmt.Sprintf("%x", hash.Sum(nil))
// Create Gravatar URL
gravatarUrl := fmt.Sprintf("https://www.gravatar.com/avatar/%s", hashedEmail)
return downloadImage(client, gravatarUrl)
}

View File

@@ -0,0 +1,80 @@
// 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 (
"bytes"
"crypto/md5"
"fmt"
"image"
"image/color"
"image/png"
"io"
"strings"
"github.com/fogleman/gg"
)
func getColor(data []byte) color.RGBA {
r := int(data[0]) % 256
g := int(data[1]) % 256
b := int(data[2]) % 256
return color.RGBA{uint8(r), uint8(g), uint8(b), 255}
}
func getIdenticonFileBuffer(username string) (*bytes.Buffer, string, error) {
username = strings.TrimSpace(strings.ToLower(username))
hash := md5.New()
io.WriteString(hash, username)
hashedUsername := hash.Sum(nil)
// Define the size of the image
const imageSize = 420
const cellSize = imageSize / 7
// Create a new image
img := image.NewRGBA(image.Rect(0, 0, imageSize, imageSize))
// Create a context
dc := gg.NewContextForRGBA(img)
// Set a background color
dc.SetColor(color.RGBA{240, 240, 240, 255})
dc.Clear()
// Get avatar color
avatarColor := getColor(hashedUsername)
// Draw cells
for i := 0; i < 7; i++ {
for j := 0; j < 7; j++ {
if (hashedUsername[i] >> uint(j) & 1) == 1 {
dc.SetColor(avatarColor)
dc.DrawRectangle(float64(j*cellSize), float64(i*cellSize), float64(cellSize), float64(cellSize))
dc.Fill()
}
}
}
// Save image to a bytes.Buffer
buffer := &bytes.Buffer{}
err := png.Encode(buffer, img)
if err != nil {
return nil, "", fmt.Errorf("failed to save image: %w", err)
}
return buffer, ".png", nil
}

View File

@@ -16,7 +16,6 @@ package object
import (
"fmt"
"strings"
"testing"
"github.com/casdoor/casdoor/proxy"
@@ -58,7 +57,11 @@ func TestUpdateAvatars(t *testing.T) {
}
for _, user := range users {
if strings.HasPrefix(user.Avatar, "http") {
//if strings.HasPrefix(user.Avatar, "http") {
// continue
//}
if user.AvatarType != "Auto" {
continue
}
@@ -69,7 +72,7 @@ func TestUpdateAvatars(t *testing.T) {
if updated {
user.PermanentAvatar = "*"
_, err = UpdateUser(user.GetId(), user, []string{"avatar"}, true)
_, err = UpdateUser(user.GetId(), user, []string{"avatar", "avatar_type"}, true)
if err != nil {
panic(err)
}

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"`
GroupName string `xorm:"varchar(100) notnull pk" json:"groupName"`
CreatedTime string `xorm:"created" json:"createdTime"`
UpdatedTime string `xorm:"updated" json:"updatedTime"`
}
func updateUserGroupRelation(session *xorm.Session, user *User) (int64, error) {
physicalGroupCount, err := session.In("name", user.Groups).Count(Group{Type: "Physical"})
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("name", 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, GroupName: group.Name})
}
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(userId, groupId string) (int64, error) {
affected, err := adapter.Engine.ID(core.PK{userId, groupId}).Delete(&UserGroupRelation{})
return affected, err
}
func DeleteRelationByUserId(id string) (int64, error) {
affected, err := adapter.Engine.Delete(&UserGroupRelation{UserId: id})
return affected, err
}
func GetGroupUserCount(groupName string, field, value string) (int64, error) {
group, err := getGroupByName(groupName)
if group == nil || err != nil {
return 0, err
}
if field == "" && value == "" {
return adapter.Engine.Count(UserGroupRelation{GroupName: group.Name})
} else {
return adapter.Engine.Table("user").
Join("INNER", []string{"user_group_relation", "r"}, "user.id = r.user_id").
Where("r.group_name = ?", group.Name).
And(fmt.Sprintf("user.%s LIKE ?", util.CamelToSnakeCase(field)), "%"+value+"%").
Count()
}
}
func GetPaginationGroupUsers(groupName string, offset, limit int, field, value, sortField, sortOrder string) ([]*User, error) {
group, err := getGroupByName(groupName)
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_name = ?", group.Name)
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(groupName string) ([]*User, error) {
group, err := getGroupByName(groupName)
if group == nil || err != nil {
return []*User{}, err
}
users := []*User{}
err = adapter.Engine.Table("user").
Join("INNER", []string{"user_group_relation", "r"}, "user.id = r.user_id").
Where("r.group_name = ?", group.Name).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

@@ -15,6 +15,7 @@
package proxy
import (
"crypto/tls"
"fmt"
"net"
"net/http"
@@ -71,7 +72,7 @@ func getProxyHttpClient() *http.Client {
panic(err)
}
tr := &http.Transport{Dial: dialer.Dial}
tr := &http.Transport{Dial: dialer.Dial, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
return &http.Client{
Transport: tr,
}

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"`
AccessSecret string `json:"accessSecret"`
}
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")
accessSecret := ctx.Input.Query("accessSecret")
return accessKey, accessSecret
} else {
body := ctx.Input.RequestBody
if len(body) == 0 {
return ctx.Request.Form.Get("accessKey"), ctx.Request.Form.Get("accessSecret")
}
var obj Object
err := json.Unmarshal(body, &obj)
if err != nil {
return "", ""
}
return obj.AccessKey, obj.AccessSecret
}
}
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,19 @@ func getUsernameByClientIdSecret(ctx *context.Context) string {
return fmt.Sprintf("app/%s", application.Name)
}
func getUsernameByKeys(ctx *context.Context) string {
accessKey, accessSecret := getKeys(ctx)
user, err := object.GetUserByAccessKey(accessKey)
if err != nil {
panic(err)
}
if user != nil && accessSecret == user.AccessSecret {
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

@@ -29,7 +29,7 @@ func NewMinIOS3StorageProvider(clientId string, clientSecret string, region stri
Endpoint: endpoint,
S3Endpoint: endpoint,
ACL: awss3.BucketCannedACLPublicRead,
S3ForcePathStyle: false,
S3ForcePathStyle: true,
})
return sp

View File

@@ -278,3 +278,13 @@ func GetEndPoint(endpoint string) string {
}
return endpoint
}
// HasString reports if slice has input string.
func HasString(strs []string, str string) bool {
for _, i := range strs {
if i == str {
return true
}
}
return false
}

View File

@@ -32,7 +32,7 @@ class AdapterEditPage extends React.Component {
super(props);
this.state = {
classes: props,
owner: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
adapterName: props.match.params.adapterName,
adapter: null,
organizations: [],
@@ -47,7 +47,7 @@ class AdapterEditPage extends React.Component {
}
getAdapter() {
AdapterBackend.getAdapter("admin", this.state.adapterName)
AdapterBackend.getAdapter(this.state.organizationName, this.state.adapterName)
.then((res) => {
if (res.status === "ok") {
if (res.data === null) {
@@ -59,13 +59,13 @@ class AdapterEditPage extends React.Component {
adapter: res.data,
});
this.getModels(this.state.owner);
this.getModels(this.state.organizationName);
}
});
}
getOrganizations() {
OrganizationBackend.getOrganizations(this.state.organizationName)
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
@@ -114,9 +114,8 @@ class AdapterEditPage extends React.Component {
{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.adapter.organization} onChange={(value => {
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.adapter.owner} onChange={(value => {
this.getModels(value);
this.updateAdapterField("organization", value);
this.updateAdapterField("owner", value);
})}>
{
@@ -253,7 +252,7 @@ class AdapterEditPage extends React.Component {
{Setting.getLabel(i18next.t("adapter:Policies"), i18next.t("adapter:Policies - Tooltip"))} :
</Col>
<Col span={22}>
<PolicyTable owner={this.state.owner} name={this.state.adapterName} mode={this.state.mode} />
<PolicyTable owner={this.state.organizationName} name={this.state.adapterName} mode={this.state.mode} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
@@ -272,7 +271,7 @@ class AdapterEditPage extends React.Component {
submitAdapterEdit(willExist) {
const adapter = Setting.deepCopy(this.state.adapter);
AdapterBackend.updateAdapter(this.state.owner, this.state.adapterName, adapter)
AdapterBackend.updateAdapter(this.state.organizationName, this.state.adapterName, adapter)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
@@ -283,7 +282,7 @@ class AdapterEditPage extends React.Component {
if (willExist) {
this.props.history.push("/adapters");
} else {
this.props.history.push(`/adapters/${this.state.owner}/${this.state.adapter.name}`);
this.props.history.push(`/adapters/${this.state.organizationName}/${this.state.adapter.name}`);
}
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);

View File

@@ -26,10 +26,9 @@ class AdapterListPage extends BaseListPage {
newAdapter() {
const randomName = Setting.getRandomName();
return {
owner: "admin",
owner: this.props.account.owner,
name: `adapter_${randomName}`,
createdTime: moment().format(),
organization: this.props.account.owner,
type: "Database",
host: "localhost",
port: 3306,
@@ -47,7 +46,7 @@ class AdapterListPage extends BaseListPage {
AdapterBackend.addAdapter(newAdapter)
.then((res) => {
if (res.status === "ok") {
this.props.history.push({pathname: `/adapters/${newAdapter.organization}/${newAdapter.name}`, mode: "add"});
this.props.history.push({pathname: `/adapters/${newAdapter.owner}/${newAdapter.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
@@ -96,11 +95,11 @@ class AdapterListPage extends BaseListPage {
},
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("organization"),
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
@@ -247,7 +246,7 @@ class AdapterListPage extends BaseListPage {
value = params.type;
}
this.setState({loading: true});
AdapterBackend.getAdapters("admin", Setting.isAdminUser(this.props.account) ? "" : this.props.account.owner, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
AdapterBackend.getAdapters(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

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

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

@@ -0,0 +1,263 @@
// 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") {
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.name !== this.state.group.name);
const organization = this.state.organizations.find((organization) => organization.name === this.state.group.owner);
if (organization !== undefined) {
groups.push({name: organization.name, displayName: organization.displayName});
}
return groups.map((group) => ({label: group.displayName, value: group.name}));
}
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 disabled={true} 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.name === 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;

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

@@ -0,0 +1,315 @@
// 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,
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,
});
} else {
Setting.showMessage("error", res.msg);
}
});
}
setTreeTitle(treeData) {
const haveChildren = Array.isArray(treeData.children) && treeData.children.length > 0;
const isSelected = this.state.groupName === treeData.key;
return {
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,
});
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.groupName,
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: 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}
{...this.props}
/>
</Col>
</Row>
</div>
);
}
}
export default GroupTreePage;

View File

@@ -49,15 +49,20 @@ class OrganizationEditPage extends React.Component {
getOrganization() {
OrganizationBackend.getOrganization("admin", this.state.organizationName)
.then((organization) => {
if (organization === null) {
this.props.history.push("/404");
return;
}
.then((res) => {
if (res.status === "ok") {
const organization = res.data;
if (organization === null) {
this.props.history.push("/404");
return;
}
this.setState({
organization: organization,
});
this.setState({
organization: organization,
});
} else {
Setting.showMessage("error", res.msg);
}
});
}
@@ -188,6 +193,29 @@ class OrganizationEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Password complexity options"), i18next.t("general:Password complexity options - Tooltip"))} :
</Col>
<Col span={22} >
<Select
virtual={false}
style={{width: "100%"}}
mode="multiple"
value={this.state.organization.passwordOptions}
onChange={(value => {
this.updateOrganizationField("passwordOptions", value);
})}
options={[
{value: "AtLeast6", name: i18next.t("user:The password must have at least 6 characters")},
{value: "AtLeast8", name: i18next.t("user:The password must have at least 8 characters")},
{value: "Aa123", name: i18next.t("user:The password must contain at least one uppercase letter, one lowercase letter and one digit")},
{value: "SpecialChar", name: i18next.t("user:The password must contain at least one special character")},
{value: "NoRepeat", name: i18next.t("user:The password must not contain any repeated characters")},
].map((item) => Setting.getOption(item.name, item.value))}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Supported country codes"), i18next.t("general:Supported country codes - Tooltip"))} :

View File

@@ -34,6 +34,7 @@ class OrganizationListPage extends BaseListPage {
favicon: `${Setting.StaticBaseUrl}/img/favicon.png`,
passwordType: "plain",
PasswordSalt: "",
passwordOptions: [],
countryCodes: ["CN"],
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
defaultApplication: "",
@@ -60,8 +61,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 +224,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 +257,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

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

@@ -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,6 +65,12 @@ 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) => {
@@ -83,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) => {
@@ -107,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");
@@ -212,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;
@@ -249,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"}} >
@@ -259,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>)
@@ -267,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.name))
.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.name} value={group.name}>
<Space>
{group.type === "Physical" ? <UsergroupAddOutlined /> : <HolderOutlined />}
{group.displayName}
</Space>
</Option>)
}
</Select>
</Col>
</Row>
);
} else if (accountItem.name === "ID") {
return (
<Row style={{marginTop: "20px"}} >
@@ -644,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:Access secret"), i18next.t("general:Access secret - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.user.accessSecret} disabled={true} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<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"}} >
@@ -925,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.groupName ? [this.props.groupName] : [],
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.groupName;
UserBackend.removeUserFromGroup({groupName: 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.groupName !== 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.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

@@ -24,6 +24,8 @@ import * as UserBackend from "../backend/UserBackend";
import {CheckCircleOutlined, KeyOutlined, LockOutlined, SolutionOutlined, UserOutlined} from "@ant-design/icons";
import CustomGithubCorner from "../common/CustomGithubCorner";
import {withRouter} from "react-router-dom";
import * as PasswordChecker from "../common/PasswordChecker";
const {Option} = Select;
class ForgetPage extends React.Component {
@@ -45,7 +47,6 @@ class ForgetPage extends React.Component {
this.form = React.createRef();
}
componentDidMount() {
if (this.getApplicationObj() === undefined) {
if (this.state.applicationName !== undefined) {
@@ -66,7 +67,6 @@ class ForgetPage extends React.Component {
this.onUpdateApplication(application);
});
}
getApplicationObj() {
return this.props.application;
}
@@ -378,7 +378,15 @@ class ForgetPage extends React.Component {
rules={[
{
required: true,
message: i18next.t("login:Please input your password!"),
validateTrigger: "onChange",
validator: (rule, value) => {
const errorMsg = PasswordChecker.checkPasswordComplexity(value, application.organizationObj.passwordOptions);
if (errorMsg === "") {
return Promise.resolve();
} else {
return Promise.reject(errorMsg);
}
},
},
]}
hasFeedback

View File

@@ -28,6 +28,7 @@ import CustomGithubCorner from "../common/CustomGithubCorner";
import LanguageSelect from "../common/select/LanguageSelect";
import {withRouter} from "react-router-dom";
import {CountryCodeSelect} from "../common/select/CountryCodeSelect";
import * as PasswordChecker from "../common/PasswordChecker";
const formItemLayout = {
labelCol: {
@@ -458,8 +459,15 @@ class SignupPage extends React.Component {
rules={[
{
required: required,
min: 6,
message: i18next.t("login:Please input your password, at least 6 characters!"),
validateTrigger: "onChange",
validator: (rule, value) => {
const errorMsg = PasswordChecker.checkPasswordComplexity(value, application.organizationObj.passwordOptions);
if (errorMsg === "") {
return Promise.resolve();
} else {
return Promise.reject(errorMsg);
}
},
},
]}
hasFeedback

View File

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

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 = "", groupName = "") {
return fetch(`${Setting.ServerUrl}/api/get-users?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}&groupName=${groupName}`, {
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, groupName}) {
const formData = new FormData();
formData.append("owner", owner);
formData.append("name", name);
formData.append("groupName", groupName);
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

@@ -0,0 +1,82 @@
// 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 i18next from "i18next";
function isValidOption_AtLeast6(password) {
if (password.length < 6) {
return i18next.t("user:The password must have at least 6 characters");
}
return "";
}
function isValidOption_AtLeast8(password) {
if (password.length < 8) {
return i18next.t("user:The password must have at least 8 characters");
}
return "";
}
function isValidOption_Aa123(password) {
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).+$/;
if (!regex.test(password)) {
return i18next.t("user:The password must contain at least one uppercase letter, one lowercase letter and one digit");
}
return "";
}
function isValidOption_SpecialChar(password) {
const regex = /^(?=.*[!@#$%^&*]).+$/;
if (!regex.test(password)) {
return i18next.t("user:The password must contain at least one special character");
}
return "";
}
function isValidOption_NoRepeat(password) {
const regex = /(.)\1+/;
if (regex.test(password)) {
return i18next.t("user:The password must not contain any repeated characters");
}
return "";
}
export function checkPasswordComplexity(password, options) {
if (password.length === 0) {
return i18next.t("login:Please input your password!");
}
if (!options || options.length === 0) {
options = ["AtLeast6"];
}
const checkers = {
AtLeast6: isValidOption_AtLeast6,
AtLeast8: isValidOption_AtLeast8,
Aa123: isValidOption_Aa123,
SpecialChar: isValidOption_SpecialChar,
NoRepeat: isValidOption_NoRepeat,
};
for (const option of options) {
const checkerFunc = checkers[option];
if (checkerFunc) {
const errorMsg = checkerFunc(password);
if (errorMsg !== "") {
return errorMsg;
}
}
}
return "";
}

View File

@@ -17,6 +17,8 @@ import i18next from "i18next";
import React from "react";
import * as UserBackend from "../../backend/UserBackend";
import * as Setting from "../../Setting";
import * as OrganizationBackend from "../../backend/OrganizationBackend";
import * as PasswordChecker from "../PasswordChecker";
export const PasswordModal = (props) => {
const [visible, setVisible] = React.useState(false);
@@ -27,6 +29,26 @@ export const PasswordModal = (props) => {
const {user} = props;
const {account} = props;
const [passwordOptions, setPasswordOptions] = React.useState([]);
const [newPasswordValid, setNewPasswordValid] = React.useState(false);
const [rePasswordValid, setRePasswordValid] = React.useState(false);
const [newPasswordErrorMessage, setNewPasswordErrorMessage] = React.useState("");
const [rePasswordErrorMessage, setRePasswordErrorMessage] = React.useState("");
React.useEffect(() => {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
const organizations = (res.msg === undefined) ? res : [];
// Find the user's corresponding organization
const organization = organizations.find((org) => org.name === user.owner);
if (organization) {
setPasswordOptions(organization.passwordOptions);
}
})
.catch((error) => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}, [user.owner]);
const showModal = () => {
setVisible(true);
};
@@ -34,6 +56,24 @@ export const PasswordModal = (props) => {
const handleCancel = () => {
setVisible(false);
};
const handleNewPassword = (value) => {
setNewPassword(value);
const errorMessage = PasswordChecker.checkPasswordComplexity(value, passwordOptions);
setNewPasswordValid(errorMessage === "");
setNewPasswordErrorMessage(errorMessage);
};
const handleRePassword = (value) => {
setRePassword(value);
if (value !== newPassword) {
setRePasswordErrorMessage(i18next.t("signup:Your confirmed password is inconsistent with the password!"));
setRePasswordValid(false);
} else {
setRePasswordValid(true);
}
};
const handleOk = () => {
if (newPassword === "" || rePassword === "") {
@@ -45,12 +85,44 @@ export const PasswordModal = (props) => {
return;
}
setConfirmLoading(true);
UserBackend.setPassword(user.owner, user.name, oldPassword, newPassword).then((res) => {
setConfirmLoading(false);
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("user:Password set successfully"));
setVisible(false);
} else {Setting.showMessage("error", i18next.t(`user:${res.msg}`));}
OrganizationBackend.getOrganizations("admin").then((res) => {
const organizations = (res.msg === undefined) ? res : [];
// find the users' corresponding organization
let organization = null;
for (let i = 0; i < organizations.length; i++) {
if (organizations[i].name === user.owner) {
organization = organizations[i];
break;
}
}
if (organization === null) {
Setting.showMessage("error", "organization is null");
setConfirmLoading(false);
return;
}
const errorMsg = PasswordChecker.checkPasswordComplexity(newPassword, organization.passwordOptions);
if (errorMsg !== "") {
Setting.showMessage("error", errorMsg);
setConfirmLoading(false);
return;
}
UserBackend.setPassword(user.owner, user.name, oldPassword, newPassword)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("user:Password set successfully"));
setVisible(false);
} else {
Setting.showMessage("error", i18next.t(`user:${res.msg}`));
}
})
.finally(() => {
setConfirmLoading(false);
});
});
};
@@ -79,11 +151,23 @@ export const PasswordModal = (props) => {
</Row>
) : null}
<Row style={{width: "100%", marginBottom: "20px"}}>
<Input.Password addonBefore={i18next.t("user:New Password")} placeholder={i18next.t("user:input password")} onChange={(e) => setNewPassword(e.target.value)} />
<Input.Password
addonBefore={i18next.t("user:New Password")}
placeholder={i18next.t("user:input password")}
onChange={(e) => {handleNewPassword(e.target.value);}}
status={(!newPasswordValid && newPasswordErrorMessage) ? "error" : undefined}
/>
</Row>
{!newPasswordValid && newPasswordErrorMessage && <div style={{color: "red", marginTop: "-20px"}}>{newPasswordErrorMessage}</div>}
<Row style={{width: "100%", marginBottom: "20px"}}>
<Input.Password addonBefore={i18next.t("user:Re-enter New")} placeholder={i18next.t("user:input password")} onChange={(e) => setRePassword(e.target.value)} />
<Input.Password
addonBefore={i18next.t("user:Re-enter New")}
placeholder={i18next.t("user:input password")}
onChange={(e) => handleRePassword(e.target.value)}
status={(!rePasswordValid && rePasswordErrorMessage) ? "error" : undefined}
/>
</Row>
{!rePasswordValid && rePasswordErrorMessage && <div style={{color: "red", marginTop: "-20px"}}>{rePasswordErrorMessage}</div>}
</Col>
</Modal>
</Row>

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

@@ -162,6 +162,12 @@
"Verify": "überprüfen"
},
"general": {
"API key": "API key",
"API key - Tooltip": "API key - Tooltip",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Access secret": "Access secret",
"Access secret - Tooltip": "Access secret - Tooltip",
"Action": "Aktion",
"Adapter": "Adapter",
"Adapter - Tooltip": "Tabellenname des Policy Stores",
@@ -188,6 +194,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 +216,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 +226,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",
@@ -251,6 +261,8 @@
"Organizations": "Organisationen",
"Password": "Passwort",
"Password - Tooltip": "Stellen Sie sicher, dass das Passwort korrekt ist",
"Password complexity options": "Password complexity options",
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
"Password salt": "Passwort-Salt",
"Password salt - Tooltip": "Zufälliger Parameter, der für die Verschlüsselung von Passwörtern verwendet wird",
"Password type": "Passworttyp",
@@ -299,10 +311,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 +325,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 +339,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",
@@ -369,7 +396,6 @@
"Please input your code!": "Bitte geben Sie Ihren Code ein!",
"Please input your organization name!": "Please input your organization name!",
"Please input your password!": "Bitte geben Sie Ihr Passwort ein!",
"Please input your password, at least 6 characters!": "Bitte geben Sie Ihr Passwort ein, es muss mindestens 6 Zeichen lang sein!",
"Please select an organization": "Please select an organization",
"Please select an organization to sign in": "Please select an organization to sign in",
"Please type an organization to sign in": "Please type an organization to sign in",
@@ -539,8 +565,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",
@@ -915,6 +939,11 @@
"Set password...": "Passwort festlegen...",
"Tag": "Tag",
"Tag - Tooltip": "Tags des Benutzers",
"The password must contain at least one special character": "The password must contain at least one special character",
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
"The password must have at least 6 characters": "The password must have at least 6 characters",
"The password must have at least 8 characters": "The password must have at least 8 characters",
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
"Title": "Titel",
"Title - Tooltip": "Position in der Zugehörigkeit",
"Two passwords you typed do not match.": "Zwei von Ihnen eingegebene Passwörter stimmen nicht überein.",

View File

@@ -162,6 +162,12 @@
"Verify": "Verify"
},
"general": {
"API key": "API key",
"API key - Tooltip": "API key - Tooltip",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Access secret": "Access secret",
"Access secret - Tooltip": "Access secret - Tooltip",
"Action": "Action",
"Adapter": "Adapter",
"Adapter - Tooltip": "Table name of the policy store",
@@ -188,6 +194,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 +216,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 +226,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",
@@ -251,6 +261,8 @@
"Organizations": "Organizations",
"Password": "Password",
"Password - Tooltip": "Make sure the password is correct",
"Password complexity options": "Password complexity options",
"Password complexity options - Tooltip": "Different combinations of password complexity options",
"Password salt": "Password salt",
"Password salt - Tooltip": "Random parameter used for password encryption",
"Password type": "Password type",
@@ -299,10 +311,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 +325,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 +339,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",
@@ -369,7 +396,6 @@
"Please input your code!": "Please input your code!",
"Please input your organization name!": "Please input your organization name!",
"Please input your password!": "Please input your password!",
"Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!",
"Please select an organization": "Please select an organization",
"Please select an organization to sign in": "Please select an organization to sign in",
"Please type an organization to sign in": "Please type an organization to sign in",
@@ -539,8 +565,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",
@@ -915,6 +939,11 @@
"Set password...": "Set password...",
"Tag": "Tag",
"Tag - Tooltip": "Tag of the user",
"The password must contain at least one special character": "The password must contain at least one special character",
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
"The password must have at least 6 characters": "The password must have at least 6 characters",
"The password must have at least 8 characters": "The password must have at least 8 characters",
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
"Title": "Title",
"Title - Tooltip": "Position in the affiliation",
"Two passwords you typed do not match.": "Two passwords you typed do not match.",

View File

@@ -162,6 +162,12 @@
"Verify": "Verificar"
},
"general": {
"API key": "API key",
"API key - Tooltip": "API key - Tooltip",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Access secret": "Access secret",
"Access secret - Tooltip": "Access secret - Tooltip",
"Action": "Acción",
"Adapter": "Adaptador",
"Adapter - Tooltip": "Nombre de la tabla de la tienda de políticas",
@@ -188,6 +194,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 +216,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 +226,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",
@@ -251,6 +261,8 @@
"Organizations": "Organizaciones",
"Password": "Contraseña",
"Password - Tooltip": "Asegúrate de que la contraseña sea correcta",
"Password complexity options": "Password complexity options",
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
"Password salt": "Sal de contraseña",
"Password salt - Tooltip": "Parámetro aleatorio utilizado para la encriptación de contraseñas",
"Password type": "Tipo de contraseña",
@@ -299,10 +311,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 +325,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 +339,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",
@@ -369,7 +396,6 @@
"Please input your code!": "¡Por favor ingrese su código!",
"Please input your organization name!": "Please input your organization name!",
"Please input your password!": "¡Ingrese su contraseña, por favor!",
"Please input your password, at least 6 characters!": "Por favor ingrese su contraseña, ¡de al menos 6 caracteres!",
"Please select an organization": "Please select an organization",
"Please select an organization to sign in": "Please select an organization to sign in",
"Please type an organization to sign in": "Please type an organization to sign in",
@@ -539,8 +565,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",
@@ -915,6 +939,11 @@
"Set password...": "Establecer contraseña...",
"Tag": "Etiqueta",
"Tag - Tooltip": "Etiqueta del usuario",
"The password must contain at least one special character": "The password must contain at least one special character",
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
"The password must have at least 6 characters": "The password must have at least 6 characters",
"The password must have at least 8 characters": "The password must have at least 8 characters",
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
"Title": "Título",
"Title - Tooltip": "Posición en la afiliación",
"Two passwords you typed do not match.": "Dos contraseñas que has escrito no coinciden.",

View File

@@ -162,6 +162,12 @@
"Verify": "Vérifier"
},
"general": {
"API key": "API key",
"API key - Tooltip": "API key - Tooltip",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Access secret": "Access secret",
"Access secret - Tooltip": "Access secret - Tooltip",
"Action": "Action",
"Adapter": "S'adapter",
"Adapter - Tooltip": "Nom de la table du magasin de politique",
@@ -188,6 +194,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 +216,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 +226,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é",
@@ -251,6 +261,8 @@
"Organizations": "Organisations",
"Password": "Mot de passe",
"Password - Tooltip": "Assurez-vous que le mot de passe est correct",
"Password complexity options": "Password complexity options",
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
"Password salt": "Sel de mot de passe",
"Password salt - Tooltip": "Paramètre aléatoire utilisé pour le cryptage de mot de passe",
"Password type": "Type de mot de passe",
@@ -299,10 +311,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 +325,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 +339,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",
@@ -369,7 +396,6 @@
"Please input your code!": "Veuillez entrer votre code !",
"Please input your organization name!": "Please input your organization name!",
"Please input your password!": "Veuillez entrer votre mot de passe !",
"Please input your password, at least 6 characters!": "Veuillez entrer votre mot de passe, au moins 6 caractères!",
"Please select an organization": "Please select an organization",
"Please select an organization to sign in": "Please select an organization to sign in",
"Please type an organization to sign in": "Please type an organization to sign in",
@@ -539,8 +565,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",
@@ -915,6 +939,11 @@
"Set password...": "Définir le mot de passe...",
"Tag": "Étiquette",
"Tag - Tooltip": "Tag de l'utilisateur",
"The password must contain at least one special character": "The password must contain at least one special character",
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
"The password must have at least 6 characters": "The password must have at least 6 characters",
"The password must have at least 8 characters": "The password must have at least 8 characters",
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
"Title": "Titre",
"Title - Tooltip": "Position dans l'affiliation",
"Two passwords you typed do not match.": "Deux mots de passe que vous avez tapés ne correspondent pas.",

View File

@@ -162,6 +162,12 @@
"Verify": "Memverifikasi"
},
"general": {
"API key": "API key",
"API key - Tooltip": "API key - Tooltip",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Access secret": "Access secret",
"Access secret - Tooltip": "Access secret - Tooltip",
"Action": "Aksi",
"Adapter": "Adapter",
"Adapter - Tooltip": "Nama tabel dari penyimpanan kebijakan",
@@ -188,6 +194,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 +216,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 +226,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",
@@ -251,6 +261,8 @@
"Organizations": "Organisasi",
"Password": "Kata sandi",
"Password - Tooltip": "Pastikan kata sandi yang benar",
"Password complexity options": "Password complexity options",
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
"Password salt": "Garam sandi",
"Password salt - Tooltip": "Parameter acak yang digunakan untuk enkripsi kata sandi",
"Password type": "Jenis kata sandi",
@@ -299,10 +311,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 +325,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 +339,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",
@@ -369,7 +396,6 @@
"Please input your code!": "Silakan masukkan kode Anda!",
"Please input your organization name!": "Please input your organization name!",
"Please input your password!": "Masukkan kata sandi Anda!",
"Please input your password, at least 6 characters!": "Silakan masukkan kata sandi Anda, minimal 6 karakter!",
"Please select an organization": "Please select an organization",
"Please select an organization to sign in": "Please select an organization to sign in",
"Please type an organization to sign in": "Please type an organization to sign in",
@@ -539,8 +565,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",
@@ -915,6 +939,11 @@
"Set password...": "Tetapkan kata sandi...",
"Tag": "tanda",
"Tag - Tooltip": "Tag pengguna",
"The password must contain at least one special character": "The password must contain at least one special character",
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
"The password must have at least 6 characters": "The password must have at least 6 characters",
"The password must have at least 8 characters": "The password must have at least 8 characters",
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
"Title": "Judul",
"Title - Tooltip": "Posisi dalam afiliasi",
"Two passwords you typed do not match.": "Dua password yang Anda ketikkan tidak cocok.",

View File

@@ -162,6 +162,12 @@
"Verify": "検証"
},
"general": {
"API key": "API key",
"API key - Tooltip": "API key - Tooltip",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Access secret": "Access secret",
"Access secret - Tooltip": "Access secret - Tooltip",
"Action": "アクション",
"Adapter": "アダプター",
"Adapter - Tooltip": "ポリシー・ストアのテーブル名",
@@ -188,6 +194,7 @@
"Close": "閉じる",
"Confirm": "Confirm",
"Created time": "作成された時間",
"Custom": "Custom",
"Default application": "デフォルトアプリケーション",
"Default application - Tooltip": "組織ページから直接登録されたユーザーのデフォルトアプリケーション",
"Default avatar": "デフォルトのアバター",
@@ -209,6 +216,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 +226,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",
@@ -251,6 +261,8 @@
"Organizations": "組織",
"Password": "パスワード",
"Password - Tooltip": "パスワードが正しいことを確認してください",
"Password complexity options": "Password complexity options",
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
"Password salt": "パスワードのソルト",
"Password salt - Tooltip": "ランダムパラメーターは、パスワードの暗号化に使用されます",
"Password type": "パスワードタイプ",
@@ -299,10 +311,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 +325,8 @@
"This is a read-only demo site!": "これは読み取り専用のデモサイトです!",
"Timestamp": "タイムスタンプ",
"Tokens": "トークン",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "URLリンク",
"Up": "アップ",
@@ -323,9 +339,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",
@@ -369,7 +396,6 @@
"Please input your code!": "あなたのコードを入力してください!",
"Please input your organization name!": "Please input your organization name!",
"Please input your password!": "パスワードを入力してください!",
"Please input your password, at least 6 characters!": "パスワードを入力してください。少なくとも6文字です",
"Please select an organization": "Please select an organization",
"Please select an organization to sign in": "Please select an organization to sign in",
"Please type an organization to sign in": "Please type an organization to sign in",
@@ -539,8 +565,6 @@
"Edit Pricing": "Edit Pricing",
"Free": "無料",
"Getting started": "はじめる",
"Has trial": "トライアル期間あり",
"Has trial - Tooltip": "プラン選択後のトライアル期間の有無",
"New Pricing": "New Pricing",
"Trial duration": "トライアル期間の長さ",
"Trial duration - Tooltip": "トライアル期間の長さ",
@@ -915,6 +939,11 @@
"Set password...": "パスワードの設定...",
"Tag": "タグ",
"Tag - Tooltip": "ユーザーのタグ",
"The password must contain at least one special character": "The password must contain at least one special character",
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
"The password must have at least 6 characters": "The password must have at least 6 characters",
"The password must have at least 8 characters": "The password must have at least 8 characters",
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
"Title": "タイトル",
"Title - Tooltip": "所属のポジション",
"Two passwords you typed do not match.": "2つのパスワードが一致しません。",

View File

@@ -162,6 +162,12 @@
"Verify": "검증하다"
},
"general": {
"API key": "API key",
"API key - Tooltip": "API key - Tooltip",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Access secret": "Access secret",
"Access secret - Tooltip": "Access secret - Tooltip",
"Action": "동작",
"Adapter": "어댑터",
"Adapter - Tooltip": "정책 저장소의 테이블 이름",
@@ -188,6 +194,7 @@
"Close": "닫다",
"Confirm": "Confirm",
"Created time": "작성한 시간",
"Custom": "Custom",
"Default application": "기본 애플리케이션",
"Default application - Tooltip": "조직 페이지에서 직접 등록한 사용자의 기본 응용 프로그램",
"Default avatar": "기본 아바타",
@@ -209,6 +216,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 +226,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",
@@ -251,6 +261,8 @@
"Organizations": "조직들",
"Password": "비밀번호",
"Password - Tooltip": "비밀번호가 올바른지 확인하세요",
"Password complexity options": "Password complexity options",
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
"Password salt": "비밀번호 솔트",
"Password salt - Tooltip": "암호화에 사용되는 임의 매개변수",
"Password type": "암호 유형",
@@ -299,10 +311,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 +325,8 @@
"This is a read-only demo site!": "이것은 읽기 전용 데모 사이트입니다!",
"Timestamp": "타임스탬프",
"Tokens": "토큰",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "URL 링크",
"Up": "위로",
@@ -323,9 +339,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",
@@ -369,7 +396,6 @@
"Please input your code!": "코드를 입력해주세요!",
"Please input your organization name!": "Please input your organization name!",
"Please input your password!": "비밀번호를 입력해주세요!",
"Please input your password, at least 6 characters!": "비밀번호를 입력해주세요. 최소 6자 이상 필요합니다!",
"Please select an organization": "Please select an organization",
"Please select an organization to sign in": "Please select an organization to sign in",
"Please type an organization to sign in": "Please type an organization to sign in",
@@ -539,8 +565,6 @@
"Edit Pricing": "Edit Pricing",
"Free": "무료",
"Getting started": "시작하기",
"Has trial": "무료 체험 가능",
"Has trial - Tooltip": "플랜 선택 후 체험 기간의 가용 여부",
"New Pricing": "New Pricing",
"Trial duration": "체험 기간",
"Trial duration - Tooltip": "체험 기간의 기간",
@@ -915,6 +939,11 @@
"Set password...": "비밀번호 설정...",
"Tag": "태그",
"Tag - Tooltip": "사용자의 태그",
"The password must contain at least one special character": "The password must contain at least one special character",
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
"The password must have at least 6 characters": "The password must have at least 6 characters",
"The password must have at least 8 characters": "The password must have at least 8 characters",
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
"Title": "제목",
"Title - Tooltip": "소속 내 직위",
"Two passwords you typed do not match.": "두 개의 비밀번호가 일치하지 않습니다.",

View File

@@ -162,6 +162,12 @@
"Verify": "Verificar"
},
"general": {
"API key": "API key",
"API key - Tooltip": "API key - Tooltip",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Access secret": "Access secret",
"Access secret - Tooltip": "Access secret - Tooltip",
"Action": "Ação",
"Adapter": "Adaptador",
"Adapter - Tooltip": "Nome da tabela do armazenamento de políticas",
@@ -188,6 +194,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 +216,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 +226,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",
@@ -251,6 +261,8 @@
"Organizations": "Organizações",
"Password": "Senha",
"Password - Tooltip": "Certifique-se de que a senha está correta",
"Password complexity options": "Password complexity options",
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
"Password salt": "Salt de senha",
"Password salt - Tooltip": "Parâmetro aleatório usado para criptografia de senha",
"Password type": "Tipo de senha",
@@ -299,10 +311,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 +325,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 +339,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",
@@ -369,7 +396,6 @@
"Please input your code!": "Por favor, informe o código!",
"Please input your organization name!": "Please input your organization name!",
"Please input your password!": "Por favor, informe sua senha!",
"Please input your password, at least 6 characters!": "Por favor, informe sua senha, pelo menos 6 caracteres!",
"Please select an organization": "Please select an organization",
"Please select an organization to sign in": "Please select an organization to sign in",
"Please type an organization to sign in": "Please type an organization to sign in",
@@ -539,8 +565,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",
@@ -915,6 +939,11 @@
"Set password...": "Definir senha...",
"Tag": "Tag",
"Tag - Tooltip": "Tag do usuário",
"The password must contain at least one special character": "The password must contain at least one special character",
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
"The password must have at least 6 characters": "The password must have at least 6 characters",
"The password must have at least 8 characters": "The password must have at least 8 characters",
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
"Title": "Título",
"Title - Tooltip": "Cargo na afiliação",
"Two passwords you typed do not match.": "As duas senhas digitadas não coincidem.",

View File

@@ -162,6 +162,12 @@
"Verify": "Проверить"
},
"general": {
"API key": "API key",
"API key - Tooltip": "API key - Tooltip",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Access secret": "Access secret",
"Access secret - Tooltip": "Access secret - Tooltip",
"Action": "Действие",
"Adapter": "Адаптер",
"Adapter - Tooltip": "Имя таблицы хранилища политик",
@@ -188,6 +194,7 @@
"Close": "Близко",
"Confirm": "Confirm",
"Created time": "Созданное время",
"Custom": "Custom",
"Default application": "Приложение по умолчанию",
"Default application - Tooltip": "По умолчанию приложение для пользователей, зарегистрированных непосредственно со страницы организации",
"Default avatar": "Стандартный аватар",
@@ -209,6 +216,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 +226,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",
@@ -251,6 +261,8 @@
"Organizations": "Организации",
"Password": "Пароль",
"Password - Tooltip": "Убедитесь, что пароль правильный",
"Password complexity options": "Password complexity options",
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
"Password salt": "Соль пароля",
"Password salt - Tooltip": "Случайный параметр, используемый для шифрования пароля",
"Password type": "Тип пароля",
@@ -299,10 +311,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 +325,8 @@
"This is a read-only demo site!": "Это демонстрационный сайт только для чтения!",
"Timestamp": "Отметка времени",
"Tokens": "Токены",
"Type": "Type",
"Type - Tooltip": "Type - Tooltip",
"URL": "URL",
"URL - Tooltip": "Ссылка URL",
"Up": "Вверх",
@@ -323,9 +339,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",
@@ -369,7 +396,6 @@
"Please input your code!": "Пожалуйста, введите свой код!",
"Please input your organization name!": "Please input your organization name!",
"Please input your password!": "Пожалуйста, введите свой пароль!",
"Please input your password, at least 6 characters!": "Пожалуйста, введите свой пароль, длина должна быть не менее 6 символов!",
"Please select an organization": "Please select an organization",
"Please select an organization to sign in": "Please select an organization to sign in",
"Please type an organization to sign in": "Please type an organization to sign in",
@@ -539,8 +565,6 @@
"Edit Pricing": "Edit Pricing",
"Free": "Бесплатно",
"Getting started": "Выьрать план",
"Has trial": "Есть пробный период",
"Has trial - Tooltip": "Наличие пробного периода после выбора плана",
"New Pricing": "New Pricing",
"Trial duration": "Продолжительность пробного периода",
"Trial duration - Tooltip": "Продолжительность пробного периода",
@@ -915,6 +939,11 @@
"Set password...": "Установить пароль...",
"Tag": "Метка",
"Tag - Tooltip": "Тег пользователя",
"The password must contain at least one special character": "The password must contain at least one special character",
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
"The password must have at least 6 characters": "The password must have at least 6 characters",
"The password must have at least 8 characters": "The password must have at least 8 characters",
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
"Title": "Заголовок",
"Title - Tooltip": "Положение в аффилиации",
"Two passwords you typed do not match.": "Два введенных вами пароля не совпадают.",

View File

@@ -162,6 +162,12 @@
"Verify": "Xác thực"
},
"general": {
"API key": "API key",
"API key - Tooltip": "API key - Tooltip",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Access secret": "Access secret",
"Access secret - Tooltip": "Access secret - Tooltip",
"Action": "Hành động",
"Adapter": "Bộ chuyển đổi",
"Adapter - Tooltip": "Tên bảng của kho lưu trữ chính sách",
@@ -188,6 +194,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 +216,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 +226,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",
@@ -251,6 +261,8 @@
"Organizations": "Tổ chức",
"Password": "Mật khẩu",
"Password - Tooltip": "Hãy đảm bảo rằng mật khẩu là chính xác",
"Password complexity options": "Password complexity options",
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
"Password salt": "Muối mật khẩu",
"Password salt - Tooltip": "Tham số ngẫu nhiên được sử dụng để mã hóa mật khẩu",
"Password type": "Loại mật khẩu",
@@ -299,10 +311,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 +325,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 +339,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",
@@ -369,7 +396,6 @@
"Please input your code!": "Vui lòng nhập mã của bạn!",
"Please input your organization name!": "Please input your organization name!",
"Please input your password!": "Vui lòng nhập mật khẩu của bạn!",
"Please input your password, at least 6 characters!": "Vui lòng nhập mật khẩu của bạn, ít nhất 6 ký tự!",
"Please select an organization": "Please select an organization",
"Please select an organization to sign in": "Please select an organization to sign in",
"Please type an organization to sign in": "Please type an organization to sign in",
@@ -539,8 +565,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",
@@ -915,6 +939,11 @@
"Set password...": "Đặt mật khẩu...",
"Tag": "Thẻ",
"Tag - Tooltip": "Thẻ của người dùng",
"The password must contain at least one special character": "The password must contain at least one special character",
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
"The password must have at least 6 characters": "The password must have at least 6 characters",
"The password must have at least 8 characters": "The password must have at least 8 characters",
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
"Title": "Tiêu đề",
"Title - Tooltip": "Vị trí trong tổ chức",
"Two passwords you typed do not match.": "Hai mật khẩu mà bạn đã nhập không khớp.",

View File

@@ -162,6 +162,12 @@
"Verify": "验证"
},
"general": {
"API key": "API key",
"API key - Tooltip": "API key - Tooltip",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Access secret": "Access secret",
"Access secret - Tooltip": "Access secret - Tooltip",
"Action": "操作",
"Adapter": "适配器",
"Adapter - Tooltip": "策略存储的表名",
@@ -188,6 +194,7 @@
"Close": "关闭",
"Confirm": "确认",
"Created time": "创建时间",
"Custom": "自定义",
"Default application": "默认应用",
"Default application - Tooltip": "直接从组织页面注册的用户默认所属的应用",
"Default avatar": "默认头像",
@@ -209,6 +216,7 @@
"Failed to delete": "删除失败",
"Failed to enable": "启用失败",
"Failed to get answer": "获取回答失败",
"Failed to remove": "移除失败",
"Failed to save": "保存失败",
"Failed to verify": "验证失败",
"Favicon": "Favicon",
@@ -218,6 +226,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",
@@ -251,6 +261,8 @@
"Organizations": "组织",
"Password": "密码",
"Password - Tooltip": "请确认密码正确",
"Password complexity options": "密码复杂度选项",
"Password complexity options - Tooltip": "密码复杂度组合,登录密码复杂度必须符合该规范",
"Password salt": "密码Salt值",
"Password salt - Tooltip": "用于密码加密的随机参数",
"Password type": "密码类型",
@@ -299,10 +311,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 +325,8 @@
"This is a read-only demo site!": "这是一个只读演示站点!",
"Timestamp": "时间戳",
"Tokens": "令牌",
"Type": "类型",
"Type - Tooltip": "类型",
"URL": "链接",
"URL - Tooltip": "URL链接",
"Up": "上移",
@@ -323,9 +339,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",
@@ -369,7 +396,6 @@
"Please input your code!": "请输入您的验证码!",
"Please input your organization name!": "请输入组织的名字!",
"Please input your password!": "请输入您的密码!",
"Please input your password, at least 6 characters!": "请输入您的密码不少于6位",
"Please select an organization": "请选择一个组织",
"Please select an organization to sign in": "请选择要登录的组织",
"Please type an organization to sign in": "请输入要登录的组织",
@@ -539,8 +565,6 @@
"Edit Pricing": "编辑定价",
"Free": "免费",
"Getting started": "开始使用",
"Has trial": "有试用期",
"Has trial - Tooltip": "选择计划后是否有试用期",
"New Pricing": "添加定价",
"Trial duration": "试用期时长",
"Trial duration - Tooltip": "试用期时长",
@@ -915,6 +939,11 @@
"Set password...": "设置密码...",
"Tag": "标签",
"Tag - Tooltip": "用户的标签",
"The password must contain at least one special character": "密码必须包含至少一个特殊字符",
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "密码必须包含至少一个大写字母、一个小写字母和一个数字",
"The password must have at least 6 characters": "密码长度必须至少为6个字符",
"The password must have at least 8 characters": "密码长度必须至少为8个字符",
"The password must not contain any repeated characters": "密码不得包含任何重复字符",
"Title": "职务",
"Title - Tooltip": "在工作单位担任的职务",
"Two passwords you typed do not match.": "两次输入的密码不匹配。",

Some files were not shown because too many files have changed in this diff Show More