Compare commits

..

11 Commits

Author SHA1 Message Date
WangGuan
7867060b71 feat: add quota limitation to organizations, users, providers and applications (#1339) 2022-11-29 11:01:41 +08:00
Mr Forest
8890d1d7c7 fix: check credential existence when signing via WebAuthn (#1336)
* fix: check credential existence when signing via WebAuthn

* fix review problem
2022-11-28 21:47:17 +08:00
Chell
6e6a0a074a fix: application edit mobile view (#1331)
* fix: application edit mobile view

* fix: decompose elements

* fix: decomposition

* fix: remove space component

* Update ApplicationEditPage.js

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2022-11-28 21:10:49 +08:00
Thai
cff3007992 feat: add get-permissions-by-role API (#1335) 2022-11-28 15:30:46 +08:00
Mr Forest
fe448cbcf4 feat: check user existence when signing in via verification code (#1334)
* fix:check user existence when logining by verification code

* fix review problems

* Update verification.go

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2022-11-28 00:11:33 +08:00
Chell
2ab25df950 fix: prompt page translation (#1330)
* fix: prompt page translation

* add multiple translations

* fix: translation consistency

* fix: translation consistency

* fix: add translation

* fix: add translation

* Update data.json

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2022-11-27 21:04:45 +08:00
WangGuan
b895926754 feat: use another filename when uploading a duplicated file instead of replacing it (#1329)
* fix: upload a file with the same name, not replace

* Update resource.go

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2022-11-27 17:32:15 +08:00
YiNN
5bb7a4153f feat: add cloudflare turnstile captcha (#1327)
* feat: add cloudflare turnstile captcha

* fix: rename turnstile to cloudflare turnstile
2022-11-26 17:17:49 +08:00
Yaodong Yu
b7cd598ee8 fix: fail to return after flush the page (#1325)
* fix: fail to return after flush the page 

Old methed just get the url path parameter when click the butten. But when the page flushed, the returnUrl will disappear, so can not return to the specified page.

* Update UserEditPage.js

* Update UserEditPage.js

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2022-11-25 23:08:45 +08:00
Yaodong Yu
b10fb97c92 feat: finish policy list management (#1317) 2022-11-25 16:02:20 +08:00
Yaodong Yu
b337b908ea feat: fix the bug that admin cannot upload avatar for other users (#1323) 2022-11-25 09:36:47 +08:00
56 changed files with 953 additions and 214 deletions

View File

@@ -31,6 +31,8 @@ func GetCaptchaProvider(captchaType string) CaptchaProvider {
return NewAliyunCaptchaProvider()
} else if captchaType == "GEETEST" {
return NewGEETESTCaptchaProvider()
} else if captchaType == "Cloudflare Turnstile" {
return NewCloudflareTurnstileProvider()
}
return nil
}

66
captcha/turnstile.go Normal file
View File

@@ -0,0 +1,66 @@
// Copyright 2022 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 captcha
import (
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"strings"
)
const CloudflareTurnstileVerifyUrl = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
type CloudflareTurnstileProvider struct{}
func NewCloudflareTurnstileProvider() *CloudflareTurnstileProvider {
captcha := &CloudflareTurnstileProvider{}
return captcha
}
func (captcha *CloudflareTurnstileProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
reqData := url.Values{
"secret": {clientSecret},
"response": {token},
}
resp, err := http.PostForm(CloudflareTurnstileVerifyUrl, reqData)
if err != nil {
return false, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, err
}
type captchaResponse struct {
Success bool `json:"success"`
ErrorCodes []string `json:"error-codes"`
}
captchaResp := &captchaResponse{}
err = json.Unmarshal(body, captchaResp)
if err != nil {
return false, err
}
if len(captchaResp.ErrorCodes) > 0 {
return false, errors.New(strings.Join(captchaResp.ErrorCodes, ","))
}
return captchaResp.Success, nil
}

View File

@@ -21,3 +21,4 @@ isDemoMode = false
batchSize = 100
ldapServerPort = 389
languages = en,zh,es,fr,de,ja,ko,ru
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}

View File

@@ -15,6 +15,7 @@
package conf
import (
"encoding/json"
"fmt"
"os"
"runtime"
@@ -24,6 +25,15 @@ import (
"github.com/beego/beego"
)
type Quota struct {
Organization int `json:"organization"`
User int `json:"user"`
Application int `json:"application"`
Provider int `json:"provider"`
}
var quota = &Quota{-1, -1, -1, -1}
func init() {
// this array contains the beego configuration items that may be modified via env
presetConfigItems := []string{"httpport", "appname"}
@@ -35,6 +45,17 @@ func init() {
}
}
}
initQuota()
}
func initQuota() {
res := beego.AppConfig.String("quota")
if res != "" {
err := json.Unmarshal([]byte(res), quota)
if err != nil {
panic(err)
}
}
}
func GetConfigString(key string) string {
@@ -95,3 +116,7 @@ func GetConfigBatchSize() int {
}
return res
}
func GetConfigQuota() *Quota {
return quota
}

View File

@@ -93,3 +93,19 @@ func TestGetConfBool(t *testing.T) {
})
}
}
func TestGetConfigQuota(t *testing.T) {
scenarios := []struct {
description string
expected *Quota
}{
{"default", &Quota{-1, -1, -1, -1}},
}
err := beego.LoadAppConfig("ini", "app.conf")
assert.Nil(t, err)
for _, scenery := range scenarios {
quota := GetConfigQuota()
assert.Equal(t, scenery.expected, quota)
}
}

View File

@@ -167,6 +167,12 @@ func (c *ApiController) AddApplication() {
return
}
count := object.GetApplicationCount("", "", "")
if err := checkQuotaForApplication(count); err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddApplication(&application))
c.ServeJSON()
}

View File

@@ -307,7 +307,7 @@ func (c *ApiController) Login() {
}
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", application.Organization))
provider := object.GetProvider(util.GetId(form.Provider))
provider := object.GetProvider(util.GetId("admin", form.Provider))
providerItem := application.GetProviderItem(provider.Name)
if !providerItem.IsProviderVisible() {
c.ResponseError(fmt.Sprintf(c.T("ProviderErr.ProviderNotEnabled"), provider.Name))

View File

@@ -18,6 +18,7 @@ import (
"encoding/json"
"github.com/beego/beego/utils/pagination"
xormadapter "github.com/casbin/xorm-adapter/v3"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
@@ -89,6 +90,69 @@ func (c *ApiController) SyncPolicies() {
id := c.Input().Get("id")
adapter := object.GetCasbinAdapter(id)
c.Data["json"] = object.SyncPolicies(adapter)
policies, err := object.SyncPolicies(adapter)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = policies
c.ServeJSON()
}
func (c *ApiController) UpdatePolicy() {
id := c.Input().Get("id")
adapter := object.GetCasbinAdapter(id)
var policies []xormadapter.CasbinRule
err := json.Unmarshal(c.Ctx.Input.RequestBody, &policies)
if err != nil {
c.ResponseError(err.Error())
return
}
affected, err := object.UpdatePolicy(util.CasbinToSlice(policies[0]), util.CasbinToSlice(policies[1]), adapter)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(affected)
c.ServeJSON()
}
func (c *ApiController) AddPolicy() {
id := c.Input().Get("id")
adapter := object.GetCasbinAdapter(id)
var policy xormadapter.CasbinRule
err := json.Unmarshal(c.Ctx.Input.RequestBody, &policy)
if err != nil {
c.ResponseError(err.Error())
return
}
affected, err := object.AddPolicy(util.CasbinToSlice(policy), adapter)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(affected)
c.ServeJSON()
}
func (c *ApiController) RemovePolicy() {
id := c.Input().Get("id")
adapter := object.GetCasbinAdapter(id)
var policy xormadapter.CasbinRule
err := json.Unmarshal(c.Ctx.Input.RequestBody, &policy)
if err != nil {
c.ResponseError(err.Error())
return
}
affected, err := object.RemovePolicy(util.CasbinToSlice(policy), adapter)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(affected)
c.ServeJSON()
}

View File

@@ -99,6 +99,12 @@ func (c *ApiController) AddOrganization() {
return
}
count := object.GetOrganizationCount("", "", "")
if err := checkQuotaForOrganization(count); err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddOrganization(&organization))
c.ServeJSON()
}

View File

@@ -65,6 +65,20 @@ func (c *ApiController) GetPermissionsBySubmitter() {
return
}
// GetPermissionsByRole
// @Title GetPermissionsByRole
// @Tag Permission API
// @Description get permissions by role
// @Param id query string true "The id of the role"
// @Success 200 {array} object.Permission The Response object
// @router /get-permissions-by-role [get]
func (c *ApiController) GetPermissionsByRole() {
id := c.Input().Get("id")
permissions := object.GetPermissionsByRole(id)
c.ResponseOk(permissions, len(permissions))
return
}
// GetPermission
// @Title GetPermission
// @Tag Permission API

View File

@@ -122,6 +122,12 @@ func (c *ApiController) AddProvider() {
return
}
count := object.GetProviderCount("", "", "")
if err := checkQuotaForProvider(count); err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddProvider(&provider))
c.ServeJSON()
}

View File

@@ -156,7 +156,7 @@ func (c *ApiController) UploadResource() {
return
}
provider, user, ok := c.GetProviderFromContext("Storage")
provider, _, ok := c.GetProviderFromContext("Storage")
if !ok {
return
}
@@ -171,6 +171,20 @@ func (c *ApiController) UploadResource() {
fileType, _ = util.GetOwnerAndNameFromId(mimeType)
}
if tag != "avatar" && tag != "termsOfUse" {
ext := filepath.Ext(filepath.Base(fullFilePath))
index := len(fullFilePath) - len(ext)
for i := 1; ; i++ {
_, objectKey := object.GetUploadFileUrl(provider, fullFilePath, true)
if object.GetResourceCount(owner, username, "name", objectKey) == 0 {
break
}
// duplicated fullFilePath found, change it
fullFilePath = fullFilePath[:index] + fmt.Sprintf("-%d", i) + ext
}
}
fileUrl, objectKey, err := object.UploadFileSafe(provider, fullFilePath, fileBuffer)
if err != nil {
c.ResponseError(err.Error())
@@ -202,12 +216,10 @@ func (c *ApiController) UploadResource() {
switch tag {
case "avatar":
user := object.GetUserNoCheck(util.GetId(owner, username))
if user == nil {
user = object.GetUserNoCheck(username)
if user == nil {
c.ResponseError(c.T("ResourceErr.UserIsNil"))
return
}
c.ResponseError(c.T("ResourceErr.UserIsNil"))
return
}
user.Avatar = fileUrl

View File

@@ -60,7 +60,7 @@ func (c *ApiController) SendEmail() {
var provider *object.Provider
if emailForm.Provider != "" {
// called by frontend's TestEmailWidget, provider name is set by frontend
provider = object.GetProvider(util.GetId(emailForm.Provider))
provider = object.GetProvider(util.GetId("admin", emailForm.Provider))
} else {
// called by Casdoor SDK via Client ID & Client Secret, so the used Email provider will be the application' Email provider or the default Email provider
var ok bool

View File

@@ -183,6 +183,12 @@ func (c *ApiController) AddUser() {
return
}
count := object.GetUserCount("", "", "")
if err := checkQuotaForUser(count); err != nil {
c.ResponseError(err.Error())
return
}
msg := object.CheckUsername(user.Name, c.GetAcceptLanguage())
if msg != "" {
c.ResponseError(msg)

View File

@@ -126,7 +126,7 @@ func getInitScore() (int, error) {
func (c *ApiController) GetProviderFromContext(category string) (*object.Provider, *object.User, bool) {
providerName := c.Input().Get("provider")
if providerName != "" {
provider := object.GetProvider(util.GetId(providerName))
provider := object.GetProvider(util.GetId("admin", providerName))
if provider == nil {
c.ResponseError(c.T("ProviderErr.ProviderNotFound"), providerName)
return nil, nil, false
@@ -153,3 +153,47 @@ func (c *ApiController) GetProviderFromContext(category string) (*object.Provide
return provider, user, true
}
func checkQuotaForApplication(count int) error {
quota := conf.GetConfigQuota().Application
if quota == -1 {
return nil
}
if count >= quota {
return fmt.Errorf("application quota is exceeded")
}
return nil
}
func checkQuotaForOrganization(count int) error {
quota := conf.GetConfigQuota().Organization
if quota == -1 {
return nil
}
if count >= quota {
return fmt.Errorf("organization quota is exceeded")
}
return nil
}
func checkQuotaForProvider(count int) error {
quota := conf.GetConfigQuota().Provider
if quota == -1 {
return nil
}
if count >= quota {
return fmt.Errorf("provider quota is exceeded")
}
return nil
}
func checkQuotaForUser(count int) error {
quota := conf.GetConfigQuota().User
if quota == -1 {
return nil
}
if count >= quota {
return fmt.Errorf("user quota is exceeded")
}
return nil
}

View File

@@ -92,6 +92,10 @@ func (c *ApiController) SendVerificationCode() {
user := c.getCurrentUser()
application := object.GetApplication(applicationId)
organization := object.GetOrganization(fmt.Sprintf("%s/%s", application.Owner, application.Organization))
if organization == nil {
c.ResponseError(c.T("OrgErr.DoNotExist"))
return
}
if checkUser == "true" && user == nil && object.GetUserByFields(organization.Name, dest) == nil {
c.ResponseError(c.T("LoginErr.LoginFirst"))
@@ -114,6 +118,12 @@ func (c *ApiController) SendVerificationCode() {
return
}
userByEmail := object.GetUserByEmail(organization.Name, dest)
if userByEmail == nil {
c.ResponseError(c.T("UserErr.DoNotExistSignUp"))
return
}
provider := application.GetEmailProvider()
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, dest)
case "phone":
@@ -124,8 +134,10 @@ func (c *ApiController) SendVerificationCode() {
c.ResponseError(c.T("PhoneErr.NumberInvalid"))
return
}
if organization == nil {
c.ResponseError(c.T("OrgErr.DoNotExist"))
userByPhone := object.GetUserByPhone(organization.Name, dest)
if userByPhone == nil {
c.ResponseError(c.T("UserErr.DoNotExistSignUp"))
return
}

View File

@@ -104,6 +104,11 @@ func (c *ApiController) WebAuthnSigninBegin() {
c.ResponseError(fmt.Sprintf(c.T("UserErr.DoNotExistInOrg"), userOwner, userName))
return
}
if len(user.WebauthnCredentials) == 0 {
c.ResponseError(c.T("UserErr.NoWebAuthnCredential"))
return
}
options, sessionData, err := webauthnObj.BeginLogin(user)
if err != nil {
c.ResponseError(err.Error())

View File

@@ -25,6 +25,6 @@ import (
)
func TestDeployStaticFiles(t *testing.T) {
provider := object.GetProvider(util.GetId("provider_storage_aliyun_oss"))
provider := object.GetProvider(util.GetId("admin", "provider_storage_aliyun_oss"))
deployStaticFiles(provider)
}

View File

@@ -134,4 +134,5 @@ NameTooLang = Username is too long (maximum is 39 characters).
NameFormatErr = 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.
PasswordLessThanSixCharacters = Password must have at least 6 characters
InvalidInformation = Invalid information
NoWebAuthnCredential = Found no credentials for this user

View File

@@ -134,4 +134,5 @@ NameTooLang = Username is too long (maximum is 39 characters).
NameFormatErr = 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.
PasswordLessThanSixCharacters = Password must have at least 6 characters
InvalidInformation = Invalid information
NoWebAuthnCredential = Found no credentials for this user

View File

@@ -134,4 +134,5 @@ NameTooLang = Username is too long (maximum is 39 characters).
NameFormatErr = 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.
PasswordLessThanSixCharacters = Password must have at least 6 characters
InvalidInformation = Invalid information
NoWebAuthnCredential = Found no credentials for this user

View File

@@ -134,4 +134,5 @@ NameTooLang = Username is too long (maximum is 39 characters).
NameFormatErr = 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.
PasswordLessThanSixCharacters = Password must have at least 6 characters
InvalidInformation = Invalid information
NoWebAuthnCredential = Found no credentials for this user

View File

@@ -134,4 +134,5 @@ NameTooLang = Username is too long (maximum is 39 characters).
NameFormatErr = 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.
PasswordLessThanSixCharacters = Password must have at least 6 characters
InvalidInformation = Invalid information
NoWebAuthnCredential = Found no credentials for this user

View File

@@ -134,4 +134,5 @@ NameTooLang = Username is too long (maximum is 39 characters).
NameFormatErr = 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.
PasswordLessThanSixCharacters = Password must have at least 6 characters
InvalidInformation = Invalid information
NoWebAuthnCredential = Found no credentials for this user

View File

@@ -134,4 +134,5 @@ NameTooLang = Username is too long (maximum is 39 characters).
NameFormatErr = 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.
PasswordLessThanSixCharacters = Password must have at least 6 characters
InvalidInformation = Invalid information
NoWebAuthnCredential = Found no credentials for this user

View File

@@ -1,5 +1,5 @@
[ApplicationErr]
AppNotFound = 应用 %%s 未找到
AppNotFound = 应用 %s 未找到
AppNotFoundForUserID = 找不到该用户的应用程序 %s
GrantTypeNotSupport = 此应用中不支持此授权类型
HasNoProviders = 该应用无提供商
@@ -25,7 +25,7 @@ EmptyErr = 邮箱不可为空
EmailInvalid = 无效邮箱
EmailCheckResult = Email: %s
EmptyParam = 邮件参数为空: %v
InvalidReceivers = 无效的邮箱接收者: %%s
InvalidReceivers = 无效的邮箱接收者: %s
UnableGetModifyRule = 无法得到Email修改规则
[EnforcerErr]
@@ -131,6 +131,7 @@ NameFormatErr = 用户名只能包含字母数字字符、下划线或连字符
PasswordLessThanSixCharacters = 密码至少为6字符
DoNotExistSignUp = 用户不存在,请先注册
InvalidInformation = 无效信息
NoWebAuthnCredential = 该用户没有WebAuthn凭据
[StorageErr]
ObjectKeyNotAllowed = object key :%s 不被允许

View File

@@ -60,7 +60,7 @@ func getPermanentAvatarUrl(organization string, username string, url string, upl
}
fullFilePath := fmt.Sprintf("/avatar/%s/%s.png", organization, username)
uploadedFileUrl, _ := getUploadFileUrl(defaultStorageProvider, fullFilePath, false)
uploadedFileUrl, _ := GetUploadFileUrl(defaultStorageProvider, fullFilePath, false)
if upload {
DownloadAndUpload(url, fullFilePath)

View File

@@ -148,34 +148,7 @@ func (casbinAdapter *CasbinAdapter) getTable() string {
}
}
func safeReturn(policy []string, i int) string {
if len(policy) > i {
return policy[i]
} else {
return ""
}
}
func matrixToCasbinRules(pType string, policies [][]string) []*xormadapter.CasbinRule {
res := []*xormadapter.CasbinRule{}
for _, policy := range policies {
line := xormadapter.CasbinRule{
Ptype: pType,
V0: safeReturn(policy, 0),
V1: safeReturn(policy, 1),
V2: safeReturn(policy, 2),
V3: safeReturn(policy, 3),
V4: safeReturn(policy, 4),
V5: safeReturn(policy, 5),
}
res = append(res, &line)
}
return res
}
func SyncPolicies(casbinAdapter *CasbinAdapter) []*xormadapter.CasbinRule {
func initEnforcer(modelObj *Model, casbinAdapter *CasbinAdapter) (*casbin.Enforcer, error) {
// init Adapter
if casbinAdapter.Adapter == nil {
var dataSourceName string
@@ -191,20 +164,60 @@ func SyncPolicies(casbinAdapter *CasbinAdapter) []*xormadapter.CasbinRule {
dataSourceName = strings.ReplaceAll(dataSourceName, "dbi.", "db.")
}
casbinAdapter.Adapter, _ = xormadapter.NewAdapterByEngineWithTableName(NewAdapter(casbinAdapter.DatabaseType, dataSourceName, casbinAdapter.Database).Engine, casbinAdapter.getTable(), "")
var err error
casbinAdapter.Adapter, err = xormadapter.NewAdapterByEngineWithTableName(NewAdapter(casbinAdapter.DatabaseType, dataSourceName, casbinAdapter.Database).Engine, casbinAdapter.getTable(), "")
if err != nil {
return nil, err
}
}
// init Model
modelObj := getModel(casbinAdapter.Owner, casbinAdapter.Model)
m, err := model.NewModelFromString(modelObj.ModelText)
if err != nil {
panic(err)
return nil, err
}
// init Enforcer
enforcer, err := casbin.NewEnforcer(m, casbinAdapter.Adapter)
if err != nil {
panic(err)
return nil, err
}
return enforcer, nil
}
func safeReturn(policy []string, i int) string {
if len(policy) > i {
return policy[i]
} else {
return ""
}
}
func matrixToCasbinRules(Ptype string, policies [][]string) []*xormadapter.CasbinRule {
res := []*xormadapter.CasbinRule{}
for _, policy := range policies {
line := xormadapter.CasbinRule{
Ptype: Ptype,
V0: safeReturn(policy, 0),
V1: safeReturn(policy, 1),
V2: safeReturn(policy, 2),
V3: safeReturn(policy, 3),
V4: safeReturn(policy, 4),
V5: safeReturn(policy, 5),
}
res = append(res, &line)
}
return res
}
func SyncPolicies(casbinAdapter *CasbinAdapter) ([]*xormadapter.CasbinRule, error) {
modelObj := getModel(casbinAdapter.Owner, casbinAdapter.Model)
enforcer, err := initEnforcer(modelObj, casbinAdapter)
if err != nil {
return nil, err
}
policies := matrixToCasbinRules("p", enforcer.GetPolicy())
@@ -212,5 +225,48 @@ func SyncPolicies(casbinAdapter *CasbinAdapter) []*xormadapter.CasbinRule {
policies = append(policies, matrixToCasbinRules("g", enforcer.GetGroupingPolicy())...)
}
return policies
return policies, nil
}
func UpdatePolicy(oldPolicy, newPolicy []string, casbinAdapter *CasbinAdapter) (bool, error) {
modelObj := getModel(casbinAdapter.Owner, casbinAdapter.Model)
enforcer, err := initEnforcer(modelObj, casbinAdapter)
if err != nil {
return false, err
}
affected, err := enforcer.UpdatePolicy(oldPolicy, newPolicy)
if err != nil {
return affected, err
}
return affected, nil
}
func AddPolicy(policy []string, casbinAdapter *CasbinAdapter) (bool, error) {
modelObj := getModel(casbinAdapter.Owner, casbinAdapter.Model)
enforcer, err := initEnforcer(modelObj, casbinAdapter)
if err != nil {
return false, err
}
affected, err := enforcer.AddPolicy(policy)
if err != nil {
return affected, err
}
return affected, nil
}
func RemovePolicy(policy []string, casbinAdapter *CasbinAdapter) (bool, error) {
modelObj := getModel(casbinAdapter.Owner, casbinAdapter.Model)
enforcer, err := initEnforcer(modelObj, casbinAdapter)
if err != nil {
return false, err
}
affected, err := enforcer.RemovePolicy(policy)
if err != nil {
return affected, err
}
return affected, nil
}

View File

@@ -222,7 +222,7 @@ func initBuiltInLdap() {
}
func initBuiltInProvider() {
provider := GetProvider(util.GetId("provider_captcha_default"))
provider := GetProvider(util.GetId("admin", "provider_captcha_default"))
if provider != nil {
return
}

View File

@@ -168,7 +168,7 @@ func initDefinedLdap(ldap *Ldap) {
}
func initDefinedProvider(provider *Provider) {
existed := GetProvider(util.GetId(provider.Name))
existed := GetProvider(util.GetId("admin", provider.Name))
if existed != nil {
return
}

View File

@@ -54,7 +54,7 @@ func escapePath(path string) string {
return res
}
func getUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool) (string, string) {
func GetUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool) (string, string) {
escapedPath := util.UrlJoin(provider.PathPrefix, escapePath(fullFilePath))
objectKey := util.UrlJoin(util.GetUrlPath(provider.Domain), escapedPath)
@@ -93,7 +93,7 @@ func uploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffe
UpdateProvider(provider.GetId(), provider)
}
fileUrl, objectKey := getUploadFileUrl(provider, fullFilePath, true)
fileUrl, objectKey := GetUploadFileUrl(provider, fullFilePath, true)
_, err := storageProvider.Put(objectKey, fileBuffer)
if err != nil {

View File

@@ -678,7 +678,7 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin
ErrorDescription: "the application does not support wechat mini program",
}
}
provider := GetProvider(util.GetId(mpProvider.Name))
provider := GetProvider(util.GetId("admin", mpProvider.Name))
mpIdp := idp.NewWeChatMiniProgramIdProvider(provider.ClientId, provider.ClientSecret)
session, err := mpIdp.GetSessionByCode(code)
if err != nil {

View File

@@ -82,6 +82,7 @@ func initAPI() {
beego.Router("/api/get-permissions", &controllers.ApiController{}, "GET:GetPermissions")
beego.Router("/api/get-permissions-by-submitter", &controllers.ApiController{}, "GET:GetPermissionsBySubmitter")
beego.Router("/api/get-permissions-by-role", &controllers.ApiController{}, "GET:GetPermissionsByRole")
beego.Router("/api/get-permission", &controllers.ApiController{}, "GET:GetPermission")
beego.Router("/api/update-permission", &controllers.ApiController{}, "POST:UpdatePermission")
beego.Router("/api/add-permission", &controllers.ApiController{}, "POST:AddPermission")
@@ -105,6 +106,9 @@ func initAPI() {
beego.Router("/api/add-adapter", &controllers.ApiController{}, "POST:AddCasbinAdapter")
beego.Router("/api/delete-adapter", &controllers.ApiController{}, "POST:DeleteCasbinAdapter")
beego.Router("/api/sync-policies", &controllers.ApiController{}, "GET:SyncPolicies")
beego.Router("/api/update-policy", &controllers.ApiController{}, "POST:UpdatePolicy")
beego.Router("/api/add-policy", &controllers.ApiController{}, "POST:AddPolicy")
beego.Router("/api/remove-policy", &controllers.ApiController{}, "POST:RemovePolicy")
beego.Router("/api/set-password", &controllers.ApiController{}, "POST:SetPassword")
beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword")

View File

@@ -123,8 +123,8 @@ func GenerateSimpleTimeId() string {
return t
}
func GetId(name string) string {
return fmt.Sprintf("admin/%s", name)
func GetId(owner, name string) string {
return fmt.Sprintf("%s/%s", owner, name)
}
func GetMd5Hash(text string) string {

View File

@@ -137,16 +137,16 @@ func TestGenerateId(t *testing.T) {
func TestGetId(t *testing.T) {
scenarios := []struct {
description string
input string
input []string
expected interface{}
}{
{"Scenery one", "casdoor", "admin/casdoor"},
{"Scenery two", "casbin", "admin/casbin"},
{"Scenery three", "lorem ipsum", "admin/lorem ipsum"},
{"Scenery one", []string{"admin", "casdoor"}, "admin/casdoor"},
{"Scenery two", []string{"admin", "casbin"}, "admin/casbin"},
{"Scenery three", []string{"test", "lorem ipsum"}, "test/lorem ipsum"},
}
for _, scenery := range scenarios {
t.Run(scenery.description, func(t *testing.T) {
actual := GetId(scenery.input)
actual := GetId(scenery.input[0], scenery.input[1])
assert.Equal(t, scenery.expected, actual, "This not is a valid MD5")
})
}

29
util/struct.go Normal file
View File

@@ -0,0 +1,29 @@
// Copyright 2022 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 util
import xormadapter "github.com/casbin/xorm-adapter/v3"
func CasbinToSlice(casbinRule xormadapter.CasbinRule) []string {
s := []string{
casbinRule.V0,
casbinRule.V1,
casbinRule.V2,
casbinRule.V3,
casbinRule.V4,
casbinRule.V5,
}
return s
}

View File

@@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select, Switch, Table, Tooltip} from "antd";
import {Button, Card, Col, Input, InputNumber, Row, Select, Switch} from "antd";
import * as AdapterBackend from "./backend/AdapterBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting";
@@ -21,7 +21,7 @@ import i18next from "i18next";
import "codemirror/lib/codemirror.css";
import * as ModelBackend from "./backend/ModelBackend";
import {EditOutlined, MinusOutlined} from "@ant-design/icons";
import PolicyTable from "./common/PoliciyTable";
require("codemirror/theme/material-darker.css");
require("codemirror/mode/javascript/javascript");
@@ -32,12 +32,11 @@ class AdapterEditPage extends React.Component {
super(props);
this.state = {
classes: props,
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
owner: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
adapterName: props.match.params.adapterName,
adapter: null,
organizations: [],
models: [],
policyLists: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@@ -48,7 +47,7 @@ class AdapterEditPage extends React.Component {
}
getAdapter() {
AdapterBackend.getAdapter(this.state.organizationName, this.state.adapterName)
AdapterBackend.getAdapter(this.state.owner, this.state.adapterName)
.then((adapter) => {
this.setState({
adapter: adapter,
@@ -93,93 +92,6 @@ class AdapterEditPage extends React.Component {
});
}
synPolicies() {
this.setState({loading: true});
AdapterBackend.syncPolicies(this.state.adapter.owner, this.state.adapter.name)
.then((res) => {
this.setState({loading: false, policyLists: res});
})
.catch(error => {
this.setState({loading: false});
Setting.showMessage("error", `Adapter failed to get policies: ${error}`);
});
}
renderTable(table) {
const columns = [
{
title: "Rule Type",
dataIndex: "PType",
key: "PType",
width: "100px",
},
{
title: "V0",
dataIndex: "V0",
key: "V0",
width: "100px",
},
{
title: "V1",
dataIndex: "V1",
key: "V1",
width: "100px",
},
{
title: "V2",
dataIndex: "V2",
key: "V2",
width: "100px",
},
{
title: "V3",
dataIndex: "V3",
key: "V3",
width: "100px",
},
{
title: "V4",
dataIndex: "V4",
key: "V4",
width: "100px",
},
{
title: "V5",
dataIndex: "V5",
key: "V5",
width: "100px",
},
{
title: "Option",
key: "option",
width: "100px",
render: (text, record, index) => {
return (
<div>
<Tooltip placement="topLeft" title="Edit">
<Button style={{marginRight: "0.5rem"}} icon={<EditOutlined />} size="small" />
</Tooltip>
<Tooltip placement="topLeft" title="Delete">
<Button icon={<MinusOutlined />} size="small" />
</Tooltip>
</div>
);
},
}];
return (
<div>
<Table
pagination={{
defaultPageSize: 10,
}}
columns={columns} dataSource={table} rowKey="name" size="middle" bordered
loading={this.state.loading}
/>
</div>
);
}
renderAdapter() {
return (
<Card size="small" title={
@@ -329,19 +241,8 @@ class AdapterEditPage extends React.Component {
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("adapter:Policies"), i18next.t("adapter:Policies - Tooltip"))} :
</Col>
<Col span={2}>
<Button type="primary" onClick={() => {this.synPolicies();}}>
{i18next.t("adapter:Sync")}
</Button>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
</Col>
<Col span={22} >
{
this.renderTable(this.state.policyLists)
}
<Col span={22}>
<PolicyTable owner={this.state.owner} name={this.state.adapterName} mode={this.state.mode} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
@@ -371,7 +272,7 @@ class AdapterEditPage extends React.Component {
if (willExist) {
this.props.history.push("/adapters");
} else {
this.props.history.push(`/adapters/${this.state.adapter.name}`);
this.props.history.push(`/adapters/${this.state.owner}/${this.state.adapter.name}`);
}
} else {
Setting.showMessage("error", res.msg);

View File

@@ -45,7 +45,7 @@ class AdapterListPage extends BaseListPage {
const newAdapter = this.newAdapter();
AdapterBackend.addAdapter(newAdapter)
.then((res) => {
this.props.history.push({pathname: `/adapters/${newAdapter.owner}/${newAdapter.name}`, mode: "add"});
this.props.history.push({pathname: `/adapters/${newAdapter.organization}/${newAdapter.name}`, mode: "add"});
}
)
.catch(error => {

View File

@@ -49,6 +49,9 @@ const template = `<style>
}
</style>`;
const previewGrid = Setting.isMobile() ? 22 : 11;
const previewWidth = Setting.isMobile() ? "110%" : "90%";
const sideTemplate = `<style>
.left-model{
text-align: center;
@@ -720,7 +723,7 @@ class ApplicationEditPage extends React.Component {
return (
<React.Fragment>
<Col span={11}>
<Col span={previewGrid}>
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}${signUpUrl}`);
Setting.showMessage("success", i18next.t("application:Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
@@ -729,7 +732,7 @@ class ApplicationEditPage extends React.Component {
{i18next.t("application:Copy signup page URL")}
</Button>
<br />
<div style={{position: "relative", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
<div style={{position: "relative", width: previewWidth, border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
{
this.state.application.enablePassword ? (
<SignupPage application={this.state.application} />
@@ -740,8 +743,8 @@ class ApplicationEditPage extends React.Component {
<div style={maskStyle} />
</div>
</Col>
<Col span={11}>
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
<Col span={previewGrid}>
<Button style={{marginBottom: "10px", marginTop: Setting.isMobile() ? "15px" : "0"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}${signInUrl}`);
Setting.showMessage("success", i18next.t("application:Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
}}
@@ -749,7 +752,7 @@ class ApplicationEditPage extends React.Component {
{i18next.t("application:Copy signin page URL")}
</Button>
<br />
<div style={{position: "relative", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
<div style={{position: "relative", width: previewWidth, border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
<LoginPage type={"login"} mode={"signin"} application={this.state.application} />
<div style={maskStyle} />
</div>
@@ -762,7 +765,7 @@ class ApplicationEditPage extends React.Component {
const promptUrl = `/prompt/${this.state.application.name}`;
const maskStyle = {position: "absolute", top: "0px", left: "0px", zIndex: 10, height: "100%", width: "100%", background: "rgba(0,0,0,0.4)"};
return (
<Col span={11}>
<Col span={previewGrid}>
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}${promptUrl}`);
Setting.showMessage("success", i18next.t("application:Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
@@ -771,7 +774,7 @@ class ApplicationEditPage extends React.Component {
{i18next.t("application:Copy prompt page URL")}
</Button>
<br />
<div style={{position: "relative", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", flexDirection: "column", flex: "auto"}}>
<div style={{position: "relative", width: previewWidth, border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", flexDirection: "column", flex: "auto"}}>
<PromptPage application={this.state.application} account={this.props.account} />
<div style={maskStyle} />
</div>

View File

@@ -30,6 +30,7 @@ export const CropperDiv = (props) => {
const {title} = props;
const {user} = props;
const {buttonText} = props;
const {organization} = props;
let uploadButton;
const onChange = (e) => {
@@ -92,9 +93,8 @@ export const CropperDiv = (props) => {
const getOptions = (data) => {
const options = [];
if (props.account.organization.defaultAvatar !== null) {
options.push({value: props.account.organization.defaultAvatar});
}
options.push({value: organization?.defaultAvatar});
for (let i = 0; i < data.length; i++) {
if (data[i].fileType === "image") {
const url = `${data[i].url}`;
@@ -125,7 +125,7 @@ export const CropperDiv = (props) => {
useEffect(() => {
setLoading(true);
ResourceBackend.getResources(props.account.owner, props.account.name, "", "", "", "", "", "")
ResourceBackend.getResources(user.owner, user.name, "", "", "", "", "", "")
.then((res) => {
setLoading(false);
setOptions(getOptions(res));

View File

@@ -145,6 +145,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_geetest.png`,
url: "https://www.geetest.com",
},
"Cloudflare Turnstile": {
logo: `${StaticBaseUrl}/img/social_cloudflare.png`,
url: "https://www.cloudflare.com/products/turnstile/",
},
},
};
@@ -416,9 +420,7 @@ export function goToLinkSoft(ths, link) {
}
export function showMessage(type, text) {
if (type === "") {
return;
} else if (type === "success") {
if (type === "success") {
message.success(text);
} else if (type === "error") {
message.error(text);
@@ -445,8 +447,8 @@ export function deepCopy(obj) {
return Object.assign({}, obj);
}
export function addRow(array, row) {
return [...array, row];
export function addRow(array, row, position = "end") {
return position === "end" ? [...array, row] : [row, ...array];
}
export function prependRow(array, row) {
@@ -697,6 +699,7 @@ export function getProviderTypeOptions(category) {
{id: "hCaptcha", name: "hCaptcha"},
{id: "Aliyun Captcha", name: "Aliyun Captcha"},
{id: "GEETEST", name: "GEETEST"},
{id: "Cloudflare Turnstile", name: "Cloudflare Turnstile"},
]);
} else {
return [];

View File

@@ -49,6 +49,7 @@ class UserEditPage extends React.Component {
applications: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
loading: true,
returnUrl: null,
};
}
@@ -57,6 +58,7 @@ class UserEditPage extends React.Component {
this.getOrganizations();
this.getApplicationsByOrganization(this.state.organizationName);
this.getUserApplication();
this.setReturnUrl();
}
getUser() {
@@ -100,9 +102,14 @@ class UserEditPage extends React.Component {
});
}
getReturnUrl() {
setReturnUrl() {
const searchParams = new URLSearchParams(this.props.location.search);
return searchParams.get("returnUrl");
const returnUrl = searchParams.get("returnUrl");
if (returnUrl !== null) {
this.setState({
returnUrl: returnUrl,
});
}
}
parseUserField(key, value) {
@@ -242,7 +249,7 @@ class UserEditPage extends React.Component {
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<CropperDiv buttonText={`${i18next.t("user:Upload a photo")}...`} title={i18next.t("user:Upload a photo")} user={this.state.user} account={this.props.account} />
<CropperDiv buttonText={`${i18next.t("user:Upload a photo")}...`} title={i18next.t("user:Upload a photo")} user={this.state.user} organization={this.state.organizations.find(organization => organization.name === this.state.organizationName)} />
</Row>
</Col>
</Row>
@@ -619,9 +626,8 @@ class UserEditPage extends React.Component {
}
} else {
if (willExist) {
const returnUrl = this.getReturnUrl();
if (returnUrl) {
window.location.href = returnUrl;
if (this.state.returnUrl) {
window.location.href = this.state.returnUrl;
}
}
}

View File

@@ -335,8 +335,8 @@ class LoginPage extends React.Component {
return (
<Result
status="error"
title="Sign Up Error"
subTitle={"The application does not allow to sign up new account"}
title={i18next.t("application:Sign Up Error")}
subTitle={i18next.t("application:The application does not allow to sign up new account")}
extra={[
<Button type="primary" key="signin" onClick={() => Setting.redirectToLoginPage(application, this.props.history)}>
{
@@ -368,7 +368,7 @@ class LoginPage extends React.Component {
rules={[
{
required: true,
message: "Please input your application!",
message: i18next.t("application:Please input your application!"),
},
]}
>
@@ -379,7 +379,7 @@ class LoginPage extends React.Component {
rules={[
{
required: true,
message: "Please input your organization!",
message: i18next.t("application:Please input your organization!"),
},
]}
>
@@ -681,7 +681,7 @@ class LoginPage extends React.Component {
const accessToken = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}#${responseType}=${accessToken}?state=${oAuthParams.state}&token_type=bearer`);
} else {
Setting.showMessage("success", "Successfully logged in with webauthn credentials");
Setting.showMessage("success", i18next.t("login:Successfully logged in with webauthn credentials"));
Setting.goToLink("/");
}
} else {
@@ -689,7 +689,7 @@ class LoginPage extends React.Component {
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("application:Failed to connect to server: ")}${error}`);
});
});
}

View File

@@ -232,8 +232,8 @@ class PromptPage extends React.Component {
return (
<Result
status="error"
title="Sign Up Error"
subTitle={"You are unexpected to see this prompt page"}
title={i18next.t("application:Sign Up Error")}
subTitle={i18next.t("application:You are unexpected to see this prompt page")}
extra={[
<Button type="primary" key="signin" onClick={() => Setting.redirectToLoginPage(application, this.props.history)}>
{

View File

@@ -539,8 +539,8 @@ class SignupPage extends React.Component {
return (
<Result
status="error"
title="Sign Up Error"
subTitle={"The application does not allow to sign up new account"}
title={i18next.t("application:Sign Up Error")}
subTitle={i18next.t("application:The application does not allow to sign up new account")}
extra={[
<Button type="primary" key="signin" onClick={() => Setting.redirectToLoginPage(application, this.props.history)}>
{

View File

@@ -70,6 +70,41 @@ export function deleteAdapter(Adapter) {
}).then(res => res.json());
}
export function UpdatePolicy(owner, name, policy) {
// eslint-disable-next-line no-console
console.log(policy);
return fetch(`${Setting.ServerUrl}/api/update-policy?id=${owner}/${encodeURIComponent(name)}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(policy),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function AddPolicy(owner, name, policy) {
return fetch(`${Setting.ServerUrl}/api/add-policy?id=${owner}/${encodeURIComponent(name)}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(policy),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function RemovePolicy(owner, name, policy) {
return fetch(`${Setting.ServerUrl}/api/remove-policy?id=${owner}/${encodeURIComponent(name)}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(policy),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function syncPolicies(owner, name) {
return fetch(`${Setting.ServerUrl}/api/sync-policies?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",

View File

@@ -105,6 +105,21 @@ export const CaptchaWidget = ({captchaType, subType, siteKey, clientSecret, onCh
}, 500);
break;
}
case "Cloudflare Turnstile": {
const tTimer = setInterval(() => {
if (!window.turnstile) {
loadScript("https://challenges.cloudflare.com/turnstile/v0/api.js");
}
if (window.turnstile && window.turnstile.render) {
window.turnstile.render("#captcha", {
sitekey: siteKey,
callback: onChange,
});
clearInterval(tTimer);
}
}, 300);
break;
}
default:
break;
}

View File

@@ -0,0 +1,308 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {DeleteOutlined, EditOutlined} from "@ant-design/icons";
import {Button, Input, Popconfirm, Table, Tooltip} from "antd";
import * as Setting from "../Setting";
import * as AdapterBackend from "../backend/AdapterBackend";
import i18next from "i18next";
class PolicyTable extends React.Component {
constructor(props) {
super(props);
this.state = {
policyLists: [],
loading: false,
editingIndex: "",
oldPolicy: "",
add: false,
};
}
UNSAFE_componentWillMount() {
if (this.props.mode === "edit") {
this.synPolicies();
}
}
isEditing = (index) => {
return index === this.state.editingIndex;
};
edit = (record, index) => {
this.setState({editingIndex: index, oldPolicy: Setting.deepCopy(record)});
};
cancel = (table, index) => {
Object.keys(table[index]).forEach((key) => {
table[index][key] = this.state.oldPolicy[key];
});
this.updateTable(table);
this.setState({editingIndex: "", oldPolicy: ""});
if (this.state.add) {
this.deleteRow(this.state.policyLists, index);
this.setState({add: false});
}
};
updateTable(table) {
this.setState({policyLists: table});
}
updateField(table, index, key, value) {
table[index][key] = value;
this.updateTable(table);
}
addRow(table) {
const row = {Ptype: "p"};
if (table === undefined) {
table = [];
}
table = Setting.addRow(table, row, "top");
this.updateTable(table);
this.edit(row, 0);
this.setState({add: true});
}
deleteRow(table, i) {
table = Setting.deleteRow(table, i);
this.updateTable(table);
}
save(table, i) {
this.state.add ? this.addPolicy(table, i) : this.updatePolicy(table, i);
}
synPolicies() {
this.setState({loading: true});
AdapterBackend.syncPolicies(this.props.owner, this.props.name)
.then((res) => {
if (res.status !== "error") {
this.setState({loading: false, policyLists: res});
} else {
this.setState({loading: false});
Setting.showMessage("error", `Adapter failed to get policies, ${res.msg}`);
}
})
.catch(error => {
this.setState({loading: false});
Setting.showMessage("error", `Adapter failed to get policies, ${error}`);
});
}
updatePolicy(table, i) {
AdapterBackend.UpdatePolicy(this.props.owner, this.props.name, [this.state.oldPolicy, table[i]]).then(res => {
if (res.status === "ok") {
this.setState({editingIndex: "", oldPolicy: ""});
Setting.showMessage("success", i18next.t("adapter:Update policy successfully"));
} else {
Setting.showMessage("error", i18next.t(`adapter:Update policy failed, ${res.msg}`));
}
});
}
addPolicy(table, i) {
AdapterBackend.AddPolicy(this.props.owner, this.props.name, table[i]).then(res => {
if (res.status === "ok") {
this.setState({editingIndex: "", oldPolicy: "", add: false});
if (res.data !== "Affected") {
Setting.showMessage("info", i18next.t("adapter:Repeated policy"));
} else {
Setting.showMessage("success", i18next.t("adapter:Add policy successfully"));
}
} else {
Setting.showMessage("error", i18next.t(`adapter:Add policy failed, ${res.msg}`));
}
});
}
deletePolicy(table, i) {
AdapterBackend.RemovePolicy(this.props.owner, this.props.name, table[i]).then(res => {
if (res.status === "ok") {
table = Setting.deleteRow(table, i);
this.updateTable(table);
Setting.showMessage("success", i18next.t("adapter:Delete policy successfully"));
} else {
Setting.showMessage("error", i18next.t(`adapter:Delete policy failed, ${res.msg}`));
}
});
}
renderTable(table) {
const columns = [
{
title: "Rule Type",
dataIndex: "Ptype",
width: "100px",
// render: (text, record, index) => {
// const editing = this.isEditing(index);
// return (
// editing ?
// <Input value={text} onChange={e => {
// this.updateField(table, index, "Ptype", e.target.value);
// }} />
// : text
// );
// },
},
{
title: "V0",
dataIndex: "V0",
width: "100px",
render: (text, record, index) => {
const editing = this.isEditing(index);
return (
editing ?
<Input value={text} onChange={e => {
this.updateField(table, index, "V0", e.target.value);
}} />
: text
);
},
},
{
title: "V1",
dataIndex: "V1",
width: "100px",
render: (text, record, index) => {
const editing = this.isEditing(index);
return (
editing ?
<Input value={text} onChange={e => {
this.updateField(table, index, "V1", e.target.value);
}} />
: text
);
},
},
{
title: "V2",
dataIndex: "V2",
width: "100px",
render: (text, record, index) => {
const editing = this.isEditing(index);
return (
editing ?
<Input value={text} onChange={e => {
this.updateField(table, index, "V2", e.target.value);
}} />
: text
);
},
},
{
title: "V3",
dataIndex: "V3",
width: "100px",
render: (text, record, index) => {
const editing = this.isEditing(index);
return (
editing ?
<Input value={text} onChange={e => {
this.updateField(table, index, "V3", e.target.value);
}} />
: text
);
},
},
{
title: "V4",
dataIndex: "V4",
width: "100px",
render: (text, record, index) => {
const editing = this.isEditing(index);
return (
editing ?
<Input value={text} onChange={e => {
this.updateField(table, index, "V4", e.target.value);
}} />
: text
);
},
},
{
title: "V5",
dataIndex: "V5",
width: "100px",
render: (text, record, index) => {
const editing = this.isEditing(index);
return (
editing ?
<Input value={text} onChange={e => {
this.updateField(table, index, "V5", e.target.value);
}} />
: text
);
},
},
{
title: "Option",
key: "option",
width: "100px",
render: (text, record, index) => {
const editable = this.isEditing(index);
return editable ? (
<span>
<Button style={{marginRight: 8}} onClick={() => this.save(table, index)}>
Save
</Button>
<Popconfirm title="Sure to cancel?" onConfirm={() => this.cancel(table, index)}>
<a>Cancel</a>
</Popconfirm>
</span>
) : (
<div>
<Tooltip placement="topLeft" title="Edit">
<Button disabled={this.state.editingIndex !== ""} style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => this.edit(record, index)} />
</Tooltip>
<Tooltip placement="topLeft" title="Delete">
<Button disabled={this.state.editingIndex !== ""} style={{marginRight: "5px"}} icon={<DeleteOutlined />} size="small" onClick={() => this.deletePolicy(table, index)} />
</Tooltip>
</div>
);
},
}];
return (
<Table
pagination={{
defaultPageSize: 10,
}}
columns={columns} dataSource={table} rowKey="index" size="middle" bordered
loading={this.state.loading}
title={() => (
<div>
<Button disabled={this.state.editingIndex !== ""} style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
</div>
)}
/>
);
}
render() {
return (<>
<Button type="primary" onClick={() => {this.synPolicies();}}>
{i18next.t("adapter:Sync")}
</Button>
{
this.renderTable(this.state.policyLists)
}
</>
);
}
}
export default PolicyTable;

View File

@@ -6,11 +6,15 @@
"Sign Up": "Registrieren"
},
"adapter": {
"Add policy successfully": "Add policy successfully",
"Delete policy successfully": "Delete policy successfully",
"Edit Adapter": "Edit Adapter",
"New Adapter": "New Adapter",
"Policies": "Policies",
"Policies - Tooltip": "Policies - Tooltip",
"Sync": "Sync"
"Repeated policy": "Repeated policy",
"Sync": "Sync",
"Update policy successfully": "Update policy successfully"
},
"application": {
"Always": "Always",
@@ -34,6 +38,7 @@
"Enable signin session - Tooltip": "Aktiviere Anmeldesession - Tooltip",
"Enable signup": "Anmeldung aktivieren",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"Failed to connect to server": "Failed to connect to server",
"File uploaded successfully": "Datei erfolgreich hochgeladen",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
@@ -47,6 +52,8 @@
"None": "None",
"Password ON": "Passwort AN",
"Password ON - Tooltip": "Whether to allow password login",
"Please input your application!": "Please input your application!",
"Please input your organization!": "Please input your organization!",
"Please select a HTML file": "Bitte wählen Sie eine HTML-Datei",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Redirect URL": "Weiterleitungs-URL",
@@ -62,15 +69,18 @@
"Side panel HTML": "Side panel HTML",
"Side panel HTML - Edit": "Side panel HTML - Edit",
"Side panel HTML - Tooltip": "Side panel HTML - Tooltip",
"Sign Up Error": "Sign Up Error",
"Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Signin session": "Anmeldesitzung",
"Signup items": "Artikel registrieren",
"Signup items - Tooltip": "Signup items that need to be filled in when users register",
"Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"The application does not allow to sign up new account": "The application does not allow to sign up new account",
"Token expire": "Token läuft ab",
"Token expire - Tooltip": "Token läuft ab - Tooltip",
"Token format": "Token-Format",
"Token format - Tooltip": "Token-Format - Tooltip"
"Token format - Tooltip": "Token-Format - Tooltip",
"You are unexpected to see this prompt page": "You are unexpected to see this prompt page"
},
"cert": {
"Bit size": "Bitgröße",
@@ -292,6 +302,7 @@
"Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with {type}": "Mit {type} anmelden",
"Signing in...": "Anmelden...",
"Successfully logged in with webauthn credentials": "Successfully logged in with webauthn credentials",
"The input is not valid Email or Phone!": "Die Eingabe ist keine gültige E-Mail oder Telefon!",
"To access": "Zu Zugriff",
"Verification Code": "Verification Code",
@@ -555,6 +566,7 @@
"UserInfo URL": "UserInfo URL",
"UserInfo URL - Tooltip": "UserInfo URL - Tooltip",
"Visible": "Visible",
"admin (share)": "admin (share)",
"alertType": "alarmtyp"
},
"record": {

View File

@@ -6,11 +6,15 @@
"Sign Up": "Sign Up"
},
"adapter": {
"Add policy successfully": "Add policy successfully",
"Delete policy successfully": "Delete policy successfully",
"Edit Adapter": "Edit Adapter",
"New Adapter": "New Adapter",
"Policies": "Policies",
"Policies - Tooltip": "Policies - Tooltip",
"Sync": "Sync"
"Repeated policy": "Repeated policy",
"Sync": "Sync",
"Update policy successfully": "Update policy successfully"
},
"application": {
"Always": "Always",
@@ -34,6 +38,7 @@
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
"Enable signup": "Enable signup",
"Enable signup - Tooltip": "Enable signup - Tooltip",
"Failed to connect to server": "Failed to connect to server",
"File uploaded successfully": "File uploaded successfully",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
@@ -47,6 +52,8 @@
"None": "None",
"Password ON": "Password ON",
"Password ON - Tooltip": "Password ON - Tooltip",
"Please input your application!": "Please input your application!",
"Please input your organization!": "Please input your organization!",
"Please select a HTML file": "Please select a HTML file",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Redirect URL": "Redirect URL",
@@ -62,15 +69,18 @@
"Side panel HTML": "Side panel HTML",
"Side panel HTML - Edit": "Side panel HTML - Edit",
"Side panel HTML - Tooltip": "Side panel HTML - Tooltip",
"Sign Up Error": "Sign Up Error",
"Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Signin session": "Signin session",
"Signup items": "Signup items",
"Signup items - Tooltip": "Signup items - Tooltip",
"Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"The application does not allow to sign up new account": "The application does not allow to sign up new account",
"Token expire": "Token expire",
"Token expire - Tooltip": "Token expire - Tooltip",
"Token format": "Token format",
"Token format - Tooltip": "Token format - Tooltip"
"Token format - Tooltip": "Token format - Tooltip",
"You are unexpected to see this prompt page": "You are unexpected to see this prompt page"
},
"cert": {
"Bit size": "Bit size",
@@ -292,6 +302,7 @@
"Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with {type}": "Sign in with {type}",
"Signing in...": "Signing in...",
"Successfully logged in with webauthn credentials": "Successfully logged in with webauthn credentials",
"The input is not valid Email or Phone!": "The input is not valid Email or Phone!",
"To access": "To access",
"Verification Code": "Verification Code",
@@ -555,6 +566,7 @@
"UserInfo URL": "UserInfo URL",
"UserInfo URL - Tooltip": "UserInfo URL - Tooltip",
"Visible": "Visible",
"admin (share)": "admin (share)",
"alertType": "alertType"
},
"record": {

View File

@@ -38,6 +38,7 @@
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copiado al portapapeles con éxito",
"Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Signin session": "Signin session",
"Sign Up Error": "Sign Up Error",
"Signup items": "Signup items",
"Signup items - Tooltip": "Signup items - Tooltip",
"Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
@@ -45,6 +46,7 @@
"Token expire - Tooltip": "Expiración del Token - Tooltip",
"Token format": "Formato del Token",
"Token format - Tooltip": "Formato del Token - Tooltip",
"You are unexpected to see this prompt page": "You are unexpected to see this prompt page",
"Rule": "Rule",
"None": "None",
"Always": "Always"

View File

@@ -6,11 +6,15 @@
"Sign Up": "S'inscrire"
},
"adapter": {
"Add policy successfully": "Add policy successfully",
"Delete policy successfully": "Delete policy successfully",
"Edit Adapter": "Edit Adapter",
"New Adapter": "New Adapter",
"Policies": "Policies",
"Policies - Tooltip": "Policies - Tooltip",
"Sync": "Sync"
"Repeated policy": "Repeated policy",
"Sync": "Sync",
"Update policy successfully": "Update policy successfully"
},
"application": {
"Always": "Always",
@@ -34,6 +38,7 @@
"Enable signin session - Tooltip": "Activer la session de connexion - infobulle",
"Enable signup": "Activer l'inscription",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"Failed to connect to server": "Failed to connect to server",
"File uploaded successfully": "Fichier téléchargé avec succès",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
@@ -47,6 +52,8 @@
"None": "None",
"Password ON": "Mot de passe activé",
"Password ON - Tooltip": "Whether to allow password login",
"Please input your application!": "Please input your application!",
"Please input your organization!": "Please input your organization!",
"Please select a HTML file": "Veuillez sélectionner un fichier HTML",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Redirect URL": "URL de redirection",
@@ -62,15 +69,18 @@
"Side panel HTML": "Side panel HTML",
"Side panel HTML - Edit": "Side panel HTML - Edit",
"Side panel HTML - Tooltip": "Side panel HTML - Tooltip",
"Sign Up Error": "Sign Up Error",
"Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Signin session": "Connexion à la session",
"Signup items": "Inscrire des éléments",
"Signup items - Tooltip": "Signup items that need to be filled in when users register",
"Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"The application does not allow to sign up new account": "The application does not allow to sign up new account",
"Token expire": "Expiration du jeton",
"Token expire - Tooltip": "Expiration du jeton - Info-bulle",
"Token format": "Format du jeton",
"Token format - Tooltip": "Format du jeton - infobulle"
"Token format - Tooltip": "Format du jeton - infobulle",
"You are unexpected to see this prompt page": "You are unexpected to see this prompt page"
},
"cert": {
"Bit size": "Taille du bit",
@@ -292,6 +302,7 @@
"Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with {type}": "Se connecter avec {type}",
"Signing in...": "Connexion en cours...",
"Successfully logged in with webauthn credentials": "Successfully logged in with webauthn credentials",
"The input is not valid Email or Phone!": "L'entrée n'est pas valide Email ou Téléphone !",
"To access": "Pour accéder à",
"Verification Code": "Verification Code",
@@ -555,6 +566,7 @@
"UserInfo URL": "UserInfo URL",
"UserInfo URL - Tooltip": "UserInfo URL - Tooltip",
"Visible": "Visible",
"admin (share)": "admin (share)",
"alertType": "Type d'alerte"
},
"record": {

View File

@@ -6,11 +6,15 @@
"Sign Up": "新規登録"
},
"adapter": {
"Add policy successfully": "Add policy successfully",
"Delete policy successfully": "Delete policy successfully",
"Edit Adapter": "Edit Adapter",
"New Adapter": "New Adapter",
"Policies": "Policies",
"Policies - Tooltip": "Policies - Tooltip",
"Sync": "Sync"
"Repeated policy": "Repeated policy",
"Sync": "Sync",
"Update policy successfully": "Update policy successfully"
},
"application": {
"Always": "Always",
@@ -34,6 +38,7 @@
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
"Enable signup": "サインアップを有効にする",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"Failed to connect to server": "Failed to connect to server",
"File uploaded successfully": "ファイルが正常にアップロードされました",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
@@ -47,6 +52,8 @@
"None": "None",
"Password ON": "パスワードON",
"Password ON - Tooltip": "Whether to allow password login",
"Please input your application!": "Please input your application!",
"Please input your organization!": "Please input your organization!",
"Please select a HTML file": "HTMLファイルを選択してください",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Redirect URL": "リダイレクトURL",
@@ -62,15 +69,18 @@
"Side panel HTML": "Side panel HTML",
"Side panel HTML - Edit": "Side panel HTML - Edit",
"Side panel HTML - Tooltip": "Side panel HTML - Tooltip",
"Sign Up Error": "Sign Up Error",
"Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Signin session": "サインインセッション",
"Signup items": "アイテムの登録",
"Signup items - Tooltip": "Signup items that need to be filled in when users register",
"Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"The application does not allow to sign up new account": "The application does not allow to sign up new account",
"Token expire": "トークンの有効期限",
"Token expire - Tooltip": "トークンの有効期限 - ツールチップ",
"Token format": "トークンのフォーマット",
"Token format - Tooltip": "トークンフォーマット - ツールチップ"
"Token format - Tooltip": "トークンフォーマット - ツールチップ",
"You are unexpected to see this prompt page": "You are unexpected to see this prompt page"
},
"cert": {
"Bit size": "ビットサイズ",
@@ -292,6 +302,7 @@
"Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with {type}": "{type} でサインイン",
"Signing in...": "サインイン中...",
"Successfully logged in with webauthn credentials": "Successfully logged in with webauthn credentials",
"The input is not valid Email or Phone!": "入力されたメールアドレスまたは電話番号が正しくありません。",
"To access": "アクセスするには",
"Verification Code": "Verification Code",
@@ -555,6 +566,7 @@
"UserInfo URL": "UserInfo URL",
"UserInfo URL - Tooltip": "UserInfo URL - Tooltip",
"Visible": "Visible",
"admin (share)": "admin (share)",
"alertType": "alertType"
},
"record": {

View File

@@ -6,11 +6,15 @@
"Sign Up": "Sign Up"
},
"adapter": {
"Add policy successfully": "Add policy successfully",
"Delete policy successfully": "Delete policy successfully",
"Edit Adapter": "Edit Adapter",
"New Adapter": "New Adapter",
"Policies": "Policies",
"Policies - Tooltip": "Policies - Tooltip",
"Sync": "Sync"
"Repeated policy": "Repeated policy",
"Sync": "Sync",
"Update policy successfully": "Update policy successfully"
},
"application": {
"Always": "Always",
@@ -34,6 +38,7 @@
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
"Enable signup": "Enable signup",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"Failed to connect to server": "Failed to connect to server",
"File uploaded successfully": "File uploaded successfully",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
@@ -47,6 +52,8 @@
"None": "None",
"Password ON": "Password ON",
"Password ON - Tooltip": "Whether to allow password login",
"Please input your application!": "Please input your application!",
"Please input your organization!": "Please input your organization!",
"Please select a HTML file": "Please select a HTML file",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Redirect URL": "Redirect URL",
@@ -62,15 +69,18 @@
"Side panel HTML": "Side panel HTML",
"Side panel HTML - Edit": "Side panel HTML - Edit",
"Side panel HTML - Tooltip": "Side panel HTML - Tooltip",
"Sign Up Error": "Sign Up Error",
"Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Signin session": "Signin session",
"Signup items": "Signup items",
"Signup items - Tooltip": "Signup items that need to be filled in when users register",
"Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"The application does not allow to sign up new account": "The application does not allow to sign up new account",
"Token expire": "Token expire",
"Token expire - Tooltip": "Token expire - Tooltip",
"Token format": "Token format",
"Token format - Tooltip": "Token format - Tooltip"
"Token format - Tooltip": "Token format - Tooltip",
"You are unexpected to see this prompt page": "You are unexpected to see this prompt page"
},
"cert": {
"Bit size": "Bit size",
@@ -292,6 +302,7 @@
"Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with {type}": "Sign in with {type}",
"Signing in...": "Signing in...",
"Successfully logged in with webauthn credentials": "Successfully logged in with webauthn credentials",
"The input is not valid Email or Phone!": "The input is not valid Email or Phone!",
"To access": "To access",
"Verification Code": "Verification Code",
@@ -555,6 +566,7 @@
"UserInfo URL": "UserInfo URL",
"UserInfo URL - Tooltip": "UserInfo URL - Tooltip",
"Visible": "Visible",
"admin (share)": "admin (share)",
"alertType": "alertType"
},
"record": {

View File

@@ -6,11 +6,15 @@
"Sign Up": "Регистрация"
},
"adapter": {
"Add policy successfully": "Add policy successfully",
"Delete policy successfully": "Delete policy successfully",
"Edit Adapter": "Edit Adapter",
"New Adapter": "New Adapter",
"Policies": "Policies",
"Policies - Tooltip": "Policies - Tooltip",
"Sync": "Sync"
"Repeated policy": "Repeated policy",
"Sync": "Sync",
"Update policy successfully": "Update policy successfully"
},
"application": {
"Always": "Always",
@@ -34,6 +38,7 @@
"Enable signin session - Tooltip": "Включить сеанс входа - Подсказка",
"Enable signup": "Включить регистрацию",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"Failed to connect to server": "Failed to connect to server",
"File uploaded successfully": "Файл успешно загружен",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
@@ -47,6 +52,8 @@
"None": "None",
"Password ON": "Пароль ВКЛ",
"Password ON - Tooltip": "Whether to allow password login",
"Please input your application!": "Please input your application!",
"Please input your organization!": "Please input your organization!",
"Please select a HTML file": "Пожалуйста, выберите HTML-файл",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Ссылка на страницу успешно скопирована в буфер обмена, пожалуйста, вставьте ее в окно инкогнито или другой браузер",
"Redirect URL": "URL перенаправления",
@@ -62,15 +69,18 @@
"Side panel HTML": "Side panel HTML",
"Side panel HTML - Edit": "Side panel HTML - Edit",
"Side panel HTML - Tooltip": "Side panel HTML - Tooltip",
"Sign Up Error": "Sign Up Error",
"Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "URL страницы входа успешно скопирован в буфер обмена, пожалуйста, вставьте его в окно инкогнито или другой браузер",
"Signin session": "Сессия входа",
"Signup items": "Элементы регистрации",
"Signup items - Tooltip": "Signup items that need to be filled in when users register",
"Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "URL страницы регистрации успешно скопирован в буфер обмена, пожалуйста, вставьте его в окно инкогнито или другой браузер",
"The application does not allow to sign up new account": "The application does not allow to sign up new account",
"Token expire": "Токен истекает",
"Token expire - Tooltip": "Истек токен - Подсказка",
"Token format": "Формат токена",
"Token format - Tooltip": "Формат токена - Подсказка"
"Token format - Tooltip": "Формат токена - Подсказка",
"You are unexpected to see this prompt page": "You are unexpected to see this prompt page"
},
"cert": {
"Bit size": "Размер бита",
@@ -292,6 +302,7 @@
"Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with {type}": "Войти с помощью {type}",
"Signing in...": "Вход...",
"Successfully logged in with webauthn credentials": "Successfully logged in with webauthn credentials",
"The input is not valid Email or Phone!": "Введен неверный адрес электронной почты или телефон!",
"To access": "На доступ",
"Verification Code": "Verification Code",
@@ -555,6 +566,7 @@
"UserInfo URL": "UserInfo URL",
"UserInfo URL - Tooltip": "UserInfo URL - Tooltip",
"Visible": "Visible",
"admin (share)": "admin (share)",
"alertType": "тип оповещения"
},
"record": {

View File

@@ -6,11 +6,15 @@
"Sign Up": "注册"
},
"adapter": {
"Add policy successfully": "添加策略成功",
"Delete policy successfully": "删除策略成功",
"Edit Adapter": "编辑适配器",
"New Adapter": "添加适配器",
"Policies": "策略",
"Policies - Tooltip": "策略",
"Sync": "同步"
"Repeated policy": "策略重复",
"Sync": "同步",
"Update policy successfully": "更新策略成功"
},
"application": {
"Always": "始终开启",
@@ -34,6 +38,7 @@
"Enable signin session - Tooltip": "从应用登录Casdoor后Casdoor是否保持会话",
"Enable signup": "启用注册",
"Enable signup - Tooltip": "是否允许用户注册",
"Failed to connect to server": "无法连接服务器",
"File uploaded successfully": "文件上传成功",
"Form CSS": "表单CSS",
"Form CSS - Edit": "编辑表单CSS",
@@ -47,6 +52,8 @@
"None": "关闭",
"Password ON": "开启密码",
"Password ON - Tooltip": "是否允许密码登录",
"Please input your application!": "请输入你的应用",
"Please input your organization!": "请输入你的组织",
"Please select a HTML file": "请选择一个HTML文件",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "提醒页面URL已成功复制到剪贴板请粘贴到当前浏览器的隐身模式窗口或另一个浏览器访问",
"Redirect URL": "重定向 URL",
@@ -62,15 +69,18 @@
"Side panel HTML": "侧面板 HTML",
"Side panel HTML - Edit": "侧面板 HTML - 编辑",
"Side panel HTML - Tooltip": "侧面板 HTML - Tooltip",
"Sign Up Error": "注册错误",
"Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "登录页面URL已成功复制到剪贴板请粘贴到当前浏览器的隐身模式窗口或另一个浏览器访问",
"Signin session": "保持登录会话",
"Signup items": "注册项",
"Signup items - Tooltip": "注册用户注册时需要填写的项目",
"Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "注册页面URL已成功复制到剪贴板请粘贴到当前浏览器的隐身模式窗口或另一个浏览器访问",
"The application does not allow to sign up new account": "该应用不允许注册新账户",
"Token expire": "Access Token过期",
"Token expire - Tooltip": "Access Token过期时间",
"Token format": "Access Token格式",
"Token format - Tooltip": "Access Token格式"
"Token format - Tooltip": "Access Token格式",
"You are unexpected to see this prompt page": "错误跳转至提醒页面"
},
"cert": {
"Bit size": "位大小",
@@ -292,6 +302,7 @@
"Sign in with WebAuthn": "WebAuthn登录",
"Sign in with {type}": "{type}登录",
"Signing in...": "正在登录...",
"Successfully logged in with webauthn credentials": "成功使用WebAuthn证书登录",
"The input is not valid Email or Phone!": "您输入的电子邮箱格式或手机号有误!",
"To access": "访问",
"Verification Code": "验证码",
@@ -555,6 +566,7 @@
"UserInfo URL": "UserInfo URL",
"UserInfo URL - Tooltip": "UserInfo URL - 工具提示",
"Visible": "是否可见",
"admin (share)": "admin (share)",
"alertType": "警报类型"
},
"record": {