mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-08 17:10:27 +08:00
Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
fe448cbcf4 | |||
2ab25df950 | |||
b895926754 | |||
5bb7a4153f | |||
b7cd598ee8 | |||
b10fb97c92 | |||
b337b908ea | |||
ba9d1e2388 | |||
29ec1d2d9c | |||
84a03f6c8e | |||
56ff06bbea | |||
7e756b8ee2 | |||
19ba37e0c2 | |||
b98ce19211 | |||
37d1a73c0c | |||
727877cf54 | |||
939b416717 | |||
f115843fbb | |||
aa6a4dc74f | |||
462a82a3d5 | |||
262aeba7e2 |
@ -85,6 +85,8 @@ p, *, *, POST, /api/logout, *, *
|
||||
p, *, *, GET, /api/logout, *, *
|
||||
p, *, *, GET, /api/get-account, *, *
|
||||
p, *, *, GET, /api/userinfo, *, *
|
||||
p, *, *, POST, /api/webhook, *, *
|
||||
p, *, *, GET, /api/get-webhook-event, *, *
|
||||
p, *, *, *, /api/login/oauth, *, *
|
||||
p, *, *, GET, /api/get-application, *, *
|
||||
p, *, *, GET, /api/get-organization-applications, *, *
|
||||
|
@ -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
66
captcha/turnstile.go
Normal 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
|
||||
}
|
@ -46,7 +46,7 @@ func (c *ApiController) GetApplications() {
|
||||
if organization == "" {
|
||||
applications = object.GetApplications(owner)
|
||||
} else {
|
||||
applications = object.GetApplicationsByOrganizationName(owner, organization)
|
||||
applications = object.GetOrganizationApplications(owner, organization)
|
||||
}
|
||||
|
||||
c.Data["json"] = object.GetMaskedApplications(applications, userId)
|
||||
@ -103,17 +103,31 @@ func (c *ApiController) GetUserApplication() {
|
||||
// @router /get-organization-applications [get]
|
||||
func (c *ApiController) GetOrganizationApplications() {
|
||||
userId := c.GetSessionUsername()
|
||||
owner := c.Input().Get("owner")
|
||||
organization := c.Input().Get("organization")
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
|
||||
if organization == "" {
|
||||
c.ResponseError(c.T("ParameterErr.OrgMissingErr"))
|
||||
return
|
||||
}
|
||||
|
||||
applications := object.GetApplicationsByOrganizationName(owner, organization)
|
||||
c.Data["json"] = object.GetMaskedApplications(applications, userId)
|
||||
c.ServeJSON()
|
||||
if limit == "" || page == "" {
|
||||
var applications []*object.Application
|
||||
applications = object.GetOrganizationApplications(owner, organization)
|
||||
c.Data["json"] = object.GetMaskedApplications(applications, userId)
|
||||
c.ServeJSON()
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetOrganizationApplicationCount(owner, organization, field, value)))
|
||||
applications := object.GetMaskedApplications(object.GetPaginationOrganizationApplications(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder), userId)
|
||||
c.ResponseOk(applications, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateApplication
|
||||
|
@ -17,14 +17,16 @@ package controllers
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/captcha"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/idp"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@ -33,6 +35,11 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
wechatScanType string
|
||||
lock sync.RWMutex
|
||||
)
|
||||
|
||||
func codeToResponse(code *object.Code) *Response {
|
||||
if code.Code == "" {
|
||||
return &Response{Status: "error", Msg: code.Message, Data: code.Code}
|
||||
@ -300,7 +307,7 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
|
||||
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", application.Organization))
|
||||
provider := object.GetProvider(fmt.Sprintf("admin/%s", 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))
|
||||
@ -531,3 +538,46 @@ func (c *ApiController) HandleSamlLogin() {
|
||||
slice[4], relayState, samlResponse)
|
||||
c.Redirect(targetUrl, 303)
|
||||
}
|
||||
|
||||
// HandleOfficialAccountEvent ...
|
||||
// @Tag HandleOfficialAccountEvent API
|
||||
// @Title HandleOfficialAccountEvent
|
||||
// @router /api/webhook [POST]
|
||||
func (c *ApiController) HandleOfficialAccountEvent() {
|
||||
respBytes, err := ioutil.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
}
|
||||
var data struct {
|
||||
MsgType string `xml:"MsgType"`
|
||||
Event string `xml:"Event"`
|
||||
EventKey string `xml:"EventKey"`
|
||||
}
|
||||
err = xml.Unmarshal(respBytes, &data)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
}
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
if data.EventKey != "" {
|
||||
wechatScanType = data.Event
|
||||
c.Ctx.WriteString("")
|
||||
}
|
||||
}
|
||||
|
||||
// GetWebhookEventType ...
|
||||
// @Tag GetWebhookEventType API
|
||||
// @Title GetWebhookEventType
|
||||
// @router /api/get-webhook-event [GET]
|
||||
func (c *ApiController) GetWebhookEventType() {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
resp := &Response{
|
||||
Status: "ok",
|
||||
Msg: "",
|
||||
Data: wechatScanType,
|
||||
}
|
||||
c.Data["json"] = resp
|
||||
wechatScanType = ""
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -21,12 +21,6 @@ import (
|
||||
)
|
||||
|
||||
func (c *ApiController) Enforce() {
|
||||
userId := c.GetSessionUsername()
|
||||
if userId == "" {
|
||||
c.ResponseError(c.T("EnforcerErr.SignInFirst"))
|
||||
return
|
||||
}
|
||||
|
||||
var permissionRule object.PermissionRule
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &permissionRule)
|
||||
if err != nil {
|
||||
@ -34,17 +28,11 @@ func (c *ApiController) Enforce() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = object.Enforce(userId, &permissionRule)
|
||||
c.Data["json"] = object.Enforce(&permissionRule)
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *ApiController) BatchEnforce() {
|
||||
userId := c.GetSessionUsername()
|
||||
if userId == "" {
|
||||
c.ResponseError(c.T("EnforcerErr.SignInFirst"))
|
||||
return
|
||||
}
|
||||
|
||||
var permissionRules []object.PermissionRule
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &permissionRules)
|
||||
if err != nil {
|
||||
@ -52,7 +40,7 @@ func (c *ApiController) BatchEnforce() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = object.BatchEnforce(userId, permissionRules)
|
||||
c.Data["json"] = object.BatchEnforce(permissionRules)
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,6 @@ func (c *ApiController) GetGlobalProviders() {
|
||||
// @router /get-provider [get]
|
||||
func (c *ApiController) GetProvider() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
c.Data["json"] = object.GetMaskedProvider(object.GetProvider(id))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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(fmt.Sprintf("admin/%s", 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
|
||||
|
@ -17,6 +17,7 @@ package controllers
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
@ -56,7 +57,7 @@ func (c *ApiController) T(error string) string {
|
||||
// GetAcceptLanguage ...
|
||||
func (c *ApiController) GetAcceptLanguage() string {
|
||||
lang := c.Ctx.Request.Header.Get("Accept-Language")
|
||||
if lang == "" {
|
||||
if lang == "" || !strings.Contains(conf.GetConfigString("languages"), lang[0:2]) {
|
||||
lang = "en"
|
||||
}
|
||||
return lang[0:2]
|
||||
@ -125,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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -21,9 +21,10 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func TestDeployStaticFiles(t *testing.T) {
|
||||
provider := object.GetProvider("admin/provider_storage_aliyun_oss")
|
||||
provider := object.GetProvider(util.GetId("admin", "provider_storage_aliyun_oss"))
|
||||
deployStaticFiles(provider)
|
||||
}
|
||||
|
5
go.mod
5
go.mod
@ -23,6 +23,7 @@ require (
|
||||
github.com/go-pay/gopay v1.5.72
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-cmp v0.5.8 // indirect
|
||||
github.com/google/uuid v1.2.0
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
|
||||
@ -36,6 +37,7 @@ require (
|
||||
github.com/russellhaering/goxmldsig v1.1.1
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/tealeg/xlsx v1.0.5
|
||||
@ -51,6 +53,7 @@ require (
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||
xorm.io/builder v0.3.12 // indirect
|
||||
xorm.io/core v0.7.2
|
||||
xorm.io/xorm v1.0.4
|
||||
xorm.io/xorm v1.0.5
|
||||
)
|
||||
|
12
go.sum
12
go.sum
@ -213,8 +213,9 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
|
||||
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
@ -394,6 +395,8 @@ github.com/siddontang/goredis v0.0.0-20150324035039-760763f78400/go.mod h1:DDcKz
|
||||
github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
@ -787,10 +790,11 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
xorm.io/builder v0.3.7 h1:2pETdKRK+2QG4mLX4oODHEhn5Z8j1m8sXa7jfu+/SZI=
|
||||
xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||
xorm.io/builder v0.3.12 h1:ASZYX7fQmy+o8UJdhlLHSW57JDOkM8DNhcAF5d0LiJM=
|
||||
xorm.io/builder v0.3.12/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||
xorm.io/core v0.7.2 h1:mEO22A2Z7a3fPaZMk6gKL/jMD80iiyNwRrX5HOv3XLw=
|
||||
xorm.io/core v0.7.2/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM=
|
||||
xorm.io/xorm v1.0.3/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
|
||||
xorm.io/xorm v1.0.4 h1:UBXA4I3NhiyjXfPqxXUkS2t5hMta9SSPATeMMaZg9oA=
|
||||
xorm.io/xorm v1.0.4/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
|
||||
xorm.io/xorm v1.0.5 h1:LRr5PfOUb4ODPR63YwbowkNDwcolT2LnkwP/TUaMaB0=
|
||||
xorm.io/xorm v1.0.5/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
|
||||
|
12
i18n/util.go
12
i18n/util.go
@ -17,7 +17,6 @@ package i18n
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@ -27,10 +26,7 @@ import (
|
||||
//go:embed languages/*.ini
|
||||
var f embed.FS
|
||||
|
||||
var (
|
||||
langMapConfig = make(map[string]*ini.File)
|
||||
isNotFirstLoad = make(map[string]bool)
|
||||
)
|
||||
var langMapConfig = make(map[string]*ini.File)
|
||||
|
||||
func getI18nFilePath(language string) string {
|
||||
return fmt.Sprintf("../web/src/locales/%s/data.json", language)
|
||||
@ -77,16 +73,14 @@ func applyData(data1 *I18nData, data2 *I18nData) {
|
||||
func Translate(lang string, error string) string {
|
||||
parts := strings.Split(error, ".")
|
||||
if !strings.Contains(error, ".") || len(parts) != 2 {
|
||||
log.Println("Invalid Error Name")
|
||||
return ""
|
||||
return "Translate Error: " + error
|
||||
}
|
||||
|
||||
if isNotFirstLoad[lang] {
|
||||
if langMapConfig[lang] != nil {
|
||||
return langMapConfig[lang].Section(parts[0]).Key(parts[1]).String()
|
||||
} else {
|
||||
file, _ := f.ReadFile("languages/locale_" + lang + ".ini")
|
||||
langMapConfig[lang], _ = ini.Load(file)
|
||||
isNotFirstLoad[lang] = true
|
||||
return langMapConfig[lang].Section(parts[0]).Key(parts[1]).String()
|
||||
}
|
||||
}
|
||||
|
@ -16,14 +16,17 @@ package idp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/skip2/go-qrcode"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
@ -191,3 +194,54 @@ func (idp *WeChatIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
|
||||
}
|
||||
return &userInfo, nil
|
||||
}
|
||||
|
||||
func GetWechatOfficialAccountAccessToken(clientId string, clientSecret string) (string, error) {
|
||||
accessTokenUrl := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", clientId, clientSecret)
|
||||
request, err := http.NewRequest("GET", accessTokenUrl, nil)
|
||||
client := new(http.Client)
|
||||
resp, err := client.Do(request)
|
||||
respBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var data struct {
|
||||
ExpireIn int `json:"expires_in"`
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
err = json.Unmarshal(respBytes, &data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return data.AccessToken, nil
|
||||
}
|
||||
|
||||
func GetWechatOfficialAccountQRCode(clientId string, clientSecret string) (string, error) {
|
||||
accessToken, err := GetWechatOfficialAccountAccessToken(clientId, clientSecret)
|
||||
client := new(http.Client)
|
||||
params := "{\"action_name\": \"QR_LIMIT_STR_SCENE\", \"action_info\": {\"scene\": {\"scene_str\": \"test\"}}}"
|
||||
bodyData := bytes.NewReader([]byte(params))
|
||||
qrCodeUrl := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s", accessToken)
|
||||
requeset, err := http.NewRequest("POST", qrCodeUrl, bodyData)
|
||||
resp, err := client.Do(requeset)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
respBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var data struct {
|
||||
Ticket string `json:"ticket"`
|
||||
ExpireSeconds int `json:"expire_seconds"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
err = json.Unmarshal(respBytes, &data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var png []byte
|
||||
png, err = qrcode.Encode(data.URL, qrcode.Medium, 256)
|
||||
base64Image := base64.StdEncoding.EncodeToString(png)
|
||||
return base64Image, nil
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/idp"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"xorm.io/core"
|
||||
)
|
||||
@ -84,6 +85,16 @@ func GetApplicationCount(owner, field, value string) int {
|
||||
return int(count)
|
||||
}
|
||||
|
||||
func GetOrganizationApplicationCount(owner, Organization, field, value string) int {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
count, err := session.Count(&Application{Organization: Organization})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return int(count)
|
||||
}
|
||||
|
||||
func GetApplications(owner string) []*Application {
|
||||
applications := []*Application{}
|
||||
err := adapter.Engine.Desc("created_time").Find(&applications, &Application{Owner: owner})
|
||||
@ -94,8 +105,18 @@ func GetApplications(owner string) []*Application {
|
||||
return applications
|
||||
}
|
||||
|
||||
func GetPaginationApplications(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Application {
|
||||
func GetOrganizationApplications(owner string, organization string) []*Application {
|
||||
applications := []*Application{}
|
||||
err := adapter.Engine.Desc("created_time").Find(&applications, &Application{Owner: owner, Organization: organization})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return applications
|
||||
}
|
||||
|
||||
func GetPaginationApplications(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Application {
|
||||
var applications []*Application
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Find(&applications)
|
||||
if err != nil {
|
||||
@ -105,9 +126,10 @@ func GetPaginationApplications(owner string, offset, limit int, field, value, so
|
||||
return applications
|
||||
}
|
||||
|
||||
func GetApplicationsByOrganizationName(owner string, organization string) []*Application {
|
||||
func GetPaginationOrganizationApplications(owner, organization string, offset, limit int, field, value, sortField, sortOrder string) []*Application {
|
||||
applications := []*Application{}
|
||||
err := adapter.Engine.Desc("created_time").Find(&applications, &Application{Owner: owner, Organization: organization})
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Find(&applications, &Application{Owner: owner, Organization: organization})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -119,9 +141,11 @@ func getProviderMap(owner string) map[string]*Provider {
|
||||
providers := GetProviders(owner)
|
||||
m := map[string]*Provider{}
|
||||
for _, provider := range providers {
|
||||
//if provider.Category != "OAuth" {
|
||||
// continue
|
||||
//}
|
||||
// Get QRCode only once
|
||||
if provider.Type == "WeChat" && provider.DisableSsl == true && provider.Content == "" {
|
||||
provider.Content, _ = idp.GetWechatOfficialAccountQRCode(provider.ClientId2, provider.ClientSecret2)
|
||||
UpdateProvider(provider.Owner+"/"+provider.Name, provider)
|
||||
}
|
||||
|
||||
m[provider.Name] = GetMaskedProvider(provider)
|
||||
}
|
||||
@ -129,7 +153,7 @@ func getProviderMap(owner string) map[string]*Provider {
|
||||
}
|
||||
|
||||
func extendApplicationWithProviders(application *Application) {
|
||||
m := getProviderMap(application.Owner)
|
||||
m := getProviderMap(application.Organization)
|
||||
for _, providerItem := range application.Providers {
|
||||
if provider, ok := m[providerItem.Name]; ok {
|
||||
providerItem.Provider = provider
|
||||
@ -378,7 +402,7 @@ func IsAllowOrigin(origin string) bool {
|
||||
}
|
||||
|
||||
func getApplicationMap(organization string) map[string]*Application {
|
||||
applications := GetApplicationsByOrganizationName("admin", organization)
|
||||
applications := GetOrganizationApplications("admin", organization)
|
||||
|
||||
applicationMap := make(map[string]*Application)
|
||||
for _, application := range applications {
|
||||
|
@ -15,7 +15,7 @@
|
||||
package object
|
||||
|
||||
func (application *Application) GetProviderByCategory(category string) *Provider {
|
||||
providers := GetProviders(application.Owner)
|
||||
providers := GetProviders(application.Organization)
|
||||
m := map[string]*Provider{}
|
||||
for _, provider := range providers {
|
||||
if provider.Category != category {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -58,6 +58,7 @@ func initBuiltInOrganization() bool {
|
||||
PhonePrefix: "86",
|
||||
DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
|
||||
Tags: []string{},
|
||||
Languages: []string{"en", "zh", "es", "fr", "de", "ja", "ko", "ru"},
|
||||
AccountItems: []*AccountItem{
|
||||
{Name: "Organization", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
|
||||
{Name: "ID", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
|
||||
@ -221,7 +222,7 @@ func initBuiltInLdap() {
|
||||
}
|
||||
|
||||
func initBuiltInProvider() {
|
||||
provider := GetProvider("admin/provider_captcha_default")
|
||||
provider := GetProvider(util.GetId("admin", "provider_captcha_default"))
|
||||
if provider != nil {
|
||||
return
|
||||
}
|
||||
|
@ -168,7 +168,7 @@ func initDefinedLdap(ldap *Ldap) {
|
||||
}
|
||||
|
||||
func initDefinedProvider(provider *Provider) {
|
||||
existed := GetProvider(provider.GetId())
|
||||
existed := GetProvider(util.GetId("admin", provider.Name))
|
||||
if existed != nil {
|
||||
return
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ type Organization struct {
|
||||
DefaultAvatar string `xorm:"varchar(100)" json:"defaultAvatar"`
|
||||
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
|
||||
Tags []string `xorm:"mediumtext" json:"tags"`
|
||||
Languages []string `xorm:"varchar(255)" json:"languages"`
|
||||
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
|
||||
EnableSoftDeletion bool `json:"enableSoftDeletion"`
|
||||
IsProfilePublic bool `json:"isProfilePublic"`
|
||||
@ -222,7 +223,12 @@ func GetDefaultApplication(id string) (*Application, error) {
|
||||
}
|
||||
|
||||
if organization.DefaultApplication != "" {
|
||||
return getApplication("admin", organization.DefaultApplication), fmt.Errorf("The default application: %s does not exist", organization.DefaultApplication)
|
||||
defaultApplication := getApplication("admin", organization.DefaultApplication)
|
||||
if defaultApplication == nil {
|
||||
return nil, fmt.Errorf("The default application: %s does not exist", organization.DefaultApplication)
|
||||
} else {
|
||||
return defaultApplication, nil
|
||||
}
|
||||
}
|
||||
|
||||
applications := []*Application{}
|
||||
|
@ -152,20 +152,20 @@ func removePolicies(permission *Permission) {
|
||||
}
|
||||
}
|
||||
|
||||
func Enforce(userId string, permissionRule *PermissionRule) bool {
|
||||
func Enforce(permissionRule *PermissionRule) bool {
|
||||
permission := GetPermission(permissionRule.Id)
|
||||
enforcer := getEnforcer(permission)
|
||||
allow, err := enforcer.Enforce(userId, permissionRule.V1, permissionRule.V2)
|
||||
allow, err := enforcer.Enforce(permissionRule.V0, permissionRule.V1, permissionRule.V2)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return allow
|
||||
}
|
||||
|
||||
func BatchEnforce(userId string, permissionRules []PermissionRule) []bool {
|
||||
func BatchEnforce(permissionRules []PermissionRule) []bool {
|
||||
var requests [][]interface{}
|
||||
for _, permissionRule := range permissionRules {
|
||||
requests = append(requests, []interface{}{userId, permissionRule.V1, permissionRule.V2})
|
||||
requests = append(requests, []interface{}{permissionRule.V0, permissionRule.V1, permissionRule.V2})
|
||||
}
|
||||
permission := GetPermission(permissionRules[0].Id)
|
||||
enforcer := getEnforcer(permission)
|
||||
|
@ -25,7 +25,7 @@ import (
|
||||
|
||||
type Provider struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
Name string `xorm:"varchar(100) notnull pk unique" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
@ -46,9 +46,9 @@ type Provider struct {
|
||||
|
||||
Host string `xorm:"varchar(100)" json:"host"`
|
||||
Port int `json:"port"`
|
||||
DisableSsl bool `json:"disableSsl"`
|
||||
DisableSsl bool `json:"disableSsl"` // If the provider type is WeChat, DisableSsl means EnableQRCode
|
||||
Title string `xorm:"varchar(100)" json:"title"`
|
||||
Content string `xorm:"varchar(1000)" json:"content"`
|
||||
Content string `xorm:"varchar(1000)" json:"content"` // If provider type is WeChat, Content means QRCode string by Base64 encoding
|
||||
Receiver string `xorm:"varchar(100)" json:"receiver"`
|
||||
|
||||
RegionId string `xorm:"varchar(100)" json:"regionId"`
|
||||
@ -93,8 +93,8 @@ func GetMaskedProviders(providers []*Provider) []*Provider {
|
||||
}
|
||||
|
||||
func GetProviderCount(owner, field, value string) int {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
count, err := session.Count(&Provider{})
|
||||
session := GetSession("", -1, -1, field, value, "", "")
|
||||
count, err := session.Where("owner = ? or owner = ? ", "admin", owner).Count(&Provider{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -114,7 +114,7 @@ func GetGlobalProviderCount(field, value string) int {
|
||||
|
||||
func GetProviders(owner string) []*Provider {
|
||||
providers := []*Provider{}
|
||||
err := adapter.Engine.Desc("created_time").Find(&providers, &Provider{Owner: owner})
|
||||
err := adapter.Engine.Where("owner = ? or owner = ? ", "admin", owner).Desc("created_time").Find(&providers, &Provider{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -133,9 +133,9 @@ func GetGlobalProviders() []*Provider {
|
||||
}
|
||||
|
||||
func GetPaginationProviders(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Provider {
|
||||
var providers []*Provider
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Find(&providers)
|
||||
providers := []*Provider{}
|
||||
session := GetSession("", offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Where("owner = ? or owner = ? ", "admin", owner).Find(&providers)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -144,7 +144,7 @@ func GetPaginationProviders(owner string, offset, limit int, field, value, sortF
|
||||
}
|
||||
|
||||
func GetPaginationGlobalProviders(offset, limit int, field, value, sortField, sortOrder string) []*Provider {
|
||||
var providers []*Provider
|
||||
providers := []*Provider{}
|
||||
session := GetSession("", offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Find(&providers)
|
||||
if err != nil {
|
||||
@ -159,7 +159,7 @@ func getProvider(owner string, name string) *Provider {
|
||||
return nil
|
||||
}
|
||||
|
||||
provider := Provider{Owner: owner, Name: name}
|
||||
provider := Provider{Name: name}
|
||||
existed, err := adapter.Engine.Get(&provider)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -15,7 +15,9 @@
|
||||
package object
|
||||
|
||||
type ProviderItem struct {
|
||||
Name string `json:"name"`
|
||||
Owner string `json:"owner"`
|
||||
Name string `json:"name"`
|
||||
|
||||
CanSignUp bool `json:"canSignUp"`
|
||||
CanSignIn bool `json:"canSignIn"`
|
||||
CanUnlink bool `json:"canUnlink"`
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -24,9 +24,10 @@ import (
|
||||
|
||||
type Claims struct {
|
||||
*User
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
TokenType string `json:"tokenType,omitempty"`
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@ -37,8 +38,9 @@ type UserShort struct {
|
||||
|
||||
type ClaimsShort struct {
|
||||
*UserShort
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
TokenType string `json:"tokenType,omitempty"`
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@ -53,6 +55,7 @@ func getShortUser(user *User) *UserShort {
|
||||
func getShortClaims(claims Claims) ClaimsShort {
|
||||
res := ClaimsShort{
|
||||
UserShort: getShortUser(claims.User),
|
||||
TokenType: claims.TokenType,
|
||||
Nonce: claims.Nonce,
|
||||
Scope: claims.Scope,
|
||||
RegisteredClaims: claims.RegisteredClaims,
|
||||
@ -72,8 +75,9 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
|
||||
jti := fmt.Sprintf("%s/%s", application.Owner, name)
|
||||
|
||||
claims := Claims{
|
||||
User: user,
|
||||
Nonce: nonce,
|
||||
User: user,
|
||||
TokenType: "access-token",
|
||||
Nonce: nonce,
|
||||
// FIXME: A workaround for custom claim by reusing `tag` in user info
|
||||
Tag: user.Tag,
|
||||
Scope: scope,
|
||||
@ -97,10 +101,12 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
|
||||
|
||||
token = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsShort)
|
||||
claimsShort.ExpiresAt = jwt.NewNumericDate(refreshExpireTime)
|
||||
claimsShort.TokenType = "refresh-token"
|
||||
refreshToken = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsShort)
|
||||
} else {
|
||||
token = jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
claims.ExpiresAt = jwt.NewNumericDate(refreshExpireTime)
|
||||
claims.TokenType = "refresh-token"
|
||||
refreshToken = jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
}
|
||||
|
||||
|
@ -41,38 +41,7 @@ type VerificationRecord struct {
|
||||
IsUsed bool
|
||||
}
|
||||
|
||||
func SendVerificationCodeToEmail(organization *Organization, user *User, provider *Provider, remoteAddr string, dest string) error {
|
||||
if provider == nil {
|
||||
return fmt.Errorf("please set an Email provider first")
|
||||
}
|
||||
|
||||
sender := organization.DisplayName
|
||||
title := provider.Title
|
||||
code := getRandomCode(6)
|
||||
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
|
||||
content := fmt.Sprintf(provider.Content, code)
|
||||
|
||||
if err := SendEmail(provider, title, content, dest, sender); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code)
|
||||
}
|
||||
|
||||
func SendVerificationCodeToPhone(organization *Organization, user *User, provider *Provider, remoteAddr string, dest string) error {
|
||||
if provider == nil {
|
||||
return errors.New("please set a SMS provider first")
|
||||
}
|
||||
|
||||
code := getRandomCode(6)
|
||||
if err := SendSms(provider, code, dest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code)
|
||||
}
|
||||
|
||||
func AddToVerificationRecord(user *User, provider *Provider, remoteAddr, recordType, dest, code string) error {
|
||||
func IsAllowSend(user *User, remoteAddr, recordType string) error {
|
||||
var record VerificationRecord
|
||||
record.RemoteAddr = remoteAddr
|
||||
record.Type = recordType
|
||||
@ -89,6 +58,63 @@ func AddToVerificationRecord(user *User, provider *Provider, remoteAddr, recordT
|
||||
return errors.New("you can only send one code in 60s")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SendVerificationCodeToEmail(organization *Organization, user *User, provider *Provider, remoteAddr string, dest string) error {
|
||||
if provider == nil {
|
||||
return fmt.Errorf("please set an Email provider first")
|
||||
}
|
||||
|
||||
sender := organization.DisplayName
|
||||
title := provider.Title
|
||||
code := getRandomCode(6)
|
||||
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
|
||||
content := fmt.Sprintf(provider.Content, code)
|
||||
|
||||
if err := IsAllowSend(user, remoteAddr, provider.Category); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := SendEmail(provider, title, content, dest, sender); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SendVerificationCodeToPhone(organization *Organization, user *User, provider *Provider, remoteAddr string, dest string) error {
|
||||
if provider == nil {
|
||||
return errors.New("please set a SMS provider first")
|
||||
}
|
||||
|
||||
if err := IsAllowSend(user, remoteAddr, provider.Category); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
code := getRandomCode(6)
|
||||
if err := SendSms(provider, code, dest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func AddToVerificationRecord(user *User, provider *Provider, remoteAddr, recordType, dest, code string) error {
|
||||
var record VerificationRecord
|
||||
record.RemoteAddr = remoteAddr
|
||||
record.Type = recordType
|
||||
if user != nil {
|
||||
record.User = user.GetId()
|
||||
}
|
||||
record.Owner = provider.Owner
|
||||
record.Name = util.GenerateId()
|
||||
record.CreatedTime = util.GetCurrentTime()
|
||||
@ -99,10 +125,10 @@ func AddToVerificationRecord(user *User, provider *Provider, remoteAddr, recordT
|
||||
|
||||
record.Receiver = dest
|
||||
record.Code = code
|
||||
record.Time = now
|
||||
record.Time = time.Now().Unix()
|
||||
record.IsUsed = false
|
||||
|
||||
_, err = adapter.Engine.Insert(record)
|
||||
_, err := adapter.Engine.Insert(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ type Webhook struct {
|
||||
Method string `xorm:"varchar(100)" json:"method"`
|
||||
ContentType string `xorm:"varchar(100)" json:"contentType"`
|
||||
Headers []*Header `xorm:"mediumtext" json:"headers"`
|
||||
Events []string `xorm:"varchar(100)" json:"events"`
|
||||
Events []string `xorm:"varchar(1000)" json:"events"`
|
||||
IsUserExtended bool `json:"isUserExtended"`
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
}
|
||||
|
@ -54,6 +54,8 @@ func initAPI() {
|
||||
beego.Router("/api/get-saml-login", &controllers.ApiController{}, "GET:GetSamlLogin")
|
||||
beego.Router("/api/acs", &controllers.ApiController{}, "POST:HandleSamlLogin")
|
||||
beego.Router("/api/saml/metadata", &controllers.ApiController{}, "GET:GetSamlMeta")
|
||||
beego.Router("/api/webhook", &controllers.ApiController{}, "POST:HandleOfficialAccountEvent")
|
||||
beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType")
|
||||
|
||||
beego.Router("/api/get-organizations", &controllers.ApiController{}, "GET:GetOrganizations")
|
||||
beego.Router("/api/get-organization", &controllers.ApiController{}, "GET:GetOrganization")
|
||||
@ -103,6 +105,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")
|
||||
|
@ -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 {
|
||||
|
@ -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
29
util/struct.go
Normal 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
|
||||
}
|
@ -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);
|
||||
|
@ -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 => {
|
||||
|
@ -420,6 +420,8 @@ class App extends Component {
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
if (Setting.isLocalAdminUser(this.state.account)) {
|
||||
res.push(
|
||||
<Menu.Item key="/applications">
|
||||
<Link to="/applications">
|
||||
@ -427,9 +429,6 @@ class App extends Component {
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
|
||||
if (Setting.isLocalAdminUser(this.state.account)) {
|
||||
res.push(
|
||||
<Menu.Item key="/providers">
|
||||
<Link to="/providers">
|
||||
@ -565,10 +564,9 @@ class App extends Component {
|
||||
<Route exact path="/adapters" render={(props) => this.renderLoginIfNotLoggedIn(<AdapterListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/adapters/:organizationName/:adapterName" render={(props) => this.renderLoginIfNotLoggedIn(<AdapterEditPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/providers" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/providers/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/providers/:organizationName/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/applications/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationEditPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/applications/:organizationName/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationEditPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/resources" render={(props) => this.renderLoginIfNotLoggedIn(<ResourceListPage account={this.state.account} {...props} />)} />
|
||||
{/* <Route exact path="/resources/:resourceName" render={(props) => this.renderLoginIfNotLoggedIn(<ResourceEditPage account={this.state.account} {...props} />)}/>*/}
|
||||
<Route exact path="/ldap/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapEditPage account={this.state.account} {...props} />)} />
|
||||
@ -636,7 +634,7 @@ class App extends Component {
|
||||
{
|
||||
this.renderAccount()
|
||||
}
|
||||
<SelectLanguageBox />
|
||||
{this.state.account && <SelectLanguageBox languages={this.state.account.organization.languages} />}
|
||||
</div>
|
||||
</Header>
|
||||
<Layout style={{backgroundColor: "#f5f5f5", alignItems: "stretch"}}>
|
||||
@ -680,7 +678,7 @@ class App extends Component {
|
||||
{
|
||||
this.renderAccount()
|
||||
}
|
||||
<SelectLanguageBox />
|
||||
{this.state.account && <SelectLanguageBox languages={this.state.account.organization.languages} />}
|
||||
</div>
|
||||
</Header>
|
||||
{
|
||||
|
@ -59,12 +59,6 @@
|
||||
height: 70px; /* Footer height */
|
||||
}
|
||||
|
||||
#language-box-corner {
|
||||
position: absolute;
|
||||
top: 75px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.language-box {
|
||||
background: url("@{StaticBaseUrl}/img/muti_language.svg");
|
||||
background-size: 25px, 25px;
|
||||
|
@ -91,7 +91,7 @@ class ApplicationEditPage extends React.Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
owner: props.account.owner,
|
||||
owner: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||
applicationName: props.match.params.applicationName,
|
||||
application: null,
|
||||
organizations: [],
|
||||
@ -142,21 +142,11 @@ class ApplicationEditPage extends React.Component {
|
||||
}
|
||||
|
||||
getProviders() {
|
||||
if (Setting.isAdminUser(this.props.account)) {
|
||||
ProviderBackend.getGlobalProviders()
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
providers: res,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
ProviderBackend.getProviders(this.state.owner)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
providers: res,
|
||||
});
|
||||
});
|
||||
}
|
||||
ProviderBackend.getProviders(this.state.owner).then((res => {
|
||||
this.setState({
|
||||
providers: res,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
getSamlMetadata() {
|
||||
@ -287,7 +277,7 @@ class ApplicationEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.application.organization} onChange={(value => {this.updateApplicationField("organization", value);})}>
|
||||
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.application.organization} onChange={(value => {this.updateApplicationField("organization", value);})}>
|
||||
{
|
||||
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
|
||||
}
|
||||
@ -791,7 +781,7 @@ class ApplicationEditPage extends React.Component {
|
||||
|
||||
submitApplicationEdit(willExist) {
|
||||
const application = Setting.deepCopy(this.state.application);
|
||||
ApplicationBackend.updateApplication(this.state.application.owner, this.state.applicationName, application)
|
||||
ApplicationBackend.updateApplication("admin", this.state.applicationName, application)
|
||||
.then((res) => {
|
||||
if (res.msg === "") {
|
||||
Setting.showMessage("success", "Successfully saved");
|
||||
@ -802,7 +792,7 @@ class ApplicationEditPage extends React.Component {
|
||||
if (willExist) {
|
||||
this.props.history.push("/applications");
|
||||
} else {
|
||||
this.props.history.push(`/applications/${this.state.application.name}`);
|
||||
this.props.history.push(`/applications/${this.state.application.organization}/${this.state.application.name}`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
|
@ -23,11 +23,28 @@ import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
|
||||
class ApplicationListPage extends BaseListPage {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
organizationName: props.account.owner,
|
||||
data: [],
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
loading: false,
|
||||
searchText: "",
|
||||
searchedColumn: "",
|
||||
};
|
||||
}
|
||||
|
||||
newApplication() {
|
||||
const randomName = Setting.getRandomName();
|
||||
return {
|
||||
owner: "admin", // this.props.account.applicationname,
|
||||
owner: "admin", // this.props.account.applicationName,
|
||||
name: `application_${randomName}`,
|
||||
organization: this.state.organizationName,
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Application - ${randomName}`,
|
||||
logo: `${Setting.StaticBaseUrl}/img/casdoor-logo_1185x256.png`,
|
||||
@ -61,7 +78,7 @@ class ApplicationListPage extends BaseListPage {
|
||||
const newApplication = this.newApplication();
|
||||
ApplicationBackend.addApplication(newApplication)
|
||||
.then((res) => {
|
||||
this.props.history.push({pathname: `/applications/${newApplication.name}`, mode: "add"});
|
||||
this.props.history.push({pathname: `/applications/${newApplication.organization}/${newApplication.name}`, mode: "add"});
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
@ -96,7 +113,7 @@ class ApplicationListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("name"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/applications/${text}`}>
|
||||
<Link to={`/applications/${record.organization}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
@ -213,7 +230,7 @@ class ApplicationListPage extends BaseListPage {
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/applications/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/applications/${record.organization}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<Popconfirm
|
||||
title={`Sure to delete application: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteApplication(index)}
|
||||
@ -254,7 +271,8 @@ class ApplicationListPage extends BaseListPage {
|
||||
const field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
this.setState({loading: true});
|
||||
ApplicationBackend.getApplications("admin", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
(Setting.isAdminUser(this.props.account) ? ApplicationBackend.getApplications("admin", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder) :
|
||||
ApplicationBackend.getApplicationsByOrganization("admin", this.state.organizationName, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder))
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
|
@ -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));
|
||||
|
@ -255,6 +255,31 @@ 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:Languages"), i18next.t("general:Languages - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" style={{width: "100%"}}
|
||||
value={this.state.organization.languages}
|
||||
onChange={(value => {
|
||||
this.updateOrganizationField("languages", value);
|
||||
})} >
|
||||
{
|
||||
[
|
||||
{value: "en", label: "English"},
|
||||
{value: "zh", label: "简体中文"},
|
||||
{value: "es", label: "Español"},
|
||||
{value: "fr", label: "Français"},
|
||||
{value: "de", label: "Deutsch"},
|
||||
{value: "ja", label: "日本語"},
|
||||
{value: "ko", label: "한국어"},
|
||||
{value: "ru", label: "Русский"},
|
||||
].map((item, index) => <Option key={index} value={item.value}>{item.label}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Soft deletion"), i18next.t("organization:Soft deletion - Tooltip"))} :
|
||||
|
@ -37,6 +37,7 @@ class OrganizationListPage extends BaseListPage {
|
||||
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
|
||||
defaultApplication: "",
|
||||
tags: [],
|
||||
languages: ["en", "zh", "es", "fr", "de", "ja", "ko", "ru"],
|
||||
masterPassword: "",
|
||||
enableSoftDeletion: false,
|
||||
isProfilePublic: true,
|
||||
|
@ -29,7 +29,7 @@ class PermissionListPage extends BaseListPage {
|
||||
name: `permission_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Permission - ${randomName}`,
|
||||
users: [this.props.account.name],
|
||||
users: [`${this.props.account.owner}/${this.props.account.name}`],
|
||||
roles: [],
|
||||
domains: [],
|
||||
resourceType: "Application",
|
||||
|
@ -22,6 +22,7 @@ import {authConfig} from "./auth/Auth";
|
||||
import * as ProviderEditTestEmail from "./TestEmailWidget";
|
||||
import copy from "copy-to-clipboard";
|
||||
import {CaptchaPreview} from "./common/CaptchaPreview";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
|
||||
const {Option} = Select;
|
||||
const {TextArea} = Input;
|
||||
@ -34,11 +35,13 @@ class ProviderEditPage extends React.Component {
|
||||
providerName: props.match.params.providerName,
|
||||
owner: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||
provider: null,
|
||||
organizations: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getOrganizations();
|
||||
this.getProvider();
|
||||
}
|
||||
|
||||
@ -51,6 +54,17 @@ class ProviderEditPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
if (Setting.isAdminUser(this.props.account)) {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
organizations: res.msg === undefined ? res : [],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
parseProviderField(key, value) {
|
||||
if (["port"].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
@ -191,6 +205,19 @@ class ProviderEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.provider.owner} onChange={(value => {this.updateProviderField("owner", value);})}>
|
||||
{Setting.isAdminUser(this.props.account) ? <Option key={"admin"} value={"admin"}>{i18next.t("provider:admin (share)")}</Option> : null}
|
||||
{
|
||||
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Category"), i18next.t("provider:Category - Tooltip"))} :
|
||||
@ -424,6 +451,20 @@ class ProviderEditPage extends React.Component {
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.provider.type !== "WeChat" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Enable QR code"), i18next.t("provider:Enable QR code - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.provider.disableSsl} onChange={checked => {
|
||||
this.updateProviderField("disableSsl", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.provider.type !== "Adfs" && this.state.provider.type !== "Casdoor" && this.state.provider.type !== "Okta" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
@ -746,18 +787,19 @@ class ProviderEditPage extends React.Component {
|
||||
|
||||
submitProviderEdit(willExist) {
|
||||
const provider = Setting.deepCopy(this.state.provider);
|
||||
ProviderBackend.updateProvider(this.state.provider.owner, this.state.providerName, provider)
|
||||
ProviderBackend.updateProvider(this.state.owner, this.state.providerName, provider)
|
||||
.then((res) => {
|
||||
if (res.msg === "") {
|
||||
Setting.showMessage("success", "Successfully saved");
|
||||
this.setState({
|
||||
owner: this.state.provider.owner,
|
||||
providerName: this.state.provider.name,
|
||||
});
|
||||
|
||||
if (willExist) {
|
||||
this.props.history.push("/providers");
|
||||
} else {
|
||||
this.props.history.push(`/providers/${this.state.provider.name}`);
|
||||
this.props.history.push(`/providers/${this.state.provider.owner}/${this.state.provider.name}`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
|
@ -27,7 +27,7 @@ class ProviderListPage extends BaseListPage {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
owner: Setting.isAdminUser(props.account) ? "admin" : props.account.organization.name,
|
||||
owner: Setting.isAdminUser(props.account) ? "admin" : props.account.owner,
|
||||
data: [],
|
||||
pagination: {
|
||||
current: 1,
|
||||
@ -96,12 +96,20 @@ class ProviderListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("name"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/providers/${text}`}>
|
||||
<Link to={`/providers/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Organization"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
width: "150px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("owner"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Created time"),
|
||||
dataIndex: "createdTime",
|
||||
@ -192,12 +200,12 @@ class ProviderListPage extends BaseListPage {
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/providers/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<Button disabled={!Setting.isAdminUser(this.props.account) && (record.owner !== this.props.account.owner)} style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/providers/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<Popconfirm
|
||||
title={`Sure to delete provider: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteProvider(index)}
|
||||
>
|
||||
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
|
||||
<Button disabled={!Setting.isAdminUser(this.props.account) && (record.owner !== this.props.account.owner)} style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
);
|
||||
|
@ -95,7 +95,7 @@ class ResourceListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("provider"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/providers/${text}`}>
|
||||
<Link to={`/providers/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
|
@ -173,7 +173,7 @@ class RoleEditPage extends React.Component {
|
||||
this.updateRoleField("domains", value);
|
||||
})}>
|
||||
{
|
||||
this.state.role.domains.map((domain, index) => <Option key={index} value={domain}>{domain}</Option>)
|
||||
this.state.role.domains?.map((domain, index) => <Option key={index} value={domain}>{domain}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
|
@ -28,28 +28,45 @@ class SelectLanguageBox extends React.Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
languages: props.languages ?? ["en", "zh", "es", "fr", "de", "ja", "ko", "ru"],
|
||||
};
|
||||
}
|
||||
|
||||
items = [
|
||||
this.getItem("English", "en", flagIcon("US", "English")),
|
||||
this.getItem("简体中文", "zh", flagIcon("CN", "简体中文")),
|
||||
this.getItem("Español", "es", flagIcon("ES", "Español")),
|
||||
this.getItem("Français", "fr", flagIcon("FR", "Français")),
|
||||
this.getItem("Deutsch", "de", flagIcon("DE", "Deutsch")),
|
||||
this.getItem("日本語", "ja", flagIcon("JP", "日本語")),
|
||||
this.getItem("한국어", "ko", flagIcon("KR", "한국어")),
|
||||
this.getItem("Русский", "ru", flagIcon("RU", "Русский")),
|
||||
];
|
||||
|
||||
getOrganizationLanguages(languages) {
|
||||
const select = [];
|
||||
for (const language of languages) {
|
||||
this.items.map((item, index) => item.key === language ? select.push(item) : null);
|
||||
}
|
||||
return select;
|
||||
}
|
||||
|
||||
getItem(label, key, icon) {
|
||||
return {key, icon, label};
|
||||
}
|
||||
|
||||
render() {
|
||||
const languageItems = this.getOrganizationLanguages(this.state.languages);
|
||||
const menu = (
|
||||
<Menu onClick={(e) => {
|
||||
Setting.changeLanguage(e.key);
|
||||
<Menu items={languageItems} onClick={(e) => {
|
||||
Setting.setLanguage(e.key);
|
||||
}}>
|
||||
<Menu.Item key="en" icon={flagIcon("US", "English")}>English</Menu.Item>
|
||||
<Menu.Item key="zh" icon={flagIcon("CN", "简体中文")}>简体中文</Menu.Item>
|
||||
<Menu.Item key="es" icon={flagIcon("ES", "Español")}>Español</Menu.Item>
|
||||
<Menu.Item key="fr" icon={flagIcon("FR", "Français")}>Français</Menu.Item>
|
||||
<Menu.Item key="de" icon={flagIcon("DE", "Deutsch")}>Deutsch</Menu.Item>
|
||||
<Menu.Item key="ja" icon={flagIcon("JP", "日本語")}>日本語</Menu.Item>
|
||||
<Menu.Item key="ko" icon={flagIcon("KR", "한국어")}>한국어</Menu.Item>
|
||||
<Menu.Item key="ru" icon={flagIcon("RU", "Русский")}>Русский</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown overlay={menu} >
|
||||
<div className="language-box" id={this.props.id} style={this.props.style} />
|
||||
<div className="language-box" style={{display: languageItems.length === 0 ? "none" : null, ...this.props.style}} />
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
@ -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) {
|
||||
@ -552,14 +554,10 @@ export function setLanguage(language) {
|
||||
i18next.changeLanguage(language);
|
||||
}
|
||||
|
||||
export function changeLanguage(language) {
|
||||
localStorage.setItem("language", language);
|
||||
changeMomentLanguage(language);
|
||||
i18next.changeLanguage(language);
|
||||
// window.location.reload(true);
|
||||
}
|
||||
|
||||
export function getAcceptLanguage() {
|
||||
if (i18next.language === null || i18next.language === "") {
|
||||
return "en;q=0.9,en;q=0.8";
|
||||
}
|
||||
return i18next.language + ";q=0.9,en;q=0.8";
|
||||
}
|
||||
|
||||
@ -701,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 [];
|
||||
|
@ -102,7 +102,7 @@ class TokenListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("application"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/applications/${text}`}>
|
||||
<Link to={`/applications/${record.organization}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -167,7 +167,7 @@ class UserListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("signupApplication"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/applications/${text}`}>
|
||||
<Link to={`/applications/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
|
@ -244,7 +244,7 @@ class WebhookEditPage extends React.Component {
|
||||
}} >
|
||||
{
|
||||
(
|
||||
["signup", "login", "logout", "update-user"].map((option, index) => {
|
||||
["signup", "login", "logout", "add-user", "update-user", "add-organization", "update-organization", "add-provider", "update-provider"].map((option, index) => {
|
||||
return (
|
||||
<Option key={option} value={option}>{option}</Option>
|
||||
);
|
||||
|
@ -54,7 +54,7 @@ export function oAuthParamsToQuery(oAuthParams) {
|
||||
}
|
||||
|
||||
// code
|
||||
return `?clientId=${oAuthParams.clientId}&responseType=${oAuthParams.responseType}&redirectUri=${oAuthParams.redirectUri}&scope=${oAuthParams.scope}&state=${oAuthParams.state}&nonce=${oAuthParams.nonce}&code_challenge_method=${oAuthParams.challengeMethod}&code_challenge=${oAuthParams.codeChallenge}`;
|
||||
return `?clientId=${oAuthParams.clientId}&responseType=${oAuthParams.responseType}&redirectUri=${encodeURIComponent(oAuthParams.redirectUri)}&scope=${oAuthParams.scope}&state=${oAuthParams.state}&nonce=${oAuthParams.nonce}&code_challenge_method=${oAuthParams.challengeMethod}&code_challenge=${oAuthParams.codeChallenge}`;
|
||||
}
|
||||
|
||||
export function getApplicationLogin(oAuthParams) {
|
||||
@ -130,3 +130,13 @@ export function loginWithSaml(values, param) {
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getWechatMessageEvent() {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-webhook-event`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
@ -28,9 +28,7 @@ import i18next from "i18next";
|
||||
import CustomGithubCorner from "../CustomGithubCorner";
|
||||
import {CountDownInput} from "../common/CountDownInput";
|
||||
import SelectLanguageBox from "../SelectLanguageBox";
|
||||
import {withTranslation} from "react-i18next";
|
||||
import {CaptchaModal} from "../common/CaptchaModal";
|
||||
import {withRouter} from "react-router-dom";
|
||||
|
||||
const {TabPane} = Tabs;
|
||||
|
||||
@ -337,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)}>
|
||||
{
|
||||
@ -370,7 +368,7 @@ class LoginPage extends React.Component {
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please input your application!",
|
||||
message: i18next.t("application:Please input your application!"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
@ -381,7 +379,7 @@ class LoginPage extends React.Component {
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please input your organization!",
|
||||
message: i18next.t("application:Please input your organization!"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
@ -683,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 {
|
||||
@ -691,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}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -800,7 +798,7 @@ class LoginPage extends React.Component {
|
||||
{/* {*/}
|
||||
{/* this.state.clientId !== null ? "Redirect" : null*/}
|
||||
{/* }*/}
|
||||
<SelectLanguageBox id="language-box-corner" style={{top: "55px", right: "5px", position: "absolute"}} />
|
||||
<SelectLanguageBox languages={application.organizationObj.languages} style={{top: "55px", right: "5px", position: "absolute"}} />
|
||||
{
|
||||
this.renderSignedInBox()
|
||||
}
|
||||
@ -817,4 +815,4 @@ class LoginPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation()(withRouter(LoginPage));
|
||||
export default LoginPage;
|
||||
|
@ -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)}>
|
||||
{
|
||||
|
@ -40,6 +40,8 @@ import BilibiliLoginButton from "./BilibiliLoginButton";
|
||||
import OktaLoginButton from "./OktaLoginButton";
|
||||
import DouyinLoginButton from "./DouyinLoginButton";
|
||||
import * as AuthBackend from "./AuthBackend";
|
||||
import {getEvent} from "./Util";
|
||||
import {Modal} from "antd";
|
||||
|
||||
function getSigninButton(type) {
|
||||
const text = i18next.t("login:Sign in with {type}").replace("{type}", type);
|
||||
@ -116,11 +118,33 @@ function getSamlUrl(provider, location) {
|
||||
export function renderProviderLogo(provider, application, width, margin, size, location) {
|
||||
if (size === "small") {
|
||||
if (provider.category === "OAuth") {
|
||||
return (
|
||||
<a key={provider.displayName} href={Provider.getAuthUrl(application, provider, "signup")}>
|
||||
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} style={{margin: margin}} />
|
||||
</a>
|
||||
);
|
||||
if (provider.type === "WeChat" && provider.clientId2 !== "" && provider.clientSecret2 !== "" && provider.content !== "" && provider.disableSsl === true && !navigator.userAgent.includes("MicroMessenger")) {
|
||||
const info = async() => {
|
||||
const t1 = setInterval(await getEvent, 1000, application, provider);
|
||||
{Modal.info({
|
||||
title: i18next.t("provider:Please use WeChat and scan the QR code to sign in"),
|
||||
content: (
|
||||
<div>
|
||||
<img width={256} height={256} src = {"data:image/png;base64," + provider.content} alt="Wechat QR code" style={{margin: margin}} />
|
||||
</div>
|
||||
),
|
||||
onOk() {
|
||||
window.clearInterval(t1);
|
||||
},
|
||||
});}
|
||||
};
|
||||
return (
|
||||
<a key={provider.displayName} >
|
||||
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} style={{margin: margin}} onClick={info} />
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a key={provider.displayName} href={Provider.getAuthUrl(application, provider, "signup")}>
|
||||
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} style={{margin: margin}} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
} else if (provider.category === "SAML") {
|
||||
return (
|
||||
<a key={provider.displayName} onClick={() => getSamlUrl(provider, location)}>
|
||||
|
@ -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)}>
|
||||
{
|
||||
@ -645,7 +645,7 @@ class SignupPage extends React.Component {
|
||||
{
|
||||
Setting.renderLogo(application)
|
||||
}
|
||||
<SelectLanguageBox id="language-box-corner" style={{top: "55px", right: "5px", position: "absolute"}} />
|
||||
<SelectLanguageBox languages={application.organizationObj.languages} style={{top: "55px", right: "5px", position: "absolute"}} />
|
||||
{
|
||||
this.renderForm(application)
|
||||
}
|
||||
|
@ -14,6 +14,9 @@
|
||||
|
||||
import React from "react";
|
||||
import {Alert, Button, Result, message} from "antd";
|
||||
import {getWechatMessageEvent} from "./AuthBackend";
|
||||
import * as Setting from "../Setting";
|
||||
import * as Provider from "./Provider";
|
||||
|
||||
export function showMessage(type, text) {
|
||||
if (type === "success") {
|
||||
@ -150,3 +153,12 @@ export function getQueryParamsFromState(state) {
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
export function getEvent(application, provider) {
|
||||
getWechatMessageEvent()
|
||||
.then(res => {
|
||||
if (res.data === "SCAN" || res.data === "subscribe") {
|
||||
Setting.goToLink(Provider.getAuthUrl(application, provider, "signup"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -24,8 +24,8 @@ export function getApplications(owner, page = "", pageSize = "", field = "", val
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getApplicationsByOrganization(owner, organization) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-organization-applications?owner=${owner}&organization=${organization}`, {
|
||||
export function getApplicationsByOrganization(owner, organization, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-organization-applications?owner=${owner}&organization=${organization}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
@ -68,7 +68,6 @@ export function updateApplication(owner, name, application) {
|
||||
|
||||
export function addApplication(application) {
|
||||
const newApplication = Setting.deepCopy(application);
|
||||
newApplication.organization = "built-in";
|
||||
return fetch(`${Setting.ServerUrl}/api/add-application`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
|
@ -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;
|
||||
}
|
||||
|
308
web/src/common/PoliciyTable.js
Normal file
308
web/src/common/PoliciyTable.js
Normal 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;
|
@ -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",
|
||||
@ -170,6 +180,8 @@
|
||||
"Is enabled - Tooltip": "Ist aktiviert - Tooltip",
|
||||
"LDAPs": "LDAPs",
|
||||
"LDAPs - Tooltip": "LDAPs - Tooltip",
|
||||
"Languages": "Languages",
|
||||
"Languages - Tooltip": "Languages - Tooltip",
|
||||
"Last name": "Last name",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "App's image tag",
|
||||
@ -290,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",
|
||||
@ -478,6 +491,8 @@
|
||||
"Email Content - Tooltip": "Unique string-style identifier",
|
||||
"Email Title": "E-Mail-Titel",
|
||||
"Email Title - Tooltip": "Unique string-style identifier",
|
||||
"Enable QR code": "Enable QR code",
|
||||
"Enable QR code - Tooltip": "Enable QR code - Tooltip",
|
||||
"Endpoint": "Endpoint",
|
||||
"Endpoint (Intranet)": "Endpunkt (Intranet)",
|
||||
"Host": "Host",
|
||||
@ -496,6 +511,7 @@
|
||||
"Parse": "Parse",
|
||||
"Parse Metadata successfully": "Metadaten erfolgreich analysieren",
|
||||
"Path prefix": "Path prefix",
|
||||
"Please use WeChat and scan the QR code to sign in": "Please use WeChat and scan the QR code to sign in",
|
||||
"Port": "Port",
|
||||
"Port - Tooltip": "Unique string-style identifier",
|
||||
"Prompted": "Prompted",
|
||||
@ -550,6 +566,7 @@
|
||||
"UserInfo URL": "UserInfo URL",
|
||||
"UserInfo URL - Tooltip": "UserInfo URL - Tooltip",
|
||||
"Visible": "Visible",
|
||||
"admin (share)": "admin (share)",
|
||||
"alertType": "alarmtyp"
|
||||
},
|
||||
"record": {
|
||||
|
@ -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",
|
||||
@ -170,6 +180,8 @@
|
||||
"Is enabled - Tooltip": "Is enabled - Tooltip",
|
||||
"LDAPs": "LDAPs",
|
||||
"LDAPs - Tooltip": "LDAPs - Tooltip",
|
||||
"Languages": "Languages",
|
||||
"Languages - Tooltip": "Languages - Tooltip",
|
||||
"Last name": "Last name",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Logo - Tooltip",
|
||||
@ -290,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",
|
||||
@ -478,6 +491,8 @@
|
||||
"Email Content - Tooltip": "Email Content - Tooltip",
|
||||
"Email Title": "Email Title",
|
||||
"Email Title - Tooltip": "Email Title - Tooltip",
|
||||
"Enable QR code": "Enable QR code",
|
||||
"Enable QR code - Tooltip": "Enable QR code - Tooltip",
|
||||
"Endpoint": "Endpoint",
|
||||
"Endpoint (Intranet)": "Endpoint (Intranet)",
|
||||
"Host": "Host",
|
||||
@ -496,6 +511,7 @@
|
||||
"Parse": "Parse",
|
||||
"Parse Metadata successfully": "Parse Metadata successfully",
|
||||
"Path prefix": "Path prefix",
|
||||
"Please use WeChat and scan the QR code to sign in": "Please use WeChat and scan the QR code to sign in",
|
||||
"Port": "Port",
|
||||
"Port - Tooltip": "Port - Tooltip",
|
||||
"Prompted": "Prompted",
|
||||
@ -550,6 +566,7 @@
|
||||
"UserInfo URL": "UserInfo URL",
|
||||
"UserInfo URL - Tooltip": "UserInfo URL - Tooltip",
|
||||
"Visible": "Visible",
|
||||
"admin (share)": "admin (share)",
|
||||
"alertType": "alertType"
|
||||
},
|
||||
"record": {
|
||||
|
@ -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"
|
||||
|
@ -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",
|
||||
@ -170,6 +180,8 @@
|
||||
"Is enabled - Tooltip": "Est activé - infobulle",
|
||||
"LDAPs": "LDAPs",
|
||||
"LDAPs - Tooltip": "LDAPs - Infobulle",
|
||||
"Languages": "Languages",
|
||||
"Languages - Tooltip": "Languages - Tooltip",
|
||||
"Last name": "Last name",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "App's image tag",
|
||||
@ -290,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",
|
||||
@ -478,6 +491,8 @@
|
||||
"Email Content - Tooltip": "Unique string-style identifier",
|
||||
"Email Title": "Titre de l'e-mail",
|
||||
"Email Title - Tooltip": "Unique string-style identifier",
|
||||
"Enable QR code": "Enable QR code",
|
||||
"Enable QR code - Tooltip": "Enable QR code - Tooltip",
|
||||
"Endpoint": "Endpoint",
|
||||
"Endpoint (Intranet)": "Point de terminaison (Intranet)",
|
||||
"Host": "Hôte",
|
||||
@ -496,6 +511,7 @@
|
||||
"Parse": "Parse",
|
||||
"Parse Metadata successfully": "Analyse des métadonnées réussie",
|
||||
"Path prefix": "Path prefix",
|
||||
"Please use WeChat and scan the QR code to sign in": "Please use WeChat and scan the QR code to sign in",
|
||||
"Port": "Port",
|
||||
"Port - Tooltip": "Unique string-style identifier",
|
||||
"Prompted": "Prompted",
|
||||
@ -550,6 +566,7 @@
|
||||
"UserInfo URL": "UserInfo URL",
|
||||
"UserInfo URL - Tooltip": "UserInfo URL - Tooltip",
|
||||
"Visible": "Visible",
|
||||
"admin (share)": "admin (share)",
|
||||
"alertType": "Type d'alerte"
|
||||
},
|
||||
"record": {
|
||||
|
@ -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": "ビットサイズ",
|
||||
@ -170,6 +180,8 @@
|
||||
"Is enabled - Tooltip": "有効にする - ツールチップ",
|
||||
"LDAPs": "LDAP",
|
||||
"LDAPs - Tooltip": "LDAP - ツールチップ",
|
||||
"Languages": "Languages",
|
||||
"Languages - Tooltip": "Languages - Tooltip",
|
||||
"Last name": "Last name",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "App's image tag",
|
||||
@ -290,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",
|
||||
@ -478,6 +491,8 @@
|
||||
"Email Content - Tooltip": "Unique string-style identifier",
|
||||
"Email Title": "メールタイトル",
|
||||
"Email Title - Tooltip": "Unique string-style identifier",
|
||||
"Enable QR code": "Enable QR code",
|
||||
"Enable QR code - Tooltip": "Enable QR code - Tooltip",
|
||||
"Endpoint": "Endpoint",
|
||||
"Endpoint (Intranet)": "エンドポイント (イントラネット)",
|
||||
"Host": "ホスト",
|
||||
@ -496,6 +511,7 @@
|
||||
"Parse": "Parse",
|
||||
"Parse Metadata successfully": "メタデータの解析に成功",
|
||||
"Path prefix": "Path prefix",
|
||||
"Please use WeChat and scan the QR code to sign in": "Please use WeChat and scan the QR code to sign in",
|
||||
"Port": "ポート",
|
||||
"Port - Tooltip": "Unique string-style identifier",
|
||||
"Prompted": "Prompted",
|
||||
@ -550,6 +566,7 @@
|
||||
"UserInfo URL": "UserInfo URL",
|
||||
"UserInfo URL - Tooltip": "UserInfo URL - Tooltip",
|
||||
"Visible": "Visible",
|
||||
"admin (share)": "admin (share)",
|
||||
"alertType": "alertType"
|
||||
},
|
||||
"record": {
|
||||
|
@ -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",
|
||||
@ -170,6 +180,8 @@
|
||||
"Is enabled - Tooltip": "Is enabled - Tooltip",
|
||||
"LDAPs": "LDAPs",
|
||||
"LDAPs - Tooltip": "LDAPs - Tooltip",
|
||||
"Languages": "Languages",
|
||||
"Languages - Tooltip": "Languages - Tooltip",
|
||||
"Last name": "Last name",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "App's image tag",
|
||||
@ -290,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",
|
||||
@ -478,6 +491,8 @@
|
||||
"Email Content - Tooltip": "Unique string-style identifier",
|
||||
"Email Title": "Email title",
|
||||
"Email Title - Tooltip": "Unique string-style identifier",
|
||||
"Enable QR code": "Enable QR code",
|
||||
"Enable QR code - Tooltip": "Enable QR code - Tooltip",
|
||||
"Endpoint": "Endpoint",
|
||||
"Endpoint (Intranet)": "Endpoint (Intranet)",
|
||||
"Host": "Host",
|
||||
@ -496,6 +511,7 @@
|
||||
"Parse": "Parse",
|
||||
"Parse Metadata successfully": "Parse Metadata successfully",
|
||||
"Path prefix": "Path prefix",
|
||||
"Please use WeChat and scan the QR code to sign in": "Please use WeChat and scan the QR code to sign in",
|
||||
"Port": "Port",
|
||||
"Port - Tooltip": "Unique string-style identifier",
|
||||
"Prompted": "Prompted",
|
||||
@ -550,6 +566,7 @@
|
||||
"UserInfo URL": "UserInfo URL",
|
||||
"UserInfo URL - Tooltip": "UserInfo URL - Tooltip",
|
||||
"Visible": "Visible",
|
||||
"admin (share)": "admin (share)",
|
||||
"alertType": "alertType"
|
||||
},
|
||||
"record": {
|
||||
|
@ -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": "Размер бита",
|
||||
@ -170,6 +180,8 @@
|
||||
"Is enabled - Tooltip": "Включено - Подсказка",
|
||||
"LDAPs": "LDAPы",
|
||||
"LDAPs - Tooltip": "LDAPs - Подсказки",
|
||||
"Languages": "Languages",
|
||||
"Languages - Tooltip": "Languages - Tooltip",
|
||||
"Last name": "Фамилия",
|
||||
"Logo": "Логотип",
|
||||
"Logo - Tooltip": "App's image tag",
|
||||
@ -290,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",
|
||||
@ -478,6 +491,8 @@
|
||||
"Email Content - Tooltip": "Unique string-style identifier",
|
||||
"Email Title": "Заголовок письма",
|
||||
"Email Title - Tooltip": "Unique string-style identifier",
|
||||
"Enable QR code": "Enable QR code",
|
||||
"Enable QR code - Tooltip": "Enable QR code - Tooltip",
|
||||
"Endpoint": "Endpoint",
|
||||
"Endpoint (Intranet)": "Точка входа (Intranet)",
|
||||
"Host": "Хост",
|
||||
@ -496,6 +511,7 @@
|
||||
"Parse": "Parse",
|
||||
"Parse Metadata successfully": "Анализ метаданных успешно завершен",
|
||||
"Path prefix": "Path prefix",
|
||||
"Please use WeChat and scan the QR code to sign in": "Please use WeChat and scan the QR code to sign in",
|
||||
"Port": "Порт",
|
||||
"Port - Tooltip": "Unique string-style identifier",
|
||||
"Prompted": "Prompted",
|
||||
@ -550,6 +566,7 @@
|
||||
"UserInfo URL": "UserInfo URL",
|
||||
"UserInfo URL - Tooltip": "UserInfo URL - Tooltip",
|
||||
"Visible": "Visible",
|
||||
"admin (share)": "admin (share)",
|
||||
"alertType": "тип оповещения"
|
||||
},
|
||||
"record": {
|
||||
|
@ -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": "位大小",
|
||||
@ -170,6 +180,8 @@
|
||||
"Is enabled - Tooltip": "是否启用",
|
||||
"LDAPs": "LDAP",
|
||||
"LDAPs - Tooltip": "LDAPs",
|
||||
"Languages": "语言",
|
||||
"Languages - Tooltip": "可选语言",
|
||||
"Last name": "姓氏",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "应用程序向外展示的图标",
|
||||
@ -290,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": "验证码",
|
||||
@ -478,6 +491,8 @@
|
||||
"Email Content - Tooltip": "邮件内容",
|
||||
"Email Title": "邮件标题",
|
||||
"Email Title - Tooltip": "邮件标题",
|
||||
"Enable QR code": "扫码登陆",
|
||||
"Enable QR code - Tooltip": "扫码登陆 - 工具提示",
|
||||
"Endpoint": "地域节点 (外网)",
|
||||
"Endpoint (Intranet)": "地域节点 (内网)",
|
||||
"Host": "主机",
|
||||
@ -496,6 +511,7 @@
|
||||
"Parse": "Parse",
|
||||
"Parse Metadata successfully": "解析元数据成功",
|
||||
"Path prefix": "路径前缀",
|
||||
"Please use WeChat and scan the QR code to sign in": "请使用微信扫描二维码登录",
|
||||
"Port": "端口",
|
||||
"Port - Tooltip": "端口号",
|
||||
"Prompted": "注册后提醒绑定",
|
||||
@ -550,6 +566,7 @@
|
||||
"UserInfo URL": "UserInfo URL",
|
||||
"UserInfo URL - Tooltip": "UserInfo URL - 工具提示",
|
||||
"Visible": "是否可见",
|
||||
"admin (share)": "admin (share)",
|
||||
"alertType": "警报类型"
|
||||
},
|
||||
"record": {
|
||||
|
Reference in New Issue
Block a user