mirror of
https://github.com/casdoor/casdoor.git
synced 2025-08-28 02:21:35 +08:00
Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
65716af89e | ||
![]() |
d9c4f401e3 | ||
![]() |
58aa7dba6a | ||
![]() |
29fc820578 | ||
![]() |
d0ac265c91 | ||
![]() |
3562c36817 | ||
![]() |
7884e10ca3 | ||
![]() |
12dee8afd3 | ||
![]() |
ac4b870309 | ||
![]() |
b9140e2d5a | ||
![]() |
501f0dc74f | ||
![]() |
a932b76fba | ||
![]() |
0f57ac297b | ||
![]() |
edc6aa0d50 | ||
![]() |
ebc0e0f2c9 | ||
![]() |
63dd2e781e | ||
![]() |
b01ba792bb | ||
![]() |
98fb9f25b0 | ||
![]() |
cc456f265f |
@@ -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())
|
||||
|
@@ -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)
|
||||
|
@@ -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.GetGroupUsers(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
|
||||
@@ -410,10 +410,6 @@ func (c *ApiController) SetPassword() {
|
||||
c.ResponseError(c.T("user:New password cannot contain blank space."))
|
||||
return
|
||||
}
|
||||
if len(newPassword) <= 5 {
|
||||
c.ResponseError(c.T("user:New password must have at least 6 characters"))
|
||||
return
|
||||
}
|
||||
|
||||
userId := util.GetId(userOwner, userName)
|
||||
|
||||
@@ -448,6 +444,12 @@ func (c *ApiController) SetPassword() {
|
||||
}
|
||||
}
|
||||
|
||||
msg := object.CheckPasswordComplexity(targetUser, newPassword)
|
||||
if msg != "" {
|
||||
c.ResponseError(msg)
|
||||
return
|
||||
}
|
||||
|
||||
targetUser.Password = newPassword
|
||||
_, err = object.SetUserField(targetUser, "password", targetUser.Password)
|
||||
if err != nil {
|
||||
@@ -554,8 +556,8 @@ func (c *ApiController) AddUserkeys() {
|
||||
func (c *ApiController) RemoveUserFromGroup() {
|
||||
owner := c.Ctx.Request.Form.Get("owner")
|
||||
name := c.Ctx.Request.Form.Get("name")
|
||||
groupId := c.Ctx.Request.Form.Get("groupId")
|
||||
groupName := c.Ctx.Request.Form.Get("groupName")
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.RemoveUserFromGroup(owner, name, groupId))
|
||||
c.Data["json"] = wrapActionResponse(object.RemoveUserFromGroup(owner, name, groupName))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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": "ユーザーのインポートに失敗しました"
|
||||
|
@@ -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
149
i18n/locales/pt/data.json
Normal 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"
|
||||
}
|
||||
}
|
@@ -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": "Не удалось импортировать пользователей"
|
||||
|
@@ -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"
|
||||
|
@@ -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": "导入用户失败"
|
||||
|
@@ -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": "",
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
98
object/check_password_complexity.go
Normal file
98
object/check_password_complexity.go
Normal 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 ""
|
||||
}
|
184
object/group.go
184
object/group.go
@@ -19,23 +19,23 @@ import (
|
||||
"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"`
|
||||
ParentId string `xorm:"varchar(100)" json:"parentId"`
|
||||
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"`
|
||||
@@ -95,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)
|
||||
@@ -125,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
|
||||
@@ -135,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
|
||||
@@ -164,37 +148,20 @@ func DeleteGroup(group *Group) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if count, err := adapter.Engine.Where("parent_id = ?", group.Id).Count(&Group{}); err != nil {
|
||||
if count, err := adapter.Engine.Where("parent_id = ?", group.Name).Count(&Group{}); err != nil {
|
||||
return false, err
|
||||
} else if count > 0 {
|
||||
return false, errors.New("group has children group")
|
||||
}
|
||||
|
||||
if count, err := GetGroupUserCount(group.GetId(), "", ""); err != nil {
|
||||
if count, err := GetGroupUserCount(group.Name, "", ""); err != nil {
|
||||
return false, err
|
||||
} else if count > 0 {
|
||||
return false, errors.New("group has users")
|
||||
}
|
||||
|
||||
session := adapter.Engine.NewSession()
|
||||
defer session.Close()
|
||||
|
||||
if err := session.Begin(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if _, err := session.Delete(&UserGroupRelation{GroupId: group.Id}); err != nil {
|
||||
session.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
affected, err := session.ID(core.PK{group.Owner, group.Name}).Delete(&Group{})
|
||||
affected, err := adapter.Engine.ID(core.PK{group.Owner, group.Name}).Delete(&Group{})
|
||||
if err != nil {
|
||||
session.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := session.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -215,9 +182,8 @@ func ConvertToTreeData(groups []*Group, parentId string) []*Group {
|
||||
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
|
||||
}
|
||||
@@ -226,3 +192,113 @@ func ConvertToTreeData(groups []*Group, parentId 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
|
||||
}
|
||||
|
@@ -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{},
|
||||
|
@@ -56,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"`
|
||||
@@ -401,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
|
||||
|
@@ -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) {
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
}
|
||||
|
116
object/user.go
116
object/user.go
@@ -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,9 +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"`
|
||||
SecretKey string `xorm:"varchar(100)" json:"secretKey"`
|
||||
AccessSecret string `xorm:"varchar(100)" json:"accessSecret"`
|
||||
|
||||
CreatedIp string `xorm:"varchar(100)" json:"createdIp"`
|
||||
LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"`
|
||||
@@ -168,6 +166,7 @@ type User struct {
|
||||
|
||||
Roles []*Role `json:"roles"`
|
||||
Permissions []*Permission `json:"permissions"`
|
||||
Groups []string `xorm:"groups varchar(1000)" json:"groups"`
|
||||
|
||||
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
|
||||
SigninWrongTimes int `json:"signinWrongTimes"`
|
||||
@@ -221,11 +220,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 != "" {
|
||||
return GetGroupUserCount(groupId, field, value)
|
||||
if groupName != "" {
|
||||
return GetGroupUserCount(groupName, field, value)
|
||||
}
|
||||
|
||||
return session.Count(&User{})
|
||||
@@ -265,11 +264,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 != "" {
|
||||
return GetPaginationGroupUsers(groupId, offset, limit, field, value, sortField, sortOrder)
|
||||
if groupName != "" {
|
||||
return GetPaginationGroupUsers(groupName, offset, limit, field, value, sortField, sortOrder)
|
||||
}
|
||||
|
||||
session := GetSessionForUser(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
@@ -484,17 +483,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)
|
||||
@@ -508,7 +503,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", "access_key", "secret_key",
|
||||
"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",
|
||||
@@ -522,7 +517,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
|
||||
}
|
||||
@@ -530,32 +525,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 := updateUserGroupRelation(session, user)
|
||||
if err != nil {
|
||||
session.Rollback()
|
||||
return affected, err
|
||||
}
|
||||
}
|
||||
|
||||
affected, err := session.ID(core.PK{oldUser.Owner, oldUser.Name}).Cols(columns...).Update(user)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -726,11 +706,6 @@ func DeleteUser(user *User) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
affected, err = deleteRelationByUser(user.Id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
@@ -746,7 +721,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
|
||||
@@ -777,12 +752,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
|
||||
}
|
||||
|
||||
@@ -844,46 +822,6 @@ func userChangeTrigger(oldName string, newName string) error {
|
||||
return session.Commit()
|
||||
}
|
||||
|
||||
func (user *User) refreshAvatar() (bool, error) {
|
||||
var err error
|
||||
var fileBuffer *bytes.Buffer
|
||||
var ext string
|
||||
|
||||
// Gravatar + Identicon
|
||||
if strings.Contains(user.Avatar, "Gravatar") && user.Email != "" {
|
||||
client := proxy.ProxyHttpClient
|
||||
has, err := hasGravatar(client, user.Email)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if has {
|
||||
fileBuffer, ext, err = getGravatarFileBuffer(client, user.Email)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fileBuffer == nil && strings.Contains(user.Avatar, "Identicon") {
|
||||
fileBuffer, ext, err = getIdenticonFileBuffer(user.Name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if fileBuffer != nil {
|
||||
avatarUrl, err := getPermanentAvatarUrlFromBuffer(user.Owner, user.Name, fileBuffer, ext, true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
user.Avatar = avatarUrl
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (user *User) IsMfaEnabled() bool {
|
||||
return len(user.MultiFactorAuths) > 0
|
||||
}
|
||||
@@ -922,7 +860,7 @@ func AddUserkeys(user *User, isAdmin bool) (bool, error) {
|
||||
}
|
||||
|
||||
user.AccessKey = util.GenerateId()
|
||||
user.SecretKey = util.GenerateId()
|
||||
user.AccessSecret = util.GenerateId()
|
||||
|
||||
return UpdateUser(user.GetId(), user, []string{}, isAdmin)
|
||||
}
|
||||
|
149
object/user_avatar.go
Normal file
149
object/user_avatar.go
Normal 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
|
||||
}
|
220
object/user_avatar_favicon.go
Normal file
220
object/user_avatar_favicon.go
Normal file
@@ -0,0 +1,220 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
type Link struct {
|
||||
Rel string
|
||||
Sizes string
|
||||
Href string
|
||||
}
|
||||
|
||||
func GetFaviconUrl(htmlStr string) (string, error) {
|
||||
doc, err := html.Parse(strings.NewReader(htmlStr))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var links []Link
|
||||
findLinks(doc, &links)
|
||||
|
||||
if len(links) == 0 {
|
||||
return "", fmt.Errorf("no Favicon links found")
|
||||
}
|
||||
|
||||
chosenLink := chooseFaviconLink(links)
|
||||
if chosenLink == nil {
|
||||
return "", fmt.Errorf("unable to determine favicon URL")
|
||||
}
|
||||
|
||||
return chosenLink.Href, nil
|
||||
}
|
||||
|
||||
func findLinks(n *html.Node, links *[]Link) {
|
||||
if n.Type == html.ElementNode && n.Data == "link" {
|
||||
link := parseLink(n)
|
||||
if link != nil {
|
||||
*links = append(*links, *link)
|
||||
}
|
||||
}
|
||||
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
findLinks(c, links)
|
||||
}
|
||||
}
|
||||
|
||||
func parseLink(n *html.Node) *Link {
|
||||
var link Link
|
||||
|
||||
for _, attr := range n.Attr {
|
||||
switch attr.Key {
|
||||
case "rel":
|
||||
link.Rel = attr.Val
|
||||
case "sizes":
|
||||
link.Sizes = attr.Val
|
||||
case "href":
|
||||
link.Href = attr.Val
|
||||
}
|
||||
}
|
||||
|
||||
if link.Href != "" {
|
||||
return &link
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func chooseFaviconLink(links []Link) *Link {
|
||||
var appleTouchLinks []Link
|
||||
var shortcutLinks []Link
|
||||
var iconLinks []Link
|
||||
|
||||
for _, link := range links {
|
||||
switch link.Rel {
|
||||
case "apple-touch-icon":
|
||||
appleTouchLinks = append(appleTouchLinks, link)
|
||||
case "shortcut icon":
|
||||
shortcutLinks = append(shortcutLinks, link)
|
||||
case "icon":
|
||||
iconLinks = append(iconLinks, link)
|
||||
}
|
||||
}
|
||||
|
||||
if len(appleTouchLinks) > 0 {
|
||||
return chooseFaviconLinkBySizes(appleTouchLinks)
|
||||
}
|
||||
|
||||
if len(shortcutLinks) > 0 {
|
||||
return chooseFaviconLinkBySizes(shortcutLinks)
|
||||
}
|
||||
|
||||
if len(iconLinks) > 0 {
|
||||
return chooseFaviconLinkBySizes(iconLinks)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func chooseFaviconLinkBySizes(links []Link) *Link {
|
||||
if len(links) == 1 {
|
||||
return &links[0]
|
||||
}
|
||||
|
||||
var chosenLink *Link
|
||||
|
||||
for _, link := range links {
|
||||
if chosenLink == nil || compareSizes(link.Sizes, chosenLink.Sizes) > 0 {
|
||||
chosenLink = &link
|
||||
}
|
||||
}
|
||||
|
||||
return chosenLink
|
||||
}
|
||||
|
||||
func compareSizes(sizes1, sizes2 string) int {
|
||||
if sizes1 == sizes2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
size1 := parseSize(sizes1)
|
||||
size2 := parseSize(sizes2)
|
||||
|
||||
if size1 == nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
if size2 == nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
if size1[0] == size2[0] {
|
||||
return size1[1] - size2[1]
|
||||
}
|
||||
|
||||
return size1[0] - size2[0]
|
||||
}
|
||||
|
||||
func parseSize(sizes string) []int {
|
||||
size := strings.Split(sizes, "x")
|
||||
if len(size) != 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result []int
|
||||
|
||||
for _, s := range size {
|
||||
val := strings.TrimSpace(s)
|
||||
if len(val) > 0 {
|
||||
num := 0
|
||||
for i := 0; i < len(val); i++ {
|
||||
if val[i] >= '0' && val[i] <= '9' {
|
||||
num = num*10 + int(val[i]-'0')
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
result = append(result, num)
|
||||
}
|
||||
}
|
||||
|
||||
if len(result) == 2 {
|
||||
return result
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFaviconFileBuffer(client *http.Client, email string) (*bytes.Buffer, string, error) {
|
||||
tokens := strings.Split(email, "@")
|
||||
domain := tokens[1]
|
||||
if domain == "gmail.com" || domain == "163.com" || domain == "qq.com" {
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
htmlUrl := fmt.Sprintf("https://%s", domain)
|
||||
buffer, _, err := downloadImage(client, htmlUrl)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
faviconUrl := ""
|
||||
if buffer != nil {
|
||||
faviconUrl, err = GetFaviconUrl(buffer.String())
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(faviconUrl, "http") {
|
||||
faviconUrl, err = url.JoinPath(htmlUrl, faviconUrl)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if faviconUrl == "" {
|
||||
faviconUrl = fmt.Sprintf("https://%s/favicon.ico", domain)
|
||||
}
|
||||
return downloadImage(client, faviconUrl)
|
||||
}
|
76
object/user_avatar_gravatar.go
Normal file
76
object/user_avatar_gravatar.go
Normal 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)
|
||||
}
|
80
object/user_avatar_identicon.go
Normal file
80
object/user_avatar_identicon.go
Normal 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
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -1,157 +0,0 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
"github.com/xorm-io/xorm"
|
||||
)
|
||||
|
||||
type UserGroupRelation struct {
|
||||
UserId string `xorm:"varchar(100) notnull pk" json:"userId"`
|
||||
GroupId string `xorm:"varchar(100) notnull pk" json:"groupId"`
|
||||
|
||||
CreatedTime string `xorm:"created" json:"createdTime"`
|
||||
UpdatedTime string `xorm:"updated" json:"updatedTime"`
|
||||
}
|
||||
|
||||
func updateUserGroupRelation(session *xorm.Session, user *User) (int64, error) {
|
||||
physicalGroupCount, err := session.Where("type = ?", "Physical").In("id", user.Groups).Count(Group{})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if physicalGroupCount > 1 {
|
||||
return 0, errors.New("user can only be in one physical group")
|
||||
}
|
||||
|
||||
groups := []*Group{}
|
||||
err = session.In("id", user.Groups).Find(&groups)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(groups) != len(user.Groups) {
|
||||
return 0, errors.New("group not found")
|
||||
}
|
||||
|
||||
_, err = session.Delete(&UserGroupRelation{UserId: user.Id})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
relations := []*UserGroupRelation{}
|
||||
for _, group := range groups {
|
||||
relations = append(relations, &UserGroupRelation{UserId: user.Id, GroupId: group.Id})
|
||||
}
|
||||
if len(relations) == 0 {
|
||||
return 1, nil
|
||||
}
|
||||
_, err = session.Insert(relations)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func RemoveUserFromGroup(owner, name, groupId string) (bool, error) {
|
||||
user, err := getUser(owner, name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
groups := []string{}
|
||||
for _, group := range user.Groups {
|
||||
if group != groupId {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
}
|
||||
user.Groups = groups
|
||||
|
||||
_, err = UpdateUser(util.GetId(owner, name), user, []string{"groups"}, false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func deleteUserGroupRelation(session *xorm.Session, userId, groupId string) (int64, error) {
|
||||
affected, err := session.ID(core.PK{userId, groupId}).Delete(&UserGroupRelation{})
|
||||
return affected, err
|
||||
}
|
||||
|
||||
func deleteRelationByUser(id string) (int64, error) {
|
||||
affected, err := adapter.Engine.Delete(&UserGroupRelation{UserId: id})
|
||||
return affected, err
|
||||
}
|
||||
|
||||
func GetGroupUserCount(id string, field, value string) (int64, error) {
|
||||
group, err := GetGroup(id)
|
||||
if group == nil || err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if field == "" && value == "" {
|
||||
return adapter.Engine.Count(UserGroupRelation{GroupId: group.Id})
|
||||
} else {
|
||||
return adapter.Engine.Table("user").
|
||||
Join("INNER", []string{"user_group_relation", "r"}, "user.id = r.user_id").
|
||||
Where("r.group_id = ?", group.Id).
|
||||
And(fmt.Sprintf("user.%s LIKE ?", util.CamelToSnakeCase(field)), "%"+value+"%").
|
||||
Count()
|
||||
}
|
||||
}
|
||||
|
||||
func GetPaginationGroupUsers(id string, offset, limit int, field, value, sortField, sortOrder string) ([]*User, error) {
|
||||
group, err := GetGroup(id)
|
||||
if group == nil || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := []*User{}
|
||||
session := adapter.Engine.Table("user").
|
||||
Join("INNER", []string{"user_group_relation", "r"}, "user.id = r.user_id").
|
||||
Where("r.group_id = ?", group.Id)
|
||||
|
||||
if offset != -1 && limit != -1 {
|
||||
session.Limit(limit, offset)
|
||||
}
|
||||
|
||||
if field != "" && value != "" {
|
||||
session = session.And(fmt.Sprintf("user.%s LIKE ?", util.CamelToSnakeCase(field)), "%"+value+"%")
|
||||
}
|
||||
|
||||
if sortField == "" || sortOrder == "" {
|
||||
sortField = "created_time"
|
||||
}
|
||||
if sortOrder == "ascend" {
|
||||
session = session.Asc(fmt.Sprintf("user.%s", util.SnakeString(sortField)))
|
||||
} else {
|
||||
session = session.Desc(fmt.Sprintf("user.%s", util.SnakeString(sortField)))
|
||||
}
|
||||
|
||||
err = session.Find(&users)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func GetGroupUsers(id string) ([]*User, error) {
|
||||
group, err := GetGroup(id)
|
||||
if group == nil || err != nil {
|
||||
return []*User{}, err
|
||||
}
|
||||
|
||||
users := []*User{}
|
||||
err = adapter.Engine.Table("user_group_relation").Join("INNER", []string{"user", "u"}, "user_group_relation.user_id = u.id").
|
||||
Where("user_group_relation.group_id = ?", group.Id).
|
||||
Find(&users)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
@@ -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,
|
||||
}
|
||||
|
@@ -26,10 +26,10 @@ import (
|
||||
)
|
||||
|
||||
type Object struct {
|
||||
Owner string `json:"owner"`
|
||||
Name string `json:"name"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
Owner string `json:"owner"`
|
||||
Name string `json:"name"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
AccessSecret string `json:"accessSecret"`
|
||||
}
|
||||
|
||||
func getUsername(ctx *context.Context) (username string) {
|
||||
@@ -107,14 +107,14 @@ func getKeys(ctx *context.Context) (string, string) {
|
||||
method := ctx.Request.Method
|
||||
|
||||
if method == http.MethodGet {
|
||||
accessKey := ctx.Input.Query("accesskey")
|
||||
secretKey := ctx.Input.Query("secretkey")
|
||||
return accessKey, secretKey
|
||||
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("secretkey")
|
||||
return ctx.Request.Form.Get("accessKey"), ctx.Request.Form.Get("accessSecret")
|
||||
}
|
||||
|
||||
var obj Object
|
||||
@@ -123,7 +123,7 @@ func getKeys(ctx *context.Context) (string, string) {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return obj.AccessKey, obj.SecretKey
|
||||
return obj.AccessKey, obj.AccessSecret
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -85,12 +85,13 @@ func getUsernameByClientIdSecret(ctx *context.Context) string {
|
||||
}
|
||||
|
||||
func getUsernameByKeys(ctx *context.Context) string {
|
||||
accessKey, secretKey := getKeys(ctx)
|
||||
accessKey, accessSecret := getKeys(ctx)
|
||||
user, err := object.GetUserByAccessKey(accessKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if user != nil && secretKey == user.SecretKey {
|
||||
|
||||
if user != nil && accessSecret == user.AccessSecret {
|
||||
return user.GetId()
|
||||
}
|
||||
return ""
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -278,3 +278,13 @@ func GetEndPoint(endpoint string) string {
|
||||
}
|
||||
return endpoint
|
||||
}
|
||||
|
||||
// HasString reports if slice has input string.
|
||||
func HasString(strs []string, str string) bool {
|
||||
for _, i := range strs {
|
||||
if i == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@@ -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}`);
|
||||
|
@@ -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,
|
||||
|
@@ -44,11 +44,6 @@ class GroupEditPage extends React.Component {
|
||||
GroupBackend.getGroup(this.state.organizationName, this.state.groupName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
if (res.data === null) {
|
||||
this.props.history.push("/404");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
group: res.data,
|
||||
});
|
||||
@@ -96,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() {
|
||||
|
@@ -185,7 +185,7 @@ class GroupListPage extends BaseListPage {
|
||||
{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 "";
|
||||
}
|
||||
|
@@ -30,7 +30,6 @@ class GroupTreePage extends React.Component {
|
||||
owner: Setting.isAdminUser(this.props.account) ? "" : this.props.account.owner,
|
||||
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||
groupName: this.props.match?.params.groupName,
|
||||
groupId: undefined,
|
||||
treeData: [],
|
||||
selectedKeys: [this.props.match?.params.groupName],
|
||||
};
|
||||
@@ -55,7 +54,6 @@ class GroupTreePage extends React.Component {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
treeData: res.data,
|
||||
groupId: this.findNodeId({children: res.data}, this.state.groupName),
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
@@ -63,26 +61,10 @@ class GroupTreePage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
findNodeId(node, targetName) {
|
||||
if (node.key === targetName) {
|
||||
return node.id;
|
||||
}
|
||||
if (node.children) {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
const result = this.findNodeId(node.children[i], targetName);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
setTreeTitle(treeData) {
|
||||
const haveChildren = Array.isArray(treeData.children) && treeData.children.length > 0;
|
||||
const isSelected = this.state.groupName === treeData.key;
|
||||
return {
|
||||
id: treeData.id,
|
||||
key: treeData.key,
|
||||
title: <Space>
|
||||
{treeData.type === "Physical" ? <UsergroupAddOutlined /> : <HolderOutlined />}
|
||||
@@ -201,7 +183,6 @@ class GroupTreePage extends React.Component {
|
||||
this.setState({
|
||||
selectedKeys: selectedKeys,
|
||||
groupName: info.node.key,
|
||||
groupId: info.node.id,
|
||||
});
|
||||
this.props.history.push(`/trees/${this.state.organizationName}/${info.node.key}`);
|
||||
};
|
||||
@@ -257,7 +238,7 @@ class GroupTreePage extends React.Component {
|
||||
updatedTime: moment().format(),
|
||||
displayName: `New Group - ${randomName}`,
|
||||
type: "Virtual",
|
||||
parentId: isRoot ? this.state.organizationName : this.state.groupId,
|
||||
parentId: isRoot ? this.state.organizationName : this.state.groupName,
|
||||
isTopGroup: isRoot,
|
||||
isEnabled: true,
|
||||
};
|
||||
@@ -300,8 +281,7 @@ class GroupTreePage extends React.Component {
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
selectedKeys: [],
|
||||
groupName: null,
|
||||
groupId: undefined,
|
||||
groupName: undefined,
|
||||
});
|
||||
this.props.history.push(`/trees/${this.state.organizationName}`);
|
||||
}}
|
||||
@@ -323,7 +303,6 @@ class GroupTreePage extends React.Component {
|
||||
<UserListPage
|
||||
organizationName={this.state.organizationName}
|
||||
groupName={this.state.groupName}
|
||||
groupId={this.state.groupId}
|
||||
{...this.props}
|
||||
/>
|
||||
</Col>
|
||||
|
@@ -193,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"))} :
|
||||
|
@@ -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: "",
|
||||
|
@@ -309,7 +309,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;
|
||||
@@ -319,7 +319,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}
|
||||
@@ -724,10 +724,10 @@ class UserEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
|
||||
{Setting.getLabel(i18next.t("general:Secret key"), i18next.t("general:Secret key - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("general:Access secret"), i18next.t("general:Access secret - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.user.secretKey} disabled={true} />
|
||||
<Input value={this.state.user.accessSecret} disabled={true} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
|
@@ -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 ? [this.props.groupId] : [],
|
||||
groups: this.props.groupName ? [this.props.groupName] : [],
|
||||
affiliation: "Example Inc.",
|
||||
tag: "staff",
|
||||
region: "",
|
||||
@@ -126,8 +126,8 @@ class UserListPage extends BaseListPage {
|
||||
|
||||
removeUserFromGroup(i) {
|
||||
const user = this.state.data[i];
|
||||
const group = this.props.groupId;
|
||||
UserBackend.removeUserFromGroup({groupId: group, owner: user.owner, name: user.name})
|
||||
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"));
|
||||
@@ -396,7 +396,7 @@ class UserListPage extends BaseListPage {
|
||||
width: "190px",
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
const isTreePage = this.props.groupId !== undefined;
|
||||
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 (
|
||||
<Space>
|
||||
@@ -483,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({
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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: {
|
||||
|
@@ -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: {
|
||||
@@ -223,11 +223,11 @@ export function checkUserPassword(values) {
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function removeUserFromGroup({owner, name, groupId}) {
|
||||
export function removeUserFromGroup({owner, name, groupName}) {
|
||||
const formData = new FormData();
|
||||
formData.append("owner", owner);
|
||||
formData.append("name", name);
|
||||
formData.append("groupId", groupId);
|
||||
formData.append("groupName", groupName);
|
||||
return fetch(`${Setting.ServerUrl}/api/remove-user-from-group`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
|
82
web/src/common/PasswordChecker.js
Normal file
82
web/src/common/PasswordChecker.js
Normal 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 "";
|
||||
}
|
@@ -17,6 +17,8 @@ import i18next from "i18next";
|
||||
import React from "react";
|
||||
import * as UserBackend from "../../backend/UserBackend";
|
||||
import * as Setting from "../../Setting";
|
||||
import * as OrganizationBackend from "../../backend/OrganizationBackend";
|
||||
import * as PasswordChecker from "../PasswordChecker";
|
||||
|
||||
export const PasswordModal = (props) => {
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
@@ -27,6 +29,26 @@ export const PasswordModal = (props) => {
|
||||
const {user} = props;
|
||||
const {account} = props;
|
||||
|
||||
const [passwordOptions, setPasswordOptions] = React.useState([]);
|
||||
const [newPasswordValid, setNewPasswordValid] = React.useState(false);
|
||||
const [rePasswordValid, setRePasswordValid] = React.useState(false);
|
||||
const [newPasswordErrorMessage, setNewPasswordErrorMessage] = React.useState("");
|
||||
const [rePasswordErrorMessage, setRePasswordErrorMessage] = React.useState("");
|
||||
|
||||
React.useEffect(() => {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
const organizations = (res.msg === undefined) ? res : [];
|
||||
// Find the user's corresponding organization
|
||||
const organization = organizations.find((org) => org.name === user.owner);
|
||||
if (organization) {
|
||||
setPasswordOptions(organization.passwordOptions);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}, [user.owner]);
|
||||
const showModal = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
@@ -34,6 +56,24 @@ export const PasswordModal = (props) => {
|
||||
const handleCancel = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
const handleNewPassword = (value) => {
|
||||
setNewPassword(value);
|
||||
|
||||
const errorMessage = PasswordChecker.checkPasswordComplexity(value, passwordOptions);
|
||||
setNewPasswordValid(errorMessage === "");
|
||||
setNewPasswordErrorMessage(errorMessage);
|
||||
};
|
||||
|
||||
const handleRePassword = (value) => {
|
||||
setRePassword(value);
|
||||
|
||||
if (value !== newPassword) {
|
||||
setRePasswordErrorMessage(i18next.t("signup:Your confirmed password is inconsistent with the password!"));
|
||||
setRePasswordValid(false);
|
||||
} else {
|
||||
setRePasswordValid(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
if (newPassword === "" || rePassword === "") {
|
||||
@@ -45,12 +85,44 @@ export const PasswordModal = (props) => {
|
||||
return;
|
||||
}
|
||||
setConfirmLoading(true);
|
||||
UserBackend.setPassword(user.owner, user.name, oldPassword, newPassword).then((res) => {
|
||||
setConfirmLoading(false);
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("user:Password set successfully"));
|
||||
setVisible(false);
|
||||
} else {Setting.showMessage("error", i18next.t(`user:${res.msg}`));}
|
||||
|
||||
OrganizationBackend.getOrganizations("admin").then((res) => {
|
||||
const organizations = (res.msg === undefined) ? res : [];
|
||||
|
||||
// find the users' corresponding organization
|
||||
let organization = null;
|
||||
for (let i = 0; i < organizations.length; i++) {
|
||||
if (organizations[i].name === user.owner) {
|
||||
organization = organizations[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (organization === null) {
|
||||
Setting.showMessage("error", "organization is null");
|
||||
setConfirmLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const errorMsg = PasswordChecker.checkPasswordComplexity(newPassword, organization.passwordOptions);
|
||||
if (errorMsg !== "") {
|
||||
Setting.showMessage("error", errorMsg);
|
||||
setConfirmLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
UserBackend.setPassword(user.owner, user.name, oldPassword, newPassword)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("user:Password set successfully"));
|
||||
setVisible(false);
|
||||
} else {
|
||||
Setting.showMessage("error", i18next.t(`user:${res.msg}`));
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setConfirmLoading(false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -79,11 +151,23 @@ export const PasswordModal = (props) => {
|
||||
</Row>
|
||||
) : null}
|
||||
<Row style={{width: "100%", marginBottom: "20px"}}>
|
||||
<Input.Password addonBefore={i18next.t("user:New Password")} placeholder={i18next.t("user:input password")} onChange={(e) => setNewPassword(e.target.value)} />
|
||||
<Input.Password
|
||||
addonBefore={i18next.t("user:New Password")}
|
||||
placeholder={i18next.t("user:input password")}
|
||||
onChange={(e) => {handleNewPassword(e.target.value);}}
|
||||
status={(!newPasswordValid && newPasswordErrorMessage) ? "error" : undefined}
|
||||
/>
|
||||
</Row>
|
||||
{!newPasswordValid && newPasswordErrorMessage && <div style={{color: "red", marginTop: "-20px"}}>{newPasswordErrorMessage}</div>}
|
||||
<Row style={{width: "100%", marginBottom: "20px"}}>
|
||||
<Input.Password addonBefore={i18next.t("user:Re-enter New")} placeholder={i18next.t("user:input password")} onChange={(e) => setRePassword(e.target.value)} />
|
||||
<Input.Password
|
||||
addonBefore={i18next.t("user:Re-enter New")}
|
||||
placeholder={i18next.t("user:input password")}
|
||||
onChange={(e) => handleRePassword(e.target.value)}
|
||||
status={(!rePasswordValid && rePasswordErrorMessage) ? "error" : undefined}
|
||||
/>
|
||||
</Row>
|
||||
{!rePasswordValid && rePasswordErrorMessage && <div style={{color: "red", marginTop: "-20px"}}>{rePasswordErrorMessage}</div>}
|
||||
</Col>
|
||||
</Modal>
|
||||
</Row>
|
||||
|
@@ -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",
|
||||
@@ -255,6 +261,8 @@
|
||||
"Organizations": "Organisationen",
|
||||
"Password": "Passwort",
|
||||
"Password - Tooltip": "Stellen Sie sicher, dass das Passwort korrekt ist",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
|
||||
"Password salt": "Passwort-Salt",
|
||||
"Password salt - Tooltip": "Zufälliger Parameter, der für die Verschlüsselung von Passwörtern verwendet wird",
|
||||
"Password type": "Passworttyp",
|
||||
@@ -388,7 +396,6 @@
|
||||
"Please input your code!": "Bitte geben Sie Ihren Code ein!",
|
||||
"Please input your organization name!": "Please input your organization name!",
|
||||
"Please input your password!": "Bitte geben Sie Ihr Passwort ein!",
|
||||
"Please input your password, at least 6 characters!": "Bitte geben Sie Ihr Passwort ein, es muss mindestens 6 Zeichen lang sein!",
|
||||
"Please select an organization": "Please select an organization",
|
||||
"Please select an organization to sign in": "Please select an organization to sign in",
|
||||
"Please type an organization to sign in": "Please type an organization to sign in",
|
||||
@@ -932,6 +939,11 @@
|
||||
"Set password...": "Passwort festlegen...",
|
||||
"Tag": "Tag",
|
||||
"Tag - Tooltip": "Tags des Benutzers",
|
||||
"The password must contain at least one special character": "The password must contain at least one special character",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
|
||||
"The password must have at least 6 characters": "The password must have at least 6 characters",
|
||||
"The password must have at least 8 characters": "The password must have at least 8 characters",
|
||||
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
|
||||
"Title": "Titel",
|
||||
"Title - Tooltip": "Position in der Zugehörigkeit",
|
||||
"Two passwords you typed do not match.": "Zwei von Ihnen eingegebene Passwörter stimmen nicht überein.",
|
||||
|
@@ -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",
|
||||
@@ -255,6 +261,8 @@
|
||||
"Organizations": "Organizations",
|
||||
"Password": "Password",
|
||||
"Password - Tooltip": "Make sure the password is correct",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Different combinations of password complexity options",
|
||||
"Password salt": "Password salt",
|
||||
"Password salt - Tooltip": "Random parameter used for password encryption",
|
||||
"Password type": "Password type",
|
||||
@@ -388,7 +396,6 @@
|
||||
"Please input your code!": "Please input your code!",
|
||||
"Please input your organization name!": "Please input your organization name!",
|
||||
"Please input your password!": "Please input your password!",
|
||||
"Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!",
|
||||
"Please select an organization": "Please select an organization",
|
||||
"Please select an organization to sign in": "Please select an organization to sign in",
|
||||
"Please type an organization to sign in": "Please type an organization to sign in",
|
||||
@@ -932,6 +939,11 @@
|
||||
"Set password...": "Set password...",
|
||||
"Tag": "Tag",
|
||||
"Tag - Tooltip": "Tag of the user",
|
||||
"The password must contain at least one special character": "The password must contain at least one special character",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
|
||||
"The password must have at least 6 characters": "The password must have at least 6 characters",
|
||||
"The password must have at least 8 characters": "The password must have at least 8 characters",
|
||||
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
|
||||
"Title": "Title",
|
||||
"Title - Tooltip": "Position in the affiliation",
|
||||
"Two passwords you typed do not match.": "Two passwords you typed do not match.",
|
||||
|
@@ -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",
|
||||
@@ -255,6 +261,8 @@
|
||||
"Organizations": "Organizaciones",
|
||||
"Password": "Contraseña",
|
||||
"Password - Tooltip": "Asegúrate de que la contraseña sea correcta",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
|
||||
"Password salt": "Sal de contraseña",
|
||||
"Password salt - Tooltip": "Parámetro aleatorio utilizado para la encriptación de contraseñas",
|
||||
"Password type": "Tipo de contraseña",
|
||||
@@ -388,7 +396,6 @@
|
||||
"Please input your code!": "¡Por favor ingrese su código!",
|
||||
"Please input your organization name!": "Please input your organization name!",
|
||||
"Please input your password!": "¡Ingrese su contraseña, por favor!",
|
||||
"Please input your password, at least 6 characters!": "Por favor ingrese su contraseña, ¡de al menos 6 caracteres!",
|
||||
"Please select an organization": "Please select an organization",
|
||||
"Please select an organization to sign in": "Please select an organization to sign in",
|
||||
"Please type an organization to sign in": "Please type an organization to sign in",
|
||||
@@ -932,6 +939,11 @@
|
||||
"Set password...": "Establecer contraseña...",
|
||||
"Tag": "Etiqueta",
|
||||
"Tag - Tooltip": "Etiqueta del usuario",
|
||||
"The password must contain at least one special character": "The password must contain at least one special character",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
|
||||
"The password must have at least 6 characters": "The password must have at least 6 characters",
|
||||
"The password must have at least 8 characters": "The password must have at least 8 characters",
|
||||
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
|
||||
"Title": "Título",
|
||||
"Title - Tooltip": "Posición en la afiliación",
|
||||
"Two passwords you typed do not match.": "Dos contraseñas que has escrito no coinciden.",
|
||||
|
@@ -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",
|
||||
@@ -255,6 +261,8 @@
|
||||
"Organizations": "Organisations",
|
||||
"Password": "Mot de passe",
|
||||
"Password - Tooltip": "Assurez-vous que le mot de passe est correct",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
|
||||
"Password salt": "Sel de mot de passe",
|
||||
"Password salt - Tooltip": "Paramètre aléatoire utilisé pour le cryptage de mot de passe",
|
||||
"Password type": "Type de mot de passe",
|
||||
@@ -388,7 +396,6 @@
|
||||
"Please input your code!": "Veuillez entrer votre code !",
|
||||
"Please input your organization name!": "Please input your organization name!",
|
||||
"Please input your password!": "Veuillez entrer votre mot de passe !",
|
||||
"Please input your password, at least 6 characters!": "Veuillez entrer votre mot de passe, au moins 6 caractères!",
|
||||
"Please select an organization": "Please select an organization",
|
||||
"Please select an organization to sign in": "Please select an organization to sign in",
|
||||
"Please type an organization to sign in": "Please type an organization to sign in",
|
||||
@@ -932,6 +939,11 @@
|
||||
"Set password...": "Définir le mot de passe...",
|
||||
"Tag": "Étiquette",
|
||||
"Tag - Tooltip": "Tag de l'utilisateur",
|
||||
"The password must contain at least one special character": "The password must contain at least one special character",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
|
||||
"The password must have at least 6 characters": "The password must have at least 6 characters",
|
||||
"The password must have at least 8 characters": "The password must have at least 8 characters",
|
||||
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
|
||||
"Title": "Titre",
|
||||
"Title - Tooltip": "Position dans l'affiliation",
|
||||
"Two passwords you typed do not match.": "Deux mots de passe que vous avez tapés ne correspondent pas.",
|
||||
|
@@ -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",
|
||||
@@ -255,6 +261,8 @@
|
||||
"Organizations": "Organisasi",
|
||||
"Password": "Kata sandi",
|
||||
"Password - Tooltip": "Pastikan kata sandi yang benar",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
|
||||
"Password salt": "Garam sandi",
|
||||
"Password salt - Tooltip": "Parameter acak yang digunakan untuk enkripsi kata sandi",
|
||||
"Password type": "Jenis kata sandi",
|
||||
@@ -388,7 +396,6 @@
|
||||
"Please input your code!": "Silakan masukkan kode Anda!",
|
||||
"Please input your organization name!": "Please input your organization name!",
|
||||
"Please input your password!": "Masukkan kata sandi Anda!",
|
||||
"Please input your password, at least 6 characters!": "Silakan masukkan kata sandi Anda, minimal 6 karakter!",
|
||||
"Please select an organization": "Please select an organization",
|
||||
"Please select an organization to sign in": "Please select an organization to sign in",
|
||||
"Please type an organization to sign in": "Please type an organization to sign in",
|
||||
@@ -932,6 +939,11 @@
|
||||
"Set password...": "Tetapkan kata sandi...",
|
||||
"Tag": "tanda",
|
||||
"Tag - Tooltip": "Tag pengguna",
|
||||
"The password must contain at least one special character": "The password must contain at least one special character",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
|
||||
"The password must have at least 6 characters": "The password must have at least 6 characters",
|
||||
"The password must have at least 8 characters": "The password must have at least 8 characters",
|
||||
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
|
||||
"Title": "Judul",
|
||||
"Title - Tooltip": "Posisi dalam afiliasi",
|
||||
"Two passwords you typed do not match.": "Dua password yang Anda ketikkan tidak cocok.",
|
||||
|
@@ -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": "ポリシー・ストアのテーブル名",
|
||||
@@ -255,6 +261,8 @@
|
||||
"Organizations": "組織",
|
||||
"Password": "パスワード",
|
||||
"Password - Tooltip": "パスワードが正しいことを確認してください",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
|
||||
"Password salt": "パスワードのソルト",
|
||||
"Password salt - Tooltip": "ランダムパラメーターは、パスワードの暗号化に使用されます",
|
||||
"Password type": "パスワードタイプ",
|
||||
@@ -388,7 +396,6 @@
|
||||
"Please input your code!": "あなたのコードを入力してください!",
|
||||
"Please input your organization name!": "Please input your organization name!",
|
||||
"Please input your password!": "パスワードを入力してください!",
|
||||
"Please input your password, at least 6 characters!": "パスワードを入力してください。少なくとも6文字です!",
|
||||
"Please select an organization": "Please select an organization",
|
||||
"Please select an organization to sign in": "Please select an organization to sign in",
|
||||
"Please type an organization to sign in": "Please type an organization to sign in",
|
||||
@@ -932,6 +939,11 @@
|
||||
"Set password...": "パスワードの設定...",
|
||||
"Tag": "タグ",
|
||||
"Tag - Tooltip": "ユーザーのタグ",
|
||||
"The password must contain at least one special character": "The password must contain at least one special character",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
|
||||
"The password must have at least 6 characters": "The password must have at least 6 characters",
|
||||
"The password must have at least 8 characters": "The password must have at least 8 characters",
|
||||
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
|
||||
"Title": "タイトル",
|
||||
"Title - Tooltip": "所属のポジション",
|
||||
"Two passwords you typed do not match.": "2つのパスワードが一致しません。",
|
||||
|
@@ -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": "정책 저장소의 테이블 이름",
|
||||
@@ -255,6 +261,8 @@
|
||||
"Organizations": "조직들",
|
||||
"Password": "비밀번호",
|
||||
"Password - Tooltip": "비밀번호가 올바른지 확인하세요",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
|
||||
"Password salt": "비밀번호 솔트",
|
||||
"Password salt - Tooltip": "암호화에 사용되는 임의 매개변수",
|
||||
"Password type": "암호 유형",
|
||||
@@ -388,7 +396,6 @@
|
||||
"Please input your code!": "코드를 입력해주세요!",
|
||||
"Please input your organization name!": "Please input your organization name!",
|
||||
"Please input your password!": "비밀번호를 입력해주세요!",
|
||||
"Please input your password, at least 6 characters!": "비밀번호를 입력해주세요. 최소 6자 이상 필요합니다!",
|
||||
"Please select an organization": "Please select an organization",
|
||||
"Please select an organization to sign in": "Please select an organization to sign in",
|
||||
"Please type an organization to sign in": "Please type an organization to sign in",
|
||||
@@ -932,6 +939,11 @@
|
||||
"Set password...": "비밀번호 설정...",
|
||||
"Tag": "태그",
|
||||
"Tag - Tooltip": "사용자의 태그",
|
||||
"The password must contain at least one special character": "The password must contain at least one special character",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
|
||||
"The password must have at least 6 characters": "The password must have at least 6 characters",
|
||||
"The password must have at least 8 characters": "The password must have at least 8 characters",
|
||||
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
|
||||
"Title": "제목",
|
||||
"Title - Tooltip": "소속 내 직위",
|
||||
"Two passwords you typed do not match.": "두 개의 비밀번호가 일치하지 않습니다.",
|
||||
|
@@ -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",
|
||||
@@ -255,6 +261,8 @@
|
||||
"Organizations": "Organizações",
|
||||
"Password": "Senha",
|
||||
"Password - Tooltip": "Certifique-se de que a senha está correta",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
|
||||
"Password salt": "Salt de senha",
|
||||
"Password salt - Tooltip": "Parâmetro aleatório usado para criptografia de senha",
|
||||
"Password type": "Tipo de senha",
|
||||
@@ -388,7 +396,6 @@
|
||||
"Please input your code!": "Por favor, informe o código!",
|
||||
"Please input your organization name!": "Please input your organization name!",
|
||||
"Please input your password!": "Por favor, informe sua senha!",
|
||||
"Please input your password, at least 6 characters!": "Por favor, informe sua senha, pelo menos 6 caracteres!",
|
||||
"Please select an organization": "Please select an organization",
|
||||
"Please select an organization to sign in": "Please select an organization to sign in",
|
||||
"Please type an organization to sign in": "Please type an organization to sign in",
|
||||
@@ -932,6 +939,11 @@
|
||||
"Set password...": "Definir senha...",
|
||||
"Tag": "Tag",
|
||||
"Tag - Tooltip": "Tag do usuário",
|
||||
"The password must contain at least one special character": "The password must contain at least one special character",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
|
||||
"The password must have at least 6 characters": "The password must have at least 6 characters",
|
||||
"The password must have at least 8 characters": "The password must have at least 8 characters",
|
||||
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
|
||||
"Title": "Título",
|
||||
"Title - Tooltip": "Cargo na afiliação",
|
||||
"Two passwords you typed do not match.": "As duas senhas digitadas não coincidem.",
|
||||
|
@@ -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": "Имя таблицы хранилища политик",
|
||||
@@ -255,6 +261,8 @@
|
||||
"Organizations": "Организации",
|
||||
"Password": "Пароль",
|
||||
"Password - Tooltip": "Убедитесь, что пароль правильный",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
|
||||
"Password salt": "Соль пароля",
|
||||
"Password salt - Tooltip": "Случайный параметр, используемый для шифрования пароля",
|
||||
"Password type": "Тип пароля",
|
||||
@@ -388,7 +396,6 @@
|
||||
"Please input your code!": "Пожалуйста, введите свой код!",
|
||||
"Please input your organization name!": "Please input your organization name!",
|
||||
"Please input your password!": "Пожалуйста, введите свой пароль!",
|
||||
"Please input your password, at least 6 characters!": "Пожалуйста, введите свой пароль, длина должна быть не менее 6 символов!",
|
||||
"Please select an organization": "Please select an organization",
|
||||
"Please select an organization to sign in": "Please select an organization to sign in",
|
||||
"Please type an organization to sign in": "Please type an organization to sign in",
|
||||
@@ -932,6 +939,11 @@
|
||||
"Set password...": "Установить пароль...",
|
||||
"Tag": "Метка",
|
||||
"Tag - Tooltip": "Тег пользователя",
|
||||
"The password must contain at least one special character": "The password must contain at least one special character",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
|
||||
"The password must have at least 6 characters": "The password must have at least 6 characters",
|
||||
"The password must have at least 8 characters": "The password must have at least 8 characters",
|
||||
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
|
||||
"Title": "Заголовок",
|
||||
"Title - Tooltip": "Положение в аффилиации",
|
||||
"Two passwords you typed do not match.": "Два введенных вами пароля не совпадают.",
|
||||
|
@@ -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",
|
||||
@@ -255,6 +261,8 @@
|
||||
"Organizations": "Tổ chức",
|
||||
"Password": "Mật khẩu",
|
||||
"Password - Tooltip": "Hãy đảm bảo rằng mật khẩu là chính xác",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
|
||||
"Password salt": "Muối mật khẩu",
|
||||
"Password salt - Tooltip": "Tham số ngẫu nhiên được sử dụng để mã hóa mật khẩu",
|
||||
"Password type": "Loại mật khẩu",
|
||||
@@ -388,7 +396,6 @@
|
||||
"Please input your code!": "Vui lòng nhập mã của bạn!",
|
||||
"Please input your organization name!": "Please input your organization name!",
|
||||
"Please input your password!": "Vui lòng nhập mật khẩu của bạn!",
|
||||
"Please input your password, at least 6 characters!": "Vui lòng nhập mật khẩu của bạn, ít nhất 6 ký tự!",
|
||||
"Please select an organization": "Please select an organization",
|
||||
"Please select an organization to sign in": "Please select an organization to sign in",
|
||||
"Please type an organization to sign in": "Please type an organization to sign in",
|
||||
@@ -932,6 +939,11 @@
|
||||
"Set password...": "Đặt mật khẩu...",
|
||||
"Tag": "Thẻ",
|
||||
"Tag - Tooltip": "Thẻ của người dùng",
|
||||
"The password must contain at least one special character": "The password must contain at least one special character",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
|
||||
"The password must have at least 6 characters": "The password must have at least 6 characters",
|
||||
"The password must have at least 8 characters": "The password must have at least 8 characters",
|
||||
"The password must not contain any repeated characters": "The password must not contain any repeated characters",
|
||||
"Title": "Tiêu đề",
|
||||
"Title - Tooltip": "Vị trí trong tổ chức",
|
||||
"Two passwords you typed do not match.": "Hai mật khẩu mà bạn đã nhập không khớp.",
|
||||
|
@@ -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": "策略存储的表名",
|
||||
@@ -220,7 +226,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": "应用的首页",
|
||||
@@ -255,6 +261,8 @@
|
||||
"Organizations": "组织",
|
||||
"Password": "密码",
|
||||
"Password - Tooltip": "请确认密码正确",
|
||||
"Password complexity options": "密码复杂度选项",
|
||||
"Password complexity options - Tooltip": "密码复杂度组合,登录密码复杂度必须符合该规范",
|
||||
"Password salt": "密码Salt值",
|
||||
"Password salt - Tooltip": "用于密码加密的随机参数",
|
||||
"Password type": "密码类型",
|
||||
@@ -337,11 +345,11 @@
|
||||
"{total} in total": "{total} 总计"
|
||||
},
|
||||
"group": {
|
||||
"Edit Group": "编辑用户组",
|
||||
"New Group": "新建用户组",
|
||||
"Edit Group": "编辑群组",
|
||||
"New Group": "新建群组",
|
||||
"Parent group": "上级组",
|
||||
"Parent group - Tooltip": "上级组",
|
||||
"Physical": "物理组",
|
||||
"Physical": "实体组",
|
||||
"Show all": "显示全部",
|
||||
"Virtual": "虚拟组"
|
||||
},
|
||||
@@ -388,7 +396,6 @@
|
||||
"Please input your code!": "请输入您的验证码!",
|
||||
"Please input your organization name!": "请输入组织的名字!",
|
||||
"Please input your password!": "请输入您的密码!",
|
||||
"Please input your password, at least 6 characters!": "请输入您的密码,不少于6位",
|
||||
"Please select an organization": "请选择一个组织",
|
||||
"Please select an organization to sign in": "请选择要登录的组织",
|
||||
"Please type an organization to sign in": "请输入要登录的组织",
|
||||
@@ -932,6 +939,11 @@
|
||||
"Set password...": "设置密码...",
|
||||
"Tag": "标签",
|
||||
"Tag - Tooltip": "用户的标签",
|
||||
"The password must contain at least one special character": "密码必须包含至少一个特殊字符",
|
||||
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "密码必须包含至少一个大写字母、一个小写字母和一个数字",
|
||||
"The password must have at least 6 characters": "密码长度必须至少为6个字符",
|
||||
"The password must have at least 8 characters": "密码长度必须至少为8个字符",
|
||||
"The password must not contain any repeated characters": "密码不得包含任何重复字符",
|
||||
"Title": "职务",
|
||||
"Title - Tooltip": "在工作单位担任的职务",
|
||||
"Two passwords you typed do not match.": "两次输入的密码不匹配。",
|
||||
|
Reference in New Issue
Block a user