Compare commits

...

31 Commits

Author SHA1 Message Date
a3b0f1fc74 feat: add owner to getUserByWechatId() 2023-06-21 21:29:53 +08:00
c391af4552 feat: improve MFA by using user's own Email and Phone (#2002)
* refactor: mfa

* fix: clean code

* fix: clean code

* fix: fix crash and improve robot
2023-06-21 18:56:37 +08:00
6ebca6dbe7 fix: Gosec/sec fixes (#2004)
* Customization of the initialization file

* fix: G601 (CWE-118): Implicit memory aliasing in for loop

* fix: G304 (CWE-22): Potential file inclusion via variable

* fix: G110 (CWE-409): Potential DoS vulnerability via decompression bomb
2023-06-21 18:55:20 +08:00
d505a4bf2d Remove org API calls in PasswordModal page 2023-06-21 00:49:03 +08:00
812bc5f6b2 Fix "nu" bug in GetLanguage() 2023-06-20 21:16:01 +08:00
f6f4d44444 feat: remove url.JoinPath() to be compatible with Go 1.17 (#1995) 2023-06-20 17:44:40 +08:00
926e73ed1b fix: fix "Accept-Language" parsing in request (#1996) 2023-06-20 17:43:48 +08:00
65716af89e feat: deprecate the user group relation table (#1990)
* fix: deprecate the user group relation table

* fix: clean code

* fix: fix trigger

* Update group.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-06-19 19:08:45 +08:00
d9c4f401e3 Fix error in downloadImage() 2023-06-19 17:52:01 +08:00
58aa7dba6a Fix groups in GetUserInfo() 2023-06-19 11:06:55 +08:00
29fc820578 Set User.groups to [] 2023-06-19 09:42:17 +08:00
d0ac265c91 fix: Deprecate the id field in group (#1987) 2023-06-18 23:33:13 +08:00
3562c36817 feat: Revert "fix: fix URL path in MinIO storage provider" (#1988)
This reverts commit 3699177837.
2023-06-18 23:08:40 +08:00
7884e10ca3 Refactor adapter's owner and organization 2023-06-18 00:22:12 +08:00
12dee8afd3 Fix null options in checkPasswordComplexity() 2023-06-17 22:38:02 +08:00
ac4b870309 Improve getFaviconFileBuffer() 2023-06-17 12:50:01 +08:00
b9140e2d5a Refactor refreshAvatar() 2023-06-17 11:43:46 +08:00
501f0dc74f Add user_avatar.go 2023-06-17 01:25:15 +08:00
a932b76fba Remove useless check in SetPassword() 2023-06-17 00:58:31 +08:00
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
edc6aa0d50 feat: get all role/permission of an user (#1978) 2023-06-16 22:44:21 +08:00
ebc0e0f2c9 Update i18n words 2023-06-16 22:06:54 +08:00
63dd2e781e Update backend i18n files 2023-06-16 21:55:08 +08:00
b01ba792bb Rename to accessSecret 2023-06-16 20:42:15 +08:00
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
cc456f265f feat: fix LDAP user password checking logic in GetOAuthToken() (#1975) 2023-06-15 21:04:09 +08:00
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
8e6755845f ci: fix bug in PaypalPaymentProvider (#1972) 2023-06-13 23:33:03 +08:00
967fa4be68 feat: add access key and secret key for user (#1971) 2023-06-13 22:18:17 +08:00
805cf20d04 feat: fix incorrect VerifyTypePhone value (#1968) 2023-06-13 17:26:37 +08:00
2a8001f490 fix: clean timeout when componentWillUnmount in PaymentResult page (#1962) 2023-06-13 02:00:52 +08:00
101 changed files with 2512 additions and 991 deletions

View File

@ -110,7 +110,7 @@ func GetLanguage(language string) string {
return "en"
}
if len(language) != 2 {
if len(language) != 2 || language == "nu" {
return "en"
} else {
return language

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())
@ -363,6 +370,7 @@ func (c *ApiController) GetAccount() {
user.Permissions = object.GetMaskedPermissions(user.Permissions)
user.Roles = object.GetMaskedRoles(user.Roles)
user.MultiFactorAuths = object.GetAllMfaProps(user, true)
organization, err := object.GetMaskedOrganization(object.GetOrganizationByUser(user))
if err != nil {

View File

@ -71,7 +71,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
if form.Password != "" && user.IsMfaEnabled() {
c.setMfaSessionData(&object.MfaSessionData{UserId: userId})
resp = &Response{Status: object.NextMfa, Data: user.GetPreferMfa(true)}
resp = &Response{Status: object.NextMfa, Data: user.GetPreferredMfaProps(true)}
return
}
@ -656,15 +656,20 @@ func (c *ApiController) Login() {
}
if authForm.Passcode != "" {
MfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferMfa(false))
err = MfaUtil.Verify(authForm.Passcode)
mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferredMfaProps(false))
if mfaUtil == nil {
c.ResponseError("Invalid multi-factor authentication type")
return
}
err = mfaUtil.Verify(authForm.Passcode)
if err != nil {
c.ResponseError(err.Error())
return
}
}
if authForm.RecoveryCode != "" {
err = object.RecoverTfs(user, authForm.RecoveryCode)
err = object.MfaRecover(user, authForm.RecoveryCode)
if err != nil {
c.ResponseError(err.Error())
return

View File

@ -177,6 +177,10 @@ func (c *ApiController) SetSessionData(s *SessionData) {
}
func (c *ApiController) setMfaSessionData(data *object.MfaSessionData) {
if data == nil {
c.SetSession(object.MfaSessionUserId, nil)
return
}
c.SetSession(object.MfaSessionUserId, data.UserId)
}

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)

View File

@ -143,5 +143,6 @@ func (c *ApiController) DeleteGroup() {
return
}
c.ResponseOk(wrapActionResponse(object.DeleteGroup(&group)))
c.Data["json"] = wrapActionResponse(object.DeleteGroup(&group))
c.ServeJSON()
}

View File

@ -34,7 +34,7 @@ import (
func (c *ApiController) MfaSetupInitiate() {
owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name")
authType := c.Ctx.Request.Form.Get("type")
mfaType := c.Ctx.Request.Form.Get("mfaType")
userId := util.GetId(owner, name)
if len(userId) == 0 {
@ -42,10 +42,11 @@ func (c *ApiController) MfaSetupInitiate() {
return
}
MfaUtil := object.GetMfaUtil(authType, nil)
MfaUtil := object.GetMfaUtil(mfaType, nil)
if MfaUtil == nil {
c.ResponseError("Invalid auth type")
}
user, err := object.GetUser(userId)
if err != nil {
c.ResponseError(err.Error())
@ -79,16 +80,20 @@ func (c *ApiController) MfaSetupInitiate() {
// @Success 200 {object} Response object
// @router /mfa/setup/verify [post]
func (c *ApiController) MfaSetupVerify() {
authType := c.Ctx.Request.Form.Get("type")
mfaType := c.Ctx.Request.Form.Get("mfaType")
passcode := c.Ctx.Request.Form.Get("passcode")
if authType == "" || passcode == "" {
if mfaType == "" || passcode == "" {
c.ResponseError("missing auth type or passcode")
return
}
MfaUtil := object.GetMfaUtil(authType, nil)
mfaUtil := object.GetMfaUtil(mfaType, nil)
if mfaUtil == nil {
c.ResponseError("Invalid multi-factor authentication type")
return
}
err := MfaUtil.SetupVerify(c.Ctx, passcode)
err := mfaUtil.SetupVerify(c.Ctx, passcode)
if err != nil {
c.ResponseError(err.Error())
} else {
@ -108,7 +113,7 @@ func (c *ApiController) MfaSetupVerify() {
func (c *ApiController) MfaSetupEnable() {
owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name")
authType := c.Ctx.Request.Form.Get("type")
mfaType := c.Ctx.Request.Form.Get("mfaType")
user, err := object.GetUser(util.GetId(owner, name))
if err != nil {
@ -121,8 +126,13 @@ func (c *ApiController) MfaSetupEnable() {
return
}
twoFactor := object.GetMfaUtil(authType, nil)
err = twoFactor.Enable(c.Ctx, user)
mfaUtil := object.GetMfaUtil(mfaType, nil)
if mfaUtil == nil {
c.ResponseError("Invalid multi-factor authentication type")
return
}
err = mfaUtil.Enable(c.Ctx, user)
if err != nil {
c.ResponseError(err.Error())
return
@ -137,11 +147,9 @@ func (c *ApiController) MfaSetupEnable() {
// @Description: Delete MFA
// @param owner form string true "owner of user"
// @param name form string true "name of user"
// @param id form string true "id of user's MFA props"
// @Success 200 {object} Response object
// @router /delete-mfa/ [post]
func (c *ApiController) DeleteMfa() {
id := c.Ctx.Request.Form.Get("id")
owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name")
userId := util.GetId(owner, name)
@ -151,28 +159,18 @@ func (c *ApiController) DeleteMfa() {
c.ResponseError(err.Error())
return
}
if user == nil {
c.ResponseError("User doesn't exist")
return
}
mfaProps := user.MultiFactorAuths[:0]
i := 0
for _, mfaProp := range mfaProps {
if mfaProp.Id != id {
mfaProps[i] = mfaProp
i++
}
}
user.MultiFactorAuths = mfaProps
_, err = object.UpdateUser(userId, user, []string{"multi_factor_auths"}, user.IsAdminUser())
err = object.DisabledMultiFactorAuth(user)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(user.MultiFactorAuths)
c.ResponseOk(object.GetAllMfaProps(user, true))
}
// SetPreferredMfa
@ -185,7 +183,7 @@ func (c *ApiController) DeleteMfa() {
// @Success 200 {object} Response object
// @router /set-preferred-mfa [post]
func (c *ApiController) SetPreferredMfa() {
id := c.Ctx.Request.Form.Get("id")
mfaType := c.Ctx.Request.Form.Get("mfaType")
owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name")
userId := util.GetId(owner, name)
@ -195,29 +193,15 @@ func (c *ApiController) SetPreferredMfa() {
c.ResponseError(err.Error())
return
}
if user == nil {
c.ResponseError("User doesn't exist")
return
}
mfaProps := user.MultiFactorAuths
for i, mfaProp := range user.MultiFactorAuths {
if mfaProp.Id == id {
mfaProps[i].IsPreferred = true
} else {
mfaProps[i].IsPreferred = false
}
}
_, err = object.UpdateUser(userId, user, []string{"multi_factor_auths"}, user.IsAdminUser())
err = object.SetPreferredMultiFactorAuth(user, mfaType)
if err != nil {
c.ResponseError(err.Error())
return
}
for i, mfaProp := range mfaProps {
mfaProps[i] = object.GetMaskedProps(mfaProp)
}
c.ResponseOk(mfaProps)
c.ResponseOk(object.GetAllMfaProps(user, true))
}

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

View File

@ -80,7 +80,7 @@ func (c *ApiController) GetGlobalUsers() {
// @router /get-users [get]
func (c *ApiController) GetUsers() {
owner := c.Input().Get("owner")
groupId := c.Input().Get("groupId")
groupName := c.Input().Get("groupName")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
@ -89,8 +89,8 @@ func (c *ApiController) GetUsers() {
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
if groupId != "" {
maskedUsers, err := object.GetMaskedUsers(object.GetUsersByGroup(groupId))
if groupName != "" {
maskedUsers, err := object.GetMaskedUsers(object.GetGroupUsers(groupName))
if err != nil {
c.ResponseError(err.Error())
return
@ -108,14 +108,14 @@ func (c *ApiController) GetUsers() {
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
count, err := object.GetUserCount(owner, field, value, groupId)
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, groupId)
users, err := object.GetPaginationUsers(owner, paginator.Offset(), limit, field, value, sortField, sortOrder, groupName)
if err != nil {
c.ResponseError(err.Error())
return
@ -193,6 +193,7 @@ func (c *ApiController) GetUser() {
panic(err)
}
user.MultiFactorAuths = object.GetAllMfaProps(user, true)
err = object.ExtendUserWithRolesAndPermissions(user)
if err != nil {
panic(err)
@ -410,10 +411,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)
@ -448,6 +445,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 {
@ -528,3 +531,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")
groupName := c.Ctx.Request.Form.Get("groupName")
c.Data["json"] = wrapActionResponse(object.RemoveUserFromGroup(owner, name, groupName))
c.ServeJSON()
}

View File

@ -19,13 +19,14 @@ import (
"io"
"mime/multipart"
"os"
"path/filepath"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
func saveFile(path string, file *multipart.File) (err error) {
f, err := os.Create(path)
f, err := os.Create(filepath.Clean(path))
if err != nil {
return err
}

View File

@ -55,6 +55,9 @@ func (c *ApiController) T(error string) string {
// GetAcceptLanguage ...
func (c *ApiController) GetAcceptLanguage() string {
language := c.Ctx.Request.Header.Get("Accept-Language")
if len(language) > 2 {
language = language[0:2]
}
return conf.GetLanguage(language)
}

View File

@ -93,9 +93,10 @@ func (c *ApiController) SendVerificationCode() {
}
}
// mfaSessionData != nil, means method is MfaSetupVerification
// mfaSessionData != nil, means method is MfaAuthVerification
if mfaSessionData := c.getMfaSessionData(); mfaSessionData != nil {
user, err = object.GetUser(mfaSessionData.UserId)
c.setMfaSessionData(nil)
if err != nil {
c.ResponseError(err.Error())
return
@ -129,7 +130,7 @@ func (c *ApiController) SendVerificationCode() {
} else if vform.Method == ResetVerification {
user = c.getCurrentUser()
} else if vform.Method == MfaAuthVerification {
mfaProps := user.GetPreferMfa(false)
mfaProps := user.GetPreferredMfaProps(false)
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
vform.Dest = mfaProps.Secret
}
@ -157,12 +158,14 @@ func (c *ApiController) SendVerificationCode() {
}
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
} else if vform.Method == ResetVerification {
if user = c.getCurrentUser(); user != nil {
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
} else if vform.Method == ResetVerification || vform.Method == MfaSetupVerification {
if vform.CountryCode == "" {
if user = c.getCurrentUser(); user != nil {
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
}
}
} else if vform.Method == MfaAuthVerification {
mfaProps := user.GetPreferMfa(false)
mfaProps := user.GetPreferredMfaProps(false)
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
vform.Dest = mfaProps.Secret
}

View File

@ -17,6 +17,7 @@ package deployment
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/casdoor/casdoor/object"
@ -45,7 +46,7 @@ func uploadFolder(storageProvider oss.StorageInterface, folder string) {
continue
}
file, err := os.Open(path + filename)
file, err := os.Open(filepath.Clean(path + filename))
if err != nil {
panic(err)
}

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

@ -145,11 +145,6 @@ func (a *Adapter) createTable() {
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

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

View File

@ -15,26 +15,27 @@
package object
import (
"errors"
"fmt"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/builder"
"github.com/xorm-io/core"
)
type Group struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk unique" json:"name"`
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"`
Id string `xorm:"varchar(100) not null index" json:"id"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Manager string `xorm:"varchar(100)" json:"manager"`
ContactEmail string `xorm:"varchar(100)" json:"contactEmail"`
Type string `xorm:"varchar(100)" json:"type"`
ParentGroupId string `xorm:"varchar(100)" json:"parentGroupId"`
IsTopGroup bool `xorm:"bool" json:"isTopGroup"`
Users *[]string `xorm:"-" json:"users"`
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 []*User `xorm:"-" json:"users"`
Title string `json:"title,omitempty"`
Key string `json:"key,omitempty"`
@ -94,24 +95,6 @@ func getGroup(owner string, name string) (*Group, error) {
}
}
func getGroupById(id string) (*Group, error) {
if id == "" {
return nil, nil
}
group := Group{Id: id}
existed, err := adapter.Engine.Get(&group)
if err != nil {
return nil, err
}
if existed {
return &group, nil
} else {
return nil, nil
}
}
func GetGroup(id string) (*Group, error) {
owner, name := util.GetOwnerAndNameFromId(id)
return getGroup(owner, name)
@ -124,7 +107,13 @@ func UpdateGroup(id string, group *Group) (bool, error) {
return false, err
}
group.UpdatedTime = util.GetCurrentTime()
if name != group.Name {
err := GroupChangeTrigger(name, group.Name)
if err != nil {
return false, err
}
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(group)
if err != nil {
return false, err
@ -134,10 +123,6 @@ func UpdateGroup(id string, group *Group) (bool, error) {
}
func AddGroup(group *Group) (bool, error) {
if group.Id == "" {
group.Id = util.GenerateId()
}
affected, err := adapter.Engine.Insert(group)
if err != nil {
return false, err
@ -158,6 +143,23 @@ func AddGroups(groups []*Group) (bool, error) {
}
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.Name, "", ""); err != nil {
return false, err
} else if count > 0 {
return false, errors.New("group has users")
}
affected, err := adapter.Engine.ID(core.PK{group.Owner, group.Name}).Delete(&Group{})
if err != nil {
return false, err
@ -170,19 +172,18 @@ func (group *Group) GetId() string {
return fmt.Sprintf("%s/%s", group.Owner, group.Name)
}
func ConvertToTreeData(groups []*Group, parentGroupId string) []*Group {
func ConvertToTreeData(groups []*Group, parentId string) []*Group {
treeData := []*Group{}
for _, group := range groups {
if group.ParentGroupId == parentGroupId {
if group.ParentId == parentId {
node := &Group{
Title: group.DisplayName,
Key: group.Name,
Type: group.Type,
Owner: group.Owner,
Id: group.Id,
}
children := ConvertToTreeData(groups, group.Id)
children := ConvertToTreeData(groups, group.Name)
if len(children) > 0 {
node.Children = children
}
@ -191,3 +192,113 @@ func ConvertToTreeData(groups []*Group, parentGroupId string) []*Group {
}
return treeData
}
func RemoveUserFromGroup(owner, name, groupName string) (bool, error) {
user, err := getUser(owner, name)
if err != nil {
return false, err
}
if user == nil {
return false, errors.New("user not exist")
}
user.Groups = util.DeleteVal(user.Groups, groupName)
affected, err := updateUser(user.GetId(), user, []string{"groups"})
if err != nil {
return false, err
}
return affected != 0, err
}
func GetGroupUserCount(groupName string, field, value string) (int64, error) {
if field == "" && value == "" {
return adapter.Engine.Where(builder.Like{"`groups`", groupName}).
Count(&User{})
} else {
return adapter.Engine.Table("user").
Where(builder.Like{"`groups`", groupName}).
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) {
users := []*User{}
session := adapter.Engine.Table("user").
Where(builder.Like{"`groups`", groupName})
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) {
users := []*User{}
err := adapter.Engine.Table("user").
Where(builder.Like{"`groups`", groupName}).
Find(&users)
if err != nil {
return nil, err
}
return users, nil
}
func GroupChangeTrigger(oldName, newName string) error {
session := adapter.Engine.NewSession()
defer session.Close()
err := session.Begin()
if err != nil {
return err
}
users := []*User{}
err = session.Where(builder.Like{"`groups`", oldName}).Find(&users)
if err != nil {
return err
}
for _, user := range users {
user.Groups = util.ReplaceVal(user.Groups, oldName, newName)
_, err := updateUser(user.GetId(), user, []string{"groups"})
if err != nil {
return err
}
}
groups := []*Group{}
err = session.Where("parent_id = ?", oldName).Find(&groups)
for _, group := range groups {
group.ParentId = newName
_, err := session.ID(core.PK{group.Owner, group.Name}).Cols("parent_id").Update(group)
if err != nil {
return err
}
}
err = session.Commit()
if err != nil {
return err
}
return nil
}

View File

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

@ -27,9 +27,9 @@ type MfaSessionData struct {
}
type MfaProps struct {
Id string `json:"id"`
Enabled bool `json:"enabled"`
IsPreferred bool `json:"isPreferred"`
AuthType string `json:"type" form:"type"`
MfaType string `json:"mfaType" form:"mfaType"`
Secret string `json:"secret,omitempty"`
CountryCode string `json:"countryCode,omitempty"`
URL string `json:"url,omitempty"`
@ -44,8 +44,9 @@ type MfaInterface interface {
}
const (
SmsType = "sms"
TotpType = "app"
EmailType = "email"
SmsType = "sms"
TotpType = "app"
)
const (
@ -54,10 +55,12 @@ const (
RequiredMfa = "RequiredMfa"
)
func GetMfaUtil(providerType string, config *MfaProps) MfaInterface {
switch providerType {
func GetMfaUtil(mfaType string, config *MfaProps) MfaInterface {
switch mfaType {
case SmsType:
return NewSmsTwoFactor(config)
case EmailType:
return NewEmailTwoFactor(config)
case TotpType:
return nil
}
@ -65,17 +68,17 @@ func GetMfaUtil(providerType string, config *MfaProps) MfaInterface {
return nil
}
func RecoverTfs(user *User, recoveryCode string) error {
func MfaRecover(user *User, recoveryCode string) error {
hit := false
twoFactor := user.GetPreferMfa(false)
if len(twoFactor.RecoveryCodes) == 0 {
if len(user.RecoveryCodes) == 0 {
return fmt.Errorf("do not have recovery codes")
}
for _, code := range twoFactor.RecoveryCodes {
for _, code := range user.RecoveryCodes {
if code == recoveryCode {
hit = true
user.RecoveryCodes = util.DeleteVal(user.RecoveryCodes, code)
break
}
}
@ -83,30 +86,92 @@ func RecoverTfs(user *User, recoveryCode string) error {
return fmt.Errorf("recovery code not found")
}
affected, err := UpdateUser(user.GetId(), user, []string{"two_factor_auth"}, user.IsAdminUser())
_, err := UpdateUser(user.GetId(), user, []string{"recovery_codes"}, user.IsAdminUser())
if err != nil {
return err
}
if !affected {
return fmt.Errorf("")
return nil
}
func GetAllMfaProps(user *User, masked bool) []*MfaProps {
mfaProps := []*MfaProps{}
if user.MfaPhoneEnabled {
mfaProps = append(mfaProps, user.GetMfaProps(SmsType, masked))
} else {
mfaProps = append(mfaProps, &MfaProps{
Enabled: false,
MfaType: SmsType,
})
}
if user.MfaEmailEnabled {
mfaProps = append(mfaProps, user.GetMfaProps(EmailType, masked))
} else {
mfaProps = append(mfaProps, &MfaProps{
Enabled: false,
MfaType: EmailType,
})
}
return mfaProps
}
func (user *User) GetMfaProps(mfaType string, masked bool) *MfaProps {
mfaProps := &MfaProps{}
if mfaType == SmsType {
mfaProps = &MfaProps{
Enabled: user.MfaPhoneEnabled,
MfaType: mfaType,
CountryCode: user.CountryCode,
}
if masked {
mfaProps.Secret = util.GetMaskedPhone(user.Phone)
} else {
mfaProps.Secret = user.Phone
}
} else if mfaType == EmailType {
mfaProps = &MfaProps{
Enabled: user.MfaEmailEnabled,
MfaType: mfaType,
}
if masked {
mfaProps.Secret = util.GetMaskedEmail(user.Email)
} else {
mfaProps.Secret = user.Email
}
} else if mfaType == TotpType {
mfaProps = &MfaProps{
MfaType: mfaType,
}
}
if user.PreferredMfaType == mfaType {
mfaProps.IsPreferred = true
}
return mfaProps
}
func DisabledMultiFactorAuth(user *User) error {
user.PreferredMfaType = ""
user.RecoveryCodes = []string{}
user.MfaPhoneEnabled = false
user.MfaEmailEnabled = false
_, err := UpdateUser(user.GetId(), user, []string{"preferred_mfa_type", "recovery_codes", "mfa_phone_enabled", "mfa_email_enabled"}, user.IsAdminUser())
if err != nil {
return err
}
return nil
}
func GetMaskedProps(props *MfaProps) *MfaProps {
maskedProps := &MfaProps{
AuthType: props.AuthType,
Id: props.Id,
IsPreferred: props.IsPreferred,
}
func SetPreferredMultiFactorAuth(user *User, mfaType string) error {
user.PreferredMfaType = mfaType
if props.AuthType == SmsType {
if !util.IsEmailValid(props.Secret) {
maskedProps.Secret = util.GetMaskedPhone(props.Secret)
} else {
maskedProps.Secret = util.GetMaskedEmail(props.Secret)
}
_, err := UpdateUser(user.GetId(), user, []string{"preferred_mfa_type"}, user.IsAdminUser())
if err != nil {
return err
}
return maskedProps
return nil
}

View File

@ -34,6 +34,21 @@ type SmsMfa struct {
Config *MfaProps
}
func (mfa *SmsMfa) Initiate(ctx *context.Context, name string, secret string) (*MfaProps, error) {
recoveryCode := uuid.NewString()
err := ctx.Input.CruSession.Set(MfaSmsRecoveryCodesSession, []string{recoveryCode})
if err != nil {
return nil, err
}
mfaProps := MfaProps{
MfaType: mfa.Config.MfaType,
RecoveryCodes: []string{recoveryCode},
}
return &mfaProps, nil
}
func (mfa *SmsMfa) SetupVerify(ctx *context.Context, passCode string) error {
dest := ctx.Input.CruSession.Get(MfaSmsDestSession).(string)
countryCode := ctx.Input.CruSession.Get(MfaSmsCountryCodeSession).(string)
@ -47,6 +62,45 @@ func (mfa *SmsMfa) SetupVerify(ctx *context.Context, passCode string) error {
return nil
}
func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error {
recoveryCodes := ctx.Input.CruSession.Get(MfaSmsRecoveryCodesSession).([]string)
if len(recoveryCodes) == 0 {
return fmt.Errorf("recovery codes is empty")
}
columns := []string{"recovery_codes", "preferred_mfa_type"}
user.RecoveryCodes = append(user.RecoveryCodes, recoveryCodes...)
if user.PreferredMfaType == "" {
user.PreferredMfaType = mfa.Config.MfaType
}
if mfa.Config.MfaType == SmsType {
user.MfaPhoneEnabled = true
columns = append(columns, "mfa_phone_enabled")
if user.Phone == "" {
user.Phone = ctx.Input.CruSession.Get(MfaSmsDestSession).(string)
user.CountryCode = ctx.Input.CruSession.Get(MfaSmsCountryCodeSession).(string)
columns = append(columns, "phone", "country_code")
}
} else if mfa.Config.MfaType == EmailType {
user.MfaEmailEnabled = true
columns = append(columns, "mfa_email_enabled")
if user.Email == "" {
user.Email = ctx.Input.CruSession.Get(MfaSmsDestSession).(string)
columns = append(columns, "email")
}
}
_, err := UpdateUser(user.GetId(), user, columns, false)
if err != nil {
return err
}
return nil
}
func (mfa *SmsMfa) Verify(passCode string) error {
if !util.IsEmailValid(mfa.Config.Secret) {
mfa.Config.Secret, _ = util.GetE164Number(mfa.Config.Secret, mfa.Config.CountryCode)
@ -57,65 +111,21 @@ func (mfa *SmsMfa) Verify(passCode string) error {
return nil
}
func (mfa *SmsMfa) Initiate(ctx *context.Context, name string, secret string) (*MfaProps, error) {
recoveryCode, err := uuid.NewRandom()
if err != nil {
return nil, err
}
err = ctx.Input.CruSession.Set(MfaSmsRecoveryCodesSession, []string{recoveryCode.String()})
if err != nil {
return nil, err
}
mfaProps := MfaProps{
AuthType: SmsType,
RecoveryCodes: []string{recoveryCode.String()},
}
return &mfaProps, nil
}
func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error {
dest := ctx.Input.CruSession.Get(MfaSmsDestSession).(string)
recoveryCodes := ctx.Input.CruSession.Get(MfaSmsRecoveryCodesSession).([]string)
countryCode := ctx.Input.CruSession.Get(MfaSmsCountryCodeSession).(string)
if dest == "" || len(recoveryCodes) == 0 {
return fmt.Errorf("MFA dest or recovery codes is empty")
}
if !util.IsEmailValid(dest) {
mfa.Config.CountryCode = countryCode
}
mfa.Config.AuthType = SmsType
mfa.Config.Id = uuid.NewString()
mfa.Config.Secret = dest
mfa.Config.RecoveryCodes = recoveryCodes
for i, mfaProp := range user.MultiFactorAuths {
if mfaProp.Secret == mfa.Config.Secret {
user.MultiFactorAuths = append(user.MultiFactorAuths[:i], user.MultiFactorAuths[i+1:]...)
}
}
user.MultiFactorAuths = append(user.MultiFactorAuths, mfa.Config)
affected, err := UpdateUser(user.GetId(), user, []string{"multi_factor_auths"}, user.IsAdminUser())
if err != nil {
return err
}
if !affected {
return fmt.Errorf("failed to enable two factor authentication")
}
return nil
}
func NewSmsTwoFactor(config *MfaProps) *SmsMfa {
if config == nil {
config = &MfaProps{
AuthType: SmsType,
MfaType: SmsType,
}
}
return &SmsMfa{
Config: config,
}
}
func NewEmailTwoFactor(config *MfaProps) *SmsMfa {
if config == nil {
config = &MfaProps{
MfaType: EmailType,
}
}
return &SmsMfa{

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

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

@ -43,6 +43,7 @@ func UploadPermissions(owner string, fileId string) (bool, error) {
newPermissions := []*Permission{}
for index, line := range table {
line := line
if index == 0 || parseLineItem(&line, 0) == "" {
continue
}

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

@ -43,6 +43,7 @@ func UploadRoles(owner string, fileId string) (bool, error) {
newRoles := []*Role{}
for index, line := range table {
line := line
if index == 0 || parseLineItem(&line, 0) == "" {
continue
}

View File

@ -260,10 +260,17 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
// decompress
var buffer bytes.Buffer
rdr := flate.NewReader(bytes.NewReader(defated))
_, err = io.Copy(&buffer, rdr)
if err != nil {
return "", "", "", err
for {
_, err := io.CopyN(&buffer, rdr, 1024)
if err != nil {
if err == io.EOF {
break
}
return "", "", "", err
}
}
var authnRequest saml.AuthnRequest
err = xml.Unmarshal(buffer.Bytes(), &authnRequest)
if err != nil {

View File

@ -628,7 +628,12 @@ func GetPasswordToken(application *Application, username string, password string
ErrorDescription: "the user does not exist",
}, nil
}
msg := CheckPassword(user, password, "en")
var msg string
if user.Ldap != "" {
msg = checkLdapUserPassword(user, password, "en")
} else {
msg = CheckPassword(user, password, "en")
}
if msg != "" {
return nil, &TokenError{
Error: InvalidGrant,
@ -789,7 +794,7 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin
ErrorDescription: "the wechat mini program session is invalid",
}, nil
}
user, err := getUserByWechatId(openId, unionId)
user, err := getUserByWechatId(application.Organization, openId, unionId)
if err != nil {
return nil, nil, err
}

View File

@ -216,6 +216,9 @@ func refineUser(user *User) *User {
if user.Permissions == nil {
user.Permissions = []*Permission{}
}
if user.Groups == nil {
user.Groups = []string{}
}
return user
}

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,7 +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"`
Groups []string `xorm:"varchar(1000)" json:"groups"`
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"`
@ -159,13 +159,18 @@ type User struct {
Custom string `xorm:"custom varchar(100)" json:"custom"`
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
MultiFactorAuths []*MfaProps `json:"multiFactorAuths"`
PreferredMfaType string `xorm:"varchar(100)" json:"preferredMfaType"`
RecoveryCodes []string `xorm:"varchar(1000)" json:"recoveryCodes,omitempty"`
MfaPhoneEnabled bool `json:"mfaPhoneEnabled"`
MfaEmailEnabled bool `json:"mfaEmailEnabled"`
MultiFactorAuths []*MfaProps `xorm:"-" json:"multiFactorAuths,omitempty"`
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
Properties map[string]string `json:"properties"`
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"`
@ -219,18 +224,11 @@ func GetPaginationGlobalUsers(offset, limit int, field, value, sortField, sortOr
return users, nil
}
func GetUserCount(owner, field, value string, groupId string) (int64, error) {
func GetUserCount(owner, field, value string, groupName string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "")
if groupId != "" {
group, err := GetGroup(groupId)
if group == nil || err != nil {
return 0, err
}
// users count in group
return adapter.Engine.Table("user_group_relation").Join("INNER", "user AS u", "user_group_relation.user_id = u.id").
Where("user_group_relation.group_id = ?", group.Id).
Count(&UserGroupRelation{})
if groupName != "" {
return GetGroupUserCount(groupName, field, value)
}
return session.Count(&User{})
@ -270,24 +268,11 @@ 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, groupId string) ([]*User, error) {
func GetPaginationUsers(owner string, offset, limit int, field, value, sortField, sortOrder string, groupName string) ([]*User, error) {
users := []*User{}
if groupId != "" {
group, err := GetGroup(groupId)
if group == nil || err != nil {
return []*User{}, err
}
session := adapter.Engine.Prepare()
if offset != -1 && limit != -1 {
session.Limit(limit, offset)
}
err = session.Table("user_group_relation").Join("INNER", "user AS u", "user_group_relation.user_id = u.id").
Where("user_group_relation.group_id = ?", group.Id).
Find(&users)
return users, err
if groupName != "" {
return GetPaginationGroupUsers(groupName, offset, limit, field, value, sortField, sortOrder)
}
session := GetSessionForUser(owner, offset, limit, field, value, sortField, sortOrder)
@ -298,23 +283,6 @@ func GetPaginationUsers(owner string, offset, limit int, field, value, sortField
return users, nil
}
func GetUsersByGroup(groupId string) ([]*User, error) {
group, err := GetGroup(groupId)
if group == nil || err != nil {
return []*User{}, err
}
users := []*User{}
err = adapter.Engine.Table("user_group_relation").Join("INNER", "user AS u", "user_group_relation.user_id = u.id").
Where("user_group_relation.group_id = ?", group.Id).
Find(&users)
if err != nil {
return nil, err
}
return users, nil
}
func getUser(owner string, name string) (*User, error) {
if owner == "" || name == "" {
return nil, nil
@ -351,12 +319,12 @@ func getUserById(owner string, id string) (*User, error) {
}
}
func getUserByWechatId(wechatOpenId string, wechatUnionId string) (*User, error) {
func getUserByWechatId(owner string, wechatOpenId string, wechatUnionId string) (*User, error) {
if wechatUnionId == "" {
wechatUnionId = wechatOpenId
}
user := &User{}
existed, err := adapter.Engine.Where("wechat = ? OR wechat = ?", wechatOpenId, wechatUnionId).Get(user)
existed, err := adapter.Engine.Where("owner = ?", owner).Where("wechat = ? OR wechat = ?", wechatOpenId, wechatUnionId).Get(user)
if err != nil {
return nil, err
}
@ -422,6 +390,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)
@ -444,6 +429,12 @@ func GetMaskedUser(user *User, errs ...error) (*User, error) {
if user.Password != "" {
user.Password = "***"
}
if user.AccessSecret != "" {
user.AccessSecret = "***"
}
if user.RecoveryCodes != nil {
user.RecoveryCodes = nil
}
if user.ManagedAccounts != nil {
for _, manageAccount := range user.ManagedAccounts {
@ -451,11 +442,6 @@ func GetMaskedUser(user *User, errs ...error) (*User, error) {
}
}
if user.MultiFactorAuths != nil {
for i, props := range user.MultiFactorAuths {
user.MultiFactorAuths[i] = GetMaskedProps(props)
}
}
return user, nil
}
@ -502,17 +488,13 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
if name != user.Name {
err := userChangeTrigger(name, user.Name)
if err != nil {
return false, nil
return false, err
}
}
if user.Password == "***" {
user.Password = oldUser.Password
}
err = user.UpdateUserHash()
if err != nil {
panic(err)
}
if user.Avatar != oldUser.Avatar && user.Avatar != "" && user.PermanentAvatar != "*" {
user.PermanentAvatar, err = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar, false)
@ -526,7 +508,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
"owner", "display_name", "avatar",
"location", "address", "country_code", "region", "language", "affiliation", "title", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts",
"signin_wrong_times", "last_signin_wrong_time", "groups",
"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",
@ -540,7 +522,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
columns = append(columns, "name", "email", "phone", "country_code")
}
affected, err := updateUser(oldUser, user, columns)
affected, err := updateUser(id, user, columns)
if err != nil {
return false, err
}
@ -548,32 +530,17 @@ 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 := updateGroupRelation(session, user)
if err != nil {
session.Rollback()
return affected, err
}
}
affected, err := session.ID(core.PK{oldUser.Owner, oldUser.Name}).Cols(columns...).Update(user)
func updateUser(id string, user *User, columns []string) (int64, error) {
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
err := user.UpdateUserHash()
if err != nil {
session.Rollback()
return affected, err
}
err = session.Commit()
if err != nil {
session.Rollback()
return 0, err
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).Cols(columns...).Update(user)
if err != nil {
return 0, err
}
return affected, nil
}
@ -759,7 +726,7 @@ func GetUserInfo(user *User, scope string, aud string, host string) *Userinfo {
resp.Name = user.Name
resp.DisplayName = user.DisplayName
resp.Avatar = user.Avatar
resp.Groups = []string{user.Owner}
resp.Groups = user.Groups
}
if strings.Contains(scope, "email") {
resp.Email = user.Email
@ -790,12 +757,15 @@ 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
}
if user.Groups == nil {
user.Groups = []string{}
}
user.Permissions, err = GetPermissionsByUser(user.GetId())
return
}
@ -857,74 +827,24 @@ 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
return user.PreferredMfaType != ""
}
func (user *User) GetPreferMfa(masked bool) *MfaProps {
if len(user.MultiFactorAuths) == 0 {
func (user *User) GetPreferredMfaProps(masked bool) *MfaProps {
if user.PreferredMfaType == "" {
return nil
}
if masked {
if len(user.MultiFactorAuths) == 1 {
return GetMaskedProps(user.MultiFactorAuths[0])
}
for _, v := range user.MultiFactorAuths {
if v.IsPreferred {
return GetMaskedProps(v)
}
}
return GetMaskedProps(user.MultiFactorAuths[0])
} else {
if len(user.MultiFactorAuths) == 1 {
return user.MultiFactorAuths[0]
}
for _, v := range user.MultiFactorAuths {
if v.IsPreferred {
return v
}
}
return user.MultiFactorAuths[0]
}
return user.GetMfaProps(user.PreferredMfaType, masked)
}
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") || strings.Contains(err.Error(), "no such host") {
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,218 @@
// 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"
"strings"
"github.com/casdoor/casdoor/util"
"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 {
link := link
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 = util.UrlJoin(htmlUrl, faviconUrl)
}
}
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)
}

View File

@ -1,52 +0,0 @@
package object
import (
"errors"
"github.com/xorm-io/xorm"
)
type UserGroupRelation struct {
UserId string `xorm:"varchar(100) notnull pk" json:"userId"`
GroupId string `xorm:"varchar(100) notnull pk" json:"groupId"`
CreatedTime string `xorm:"created" json:"createdTime"`
UpdatedTime string `xorm:"updated" json:"updatedTime"`
}
func updateGroupRelation(session *xorm.Session, user *User) (int64, error) {
groupIds := user.Groups
physicalGroupCount, err := session.Where("type = ?", "Physical").In("id", user.Groups).Count(Group{})
if err != nil {
return 0, err
}
if physicalGroupCount > 1 {
return 0, errors.New("user can only be in one physical group")
}
groups := []*Group{}
err = session.In("id", groupIds).Find(&groups)
if err != nil {
return 0, err
}
if len(groups) == 0 || len(groups) != len(groupIds) {
return 0, nil
}
_, err = session.Delete(&UserGroupRelation{UserId: user.Id})
if err != nil {
return 0, err
}
relations := []*UserGroupRelation{}
for _, group := range groups {
relations = append(relations, &UserGroupRelation{UserId: user.Id, GroupId: group.Id})
}
_, err = session.Insert(relations)
if err != nil {
return 0, err
}
return 1, nil
}

View File

@ -83,6 +83,7 @@ func UploadUsers(owner string, fileId string) (bool, error) {
newUsers := []*User{}
for index, line := range table {
line := line
if index == 0 || parseLineItem(&line, 0) == "" {
continue
}

View File

@ -288,9 +288,7 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
itemsChanged = append(itemsChanged, item)
}
oldUserTwoFactorAuthJson, _ := json.Marshal(oldUser.MultiFactorAuths)
newUserTwoFactorAuthJson, _ := json.Marshal(newUser.MultiFactorAuths)
if string(oldUserTwoFactorAuthJson) != string(newUserTwoFactorAuthJson) {
if oldUser.PreferredMfaType != newUser.PreferredMfaType {
item := GetAccountItemByName("Multi-factor authentication", 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

@ -59,12 +59,12 @@ func (pp *PaypalPaymentProvider) Pay(providerName string, productName string, pa
bm := make(gopay.BodyMap)
bm.Set("intent", "CAPTURE")
bm.Set("purchase_units", pus)
bm.SetBodyMap("payment_source", func(b1 gopay.BodyMap) {
b1.SetBodyMap("paypal", func(b2 gopay.BodyMap) {
b2.Set("brand_name", "Casdoor")
b2.Set("return_url", returnUrl)
})
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

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

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

View File

@ -19,6 +19,7 @@ import (
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
@ -72,7 +73,7 @@ func StaticFilter(ctx *context.Context) {
}
func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string, old string, new string) {
f, err := os.Open(name)
f, err := os.Open(filepath.Clean(name))
if err != nil {
panic(err)
}

View File

@ -70,7 +70,7 @@ func (fileSystem FileSystem) Put(path string, reader io.Reader) (*oss.Object, er
return nil, err
}
dst, err := os.Create(fullPath)
dst, err := os.Create(filepath.Clean(fullPath))
if err == nil {
if seeker, ok := reader.(io.ReadSeeker); ok {

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

@ -26,6 +26,18 @@ func DeleteVal(values []string, val string) []string {
return newValues
}
func ReplaceVal(values []string, oldVal string, newVal string) []string {
newValues := []string{}
for _, v := range values {
if v == oldVal {
newValues = append(newValues, newVal)
} else {
newValues = append(newValues, v)
}
}
return newValues
}
func ContainsString(values []string, val string) bool {
sort.Strings(values)
return sort.SearchStrings(values, val) != len(values)

View File

@ -22,6 +22,7 @@ import (
"fmt"
"math/rand"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@ -201,7 +202,7 @@ func GetMinLenStr(strs ...string) string {
}
func ReadStringFromPath(path string) string {
data, err := os.ReadFile(path)
data, err := os.ReadFile(filepath.Clean(path))
if err != nil {
panic(err)
}
@ -278,3 +279,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

@ -18,6 +18,7 @@ import (
"bufio"
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"strconv"
@ -155,7 +156,7 @@ func GetVersionInfoFromFile() (*VersionInfo, error) {
_, filename, _, _ := runtime.Caller(0)
rootPath := path.Dir(path.Dir(filename))
file, err := os.Open(path.Join(rootPath, "version_info.txt"))
file, err := os.Open(filepath.Clean(path.Join(rootPath, "version_info.txt")))
if err != nil {
return res, err
}

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

@ -131,7 +131,7 @@ 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"});
@ -410,15 +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"));
res.push(Setting.getItem(<Link to="/groups">{i18next.t("general:Groups")}</Link>,
"/groups"));
}
if (Setting.isLocalAdminUser(this.state.account)) {
res.push(Setting.getItem(<Link to="/users">{i18next.t("general:Users")}</Link>,
"/users"
));
@ -560,8 +558,8 @@ 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="/group-tree/:organizationName" render={(props) => this.renderLoginIfNotLoggedIn(<GroupTreePage account={this.state.account} {...props} />)} />
<Route exact path="/group-tree/:organizationName/:groupName" render={(props) => this.renderLoginIfNotLoggedIn(<GroupTreePage 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} />)} />
@ -632,7 +630,7 @@ class App extends Component {
isWithoutCard() {
return Setting.isMobile() || window.location.pathname === "/chat" ||
window.location.pathname.startsWith("/group-tree");
window.location.pathname.startsWith("/trees");
}
renderContent() {

View File

@ -91,12 +91,12 @@ class GroupEditPage extends React.Component {
}
getParentIdOptions() {
const groups = this.state.groups.filter((group) => group.id !== this.state.group.id);
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({id: organization.name, displayName: organization.displayName});
groups.push({name: organization.name, displayName: organization.displayName});
}
return groups.map((group) => ({label: group.displayName, value: group.id}));
return groups.map((group) => ({label: group.displayName, value: group.name}));
}
renderGroup() {
@ -171,8 +171,8 @@ class GroupEditPage extends React.Component {
<Col span={22} >
<Select style={{width: "100%"}}
options={this.getParentIdOptions()}
value={this.state.group.parentGroupId} onChange={(value => {
this.updateGroupField("parentGroupId", value);
value={this.state.group.parentId} onChange={(value => {
this.updateGroupField("parentId", value);
}
)} />
</Col>
@ -193,7 +193,7 @@ class GroupEditPage extends React.Component {
submitGroupEdit(willExist) {
const group = Setting.deepCopy(this.state.group);
group["isTopGroup"] = this.state.organizations.some((organization) => organization.name === group.parentGroupId);
group["isTopGroup"] = this.state.organizations.some((organization) => organization.name === group.parentId);
GroupBackend.updateGroup(this.state.organizationName, this.state.groupName, group)
.then((res) => {

View File

@ -56,7 +56,7 @@ class GroupListPage extends BaseListPage {
updatedTime: moment().format(),
displayName: `New Group - ${randomName}`,
type: "Virtual",
parentGroupId: this.props.account.owner,
parentId: this.props.account.owner,
isTopGroup: true,
isEnabled: true,
};
@ -96,7 +96,7 @@ class GroupListPage extends BaseListPage {
});
}
renderTable(groups) {
renderTable(data) {
const columns = [
{
title: i18next.t("general:Name"),
@ -174,18 +174,18 @@ class GroupListPage extends BaseListPage {
},
{
title: i18next.t("group:Parent group"),
dataIndex: "parentGroupId",
key: "parentGroupId",
dataIndex: "parentId",
key: "parentId",
width: "110px",
sorter: true,
...this.getColumnSearchProps("parentGroupId"),
...this.getColumnSearchProps("parentId"),
render: (text, record, index) => {
if (record.isTopGroup) {
return <Link to={`/organizations/${record.parentGroupId}`}>
{record.parentGroupId}
return <Link to={`/organizations/${record.parentId}`}>
{record.parentId}
</Link>;
}
const parentGroup = this.state.groups.find((group) => group.id === text);
const parentGroup = this.state.groups.find((group) => group.name === text);
if (parentGroup === undefined) {
return "";
}
@ -201,10 +201,12 @@ class GroupListPage extends BaseListPage {
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)}
>
@ -224,7 +226,7 @@ class GroupListPage extends BaseListPage {
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={groups} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
<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;

View File

@ -27,11 +27,11 @@ class GroupTreePage extends React.Component {
super(props);
this.state = {
classes: props,
owner: Setting.isAdminUser(this.props.account) ? "" : this.props.account.owner,
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
groupName: this.props.match?.params.groupName,
groupId: "",
treeData: [],
selectedKeys: [],
selectedKeys: [this.props.match?.params.groupName],
};
}
@ -52,9 +52,8 @@ class GroupTreePage extends React.Component {
getTreeData() {
GroupBackend.getGroups(this.state.organizationName, true).then((res) => {
if (res.status === "ok") {
const tree = res.data;
this.setState({
treeData: tree,
treeData: res.data,
});
} else {
Setting.showMessage("error", res.msg);
@ -66,7 +65,6 @@ class GroupTreePage extends React.Component {
const haveChildren = Array.isArray(treeData.children) && treeData.children.length > 0;
const isSelected = this.state.groupName === treeData.key;
return {
id: treeData.id,
key: treeData.key,
title: <Space>
{treeData.type === "Physical" ? <UsergroupAddOutlined /> : <HolderOutlined />}
@ -121,6 +119,7 @@ class GroupTreePage extends React.Component {
this.props.history.push(`/groups/${this.state.organizationName}/${treeData.key}`);
}}
/>
{!haveChildren &&
<DeleteOutlined
style={{
visibility: "visible",
@ -155,6 +154,7 @@ class GroupTreePage extends React.Component {
});
}}
/>
}
</React.Fragment>
)}
</Space>,
@ -183,9 +183,8 @@ class GroupTreePage extends React.Component {
this.setState({
selectedKeys: selectedKeys,
groupName: info.node.key,
groupId: info.node.id,
});
this.props.history.push(`/group-tree/${this.state.organizationName}/${info.node.key}`);
this.props.history.push(`/trees/${this.state.organizationName}/${info.node.key}`);
};
const onExpand = (expandedKeysValue) => {
this.setState({
@ -203,6 +202,7 @@ class GroupTreePage extends React.Component {
blockNode={true}
defaultSelectedKeys={[this.state.groupName]}
defaultExpandAll={true}
selectedKeys={this.state.selectedKeys}
expandedKeys={this.state.expandedKeys}
onSelect={onSelect}
onExpand={onExpand}
@ -213,16 +213,20 @@ class GroupTreePage extends React.Component {
}
renderOrganizationSelect() {
return <OrganizationSelect
initValue={this.state.organizationName}
style={{width: "100%"}}
onChange={(value) => {
this.setState({
organizationName: value,
});
this.props.history.push(`/group-tree/${value}`);
}}
/>;
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) {
@ -234,7 +238,7 @@ class GroupTreePage extends React.Component {
updatedTime: moment().format(),
displayName: `New Group - ${randomName}`,
type: "Virtual",
parentGroupId: isRoot ? this.state.organizationName : this.state.groupId,
parentId: isRoot ? this.state.organizationName : this.state.groupName,
isTopGroup: isRoot,
isEnabled: true,
};
@ -267,25 +271,24 @@ class GroupTreePage extends React.Component {
<Row>
<Col span={5}>
<Row>
<Col span={24} style={{textAlign: "left"}}>
<Col span={24} style={{textAlign: "center"}}>
{this.renderOrganizationSelect()}
</Col>
</Row>
<Row>
<Col span={24} style={{marginTop: "10px", textAlign: "left"}}>
<Button
<Col span={24} style={{marginTop: "10px"}}>
<Button size={"small"}
onClick={() => {
this.setState({
groupName: null,
groupId: null,
selectedKeys: [],
groupName: undefined,
});
this.props.history.push(`/group-tree/${this.state.organizationName}`);
}}>
{i18next.t("group:Show organization users")}
</Button>
<Button size={"small"} type={"primary"} style={{marginLeft: "10px"}}
onClick={() => this.addGroup(true)}
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>
@ -300,8 +303,8 @@ class GroupTreePage extends React.Component {
<UserListPage
organizationName={this.state.organizationName}
groupName={this.state.groupName}
groupId={this.state.groupId}
{...this.props} />
{...this.props}
/>
</Col>
</Row>
</div>

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,6 +61,7 @@ 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"},
@ -227,7 +229,7 @@ class OrganizationListPage extends BaseListPage {
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/group-tree/${record.name}`)}>{i18next.t("general:Groups")}</Button>
<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
@ -255,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

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

View File

@ -34,7 +34,6 @@ import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
import PopconfirmModal from "./common/modal/PopconfirmModal";
import {DeleteMfa} from "./backend/MfaBackend";
import {CheckCircleOutlined, HolderOutlined, UsergroupAddOutlined} from "@ant-design/icons";
import {SmsMfaType} from "./auth/MfaSetupPage";
import * as MfaBackend from "./backend/MfaBackend";
const {Option} = Select;
@ -91,6 +90,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) => {
@ -178,16 +188,6 @@ class UserEditPage extends React.Component {
return this.props.account.countryCode;
}
getMfaProps(type = "") {
if (!(this.state.multiFactorAuths?.length > 0)) {
return [];
}
if (type === "") {
return this.state.multiFactorAuths;
}
return this.state.multiFactorAuths.filter(mfaProps => mfaProps.type === type);
}
loadMore = (table, type) => {
return <div
style={{
@ -205,13 +205,12 @@ class UserEditPage extends React.Component {
</div>;
};
deleteMfa = (id) => {
deleteMfa = () => {
this.setState({
RemoveMfaLoading: true,
});
DeleteMfa({
id: id,
owner: this.state.user.owner,
name: this.state.user.name,
}).then((res) => {
@ -266,6 +265,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"}} >
@ -293,7 +297,7 @@ class UserEditPage extends React.Component {
</Col>
<Col span={22} >
<Select virtual={false} mode="multiple" style={{width: "100%"}} disabled={disabled} value={this.state.user.groups ?? []} onChange={(value => {
if (this.state.groups?.filter(group => value.includes(group.id))
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;
@ -303,7 +307,7 @@ class UserEditPage extends React.Component {
})}
>
{
this.state.groups?.map((group) => <Option key={group.id} value={group.id}>
this.state.groups?.map((group) => <Option key={group.name} value={group.name}>
<Space>
{group.type === "Physical" ? <UsergroupAddOutlined /> : <HolderOutlined />}
{group.displayName}
@ -394,7 +398,7 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"))} :
</Col>
<Col span={22} >
<PasswordModal user={this.state.user} account={this.props.account} disabled={disabled} />
<PasswordModal user={this.state.user} organization={this.state.application?.organizationObj} account={this.props.account} disabled={disabled} />
</Col>
</Row>
);
@ -431,7 +435,7 @@ class UserEditPage extends React.Component {
<CountryCodeSelect
style={{width: "30%"}}
// disabled={!Setting.isLocalAdminUser(this.props.account) ? true : disabled}
value={this.state.user.countryCode}
initValue={this.state.user.countryCode}
onChange={(value) => {
this.updateUserField("countryCode", value);
}}
@ -691,6 +695,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"}} >
@ -813,61 +848,61 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("mfa:Multi-factor authentication"), i18next.t("mfa:Multi-factor authentication - Tooltip "))} :
</Col>
<Col span={22} >
<Card title={i18next.t("mfa:Multi-factor methods")}>
<Card type="inner" title={i18next.t("mfa:SMS/Email message")}>
<List
itemLayout="horizontal"
dataSource={this.getMfaProps(SmsMfaType)}
loadMore={this.loadMore(this.state.multiFactorAuths, SmsMfaType)}
renderItem={(item, index) => (
<List.Item>
<div>
{item?.id === undefined ?
<Button type={"default"} onClick={() => {
Setting.goToLink("/mfa-authentication/setup");
}}>
{i18next.t("mfa:Setup")}
</Button> :
<Card title={i18next.t("mfa:Multi-factor methods")}
extra={this.state.multiFactorAuths?.some(mfaProps => mfaProps.enabled) ?
<PopconfirmModal
text={i18next.t("general:Disable")}
title={i18next.t("general:Sure to disable") + "?"}
onConfirm={() => this.deleteMfa()}
/> : null
}>
<List
rowKey="mfaType"
itemLayout="horizontal"
dataSource={this.state.multiFactorAuths}
renderItem={(item, index) => (
<List.Item>
<Space>
{i18next.t("general:Type")}: {item.mfaType}
{item.secret}
</Space>
{item.enabled ? (
<Space>
{item.enabled ?
<Tag icon={<CheckCircleOutlined />} color="success">
{i18next.t("general:Enabled")}
</Tag>
</Tag> : null
}
{item.secret}
</div>
{item?.id === undefined ? null :
<div>
{item.isPreferred ?
<Tag icon={<CheckCircleOutlined />} color="blue" style={{marginRight: 20}} >
{i18next.t("mfa:preferred")}
</Tag> :
<Button type="primary" style={{marginRight: 20}} onClick={() => {
const values = {
owner: this.state.user.owner,
name: this.state.user.name,
id: item.id,
};
MfaBackend.SetPreferredMfa(values).then((res) => {
if (res.status === "ok") {
this.setState({
multiFactorAuths: res.data,
});
}
});
}}>
{i18next.t("mfa:Set preferred")}
</Button>
}
<PopconfirmModal
title={i18next.t("general:Sure to delete") + "?"}
onConfirm={() => this.deleteMfa(item.id)}
>
</PopconfirmModal>
</div>
}
</List.Item>
)}
/>
</Card>
{item.isPreferred ?
<Tag icon={<CheckCircleOutlined />} color="blue" style={{marginRight: 20}} >
{i18next.t("mfa:preferred")}
</Tag> :
<Button type="primary" style={{marginRight: 20}} onClick={() => {
const values = {
owner: this.state.user.owner,
name: this.state.user.name,
mfaType: item.mfaType,
};
MfaBackend.SetPreferredMfa(values).then((res) => {
if (res.status === "ok") {
this.setState({
multiFactorAuths: res.data,
});
}
});
}}>
{i18next.t("mfa:Set preferred")}
</Button>
}
</Space>
) : <Button type={"default"} onClick={() => {
Setting.goToLink(`/mfa-authentication/setup?mfaType=${item.mfaType}`);
}}>
{i18next.t("mfa:Setup")}
</Button>}
</List.Item>
)}
/>
</Card>
</Col>
</Row>

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";
@ -75,7 +75,7 @@ class UserListPage extends BaseListPage {
phone: Setting.getRandomNumber(),
countryCode: this.state.organization.countryCodes?.length > 0 ? this.state.organization.countryCodes[0] : "",
address: [],
groups: this.props.groupId !== undefined ? [this.props.groupId] : [],
groups: this.props.groupName ? [this.props.groupName] : [],
affiliation: "Example Inc.",
tag: "staff",
region: "",
@ -124,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") {
@ -142,10 +162,14 @@ class UserListPage extends BaseListPage {
getOrganization(organizationName) {
OrganizationBackend.getOrganization("admin", organizationName)
.then((organization) => {
this.setState({
organization: organization,
});
.then((res) => {
if (res.status === "ok") {
this.setState({
organization: res.data,
});
} else {
Setting.showMessage("error", `Failed to get organization: ${res.msg}`);
}
});
}
@ -372,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>
);
},
},
@ -449,7 +483,7 @@ class UserListPage extends BaseListPage {
});
} else {
(this.props.groupName ?
UserBackend.getUsers(this.state.organizationName, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder, `${this.state.organizationName}/${this.props.groupName}`) :
UserBackend.getUsers(this.state.organizationName, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder, this.props.groupName) :
UserBackend.getUsers(this.state.organizationName, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder))
.then((res) => {
this.setState({

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

@ -16,8 +16,8 @@ import React, {useState} from "react";
import i18next from "i18next";
import {Button, Input} from "antd";
import * as AuthBackend from "./AuthBackend";
import {SmsMfaType} from "./MfaSetupPage";
import {MfaSmsVerifyForm} from "./MfaVerifyForm";
import {EmailMfaType, SmsMfaType} from "./MfaSetupPage";
import {MfaSmsVerifyForm, mfaAuth} from "./MfaVerifyForm";
export const NextMfa = "NextMfa";
export const RequiredMfa = "RequiredMfa";
@ -26,20 +26,20 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
formValues.password = "";
formValues.username = "";
const [loading, setLoading] = useState(false);
const [type, setType] = useState(mfaProps.type);
const [mfaType, setMfaType] = useState(mfaProps.mfaType);
const [recoveryCode, setRecoveryCode] = useState("");
const verify = ({passcode}) => {
setLoading(true);
const values = {...formValues, passcode, mfaType: type};
const values = {...formValues, passcode, mfaType};
AuthBackend.login(values, oAuthParams).then((res) => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res.msg);
}
}).catch((reason) => {
onFail(reason.message);
}).catch((res) => {
onFail(res.message);
}).finally(() => {
setLoading(false);
});
@ -49,19 +49,18 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
setLoading(true);
AuthBackend.login({...formValues, recoveryCode}, oAuthParams).then(res => {
if (res.status === "ok") {
onSuccess();
onSuccess(res);
} else {
onFail(res.msg);
}
}).catch((reason) => {
onFail(reason.message);
}).catch((res) => {
onFail(res.message);
}).finally(() => {
setLoading(false);
});
};
switch (type) {
case SmsMfaType:
if (mfaType === SmsMfaType || mfaType === EmailMfaType) {
return (
<div style={{width: 300, height: 350}}>
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
@ -72,20 +71,21 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
</div>
<MfaSmsVerifyForm
mfaProps={mfaProps}
method={mfaAuth}
onFinish={verify}
application={application}
/>
<span style={{float: "right"}}>
{i18next.t("mfa:Have problems?")}
<a onClick={() => {
setType("recovery");
setMfaType("recovery");
}}>
{i18next.t("mfa:Use a recovery code")}
</a>
</span>
</div>
);
case "recovery":
} else if (mfaType === "recovery") {
return (
<div style={{width: 300, height: 350}}>
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
@ -108,14 +108,12 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
<span style={{float: "right"}}>
{i18next.t("mfa:Have problems?")}
<a onClick={() => {
setType(mfaProps.type);
setMfaType(mfaProps.mfaType);
}}>
{i18next.t("mfa:Use SMS verification code")}
</a>
</span>
</div>
);
default:
return null;
}
}

View File

@ -12,18 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import React, {useState} from "react";
import React, {useEffect, useState} from "react";
import {Button, Col, Form, Input, Result, Row, Steps} from "antd";
import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as Setting from "../Setting";
import i18next from "i18next";
import * as MfaBackend from "../backend/MfaBackend";
import {CheckOutlined, KeyOutlined, LockOutlined, UserOutlined} from "@ant-design/icons";
import * as UserBackend from "../backend/UserBackend";
import {MfaSmsVerifyForm, MfaTotpVerifyForm} from "./MfaVerifyForm";
import * as ApplicationBackend from "../backend/ApplicationBackend";
import {MfaSmsVerifyForm, MfaTotpVerifyForm, mfaSetup} from "./MfaVerifyForm";
const {Step} = Steps;
export const EmailMfaType = "email";
export const SmsMfaType = "sms";
export const TotpMfaType = "app";
@ -76,12 +76,29 @@ function CheckPasswordForm({user, onSuccess, onFail}) {
);
}
export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) {
export function MfaVerifyForm({mfaType, application, user, onSuccess, onFail}) {
const [form] = Form.useForm();
mfaProps = mfaProps ?? {type: ""};
const [mfaProps, setMfaProps] = useState({mfaType: mfaType});
useEffect(() => {
if (mfaType === SmsMfaType) {
setMfaProps({
mfaType: mfaType,
secret: user.phone,
countryCode: user.countryCode,
});
}
if (mfaType === EmailMfaType) {
setMfaProps({
mfaType: mfaType,
secret: user.email,
});
}
}, [mfaType]);
const onFinish = ({passcode}) => {
const data = {passcode, type: mfaProps.type, ...user};
const data = {passcode, mfaType: mfaType, ...user};
MfaBackend.MfaSetupVerify(data)
.then((res) => {
if (res.status === "ok") {
@ -98,20 +115,24 @@ export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail})
});
};
if (mfaProps.type === SmsMfaType) {
return <MfaSmsVerifyForm onFinish={onFinish} application={application} />;
} else if (mfaProps.type === TotpMfaType) {
return <MfaTotpVerifyForm onFinish={onFinish} mfaProps={mfaProps} />;
if (mfaType === null || mfaType === undefined || mfaProps.secret === undefined) {
return <div></div>;
}
if (mfaType === SmsMfaType || mfaType === EmailMfaType) {
return <MfaSmsVerifyForm onFinish={onFinish} application={application} method={mfaSetup} mfaProps={mfaProps} />;
} else if (mfaType === TotpMfaType) {
return <MfaTotpVerifyForm onFinish={onFinish} />;
} else {
return <div></div>;
}
}
function EnableMfaForm({user, mfaProps, onSuccess, onFail}) {
function EnableMfaForm({user, mfaType, recoveryCodes, onSuccess, onFail}) {
const [loading, setLoading] = useState(false);
const requestEnableTotp = () => {
const data = {
type: mfaProps.type,
mfaType,
...user,
};
setLoading(true);
@ -131,7 +152,7 @@ function EnableMfaForm({user, mfaProps, onSuccess, onFail}) {
<div style={{width: "400px"}}>
<p>{i18next.t("mfa:Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code")}</p>
<br />
<code style={{fontStyle: "solid"}}>{mfaProps.recoveryCodes[0]}</code>
<code style={{fontStyle: "solid"}}>{recoveryCodes[0]}</code>
<Button style={{marginTop: 24}} loading={loading} onClick={() => {
requestEnableTotp();
}} block type="primary">
@ -146,12 +167,13 @@ class MfaSetupPage extends React.Component {
super(props);
this.state = {
account: props.account,
applicationName: (props.applicationName ?? props.account?.signupApplication) ?? "",
application: this.props.application ?? null,
applicationName: props.account.signupApplication ?? "",
isAuthenticated: props.isAuthenticated ?? false,
isPromptPage: props.isPromptPage,
redirectUri: props.redirectUri,
current: props.current ?? 0,
type: props.type ?? SmsMfaType,
mfaType: props.mfaType ?? new URLSearchParams(props.location?.search)?.get("mfaType") ?? SmsMfaType,
mfaProps: null,
};
}
@ -163,7 +185,7 @@ class MfaSetupPage extends React.Component {
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.isAuthenticated === true && this.state.mfaProps === null) {
MfaBackend.MfaSetupInitiate({
type: this.state.type,
mfaType: this.state.mfaType,
...this.getUser(),
}).then((res) => {
if (res.status === "ok") {
@ -178,6 +200,10 @@ class MfaSetupPage extends React.Component {
}
getApplication() {
if (this.state.application !== null) {
return;
}
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => {
if (application !== null) {
@ -217,25 +243,58 @@ class MfaSetupPage extends React.Component {
return null;
}
return <MfaVerifyForm
mfaProps={this.state.mfaProps}
application={this.state.application}
user={this.getUser()}
onSuccess={() => {
this.setState({
current: this.state.current + 1,
});
}}
onFail={(res) => {
Setting.showMessage("error", i18next.t("general:Failed to verify"));
}}
/>;
return (
<div>
<MfaVerifyForm
mfaType={this.state.mfaType}
application={this.state.application}
user={this.props.account}
onSuccess={() => {
this.setState({
current: this.state.current + 1,
});
}}
onFail={(res) => {
Setting.showMessage("error", i18next.t("general:Failed to verify"));
}}
/>
<Col span={24} style={{display: "flex", justifyContent: "left"}}>
{(this.state.mfaType === EmailMfaType || this.props.account.mfaEmailEnabled) ? null :
<Button type={"link"} onClick={() => {
if (this.state.isPromptPage) {
this.props.history.push(`/prompt/${this.state.application.name}?promptType=mfa&mfaType=${EmailMfaType}`);
} else {
this.props.history.push(`/mfa-authentication/setup?mfaType=${EmailMfaType}`);
}
this.setState({
mfaType: EmailMfaType,
});
}
}>{i18next.t("mfa:Use Email")}</Button>
}
{
(this.state.mfaType === SmsMfaType || this.props.account.mfaPhoneEnabled) ? null :
<Button type={"link"} onClick={() => {
if (this.state.isPromptPage) {
this.props.history.push(`/prompt/${this.state.application.name}?promptType=mfa&mfaType=${SmsMfaType}`);
} else {
this.props.history.push(`/mfa-authentication/setup?mfaType=${SmsMfaType}`);
}
this.setState({
mfaType: SmsMfaType,
});
}
}>{i18next.t("mfa:Use SMS")}</Button>
}
</Col>
</div>
);
case 2:
if (!this.state.isAuthenticated) {
return null;
}
return <EnableMfaForm user={this.getUser()} mfaProps={{type: this.state.type, ...this.state.mfaProps}}
return <EnableMfaForm user={this.getUser()} mfaType={this.state.mfaType} recoveryCodes={this.state.mfaProps.recoveryCodes}
onSuccess={() => {
Setting.showMessage("success", i18next.t("general:Enabled successfully"));
if (this.state.isPromptPage && this.state.redirectUri) {
@ -276,15 +335,14 @@ class MfaSetupPage extends React.Component {
</Row>
<Row>
<Col span={24}>
<Steps current={this.state.current} style={{
width: "90%",
maxWidth: "500px",
margin: "auto",
marginTop: "80px",
}} >
<Step title={i18next.t("mfa:Verify Password")} icon={<UserOutlined />} />
<Step title={i18next.t("mfa:Verify Code")} icon={<KeyOutlined />} />
<Step title={i18next.t("general:Enable")} icon={<CheckOutlined />} />
<Steps current={this.state.current}
items={[
{title: i18next.t("mfa:Verify Password"), icon: <UserOutlined />},
{title: i18next.t("mfa:Verify Code"), icon: <KeyOutlined />},
{title: i18next.t("general:Enable"), icon: <CheckOutlined />},
]}
style={{width: "90%", maxWidth: "500px", margin: "auto", marginTop: "80px",
}} >
</Steps>
</Col>
</Row>

View File

@ -20,52 +20,70 @@ import * as Setting from "../Setting";
import React from "react";
import copy from "copy-to-clipboard";
import {CountryCodeSelect} from "../common/select/CountryCodeSelect";
import {EmailMfaType} from "./MfaSetupPage";
export const MfaSmsVerifyForm = ({mfaProps, application, onFinish}) => {
const [dest, setDest] = React.useState(mfaProps?.secret ?? "");
export const mfaAuth = "mfaAuth";
export const mfaSetup = "mfaSetup";
export const MfaSmsVerifyForm = ({mfaProps, application, onFinish, method}) => {
const [dest, setDest] = React.useState(mfaProps.secret ?? "");
const [form] = Form.useForm();
const isEmail = () => {
return mfaProps.mfaType === EmailMfaType;
};
return (
<Form
form={form}
style={{width: "300px"}}
onFinish={onFinish}
initialValues={{
countryCode: mfaProps.countryCode,
}}
>
{mfaProps?.secret !== undefined ?
<div style={{marginBottom: 20}}>
{Setting.IsEmail(dest) ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest}
{mfaProps.secret !== "" ?
<div style={{marginBottom: 20, textAlign: "left", gap: 8}}>
{isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {mfaProps.secret}
</div> :
<Input.Group compact style={{width: "300Px", marginBottom: "30px"}}>
{Setting.IsEmail(dest) ? null :
(<React.Fragment>
<p>{isEmail() ? i18next.t("mfa:Please bind your email first, the system will automatically uses the mail for multi-factor authentication") :
i18next.t("mfa:Please bind your phone first, the system automatically uses the phone for multi-factor authentication")}
</p>
<Input.Group compact style={{width: "300Px", marginBottom: "30px"}}>
{isEmail() ? null :
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: false,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
initValue={mfaProps.countryCode}
style={{width: "30%"}}
countryCodes={application.organizationObj.countryCodes}
/>
</Form.Item>
}
<Form.Item
name="countryCode"
name="dest"
noStyle
rules={[
{
required: false,
message: i18next.t("signup:Please select your country code!"),
},
]}
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
>
<CountryCodeSelect
style={{width: "30%"}}
countryCodes={application.organizationObj.countryCodes}
<Input
style={{width: isEmail() ? "100% " : "70%"}}
onChange={(e) => {setDest(e.target.value);}}
prefix={<UserOutlined />}
placeholder={isEmail() ? i18next.t("general:Email") : i18next.t("general:Phone")}
/>
</Form.Item>
}
<Form.Item
name="dest"
noStyle
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
>
<Input
style={{width: Setting.IsEmail(dest) ? "100% " : "70%"}}
onChange={(e) => {setDest(e.target.value);}}
prefix={<UserOutlined />}
placeholder={i18next.t("general:Phone or email")}
/>
</Form.Item>
</Input.Group>
</Input.Group>
</React.Fragment>
)
}
<Form.Item
name="passcode"
@ -73,8 +91,8 @@ export const MfaSmsVerifyForm = ({mfaProps, application, onFinish}) => {
>
<SendCodeInput
countryCode={form.getFieldValue("countryCode")}
method={mfaProps?.id === undefined ? "mfaSetup" : "mfaAuth"}
onButtonClickArgs={[dest, Setting.IsEmail(dest) ? "email" : "phone", Setting.getApplicationName(application)]}
method={method}
onButtonClickArgs={[mfaProps.secret || dest, isEmail() ? "email" : "phone", Setting.getApplicationName(application)]}
application={application}
/>
</Form.Item>

View File

@ -28,14 +28,13 @@ import MfaSetupPage from "./MfaSetupPage";
class PromptPage extends React.Component {
constructor(props) {
super(props);
const params = new URLSearchParams(this.props.location.search);
this.state = {
classes: props,
type: props.type,
applicationName: props.applicationName ?? (props.match === undefined ? null : props.match.params.applicationName),
application: null,
user: null,
promptType: params.get("promptType"),
promptType: new URLSearchParams(this.props.location.search).get("promptType"),
};
}
@ -233,19 +232,22 @@ class PromptPage extends React.Component {
{this.renderContent(application)}
<div style={{marginTop: "50px"}}>
<Button disabled={!Setting.isPromptAnswered(this.state.user, application)} type="primary" size="large" onClick={() => {this.submitUserEdit(true);}}>{i18next.t("code:Submit and complete")}</Button>
</div>;
</div>
</>;
}
renderPromptMfa() {
return <MfaSetupPage
applicationName={this.getApplicationObj().name}
account={this.props.account}
current={1}
isAuthenticated={true}
isPromptPage={true}
redirectUri={this.getRedirectUrl()}
/>;
return (
<MfaSetupPage
application={this.getApplicationObj()}
account={this.props.account}
current={1}
isAuthenticated={true}
isPromptPage={true}
redirectUri={this.getRedirectUrl()}
{...this.props}
/>
);
}
render() {

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

@ -18,7 +18,7 @@ export function MfaSetupInitiate(values) {
const formData = new FormData();
formData.append("owner", values.owner);
formData.append("name", values.name);
formData.append("type", values.type);
formData.append("mfaType", values.mfaType);
return fetch(`${Setting.ServerUrl}/api/mfa/setup/initiate`, {
method: "POST",
credentials: "include",
@ -30,7 +30,7 @@ export function MfaSetupVerify(values) {
const formData = new FormData();
formData.append("owner", values.owner);
formData.append("name", values.name);
formData.append("type", values.type);
formData.append("mfaType", values.mfaType);
formData.append("passcode", values.passcode);
return fetch(`${Setting.ServerUrl}/api/mfa/setup/verify`, {
method: "POST",
@ -41,7 +41,7 @@ export function MfaSetupVerify(values) {
export function MfaSetupEnable(values) {
const formData = new FormData();
formData.append("type", values.type);
formData.append("mfaType", values.mfaType);
formData.append("owner", values.owner);
formData.append("name", values.name);
return fetch(`${Setting.ServerUrl}/api/mfa/setup/enable`, {
@ -53,7 +53,6 @@ export function MfaSetupEnable(values) {
export function DeleteMfa(values) {
const formData = new FormData();
formData.append("id", values.id);
formData.append("owner", values.owner);
formData.append("name", values.name);
return fetch(`${Setting.ServerUrl}/api/delete-mfa`, {
@ -65,7 +64,7 @@ export function DeleteMfa(values) {
export function SetPreferredMfa(values) {
const formData = new FormData();
formData.append("id", values.id);
formData.append("mfaType", values.mfaType);
formData.append("owner", values.owner);
formData.append("name", values.name);
return fetch(`${Setting.ServerUrl}/api/set-preferred-mfa`, {

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 = "", groupId = "") {
return fetch(`${Setting.ServerUrl}/api/get-users?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}&groupId=${groupId}`, {
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,7 @@ import i18next from "i18next";
import React from "react";
import * as UserBackend from "../../backend/UserBackend";
import * as Setting from "../../Setting";
import * as PasswordChecker from "../PasswordChecker";
export const PasswordModal = (props) => {
const [visible, setVisible] = React.useState(false);
@ -25,8 +26,20 @@ export const PasswordModal = (props) => {
const [newPassword, setNewPassword] = React.useState("");
const [rePassword, setRePassword] = React.useState("");
const {user} = props;
const {organization} = 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(() => {
if (organization) {
setPasswordOptions(organization.passwordOptions);
}
}, [user.owner]);
const showModal = () => {
setVisible(true);
};
@ -34,6 +47,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,13 +76,32 @@ export const PasswordModal = (props) => {
return;
}
setConfirmLoading(true);
UserBackend.setPassword(user.owner, user.name, oldPassword, newPassword).then((res) => {
if (organization === null) {
Setting.showMessage("error", "organization is null");
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}`));}
});
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);
});
};
const hasOldPassword = user.password !== "";
@ -79,11 +129,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

@ -17,13 +17,17 @@ import * as Setting from "../../Setting";
import React from "react";
export const CountryCodeSelect = (props) => {
const {onChange, style, disabled} = props;
const {onChange, style, disabled, initValue} = props;
const countryCodes = props.countryCodes ?? [];
const [value, setValue] = React.useState("");
React.useEffect(() => {
const initValue = countryCodes.length > 0 ? countryCodes[0] : "";
handleOnChange(initValue);
if (initValue !== undefined) {
setValue(initValue);
} else {
const initValue = countryCodes.length > 0 ? countryCodes[0] : "";
handleOnChange(initValue);
}
}, []);
const handleOnChange = (value) => {

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",
@ -195,6 +202,7 @@
"Delete": "Löschen",
"Description": "Beschreibung",
"Description - Tooltip": "Detaillierte Beschreibungsinformationen zur Referenz, Casdoor selbst wird es nicht verwenden",
"Disable": "Disable",
"Display name": "Anzeigename",
"Display name - Tooltip": "Ein benutzerfreundlicher, leicht lesbarer Name, der öffentlich in der Benutzeroberfläche angezeigt wird",
"Down": "Nach unten",
@ -209,6 +217,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",
@ -253,6 +262,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",
@ -262,7 +273,6 @@
"Permissions - Tooltip": "Berechtigungen, die diesem Benutzer gehören",
"Phone": "Telefon",
"Phone - Tooltip": "Telefonnummer",
"Phone or email": "Phone or email",
"Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip",
"Plans": "Pläne",
@ -301,10 +311,13 @@
"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 disable": "Sure to disable",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "Synchronisieren",
"Syncers": "Syncers",
@ -329,6 +342,7 @@
"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": {
@ -337,6 +351,7 @@
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
@ -382,7 +397,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",
@ -424,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
@ -926,6 +943,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",
@ -195,6 +202,7 @@
"Delete": "Delete",
"Description": "Description",
"Description - Tooltip": "Detailed description information for reference, Casdoor itself will not use it",
"Disable": "Disable",
"Display name": "Display name",
"Display name - Tooltip": "A user-friendly, easily readable name displayed publicly in the UI",
"Down": "Down",
@ -209,6 +217,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",
@ -253,6 +262,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",
@ -262,7 +273,6 @@
"Permissions - Tooltip": "Permissions owned by this user",
"Phone": "Phone",
"Phone - Tooltip": "Phone number",
"Phone or email": "Phone or email",
"Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip",
"Plans": "Plans",
@ -301,10 +311,13 @@
"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 disable": "Sure to disable",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "Sync",
"Syncers": "Syncers",
@ -329,6 +342,7 @@
"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": {
@ -337,6 +351,7 @@
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
@ -382,7 +397,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",
@ -424,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
@ -926,6 +943,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",
@ -195,6 +202,7 @@
"Delete": "Eliminar",
"Description": "Descripción",
"Description - Tooltip": "Información detallada de descripción para referencia, Casdoor en sí no la utilizará",
"Disable": "Disable",
"Display name": "Nombre de pantalla",
"Display name - Tooltip": "Un nombre fácil de usar y leer que se muestra públicamente en la interfaz de usuario",
"Down": "Abajo",
@ -209,6 +217,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",
@ -253,6 +262,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",
@ -262,7 +273,6 @@
"Permissions - Tooltip": "Permisos propiedad de este usuario",
"Phone": "Teléfono",
"Phone - Tooltip": "Número de teléfono",
"Phone or email": "Phone or email",
"Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip",
"Plans": "Planes",
@ -301,10 +311,13 @@
"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 disable": "Sure to disable",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "Sincronización",
"Syncers": "Sincronizadores",
@ -329,6 +342,7 @@
"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": {
@ -337,6 +351,7 @@
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
@ -382,7 +397,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",
@ -424,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
@ -926,6 +943,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",
@ -195,6 +202,7 @@
"Delete": "Supprimer",
"Description": "Description",
"Description - Tooltip": "Informations détaillées pour référence, Casdoor ne l'utilisera pas en soi",
"Disable": "Disable",
"Display name": "Nom d'affichage",
"Display name - Tooltip": "Un nom convivial et facilement lisible affiché publiquement dans l'interface utilisateur",
"Down": "En bas",
@ -209,6 +217,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",
@ -253,6 +262,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",
@ -262,7 +273,6 @@
"Permissions - Tooltip": "Autorisations détenues par cet utilisateur",
"Phone": "Téléphone",
"Phone - Tooltip": "Numéro de téléphone",
"Phone or email": "Phone or email",
"Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip",
"Plans": "Plans",
@ -301,10 +311,13 @@
"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 disable": "Sure to disable",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "Synchronisation",
"Syncers": "Synchroniseurs",
@ -329,6 +342,7 @@
"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": {
@ -337,6 +351,7 @@
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
@ -382,7 +397,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",
@ -424,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
@ -926,6 +943,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",
@ -195,6 +202,7 @@
"Delete": "Hapus",
"Description": "Deskripsi",
"Description - Tooltip": "Informasi deskripsi terperinci untuk referensi, Casdoor itu sendiri tidak akan menggunakannya",
"Disable": "Disable",
"Display name": "Nama tampilan",
"Display name - Tooltip": "Sebuah nama yang mudah digunakan dan mudah dibaca yang ditampilkan secara publik di UI",
"Down": "Turun",
@ -209,6 +217,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",
@ -253,6 +262,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",
@ -262,7 +273,6 @@
"Permissions - Tooltip": "Izin dimiliki oleh pengguna ini",
"Phone": "Telepon",
"Phone - Tooltip": "Nomor telepon",
"Phone or email": "Phone or email",
"Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip",
"Plans": "Rencana",
@ -301,10 +311,13 @@
"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 disable": "Sure to disable",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "Sinkronisasi",
"Syncers": "Sinkronisasi",
@ -329,6 +342,7 @@
"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": {
@ -337,6 +351,7 @@
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
@ -382,7 +397,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",
@ -424,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
@ -926,6 +943,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": "デフォルトのアバター",
@ -195,6 +202,7 @@
"Delete": "削除",
"Description": "説明",
"Description - Tooltip": "参照用の詳細な説明情報です。Casdoor自体はそれを使用しません",
"Disable": "Disable",
"Display name": "表示名",
"Display name - Tooltip": "UIで公開されている使いやすく読みやすい名前",
"Down": "ダウン",
@ -209,6 +217,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": "ファビコン",
@ -253,6 +262,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": "パスワードタイプ",
@ -262,7 +273,6 @@
"Permissions - Tooltip": "このユーザーが所有する権限",
"Phone": "電話",
"Phone - Tooltip": "電話番号",
"Phone or email": "Phone or email",
"Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip",
"Plans": "プラン",
@ -301,10 +311,13 @@
"Subscriptions": "サブスクリプション",
"Successfully added": "正常に追加されました",
"Successfully deleted": "正常に削除されました",
"Successfully removed": "Successfully removed",
"Successfully saved": "成功的に保存されました",
"Supported country codes": "サポートされている国コード",
"Supported country codes - Tooltip": "組織でサポートされている国コード。これらのコードは、SMS認証コードのプレフィックスとして選択できます",
"Sure to delete": "削除することが確実です",
"Sure to disable": "Sure to disable",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "同期",
"Syncers": "シンカーズ",
@ -329,6 +342,7 @@
"Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group",
"empty": "空",
"remove": "remove",
"{total} in total": "総計{total}"
},
"group": {
@ -337,6 +351,7 @@
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
@ -382,7 +397,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",
@ -424,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
@ -926,6 +943,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": "기본 아바타",
@ -195,6 +202,7 @@
"Delete": "삭제하기",
"Description": "설명",
"Description - Tooltip": "참고용으로 자세한 설명 정보가 제공됩니다. Casdoor 자체는 사용하지 않습니다",
"Disable": "Disable",
"Display name": "디스플레이 이름",
"Display name - Tooltip": "UI에서 공개적으로 표시되는 사용자 친화적이고 쉽게 읽을 수 있는 이름",
"Down": "아래로",
@ -209,6 +217,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": "파비콘",
@ -253,6 +262,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": "암호 유형",
@ -262,7 +273,6 @@
"Permissions - Tooltip": "이 사용자가 소유한 권한",
"Phone": "전화기",
"Phone - Tooltip": "전화 번호",
"Phone or email": "Phone or email",
"Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip",
"Plans": "플랜",
@ -301,10 +311,13 @@
"Subscriptions": "구독",
"Successfully added": "성공적으로 추가되었습니다",
"Successfully deleted": "성공적으로 삭제되었습니다",
"Successfully removed": "Successfully removed",
"Successfully saved": "성공적으로 저장되었습니다",
"Supported country codes": "지원되는 국가 코드들",
"Supported country codes - Tooltip": "조직에서 지원하는 국가 코드입니다. 이 코드들은 SMS 인증 코드를 보낼 때 접두사로 선택할 수 있습니다",
"Sure to delete": "삭제하시겠습니까?",
"Sure to disable": "Sure to disable",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "싱크",
"Syncers": "싱크어스",
@ -329,6 +342,7 @@
"Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group",
"empty": "빈",
"remove": "remove",
"{total} in total": "총 {total}개"
},
"group": {
@ -337,6 +351,7 @@
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
@ -382,7 +397,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",
@ -424,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
@ -926,6 +943,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",
@ -195,6 +202,7 @@
"Delete": "Excluir",
"Description": "Descrição",
"Description - Tooltip": "Informações de descrição detalhadas para referência, o Casdoor em si não irá utilizá-las",
"Disable": "Disable",
"Display name": "Nome de exibição",
"Display name - Tooltip": "Um nome amigável e facilmente legível exibido publicamente na interface do usuário",
"Down": "Descer",
@ -209,6 +217,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",
@ -253,6 +262,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",
@ -262,7 +273,6 @@
"Permissions - Tooltip": "Permissões pertencentes a este usuário",
"Phone": "Telefone",
"Phone - Tooltip": "Número de telefone",
"Phone or email": "Telefone ou email",
"Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip",
"Plans": "Kế hoạch",
@ -301,10 +311,13 @@
"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 disable": "Sure to disable",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "Sincronizar",
"Syncers": "Sincronizadores",
@ -329,6 +342,7 @@
"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": {
@ -337,6 +351,7 @@
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
@ -382,7 +397,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",
@ -424,12 +438,15 @@
"Multi-factor secret - Tooltip": "Segredo de vários fatores - Dica de ferramenta",
"Multi-factor secret to clipboard successfully": "Segredo de vários fatores copiado para a área de transferência com sucesso",
"Passcode": "Código de acesso",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Guarde este código de recuperação. Quando o seu dispositivo não puder fornecer um código de autenticação, você poderá redefinir a autenticação mfa usando este código de recuperação",
"Protect your account with Multi-factor authentication": "Proteja sua conta com autenticação de vários fatores",
"Recovery code": "Código de recuperação",
"SMS/Email message": "Mensagem SMS/E-mail",
"Set preferred": "Definir preferido",
"Setup": "Configuração",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Usar código de verificação SMS",
"Use a recovery code": "Usar um código de recuperação",
"Verification failed": "Verificação falhou",
@ -926,6 +943,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": "Стандартный аватар",
@ -195,6 +202,7 @@
"Delete": "Удалить",
"Description": "Описание",
"Description - Tooltip": "Подробная описательная информация для справки, Casdoor сам не будет использовать ее",
"Disable": "Disable",
"Display name": "Отображаемое имя",
"Display name - Tooltip": "Понятное для пользователя имя, легко читаемое и отображаемое публично в пользовательском интерфейсе (UI)",
"Down": "вниз",
@ -209,6 +217,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": "Фавикон",
@ -253,6 +262,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": "Тип пароля",
@ -262,7 +273,6 @@
"Permissions - Tooltip": "Разрешения, принадлежащие этому пользователю",
"Phone": "Телефон",
"Phone - Tooltip": "Номер телефона",
"Phone or email": "Phone or email",
"Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip",
"Plans": "Планы",
@ -301,10 +311,13 @@
"Subscriptions": "Подписки",
"Successfully added": "Успешно добавлено",
"Successfully deleted": "Успешно удалено",
"Successfully removed": "Successfully removed",
"Successfully saved": "Успешно сохранено",
"Supported country codes": "Поддерживаемые коды стран",
"Supported country codes - Tooltip": "Коды стран, поддерживаемые организацией. Эти коды могут быть выбраны в качестве префикса при отправке SMS-кодов подтверждения",
"Sure to delete": "Обязательное удаление",
"Sure to disable": "Sure to disable",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "Синхронизация",
"Syncers": "Синкеры",
@ -329,6 +342,7 @@
"Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group",
"empty": "пустые",
"remove": "remove",
"{total} in total": "{total} в общей сложности"
},
"group": {
@ -337,6 +351,7 @@
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
@ -382,7 +397,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",
@ -424,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
@ -926,6 +943,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",
@ -195,6 +202,7 @@
"Delete": "Xóa",
"Description": "Mô tả",
"Description - Tooltip": "Thông tin chi tiết mô tả cho tham khảo, Casdoor chính nó sẽ không sử dụng nó",
"Disable": "Disable",
"Display name": "Tên hiển thị",
"Display name - Tooltip": "Một tên dễ sử dụng, dễ đọc được hiển thị công khai trên giao diện người dùng",
"Down": "Xuống",
@ -209,6 +217,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",
@ -253,6 +262,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",
@ -262,7 +273,6 @@
"Permissions - Tooltip": "Quyền sở hữu của người dùng này",
"Phone": "Điện thoại",
"Phone - Tooltip": "Số điện thoại",
"Phone or email": "Phone or email",
"Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip",
"Plans": "Kế hoạch",
@ -301,10 +311,13 @@
"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 disable": "Sure to disable",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "Đồng bộ hoá",
"Syncers": "Đồng bộ hóa",
@ -329,6 +342,7 @@
"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": {
@ -337,6 +351,7 @@
"Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual"
},
"ldap": {
@ -382,7 +397,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",
@ -424,12 +438,15 @@
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code",
"SMS/Email message": "SMS/Email message",
"Set preferred": "Set preferred",
"Setup": "Setup",
"Use Email": "Use Email",
"Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code",
"Use a recovery code": "Use a recovery code",
"Verification failed": "Verification failed",
@ -926,6 +943,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": "默认头像",
@ -195,6 +202,7 @@
"Delete": "删除",
"Description": "描述信息",
"Description - Tooltip": "供人参考的详细描述信息Casdoor平台本身不会使用",
"Disable": "关闭",
"Display name": "显示名称",
"Display name - Tooltip": "在界面里公开显示的、易读的名称",
"Down": "下移",
@ -209,6 +217,7 @@
"Failed to delete": "删除失败",
"Failed to enable": "启用失败",
"Failed to get answer": "获取回答失败",
"Failed to remove": "移除失败",
"Failed to save": "保存失败",
"Failed to verify": "验证失败",
"Favicon": "Favicon",
@ -218,7 +227,7 @@
"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": "组",
"Groups - Tooltip": "Groups - Tooltip",
"Home": "首页",
"Home - Tooltip": "应用的首页",
@ -253,6 +262,8 @@
"Organizations": "组织",
"Password": "密码",
"Password - Tooltip": "请确认密码正确",
"Password complexity options": "密码复杂度选项",
"Password complexity options - Tooltip": "密码复杂度组合,登录密码复杂度必须符合该规范",
"Password salt": "密码Salt值",
"Password salt - Tooltip": "用于密码加密的随机参数",
"Password type": "密码类型",
@ -262,7 +273,6 @@
"Permissions - Tooltip": "该用户所拥有的权限",
"Phone": "手机号",
"Phone - Tooltip": "手机号",
"Phone or email": "手机或邮箱",
"Plan": "计划",
"Plan - Tooltip": "订阅里的计划",
"Plans": "计划",
@ -301,10 +311,13 @@
"Subscriptions": "订阅",
"Successfully added": "添加成功",
"Successfully deleted": "删除成功",
"Successfully removed": "移除成功",
"Successfully saved": "保存成功",
"Supported country codes": "支持的国家代码",
"Supported country codes - Tooltip": "该组织所支持的国家代码,发送短信验证码时可以选择这些国家的代码前缀",
"Sure to delete": "确定删除",
"Sure to disable": "确认关闭",
"Sure to remove": "确定移除",
"Swagger": "API文档",
"Sync": "同步",
"Syncers": "同步器",
@ -329,14 +342,16 @@
"Webhooks": "Webhooks",
"You can only select one physical group": "只能选择一个实体组",
"empty": "无",
"remove": "移除",
"{total} in total": "{total} 总计"
},
"group": {
"Edit Group": "编辑用户组",
"New Group": "新建用户组",
"Edit Group": "编辑组",
"New Group": "新建组",
"Parent group": "上级组",
"Parent group - Tooltip": "上级组",
"Physical": "物理组",
"Physical": "实体组",
"Show all": "显示全部",
"Virtual": "虚拟组"
},
"ldap": {
@ -382,7 +397,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": "请输入要登录的组织",
@ -424,12 +438,15 @@
"Multi-factor secret - Tooltip": "多因素密钥 - Tooltip",
"Multi-factor secret to clipboard successfully": "多因素密钥已复制到剪贴板",
"Passcode": "认证码",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "请先绑定邮箱,之后会自动使用该邮箱作为多因素认证的方式",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "请先绑定手机号,之后会自动使用该手机号作为多因素认证的方式",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "请保存此恢复代码。一旦您的设备无法提供身份验证码,您可以通过此恢复码重置多因素认证",
"Protect your account with Multi-factor authentication": "通过多因素认证保护您的帐户",
"Recovery code": "恢复码",
"SMS/Email message": "短信或邮件认证",
"Set preferred": "设为首选",
"Setup": "设置",
"Use Email": "使用电子邮件",
"Use SMS": "使用短信",
"Use SMS verification code": "使用手机或电子邮件发送验证码认证",
"Use a recovery code": "使用恢复代码",
"Verification failed": "验证失败",
@ -926,6 +943,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