Compare commits

...

37 Commits

Author SHA1 Message Date
e207fd243b feat: fix CSS issue that login error pages are not centered (#1371) 2022-12-06 14:00:17 +08:00
30b7fd963f Reduce Resource key size 2022-12-06 11:30:42 +08:00
ca314bbfb5 feat: refactor layout and fix footer CSS (#1370) 2022-12-06 00:50:17 +08:00
812c44e070 feat: add and load policy within a specific permission (#1357)
* fix: add and load policy with a specific permission

* fix: use a clear variable name
2022-12-05 17:07:10 +08:00
78e45d07cf fix: support RBAC With Domains/Tenants (#1333)
* feat: support RBAC With Domains/Tenants

* fix: add verify for `UpdatePermission`

* Update permission.go

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2022-12-05 16:08:17 +08:00
0856977b92 feat: update to antd 5.0 (#1362)
* feat: update to ant5.X

* fix: incompatible styles

* fix: adjust the style
2022-12-04 23:05:30 +08:00
a44a4b0300 feat: fix React incorrect usage to fix issue that verification code must be submitted twice to succeed (#1348)
* fix: synchronized user login fields saving

* fix: synchronized user login fields saving

* recover changes

* fix: save username in step 2

* fix: format
2022-12-04 20:58:07 +08:00
4b29dd8c41 fix: responsive certs page editor (#1360)
* fix: responsive editor width

* fix: format
2022-12-04 16:04:04 +08:00
165e2e33e3 fix: disable formcss in mobile (#1359) 2022-12-04 15:53:46 +08:00
d13a307ad5 Allow org admin to access GetResources() 2022-12-03 01:10:45 +08:00
27bd771fed feat: handle the dataSourceName when DB changes (#1352)
* fix: handle the dataSourceName when DB changes

* reduce duplication of code
2022-12-02 22:20:18 +08:00
9f3ee275a8 feat: reformat frontend alert texts with correct i18n (#1341)
* fix: add i18

* fix: standard prompt message
2022-12-02 00:06:28 +08:00
fcda64ad7d fix: provider sort alphabetical order (#1347) 2022-12-01 22:51:10 +08:00
d815bf92bd fix: handle add message in frontend (#1340) 2022-11-29 20:32:47 +08:00
7867060b71 feat: add quota limitation to organizations, users, providers and applications (#1339) 2022-11-29 11:01:41 +08:00
8890d1d7c7 fix: check credential existence when signing via WebAuthn (#1336)
* fix: check credential existence when signing via WebAuthn

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

* fix: decompose elements

* fix: decomposition

* fix: remove space component

* Update ApplicationEditPage.js

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

* fix review problems

* Update verification.go

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

* add multiple translations

* fix: translation consistency

* fix: translation consistency

* fix: add translation

* fix: add translation

* Update data.json

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

* Update resource.go

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

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

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

* Update UserEditPage.js

* Update UserEditPage.js

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2022-11-25 23:08:45 +08:00
b10fb97c92 feat: finish policy list management (#1317) 2022-11-25 16:02:20 +08:00
b337b908ea feat: fix the bug that admin cannot upload avatar for other users (#1323) 2022-11-25 09:36:47 +08:00
ba9d1e2388 fix: fix bug in GetAcceptLanguage() (#1322) 2022-11-24 20:43:35 +08:00
29ec1d2d9c feat: update Xorm to v1.0.5 to fix the PostgreSQL bug in Xorm (#1321)
* fix:update xorm version

* fix pr problems

* update xorm version to 1.2.0

* update xorm version to 1.0.5

* fix pr problems

* generate go.sum file
2022-11-24 19:28:51 +08:00
84a03f6c8e feat: add webhook for add/update org/provider (#1316) 2022-11-24 00:29:15 +08:00
56ff06bbea feat: add parameter v0 for Casbin APIs (#1315) 2022-11-23 22:39:17 +08:00
7e756b8ee2 feat: manager applications in organization scope (#1290)
* feat: manager applications in organization scope(front end)

* fix: application can use own organization and admin provider

* fix: improve methed to get provider

* fix: modify provider methods by convention
2022-11-21 01:17:55 +08:00
19ba37e0c2 feat: can specify available UI languages for an organization (#1306) 2022-11-19 22:11:19 +08:00
b98ce19211 feat: fix bug in GetDefaultApplication() that caused login error for other orgs (#1299)
* fix:fix bug in GetDefaultApplication

* fix:fix bug in GetDefaultApplication
2022-11-16 00:39:05 +08:00
37d1a73c0c feat: encode redirectUri (#1297) 2022-11-15 19:05:59 +08:00
727877cf54 fix: illegal user when new a permission (#1298) 2022-11-15 14:19:20 +08:00
939b416717 fix: limit only under PC can login by following Wechat Official Account (#1293) 2022-11-14 09:03:55 +08:00
f115843fbb feat: fix verification code send time's limit logic (#1292) 2022-11-13 22:00:48 +08:00
aa6a4dc74f feat: support login by following wechat official account (#1284)
* show QRcode when click WeChat Icon

* update how to show qrcode

* handle wechat scan qrcode

* fix api problems

* fix url problems

* fix problems

* modify get frequency

* remove useless print

* fix:fix PR problems

* fix: fix PR problems

* fix:fix PR problem

* fix IMG load delay problems

* fix:fix provider problems

* fix test problems

* use gofumpt to fmt code

* fix:delete useless variables

* feat:add button for follow official account

* fix:fix review problems

* use gofumpt to fmt code

* fix:fix scantype problems

* fix Response problem

* use gofumpt to format code
2022-11-13 15:05:15 +08:00
123 changed files with 2561 additions and 1232 deletions

View File

@ -32,7 +32,9 @@ func InitAuthz() {
var err error
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
a, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName()+conf.GetConfigString("dbName"), "casbin_rule", tableNamePrefix, true)
driverName := conf.GetConfigString("driverName")
dataSourceName := conf.GetConfigRealDataSourceName(driverName)
a, err := xormadapter.NewAdapterWithTableName(driverName, dataSourceName, "casbin_rule", tableNamePrefix, true)
if err != nil {
panic(err)
}
@ -85,6 +87,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, *, *

View File

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

66
captcha/turnstile.go Normal file
View File

@ -0,0 +1,66 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package captcha
import (
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"strings"
)
const CloudflareTurnstileVerifyUrl = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
type CloudflareTurnstileProvider struct{}
func NewCloudflareTurnstileProvider() *CloudflareTurnstileProvider {
captcha := &CloudflareTurnstileProvider{}
return captcha
}
func (captcha *CloudflareTurnstileProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
reqData := url.Values{
"secret": {clientSecret},
"response": {token},
}
resp, err := http.PostForm(CloudflareTurnstileVerifyUrl, reqData)
if err != nil {
return false, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, err
}
type captchaResponse struct {
Success bool `json:"success"`
ErrorCodes []string `json:"error-codes"`
}
captchaResp := &captchaResponse{}
err = json.Unmarshal(body, captchaResp)
if err != nil {
return false, err
}
if len(captchaResp.ErrorCodes) > 0 {
return false, errors.New(strings.Join(captchaResp.ErrorCodes, ","))
}
return captchaResp.Success, nil
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
}

View File

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

View File

@ -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()
}

View File

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

View File

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

View File

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

View File

@ -40,6 +40,15 @@ func (c *ApiController) GetResources() {
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
userObj, ok := c.RequireSignedInUser()
if !ok {
return
}
if userObj.IsAdmin {
user = ""
}
if limit == "" || page == "" {
c.Data["json"] = object.GetResources(owner, user)
c.ServeJSON()
@ -156,7 +165,7 @@ func (c *ApiController) UploadResource() {
return
}
provider, user, ok := c.GetProviderFromContext("Storage")
provider, _, ok := c.GetProviderFromContext("Storage")
if !ok {
return
}
@ -171,6 +180,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 +225,10 @@ func (c *ApiController) UploadResource() {
switch tag {
case "avatar":
user := object.GetUserNoCheck(util.GetId(owner, username))
if user == nil {
user = object.GetUserNoCheck(username)
if user == nil {
c.ResponseError(c.T("ResourceErr.UserIsNil"))
return
}
c.ResponseError(c.T("ResourceErr.UserIsNil"))
return
}
user.Avatar = fileUrl

View File

@ -60,7 +60,7 @@ func (c *ApiController) SendEmail() {
var provider *object.Provider
if emailForm.Provider != "" {
// called by frontend's TestEmailWidget, provider name is set by frontend
provider = object.GetProvider(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

View File

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

View File

@ -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
@ -152,3 +153,47 @@ func (c *ApiController) GetProviderFromContext(category string) (*object.Provide
return provider, user, true
}
func checkQuotaForApplication(count int) error {
quota := conf.GetConfigQuota().Application
if quota == -1 {
return nil
}
if count >= quota {
return fmt.Errorf("application quota is exceeded")
}
return nil
}
func checkQuotaForOrganization(count int) error {
quota := conf.GetConfigQuota().Organization
if quota == -1 {
return nil
}
if count >= quota {
return fmt.Errorf("organization quota is exceeded")
}
return nil
}
func checkQuotaForProvider(count int) error {
quota := conf.GetConfigQuota().Provider
if quota == -1 {
return nil
}
if count >= quota {
return fmt.Errorf("provider quota is exceeded")
}
return nil
}
func checkQuotaForUser(count int) error {
quota := conf.GetConfigQuota().User
if quota == -1 {
return nil
}
if count >= quota {
return fmt.Errorf("user quota is exceeded")
}
return nil
}

View File

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

View File

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

View File

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

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

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

View File

@ -83,7 +83,7 @@ func parseToData() *I18nData {
data := I18nData{}
for _, word := range allWords {
tokens := strings.Split(word, ":")
tokens := strings.SplitN(word, ":", 2)
namespace := tokens[0]
key := tokens[1]

View File

@ -134,4 +134,5 @@ NameTooLang = Username is too long (maximum is 39 characters).
NameFormatErr = The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.
PasswordLessThanSixCharacters = Password must have at least 6 characters
InvalidInformation = Invalid information
NoWebAuthnCredential = Found no credentials for this user

View File

@ -134,4 +134,5 @@ NameTooLang = Username is too long (maximum is 39 characters).
NameFormatErr = The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.
PasswordLessThanSixCharacters = Password must have at least 6 characters
InvalidInformation = Invalid information
NoWebAuthnCredential = Found no credentials for this user

View File

@ -134,4 +134,5 @@ NameTooLang = Username is too long (maximum is 39 characters).
NameFormatErr = The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.
PasswordLessThanSixCharacters = Password must have at least 6 characters
InvalidInformation = Invalid information
NoWebAuthnCredential = Found no credentials for this user

View File

@ -134,4 +134,5 @@ NameTooLang = Username is too long (maximum is 39 characters).
NameFormatErr = The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.
PasswordLessThanSixCharacters = Password must have at least 6 characters
InvalidInformation = Invalid information
NoWebAuthnCredential = Found no credentials for this user

View File

@ -134,4 +134,5 @@ NameTooLang = Username is too long (maximum is 39 characters).
NameFormatErr = The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.
PasswordLessThanSixCharacters = Password must have at least 6 characters
InvalidInformation = Invalid information
NoWebAuthnCredential = Found no credentials for this user

View File

@ -134,4 +134,5 @@ NameTooLang = Username is too long (maximum is 39 characters).
NameFormatErr = The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.
PasswordLessThanSixCharacters = Password must have at least 6 characters
InvalidInformation = Invalid information
NoWebAuthnCredential = Found no credentials for this user

View File

@ -134,4 +134,5 @@ NameTooLang = Username is too long (maximum is 39 characters).
NameFormatErr = The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.
PasswordLessThanSixCharacters = Password must have at least 6 characters
InvalidInformation = Invalid information
NoWebAuthnCredential = Found no credentials for this user

View File

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

View File

@ -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()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"},
@ -157,7 +158,7 @@ func initBuiltInApplication() {
},
RedirectUris: []string{},
ExpireInHours: 168,
FormOffset: 8,
FormOffset: 2,
}
AddApplication(application)
}
@ -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
}

View File

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

View File

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

View File

@ -111,7 +111,27 @@ func GetPermission(id string) *Permission {
return getPermission(owner, name)
}
// checkPermissionValid verifies if the permission is valid
func checkPermissionValid(permission *Permission) {
enforcer := getEnforcer(permission)
enforcer.EnableAutoSave(false)
policies, groupingPolicies := getPolicies(permission)
if len(groupingPolicies) > 0 {
_, err := enforcer.AddGroupingPolicies(groupingPolicies)
if err != nil {
panic(err)
}
}
_, err := enforcer.AddPolicies(policies)
if err != nil {
panic(err)
}
}
func UpdatePermission(id string, permission *Permission) bool {
checkPermissionValid(permission)
owner, name := util.GetOwnerAndNameFromId(id)
oldPermission := getPermission(owner, name)
if oldPermission == nil {

View File

@ -29,7 +29,9 @@ func getEnforcer(permission *Permission) *casbin.Enforcer {
tableName = permission.Adapter
}
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
adapter, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName()+conf.GetConfigString("dbName"), tableName, tableNamePrefix, true)
driverName := conf.GetConfigString("driverName")
dataSourceName := conf.GetConfigRealDataSourceName(driverName)
adapter, err := xormadapter.NewAdapterWithTableName(driverName, dataSourceName, tableName, tableNamePrefix, true)
if err != nil {
panic(err)
}
@ -63,22 +65,27 @@ m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act`
panic(err)
}
// load Policy with a specific Permission
enforcer.LoadFilteredPolicy(xormadapter.Filter{
V5: []string{permission.Owner + "/" + permission.Name},
})
return enforcer
}
func getPolicies(permission *Permission) ([][]string, [][]string) {
var policies [][]string
var groupingPolicies [][]string
permissionId := permission.Owner + "/" + permission.Name
domainExist := len(permission.Domains) > 0
for _, user := range permission.Users {
for _, resource := range permission.Resources {
for _, action := range permission.Actions {
if domainExist {
for _, domain := range permission.Domains {
policies = append(policies, []string{user, domain, resource, strings.ToLower(action)})
policies = append(policies, []string{user, domain, resource, strings.ToLower(action), "", permissionId})
}
} else {
policies = append(policies, []string{user, resource, strings.ToLower(action)})
policies = append(policies, []string{user, resource, strings.ToLower(action), "", "", permissionId})
}
}
}
@ -88,29 +95,29 @@ func getPolicies(permission *Permission) ([][]string, [][]string) {
for _, subUser := range roleObj.Users {
if domainExist {
for _, domain := range permission.Domains {
groupingPolicies = append(groupingPolicies, []string{subUser, domain, role})
groupingPolicies = append(groupingPolicies, []string{subUser, domain, role, "", "", permissionId})
}
} else {
groupingPolicies = append(groupingPolicies, []string{subUser, role})
groupingPolicies = append(groupingPolicies, []string{subUser, role, "", "", "", permissionId})
}
}
for _, subRole := range roleObj.Roles {
if domainExist {
for _, domain := range permission.Domains {
groupingPolicies = append(groupingPolicies, []string{subRole, domain, role})
groupingPolicies = append(groupingPolicies, []string{subRole, domain, role, "", "", permissionId})
}
} else {
groupingPolicies = append(groupingPolicies, []string{subRole, role})
groupingPolicies = append(groupingPolicies, []string{subRole, role, "", "", "", permissionId})
}
}
for _, resource := range permission.Resources {
for _, action := range permission.Actions {
if domainExist {
for _, domain := range permission.Domains {
policies = append(policies, []string{role, domain, resource, strings.ToLower(action)})
policies = append(policies, []string{role, domain, resource, strings.ToLower(action), "", permissionId})
}
} else {
policies = append(policies, []string{role, resource, strings.ToLower(action)})
policies = append(policies, []string{role, resource, strings.ToLower(action), "", "", permissionId})
}
}
}
@ -152,20 +159,29 @@ 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)
request := []interface{}{permissionRule.V0, permissionRule.V1, permissionRule.V2}
if permissionRule.V3 != "" {
request = append(request, permissionRule.V3)
}
allow, err := enforcer.Enforce(request...)
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})
if permissionRule.V3 != "" {
requests = append(requests, []interface{}{permissionRule.V0, permissionRule.V1, permissionRule.V2, permissionRule.V3})
} else {
requests = append(requests, []interface{}{permissionRule.V0, permissionRule.V1, permissionRule.V2})
}
}
permission := GetPermission(permissionRules[0].Id)
enforcer := getEnforcer(permission)

View File

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

View File

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

View File

@ -23,7 +23,7 @@ import (
type Resource struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(250) notnull pk" json:"name"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
User string `xorm:"varchar(100)" json:"user"`
@ -31,12 +31,12 @@ type Resource struct {
Application string `xorm:"varchar(100)" json:"application"`
Tag string `xorm:"varchar(100)" json:"tag"`
Parent string `xorm:"varchar(100)" json:"parent"`
FileName string `xorm:"varchar(1000)" json:"fileName"`
FileName string `xorm:"varchar(255)" json:"fileName"`
FileType string `xorm:"varchar(100)" json:"fileType"`
FileFormat string `xorm:"varchar(100)" json:"fileFormat"`
FileSize int `json:"fileSize"`
Url string `xorm:"varchar(1000)" json:"url"`
Description string `xorm:"varchar(1000)" json:"description"`
Url string `xorm:"varchar(255)" json:"url"`
Description string `xorm:"varchar(255)" json:"description"`
}
func GetResourceCount(owner, user, field, value string) int {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

29
util/struct.go Normal file
View File

@ -0,0 +1,29 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util
import xormadapter "github.com/casbin/xorm-adapter/v3"
func CasbinToSlice(casbinRule xormadapter.CasbinRule) []string {
s := []string{
casbinRule.V0,
casbinRule.V1,
casbinRule.V2,
casbinRule.V3,
casbinRule.V4,
casbinRule.V5,
}
return s
}

View File

@ -9,7 +9,7 @@
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"antd": "^4.22.8",
"antd": "5.0.3",
"codemirror": "^5.61.1",
"copy-to-clipboard": "^3.3.1",
"core-js": "^3.25.0",

View File

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

View File

@ -45,26 +45,33 @@ 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"});
}
)
if (res.status === "ok") {
this.props.history.push({pathname: `/adapters/${newAdapter.organization}/${newAdapter.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Adapter failed to add: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteAdapter(i) {
AdapterBackend.deleteAdapter(this.state.data[i])
.then((res) => {
Setting.showMessage("success", "Adapter deleted successfully");
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Adapter failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
@ -201,7 +208,7 @@ class AdapterListPage extends BaseListPage {
title={`Sure to delete adapter: ${record.name} ?`}
onConfirm={() => this.deleteAdapter(index)}
>
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
<Button style={{marginBottom: "10px"}} type="primary" danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

@ -17,7 +17,7 @@ import "./App.less";
import {Helmet} from "react-helmet";
import * as Setting from "./Setting";
import {BarsOutlined, DownOutlined, LogoutOutlined, SettingOutlined} from "@ant-design/icons";
import {Avatar, BackTop, Button, Card, Drawer, Dropdown, Layout, Menu, Result} from "antd";
import {Avatar, Button, Card, ConfigProvider, Drawer, Dropdown, FloatButton, Layout, Menu, Result} from "antd";
import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom";
import OrganizationListPage from "./OrganizationListPage";
import OrganizationEditPage from "./OrganizationEditPage";
@ -76,7 +76,7 @@ import AdapterListPage from "./AdapterListPage";
import AdapterEditPage from "./AdapterEditPage";
import {withTranslation} from "react-i18next";
const {Header, Footer} = Layout;
const {Header, Footer, Content} = Layout;
class App extends Component {
constructor(props) {
@ -295,23 +295,16 @@ class App extends Component {
}
renderRightDropdown() {
const menu = (
<Menu onClick={this.handleRightDropdownClick.bind(this)}>
<Menu.Item key="/account">
<SettingOutlined />
&nbsp;&nbsp;
{i18next.t("account:My Account")}
</Menu.Item>
<Menu.Item key="/logout">
<LogoutOutlined />
&nbsp;&nbsp;
{i18next.t("account:Logout")}
</Menu.Item>
</Menu>
);
const items = [];
items.push(Setting.getItem(<><SettingOutlined />&nbsp;&nbsp;{i18next.t("account:My Account")}</>,
"/account"
));
items.push(Setting.getItem(<><LogoutOutlined />&nbsp;&nbsp;{i18next.t("account:Logout")}</>,
"/logout"));
const onClick = this.handleRightDropdownClick.bind(this);
return (
<Dropdown key="/rightDropDown" overlay={menu} className="rightDropDown">
<Dropdown key="/rightDropDown" menu={{items, onClick}} className="rightDropDown">
<div className="ant-dropdown-link" style={{float: "right", cursor: "pointer"}}>
&nbsp;
&nbsp;
@ -363,156 +356,89 @@ class App extends Component {
return [];
}
res.push(
<Menu.Item key="/">
<Link to="/">
{i18next.t("general:Home")}
</Link>
</Menu.Item>
);
res.push(Setting.getItem(<Link to="/">{i18next.t("general:Home")}</Link>, "/"));
if (Setting.isAdminUser(this.state.account)) {
res.push(
<Menu.Item key="/organizations">
<Link to="/organizations">
{i18next.t("general:Organizations")}
</Link>
</Menu.Item>
);
res.push(Setting.getItem(<Link to="/organizations">{i18next.t("general:Organizations")}</Link>,
"/organizations"));
}
if (Setting.isLocalAdminUser(this.state.account)) {
res.push(
<Menu.Item key="/users">
<Link to="/users">
{i18next.t("general:Users")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/roles">
<Link to="/roles">
{i18next.t("general:Roles")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/permissions">
<Link to="/permissions">
{i18next.t("general:Permissions")}
</Link>
</Menu.Item>
);
res.push(Setting.getItem(<Link to="/users">{i18next.t("general:Users")}</Link>,
"/users"
));
res.push(Setting.getItem(<Link to="/roles">{i18next.t("general:Roles")}</Link>,
"/roles"
));
res.push(Setting.getItem(<Link to="/permissions">{i18next.t("general:Permissions")}</Link>,
"/permissions"
));
}
if (Setting.isAdminUser(this.state.account)) {
res.push(
<Menu.Item key="/models">
<Link to="/models">
{i18next.t("general:Models")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/adapters">
<Link to="/adapters">
{i18next.t("general:Adapters")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/applications">
<Link to="/applications">
{i18next.t("general:Applications")}
</Link>
</Menu.Item>
);
res.push(Setting.getItem(<Link to="/models">{i18next.t("general:Models")}</Link>,
"/models"
));
res.push(Setting.getItem(<Link to="/adapters">{i18next.t("general:Adapters")}</Link>,
"/adapters"
));
}
if (Setting.isLocalAdminUser(this.state.account)) {
res.push(
<Menu.Item key="/providers">
<Link to="/providers">
{i18next.t("general:Providers")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/resources">
<Link to="/resources">
{i18next.t("general:Resources")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/records">
<Link to="/records">
{i18next.t("general:Records")}
</Link>
</Menu.Item>
);
res.push(Setting.getItem(<Link to="/applications">{i18next.t("general:Applications")}</Link>,
"/applications"
));
res.push(Setting.getItem(<Link to="/providers">{i18next.t("general:Providers")}</Link>,
"/providers"
));
res.push(Setting.getItem(<Link to="/resources">{i18next.t("general:Resources")}</Link>,
"/resources"
));
res.push(Setting.getItem(<Link to="/records">{i18next.t("general:Records")}</Link>,
"/records"
));
}
if (Setting.isAdminUser(this.state.account)) {
res.push(
<Menu.Item key="/tokens">
<Link to="/tokens">
{i18next.t("general:Tokens")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/webhooks">
<Link to="/webhooks">
{i18next.t("general:Webhooks")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/syncers">
<Link to="/syncers">
{i18next.t("general:Syncers")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/certs">
<Link to="/certs">
{i18next.t("general:Certs")}
</Link>
</Menu.Item>
);
res.push(Setting.getItem(<Link to="/tokens">{i18next.t("general:Tokens")}</Link>,
"/tokens"
));
res.push(Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>,
"/webhooks"
));
res.push(Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>,
"/syncers"
));
res.push(Setting.getItem(<Link to="/certs">{i18next.t("general:Certs")}</Link>,
"/certs"
));
if (Conf.EnableExtraPages) {
res.push(
<Menu.Item key="/products">
<Link to="/products">
{i18next.t("general:Products")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/payments">
<Link to="/payments">
{i18next.t("general:Payments")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/sysinfo">
<Link to="/sysinfo">
{i18next.t("general:SysInfo")}
</Link>
</Menu.Item>
);
res.push(Setting.getItem(<Link to="/products">{i18next.t("general:Products")}</Link>,
"/products"
));
res.push(Setting.getItem(<Link to="/payments">{i18next.t("general:Payments")}</Link>,
"/payments"
));
res.push(Setting.getItem(<Link to="/sysinfo">{i18next.t("general:SysInfo")}</Link>,
"/sysinfo"
));
}
res.push(
<Menu.Item key="/swagger">
<a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>
{i18next.t("general:Swagger")}
</a>
</Menu.Item>
);
res.push(Setting.getItem(<a target="_blank" rel="noreferrer"
href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>{i18next.t("general:Swagger")}</a>,
"/swagger"
));
}
return res;
@ -565,10 +491,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} />)} />
@ -612,46 +537,45 @@ class App extends Component {
renderContent() {
if (!Setting.isMobile()) {
return (
<div style={{display: "flex", flex: "auto", width: "100%", flexDirection: "column"}}>
<Layout style={{display: "flex", alignItems: "stretch"}}>
<Header style={{padding: "0", marginBottom: "3px"}}>
<Layout id="parent-area">
<Header style={{marginBottom: "3px", paddingInline: 0}}>
{
Setting.isMobile() ? null : (
<Link to={"/"}>
<div className="logo" />
</Link>
)
}
<div>
<Menu
// theme="dark"
items={this.renderMenu()}
mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"}
selectedKeys={[`${this.state.selectedMenuKey}`]}
style={{lineHeight: "64px", position: "absolute", left: "145px", right: "200px"}}
>
</Menu>
{
Setting.isMobile() ? null : (
<Link to={"/"}>
<div className="logo" />
</Link>
)
this.renderAccount()
}
<div>
<Menu
// theme="dark"
mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"}
selectedKeys={[`${this.state.selectedMenuKey}`]}
style={{lineHeight: "64px", position: "absolute", left: "145px", right: "200px"}}
>
{
this.renderMenu()
}
</Menu>
{
this.renderAccount()
}
<SelectLanguageBox />
</div>
</Header>
<Layout style={{backgroundColor: "#f5f5f5", alignItems: "stretch"}}>
<Card className="content-warp-card">
{
this.renderRouter()
}
</Card>
</Layout>
</Layout>
</div>
{this.state.account && <SelectLanguageBox languages={this.state.account.organization.languages} />}
</div>
</Header>
<Content style={{backgroundColor: "#f5f5f5", alignItems: "stretch", display: "flex", flexDirection: "column"}}>
<Card className="content-warp-card">
{
this.renderRouter()
}
</Card>
</Content>
{
this.renderFooter()
}
</Layout>
);
} else {
return (
<div>
<Layout>
<Header style={{padding: "0", marginBottom: "3px"}}>
{
Setting.isMobile() ? null : (
@ -663,14 +587,12 @@ class App extends Component {
<Drawer title={i18next.t("general:Close")} placement="left" visible={this.state.menuVisible} onClose={this.onClose}>
<Menu
// theme="dark"
items={this.renderMenu()}
mode={(Setting.isMobile()) ? "inline" : "horizontal"}
selectedKeys={[`${this.state.selectedMenuKey}`]}
style={{lineHeight: "64px"}}
onClick={this.onClose}
>
{
this.renderMenu()
}
</Menu>
</Drawer>
<Button icon={<BarsOutlined />} onClick={this.showMenu} type="text">
@ -680,13 +602,14 @@ class App extends Component {
{
this.renderAccount()
}
<SelectLanguageBox />
{this.state.account && <SelectLanguageBox languages={this.state.account.organization.languages} />}
</div>
</Header>
{
this.renderRouter()
}
</div>
<Content style={{display: "flex", flexDirection: "column"}} >{
this.renderRouter()}
</Content>
{this.renderFooter()}
</Layout>
);
}
}
@ -723,50 +646,47 @@ class App extends Component {
renderPage() {
if (this.isDoorPages()) {
return (
<div style={{display: "flex", flexDirection: "column", height: "100%"}}>
<div id="login-content-wrap" style={{flexDirection: "column"}}>
<Switch>
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} />)} />
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />)} />
<Route exact path="/login" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage account={this.state.account} {...props} />)} />
<Route exact path="/login/:owner" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage account={this.state.account} {...props} />)} />
<Route exact path="/auto-signup/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signup"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
<Route exact path="/signup/oauth/authorize" render={(props) => <SignupPage account={this.state.account} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
<Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage account={this.state.account} type={"saml"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout clearAccount={() => this.setState({account: null})} {...props} />)} />
<Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage type={"cas"} mode={"signup"} account={this.state.account} {...props} />);}} />
<Route exact path="/callback" component={AuthCallback} />
<Route exact path="/callback/saml" component={SamlCallback} />
<Route exact path="/forget" render={(props) => this.renderHomeIfLoggedIn(<SelfForgetPage {...props} />)} />
<Route exact path="/forget/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ForgetPage {...props} />)} />
<Route exact path="/prompt" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage account={this.state.account} {...props} />)} />
<Route exact path="/prompt/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage account={this.state.account} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} {...props} />)} />
<Route exact path="/sysinfo" render={(props) => this.renderLoginIfNotLoggedIn(<SystemInfo {...props} />)} />
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
</Switch>
</div>
{
this.renderFooter()
}
</div>
<>
<Layout id="parent-area">
<Content style={{display: "flex", justifyContent: "center"}}>
<Switch>
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} />)} />
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />)} />
<Route exact path="/login" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage account={this.state.account} {...props} />)} />
<Route exact path="/login/:owner" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage account={this.state.account} {...props} />)} />
<Route exact path="/auto-signup/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signup"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
<Route exact path="/signup/oauth/authorize" render={(props) => <SignupPage account={this.state.account} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
<Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage account={this.state.account} type={"saml"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout clearAccount={() => this.setState({account: null})} {...props} />)} />
<Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage type={"cas"} mode={"signup"} account={this.state.account} {...props} />);}} />
<Route exact path="/callback" component={AuthCallback} />
<Route exact path="/callback/saml" component={SamlCallback} />
<Route exact path="/forget" render={(props) => this.renderHomeIfLoggedIn(<SelfForgetPage {...props} />)} />
<Route exact path="/forget/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ForgetPage {...props} />)} />
<Route exact path="/prompt" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage account={this.state.account} {...props} />)} />
<Route exact path="/prompt/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage account={this.state.account} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} {...props} />)} />
<Route exact path="/sysinfo" render={(props) => this.renderLoginIfNotLoggedIn(<SystemInfo {...props} />)} />
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
</Switch>
</Content>
{
this.renderFooter()
}
</Layout>
</>
);
}
return (
<div id="parent-area">
<BackTop />
<>
<FloatButton.BackTop />
<CustomGithubCorner />
<div id="content-wrap" style={{flexDirection: "column"}}>
{
this.renderContent()
}
</div>
{
this.renderFooter()
this.renderContent()
}
</div>
</>
);
}
@ -777,9 +697,16 @@ class App extends Component {
<Helmet>
<link rel="icon" href={"https://cdn.casdoor.com/static/favicon.png"} />
</Helmet>
{
this.renderPage()
}
<ConfigProvider theme={{
token: {
colorPrimary: "rgb(89,54,213)",
colorInfo: "rgb(89,54,213)",
},
}}>
{
this.renderPage()
}
</ConfigProvider>
</React.Fragment>
);
}
@ -791,9 +718,16 @@ class App extends Component {
<title>{organization.displayName}</title>
<link rel="icon" href={organization.favicon} />
</Helmet>
{
this.renderPage()
}
<ConfigProvider theme={{
token: {
colorPrimary: "rgb(89,54,213)",
colorInfo: "rgb(89,54,213)",
},
}}>
{
this.renderPage()
}
</ConfigProvider>
</React.Fragment>
);
}

View File

@ -1,6 +1,5 @@
/* stylelint-disable at-rule-name-case */
/* stylelint-disable selector-class-pattern */
@import "~antd/dist/antd.less";
@StaticBaseUrl: "https://cdn.casbin.org";
@ -28,6 +27,11 @@
color: #61dafb;
}
img {
border-style: none;
vertical-align: middle;
}
#root {
height: 100%;
}
@ -40,29 +44,8 @@
background-color: #f5f5f5;
}
#content-wrap {
display: flex;
flex: 1 1 0;
align-items: stretch;
width: 100%;
}
#login-content-wrap {
display: flex;
flex: 1 1 0;
width: 100%;
}
#footer {
bottom: 0;
width: 100%;
height: 70px; /* Footer height */
}
#language-box-corner {
position: absolute;
top: 75px;
right: 0;
.panel-logo {
margin-bottom: 30px;
}
.language-box {
@ -107,6 +90,7 @@
.forget-content {
padding: 10px 100px 20px;
margin: 30px auto;
border: 2px solid #fff;
border-radius: 7px;
background-color: rgb(255 255 255);
@ -137,9 +121,9 @@
}
.loginBackground {
flex: auto;
display: flex;
align-items: center;
flex: 1 1 0;
background: #fff no-repeat;
background-size: 100% 100%;
background-attachment: fixed;

View File

@ -49,6 +49,9 @@ const template = `<style>
}
</style>`;
const previewGrid = Setting.isMobile() ? 22 : 11;
const previewWidth = Setting.isMobile() ? "110%" : "90%";
const sideTemplate = `<style>
.left-model{
text-align: center;
@ -91,7 +94,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 +145,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() {
@ -198,7 +191,7 @@ class ApplicationEditPage extends React.Component {
Setting.showMessage("success", i18next.t("application:File uploaded successfully"));
this.updateApplicationField("termsOfUse", res.data);
} else {
Setting.showMessage("error", res.msg);
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
}
}).finally(() => {
this.setState({uploading: false});
@ -287,7 +280,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>)
}
@ -730,7 +723,7 @@ class ApplicationEditPage extends React.Component {
return (
<React.Fragment>
<Col span={11}>
<Col span={previewGrid}>
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}${signUpUrl}`);
Setting.showMessage("success", i18next.t("application:Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
@ -739,7 +732,7 @@ class ApplicationEditPage extends React.Component {
{i18next.t("application:Copy signup page URL")}
</Button>
<br />
<div style={{position: "relative", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
<div style={{position: "relative", width: previewWidth, border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
{
this.state.application.enablePassword ? (
<SignupPage application={this.state.application} />
@ -750,8 +743,8 @@ class ApplicationEditPage extends React.Component {
<div style={maskStyle} />
</div>
</Col>
<Col span={11}>
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
<Col span={previewGrid}>
<Button style={{marginBottom: "10px", marginTop: Setting.isMobile() ? "15px" : "0"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}${signInUrl}`);
Setting.showMessage("success", i18next.t("application:Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
}}
@ -759,7 +752,7 @@ class ApplicationEditPage extends React.Component {
{i18next.t("application:Copy signin page URL")}
</Button>
<br />
<div style={{position: "relative", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
<div style={{position: "relative", width: previewWidth, border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
<LoginPage type={"login"} mode={"signin"} application={this.state.application} />
<div style={maskStyle} />
</div>
@ -772,7 +765,7 @@ class ApplicationEditPage extends React.Component {
const promptUrl = `/prompt/${this.state.application.name}`;
const maskStyle = {position: "absolute", top: "0px", left: "0px", zIndex: 10, height: "100%", width: "100%", background: "rgba(0,0,0,0.4)"};
return (
<Col span={11}>
<Col span={previewGrid}>
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}${promptUrl}`);
Setting.showMessage("success", i18next.t("application:Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
@ -781,7 +774,7 @@ class ApplicationEditPage extends React.Component {
{i18next.t("application:Copy prompt page URL")}
</Button>
<br />
<div style={{position: "relative", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", flexDirection: "column", flex: "auto"}}>
<div style={{position: "relative", width: previewWidth, border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", flexDirection: "column", flex: "auto"}}>
<PromptPage application={this.state.application} account={this.props.account} />
<div style={maskStyle} />
</div>
@ -791,10 +784,10 @@ 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");
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
applicationName: this.state.application.name,
});
@ -802,25 +795,29 @@ 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);
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateApplicationField("name", this.state.applicationName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteApplication() {
ApplicationBackend.deleteApplication(this.state.application)
.then(() => {
this.props.history.push("/applications");
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/applications");
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Application failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}

View File

@ -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`,
@ -53,7 +70,7 @@ class ApplicationListPage extends BaseListPage {
redirectUris: ["http://localhost:9000/callback"],
tokenFormat: "JWT",
expireInHours: 24 * 7,
formOffset: 8,
formOffset: 2,
};
}
@ -61,26 +78,33 @@ class ApplicationListPage extends BaseListPage {
const newApplication = this.newApplication();
ApplicationBackend.addApplication(newApplication)
.then((res) => {
this.props.history.push({pathname: `/applications/${newApplication.name}`, mode: "add"});
}
)
if (res.status === "ok") {
this.props.history.push({pathname: `/applications/${newApplication.organization}/${newApplication.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Application failed to add: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteApplication(i) {
ApplicationBackend.deleteApplication(this.state.data[i])
.then((res) => {
Setting.showMessage("success", "Application deleted successfully");
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Application failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
@ -96,7 +120,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,13 +237,13 @@ 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)}
disabled={record.name === "app-built-in"}
>
<Button style={{marginBottom: "10px"}} disabled={record.name === "app-built-in"} type="danger">{i18next.t("general:Delete")}</Button>
<Button style={{marginBottom: "10px"}} disabled={record.name === "app-built-in"} type="primary" danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);
@ -254,7 +278,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({

View File

@ -87,7 +87,7 @@ class BaseListPage extends React.Component {
record[dataIndex]
? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase())
: "",
onFilterDropdownVisibleChange: visible => {
onFilterDropdownOpenChange: visible => {
if (visible) {
setTimeout(() => this.searchInput.select(), 100);
}

View File

@ -65,6 +65,7 @@ class CertEditPage extends React.Component {
}
renderCert() {
const editorWidth = Setting.isMobile() ? 22 : 9;
return (
<Card size="small" title={
<div>
@ -166,7 +167,7 @@ class CertEditPage extends React.Component {
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Certificate"), i18next.t("cert:Certificate - Tooltip"))} :
</Col>
<Col span={9} >
<Col span={editorWidth} >
<Button style={{marginRight: "10px", marginBottom: "10px"}} onClick={() => {
copy(this.state.cert.certificate);
Setting.showMessage("success", i18next.t("cert:Certificate copied to clipboard successfully"));
@ -189,7 +190,7 @@ class CertEditPage extends React.Component {
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Private key"), i18next.t("cert:Private key - Tooltip"))} :
</Col>
<Col span={9} >
<Col span={editorWidth} >
<Button style={{marginRight: "10px", marginBottom: "10px"}} onClick={() => {
copy(this.state.cert.privateKey);
Setting.showMessage("success", i18next.t("cert:Private key copied to clipboard successfully"));
@ -217,8 +218,8 @@ class CertEditPage extends React.Component {
const cert = Setting.deepCopy(this.state.cert);
CertBackend.updateCert(this.state.cert.owner, this.state.certName, cert)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", "Successfully saved");
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
certName: this.state.cert.name,
});
@ -229,22 +230,26 @@ class CertEditPage extends React.Component {
this.props.history.push(`/certs/${this.state.cert.name}`);
}
} else {
Setting.showMessage("error", res.msg);
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateCertField("name", this.state.certName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteCert() {
CertBackend.deleteCert(this.state.cert)
.then(() => {
this.props.history.push("/certs");
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/certs");
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Cert failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}

View File

@ -43,26 +43,33 @@ class CertListPage extends BaseListPage {
const newCert = this.newCert();
CertBackend.addCert(newCert)
.then((res) => {
this.props.history.push({pathname: `/certs/${newCert.name}`, mode: "add"});
}
)
if (res.status === "ok") {
this.props.history.push({pathname: `/certs/${newCert.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Cert failed to add: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteCert(i) {
CertBackend.deleteCert(this.state.data[i])
.then((res) => {
Setting.showMessage("success", "Cert deleted successfully");
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Cert failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
@ -165,7 +172,7 @@ class CertListPage extends BaseListPage {
title={`Sure to delete cert: ${record.name} ?`}
onConfirm={() => this.deleteCert(index)}
>
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
<Button style={{marginBottom: "10px"}} type="primary" danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

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

View File

@ -144,7 +144,7 @@ class LdapListPage extends React.Component {
onConfirm={() => this.deleteLdap(index)}
>
<Button style={{marginBottom: "10px"}}
type="danger">{i18next.t("general:Delete")}</Button>
type="primary" danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

@ -81,7 +81,7 @@ class LdapTable extends React.Component {
table = Setting.deleteRow(table, i);
this.updateTable(table);
} else {
Setting.showMessage("error", res.msg);
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
}
}
)
@ -162,7 +162,7 @@ class LdapTable extends React.Component {
onConfirm={() => this.deleteRow(table, index)}
>
<Button style={{marginBottom: "10px"}}
type="danger">{i18next.t("general:Delete")}</Button>
type="primary" danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

@ -158,8 +158,8 @@ class ModelEditPage extends React.Component {
const model = Setting.deepCopy(this.state.model);
ModelBackend.updateModel(this.state.organizationName, this.state.modelName, model)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", "Successfully saved");
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
modelName: this.state.model.name,
});
@ -170,22 +170,26 @@ class ModelEditPage extends React.Component {
this.props.history.push(`/models/${this.state.model.owner}/${this.state.model.name}`);
}
} else {
Setting.showMessage("error", res.msg);
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateModelField("name", this.state.modelName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteModel() {
ModelBackend.deleteModel(this.state.model)
.then(() => {
this.props.history.push("/models");
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/models");
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Model failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}

View File

@ -38,26 +38,33 @@ class ModelListPage extends BaseListPage {
const newModel = this.newModel();
ModelBackend.addModel(newModel)
.then((res) => {
this.props.history.push({pathname: `/models/${newModel.owner}/${newModel.name}`, mode: "add"});
}
)
if (res.status === "ok") {
this.props.history.push({pathname: `/models/${newModel.owner}/${newModel.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Model failed to add: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteModel(i) {
ModelBackend.deleteModel(this.state.data[i])
.then((res) => {
Setting.showMessage("success", "Model deleted successfully");
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Model failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
@ -139,7 +146,7 @@ class ModelListPage extends BaseListPage {
title={`Sure to delete model: ${record.name} ?`}
onConfirm={() => this.deleteModel(index)}
>
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
<Button style={{marginBottom: "10px"}} type="primary" danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

@ -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"))} :
@ -310,8 +335,8 @@ class OrganizationEditPage extends React.Component {
const organization = Setting.deepCopy(this.state.organization);
OrganizationBackend.updateOrganization(this.state.organization.owner, this.state.organizationName, organization)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", "Successfully saved");
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
organizationName: this.state.organization.name,
});
@ -322,22 +347,26 @@ class OrganizationEditPage extends React.Component {
this.props.history.push(`/organizations/${this.state.organization.name}`);
}
} else {
Setting.showMessage("error", res.msg);
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateOrganizationField("name", this.state.organizationName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteOrganization() {
OrganizationBackend.deleteOrganization(this.state.organization)
.then(() => {
this.props.history.push("/organizations");
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/organizations");
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}

View File

@ -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,
@ -74,26 +75,33 @@ class OrganizationListPage extends BaseListPage {
const newOrganization = this.newOrganization();
OrganizationBackend.addOrganization(newOrganization)
.then((res) => {
this.props.history.push({pathname: `/organizations/${newOrganization.name}`, mode: "add"});
}
)
if (res.status === "ok") {
this.props.history.push({pathname: `/organizations/${newOrganization.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Organization failed to add: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteOrganization(i) {
OrganizationBackend.deleteOrganization(this.state.data[i])
.then((res) => {
Setting.showMessage("success", "Organization deleted successfully");
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Organization failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
@ -223,7 +231,7 @@ class OrganizationListPage extends BaseListPage {
onConfirm={() => this.deleteOrganization(index)}
disabled={record.name === "built-in"}
>
<Button style={{marginBottom: "10px"}} disabled={record.name === "built-in"} type="danger">{i18next.t("general:Delete")}</Button>
<Button style={{marginBottom: "10px"}} disabled={record.name === "built-in"} type="primary" danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

@ -64,7 +64,7 @@ export const PasswordModal = (props) => {
<Modal
maskClosable={false}
title={i18next.t("user:Password")}
visible={visible}
open={visible}
okText={i18next.t("user:Set Password")}
cancelText={i18next.t("user:Cancel")}
confirmLoading={confirmLoading}

View File

@ -78,7 +78,7 @@ class PaymentEditPage extends React.Component {
this.setState({
isInvoiceLoading: false,
});
if (res.msg === "") {
if (res.status === "ok") {
Setting.showMessage("success", "Successfully invoiced");
Setting.openLinkSafe(res.data);
this.getPayment();
@ -90,7 +90,7 @@ class PaymentEditPage extends React.Component {
this.setState({
isInvoiceLoading: false,
});
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
@ -117,7 +117,7 @@ class PaymentEditPage extends React.Component {
{" " + i18next.t("payment:Confirm your invoice information")}
</div>
}
visible={this.state.isModalVisible}
open={this.state.isModalVisible}
onOk={handleIssueInvoice}
onCancel={handleCancel}
okText={i18next.t("payment:Issue Invoice")}
@ -443,8 +443,8 @@ class PaymentEditPage extends React.Component {
const payment = Setting.deepCopy(this.state.payment);
PaymentBackend.updatePayment(this.state.payment.owner, this.state.paymentName, payment)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", "Successfully saved");
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
paymentName: this.state.payment.name,
});
@ -455,22 +455,26 @@ class PaymentEditPage extends React.Component {
this.props.history.push(`/payments/${this.state.payment.name}`);
}
} else {
Setting.showMessage("error", res.msg);
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updatePaymentField("name", this.state.paymentName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deletePayment() {
PaymentBackend.deletePayment(this.state.payment)
.then(() => {
this.props.history.push("/payments");
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/payments");
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Payment failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}

View File

@ -51,26 +51,34 @@ class PaymentListPage extends BaseListPage {
const newPayment = this.newPayment();
PaymentBackend.addPayment(newPayment)
.then((res) => {
this.props.history.push({pathname: `/payments/${newPayment.name}`, mode: "add"});
if (res.status === "ok") {
this.props.history.push({pathname: `/payments/${newPayment.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
}
)
.catch(error => {
Setting.showMessage("error", `Payment failed to add: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deletePayment(i) {
PaymentBackend.deletePayment(this.state.data[i])
.then((res) => {
Setting.showMessage("success", "Payment deleted successfully");
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Payment failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
@ -218,7 +226,7 @@ class PaymentListPage extends BaseListPage {
title={`Sure to delete payment: ${record.name} ?`}
onConfirm={() => this.deletePayment(index)}
>
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
<Button style={{marginBottom: "10px"}} type="primary" danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

@ -417,8 +417,8 @@ class PermissionEditPage extends React.Component {
const permission = Setting.deepCopy(this.state.permission);
PermissionBackend.updatePermission(this.state.organizationName, this.state.permissionName, permission)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", "Successfully saved");
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
permissionName: this.state.permission.name,
});
@ -429,22 +429,26 @@ class PermissionEditPage extends React.Component {
this.props.history.push(`/permissions/${this.state.permission.owner}/${this.state.permission.name}`);
}
} else {
Setting.showMessage("error", res.msg);
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updatePermissionField("name", this.state.permissionName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deletePermission() {
PermissionBackend.deletePermission(this.state.permission)
.then(() => {
this.props.history.push("/permissions");
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/permissions");
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Permission failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}

View File

@ -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",
@ -48,30 +48,33 @@ class PermissionListPage extends BaseListPage {
const newPermission = this.newPermission();
PermissionBackend.addPermission(newPermission)
.then((res) => {
if (res.msg !== "") {
Setting.showMessage("error", res.msg);
return;
if (res.status === "ok") {
this.props.history.push({pathname: `/permissions/${newPermission.owner}/${newPermission.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
this.props.history.push({pathname: `/permissions/${newPermission.owner}/${newPermission.name}`, mode: "add"});
}
)
})
.catch(error => {
Setting.showMessage("error", `Permission failed to add: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deletePermission(i) {
PermissionBackend.deletePermission(this.state.data[i])
.then((res) => {
Setting.showMessage("success", "Permission deleted successfully");
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Permission failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
@ -301,7 +304,7 @@ class PermissionListPage extends BaseListPage {
title={`Sure to delete permission: ${record.name} ?`}
onConfirm={() => this.deletePermission(index)}
>
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
<Button style={{marginBottom: "10px"}} type="primary" danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

@ -81,11 +81,11 @@ class ProductBuyPage extends React.Component {
ProductBackend.buyProduct(this.state.product.owner, this.state.productName, provider.name)
.then((res) => {
if (res.msg === "") {
if (res.status === "ok") {
const payUrl = res.data;
Setting.goToLink(payUrl);
} else {
Setting.showMessage("error", res.msg);
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.setState({
isPlacingOrder: false,
@ -93,7 +93,7 @@ class ProductBuyPage extends React.Component {
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}

View File

@ -271,8 +271,8 @@ class ProductEditPage extends React.Component {
const product = Setting.deepCopy(this.state.product);
ProductBackend.updateProduct(this.state.product.owner, this.state.productName, product)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", "Successfully saved");
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
productName: this.state.product.name,
});
@ -283,22 +283,26 @@ class ProductEditPage extends React.Component {
this.props.history.push(`/products/${this.state.product.name}`);
}
} else {
Setting.showMessage("error", res.msg);
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateProductField("name", this.state.productName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteProduct() {
ProductBackend.deleteProduct(this.state.product)
.then(() => {
this.props.history.push("/products");
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/products");
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Product failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}

View File

@ -45,26 +45,33 @@ class ProductListPage extends BaseListPage {
const newProduct = this.newProduct();
ProductBackend.addProduct(newProduct)
.then((res) => {
this.props.history.push({pathname: `/products/${newProduct.name}`, mode: "add"});
}
)
if (res.status === "ok") {
this.props.history.push({pathname: `/products/${newProduct.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Product failed to add: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteProduct(i) {
ProductBackend.deleteProduct(this.state.data[i])
.then((res) => {
Setting.showMessage("success", "Product deleted successfully");
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Product failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
@ -236,7 +243,7 @@ class ProductListPage extends BaseListPage {
title={`Sure to delete product: ${record.name} ?`}
onConfirm={() => this.deleteProduct(index)}
>
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
<Button style={{marginBottom: "10px"}} type="primary" danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

@ -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"))} :
@ -230,7 +257,9 @@ class ProviderEditPage extends React.Component {
{id: "SAML", name: "SAML"},
{id: "Payment", name: "Payment"},
{id: "Captcha", name: "Captcha"},
].map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
]
.sort((a, b) => a.name.localeCompare(b.name))
.map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
}
</Select>
</Col>
@ -253,7 +282,9 @@ class ProviderEditPage extends React.Component {
}
})}>
{
Setting.getProviderTypeOptions(this.state.provider.category).map((providerType, index) => <Option key={index} value={providerType.id}>{providerType.name}</Option>)
Setting.getProviderTypeOptions(this.state.provider.category)
.sort((a, b) => a.name.localeCompare(b.name))
.map((providerType, index) => <Option key={index} value={providerType.id}>{providerType.name}</Option>)
}
</Select>
</Col>
@ -424,6 +455,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,36 +791,41 @@ 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");
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general: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);
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateProviderField("name", this.state.providerName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteProvider() {
ProviderBackend.deleteProvider(this.state.provider)
.then(() => {
this.props.history.push("/providers");
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/providers");
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Provider failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}

View File

@ -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,
@ -61,26 +61,33 @@ class ProviderListPage extends BaseListPage {
const newProvider = this.newProvider();
ProviderBackend.addProvider(newProvider)
.then((res) => {
this.props.history.push({pathname: `/providers/${newProvider.owner}/${newProvider.name}`, mode: "add"});
}
)
if (res.status === "ok") {
this.props.history.push({pathname: `/providers/${newProvider.owner}/${newProvider.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Provider failed to add: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteProvider(i) {
ProviderBackend.deleteProvider(this.state.data[i])
.then((res) => {
Setting.showMessage("success", "Provider deleted successfully");
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Provider failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
@ -96,12 +103,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 +207,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="primary" danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

@ -71,7 +71,7 @@ export const ResetModal = (props) => {
<Modal
maskClosable={false}
title={buttonText}
visible={visible}
open={visible}
okText={buttonText}
cancelText={i18next.t("user:Cancel")}
confirmLoading={confirmLoading}

View File

@ -43,15 +43,18 @@ class ResourceListPage extends BaseListPage {
deleteResource(i) {
ResourceBackend.deleteResource(this.state.data[i])
.then((res) => {
Setting.showMessage("success", "Resource deleted successfully");
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Resource failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
@ -95,7 +98,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>
);
@ -207,16 +210,14 @@ class ResourceListPage extends BaseListPage {
if (record.fileType === "image") {
return (
<a target="_blank" rel="noreferrer" href={record.url}>
<img src={record.url} alt={record.name} width={100} />
<img src={record.url} alt={record.name} width={200} />
</a>
);
} else if (record.fileType === "video") {
return (
<div>
<video width={100} controls>
<source src={text} type="video/mp4" />
</video>
</div>
<video width={200} controls>
<source src={record.url} type="video/mp4" />
</video>
);
}
},
@ -256,7 +257,7 @@ class ResourceListPage extends BaseListPage {
okText={i18next.t("user:OK")}
cancelText={i18next.t("user:Cancel")}
>
<Button type="danger">{i18next.t("general:Delete")}</Button>
<Button type="primary" danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

@ -196,8 +196,8 @@ class RoleEditPage extends React.Component {
const role = Setting.deepCopy(this.state.role);
RoleBackend.updateRole(this.state.organizationName, this.state.roleName, role)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", "Successfully saved");
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
roleName: this.state.role.name,
});
@ -208,22 +208,26 @@ class RoleEditPage extends React.Component {
this.props.history.push(`/roles/${this.state.role.owner}/${this.state.role.name}`);
}
} else {
Setting.showMessage("error", res.msg);
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateRoleField("name", this.state.roleName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteRole() {
RoleBackend.deleteRole(this.state.role)
.then(() => {
this.props.history.push("/roles");
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/roles");
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Role failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}

View File

@ -40,26 +40,33 @@ class RoleListPage extends BaseListPage {
const newRole = this.newRole();
RoleBackend.addRole(newRole)
.then((res) => {
this.props.history.push({pathname: `/roles/${newRole.owner}/${newRole.name}`, mode: "add"});
}
)
if (res.status === "ok") {
this.props.history.push({pathname: `/roles/${newRole.owner}/${newRole.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Role failed to add: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteRole(i) {
RoleBackend.deleteRole(this.state.data[i])
.then((res) => {
Setting.showMessage("success", "Role deleted successfully");
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Role failed to delete: ${error}`);
});
}
@ -172,7 +179,7 @@ class RoleListPage extends BaseListPage {
title={`Sure to delete role: ${record.name} ?`}
onConfirm={() => this.deleteRole(index)}
>
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
<Button style={{marginBottom: "10px"}} type="primary" danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

@ -28,28 +28,41 @@ class SelectLanguageBox extends React.Component {
super(props);
this.state = {
classes: props,
languages: props.languages ?? ["en", "zh", "es", "fr", "de", "ja", "ko", "ru"],
};
}
items = [
Setting.getItem("English", "en", flagIcon("US", "English")),
Setting.getItem("简体中文", "zh", flagIcon("CN", "简体中文")),
Setting.getItem("Español", "es", flagIcon("ES", "Español")),
Setting.getItem("Français", "fr", flagIcon("FR", "Français")),
Setting.getItem("Deutsch", "de", flagIcon("DE", "Deutsch")),
Setting.getItem("日本語", "ja", flagIcon("JP", "日本語")),
Setting.getItem("한국어", "ko", flagIcon("KR", "한국어")),
Setting.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;
}
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>
);
}

View File

@ -145,6 +145,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_geetest.png`,
url: "https://www.geetest.com",
},
"Cloudflare Turnstile": {
logo: `${StaticBaseUrl}/img/social_cloudflare.png`,
url: "https://www.cloudflare.com/products/turnstile/",
},
},
};
@ -416,9 +420,7 @@ export function goToLinkSoft(ths, link) {
}
export function showMessage(type, text) {
if (type === "") {
return;
} else if (type === "success") {
if (type === "success") {
message.success(text);
} else if (type === "error") {
message.error(text);
@ -445,8 +447,8 @@ export function deepCopy(obj) {
return Object.assign({}, obj);
}
export function addRow(array, row) {
return [...array, row];
export function addRow(array, row, position = "end") {
return position === "end" ? [...array, row] : [row, ...array];
}
export function prependRow(array, row) {
@ -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 [];
@ -733,12 +732,12 @@ export function renderLogo(application) {
if (application.homepageUrl !== "") {
return (
<a target="_blank" rel="noreferrer" href={application.homepageUrl}>
<img width={250} src={application.logo} alt={application.displayName} style={{marginBottom: "10px"}} />
<img className="panel-logo" width={250} src={application.logo} alt={application.displayName} />
</a>
);
} else {
return (
<img width={250} src={application.logo} alt={application.displayName} style={{marginBottom: "10px"}} />
<img className="panel-logo" width={250} src={application.logo} alt={application.displayName} />
);
}
}
@ -859,6 +858,15 @@ export function getLabel(text, tooltip) {
);
}
export function getItem(label, key, icon, children, type) {
return {
key,
icon,
children,
label,
type,
};
}
function repeat(str, len) {
while (str.length < len) {
str += str.substr(0, len - str.length);

View File

@ -298,8 +298,8 @@ class SyncerEditPage extends React.Component {
const syncer = Setting.deepCopy(this.state.syncer);
SyncerBackend.updateSyncer(this.state.syncer.owner, this.state.syncerName, syncer)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", "Successfully saved");
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
syncerName: this.state.syncer.name,
});
@ -310,22 +310,26 @@ class SyncerEditPage extends React.Component {
this.props.history.push(`/syncers/${this.state.syncer.name}`);
}
} else {
Setting.showMessage("error", res.msg);
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateSyncerField("name", this.state.syncerName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteSyncer() {
SyncerBackend.deleteSyncer(this.state.syncer)
.then(() => {
this.props.history.push("/syncers");
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/syncers");
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Syncer failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}

View File

@ -49,26 +49,33 @@ class SyncerListPage extends BaseListPage {
const newSyncer = this.newSyncer();
SyncerBackend.addSyncer(newSyncer)
.then((res) => {
this.props.history.push({pathname: `/syncers/${newSyncer.name}`, mode: "add"});
}
)
if (res.status === "ok") {
this.props.history.push({pathname: `/syncers/${newSyncer.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Syncer failed to add: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteSyncer(i) {
SyncerBackend.deleteSyncer(this.state.data[i])
.then((res) => {
Setting.showMessage("success", "Syncer deleted successfully");
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Syncer failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
@ -229,7 +236,7 @@ class SyncerListPage extends BaseListPage {
title={`Sure to delete syncer: ${record.name} ?`}
onConfirm={() => this.deleteSyncer(index)}
>
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
<Button style={{marginBottom: "10px"}} type="primary" danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

@ -13,32 +13,33 @@
// limitations under the License.
import * as Setting from "./Setting";
import i18next from "i18next";
export function sendTestEmail(provider, email) {
testEmailProvider(provider, email)
.then((res) => {
if (res.msg === "") {
if (res.status === "ok") {
Setting.showMessage("success", "Successfully send email");
} else {
Setting.showMessage("error", res.msg);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
export function connectSmtpServer(provider) {
testEmailProvider(provider)
.then((res) => {
if (res.msg === "") {
if (res.status === "ok") {
Setting.showMessage("success", "Successfully connecting smtp server");
} else {
Setting.showMessage("error", res.msg);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}

View File

@ -167,8 +167,8 @@ class TokenEditPage extends React.Component {
const token = Setting.deepCopy(this.state.token);
TokenBackend.updateToken(this.state.token.owner, this.state.tokenName, token)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", "Successfully saved");
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
tokenName: this.state.token.name,
});
@ -179,22 +179,26 @@ class TokenEditPage extends React.Component {
this.props.history.push(`/tokens/${this.state.token.name}`);
}
} else {
Setting.showMessage("error", res.msg);
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateTokenField("name", this.state.tokenName);
}
})
.catch(error => {
Setting.showMessage("error", `failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteToken() {
TokenBackend.deleteToken(this.state.token)
.then(() => {
this.props.history.push("/tokens");
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/tokens");
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Token failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}

View File

@ -42,26 +42,33 @@ class TokenListPage extends BaseListPage {
const newToken = this.newToken();
TokenBackend.addToken(newToken)
.then((res) => {
this.props.history.push({pathname: `/tokens/${newToken.name}`, mode: "add"});
}
)
if (res.status === "ok") {
this.props.history.push({pathname: `/tokens/${newToken.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Token failed to add: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteToken(i) {
TokenBackend.deleteToken(this.state.data[i])
.then((res) => {
Setting.showMessage("success", "Token deleted successfully");
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Token failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
@ -102,7 +109,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>
);
@ -198,7 +205,7 @@ class TokenListPage extends BaseListPage {
title={`Sure to delete token: ${record.name} ?`}
onConfirm={() => this.deleteToken(index)}
>
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
<Button style={{marginBottom: "10px"}} type="primary" danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

@ -49,6 +49,7 @@ class UserEditPage extends React.Component {
applications: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
loading: true,
returnUrl: null,
};
}
@ -57,6 +58,7 @@ class UserEditPage extends React.Component {
this.getOrganizations();
this.getApplicationsByOrganization(this.state.organizationName);
this.getUserApplication();
this.setReturnUrl();
}
getUser() {
@ -100,9 +102,14 @@ class UserEditPage extends React.Component {
});
}
getReturnUrl() {
setReturnUrl() {
const searchParams = new URLSearchParams(this.props.location.search);
return searchParams.get("returnUrl");
const returnUrl = searchParams.get("returnUrl");
if (returnUrl !== null) {
this.setState({
returnUrl: returnUrl,
});
}
}
parseUserField(key, value) {
@ -242,7 +249,7 @@ class UserEditPage extends React.Component {
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<CropperDiv buttonText={`${i18next.t("user:Upload a photo")}...`} title={i18next.t("user:Upload a photo")} user={this.state.user} account={this.props.account} />
<CropperDiv buttonText={`${i18next.t("user:Upload a photo")}...`} title={i18next.t("user:Upload a photo")} user={this.state.user} organization={this.state.organizations.find(organization => organization.name === this.state.organizationName)} />
</Row>
</Col>
</Row>
@ -604,8 +611,8 @@ class UserEditPage extends React.Component {
const user = Setting.deepCopy(this.state.user);
UserBackend.updateUser(this.state.organizationName, this.state.userName, user)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", "Successfully saved");
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
organizationName: this.state.user.owner,
userName: this.state.user.name,
@ -619,30 +626,33 @@ 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;
}
}
}
} else {
Setting.showMessage("error", res.msg);
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateUserField("owner", this.state.organizationName);
this.updateUserField("name", this.state.userName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteUser() {
UserBackend.deleteUser(this.state.user)
.then(() => {
this.props.history.push("/users");
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/users");
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `User failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}

View File

@ -72,26 +72,33 @@ class UserListPage extends BaseListPage {
const newUser = this.newUser();
UserBackend.addUser(newUser)
.then((res) => {
this.props.history.push({pathname: `/users/${newUser.owner}/${newUser.name}`, mode: "add"});
}
)
if (res.status === "ok") {
this.props.history.push({pathname: `/users/${newUser.owner}/${newUser.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `User failed to add: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteUser(i) {
UserBackend.deleteUser(this.state.data[i])
.then((res) => {
Setting.showMessage("success", "User deleted successfully");
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `User failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
@ -167,7 +174,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>
);
@ -346,7 +353,7 @@ class UserListPage extends BaseListPage {
title={`Sure to delete user: ${record.name} ?`}
onConfirm={() => this.deleteUser(index)}
>
<Button disabled={disabled} style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
<Button disabled={disabled} style={{marginBottom: "10px"}} type="primary" danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

@ -26,7 +26,7 @@ class WebAuthnCredentialTable extends React.Component {
registerWebAuthn() {
UserWebauthnBackend.registerWebauthnCredential().then((res) => {
if (res.msg === "") {
if (res.status === "ok") {
Setting.showMessage("success", "Successfully added webauthn credentials");
} else {
Setting.showMessage("error", res.msg);
@ -34,7 +34,7 @@ class WebAuthnCredentialTable extends React.Component {
this.props.refresh();
}).catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
@ -51,7 +51,7 @@ class WebAuthnCredentialTable extends React.Component {
width: "170px",
render: (text, record, index) => {
return (
<Button style={{marginTop: "5px", marginBottom: "5px", marginRight: "5px"}} type="danger" onClick={() => {this.deleteRow(this.props.table, index);}}>
<Button style={{marginTop: "5px", marginBottom: "5px", marginRight: "5px"}} type="primary" danger onClick={() => {this.deleteRow(this.props.table, index);}}>
{i18next.t("general:Delete")}
</Button>
);

View File

@ -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>
);
@ -296,8 +296,8 @@ class WebhookEditPage extends React.Component {
const webhook = Setting.deepCopy(this.state.webhook);
WebhookBackend.updateWebhook(this.state.webhook.owner, this.state.webhookName, webhook)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", "Successfully saved");
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
webhookName: this.state.webhook.name,
});
@ -308,22 +308,26 @@ class WebhookEditPage extends React.Component {
this.props.history.push(`/webhooks/${this.state.webhook.name}`);
}
} else {
Setting.showMessage("error", res.msg);
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateWebhookField("name", this.state.webhookName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteWebhook() {
WebhookBackend.deleteWebhook(this.state.webhook)
.then(() => {
this.props.history.push("/webhooks");
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/webhooks");
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Webhook failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}

View File

@ -42,26 +42,33 @@ class WebhookListPage extends BaseListPage {
const newWebhook = this.newWebhook();
WebhookBackend.addWebhook(newWebhook)
.then((res) => {
this.props.history.push({pathname: `/webhooks/${newWebhook.name}`, mode: "add"});
}
)
if (res.status === "ok") {
this.props.history.push({pathname: `/webhooks/${newWebhook.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Webhook failed to add: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteWebhook(i) {
WebhookBackend.deleteWebhook(this.state.data[i])
.then((res) => {
Setting.showMessage("success", "Webhook deleted successfully");
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `Webhook failed to delete: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
@ -194,7 +201,7 @@ class WebhookListPage extends BaseListPage {
title={`Sure to delete webhook: ${record.name} ?`}
onConfirm={() => this.deleteWebhook(index)}
>
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
<Button style={{marginBottom: "10px"}} type="primary" danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

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

View File

@ -126,7 +126,7 @@ class AuthCallback extends React.Component {
// If service was not specified, Casdoor must display a message notifying the client that it has successfully initiated a single sign-on session.
msg += "Now you can visit apps protected by Casdoor.";
}
Util.showMessage("success", msg);
Setting.showMessage("success", msg);
if (casService !== "") {
const st = res.data;
@ -135,7 +135,7 @@ class AuthCallback extends React.Component {
window.location.href = newUrl.toString();
}
} else {
Util.showMessage("error", `Failed to log in: ${res.msg}`);
Setting.showMessage("error", `Failed to log in: ${res.msg}`);
}
});
return;
@ -148,7 +148,7 @@ class AuthCallback extends React.Component {
if (res.status === "ok") {
const responseType = this.getResponseType();
if (responseType === "login") {
Util.showMessage("success", "Logged in successfully");
Setting.showMessage("success", "Logged in successfully");
// Setting.goToLinkSoft(this, "/");
const link = Setting.getFromLink();
@ -156,7 +156,7 @@ class AuthCallback extends React.Component {
} else if (responseType === "code") {
const code = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
// Util.showMessage("success", `Authorization code: ${res.data}`);
// Setting.showMessage("success", `Authorization code: ${res.data}`);
} else if (responseType === "token" || responseType === "id_token") {
const token = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`);

View File

@ -61,10 +61,7 @@ class ForgetPage extends React.Component {
if (this.state.applicationName !== undefined) {
this.getApplication();
} else {
Util.showMessage(
"error",
i18next.t("forget:Unknown forget type: ") + this.state.type
);
Setting.showMessage("error", i18next.t("forget:Unknown forget type: ") + this.state.type);
}
}
@ -102,6 +99,13 @@ class ForgetPage extends React.Component {
if (res.status === "ok") {
const phone = res.data.phone;
const email = res.data.email;
const saveFields = () => {
if (this.state.isFixed) {
forms.step2.setFieldsValue({email: this.state.fixedContent});
this.setState({username: this.state.fixedContent});
}
this.setState({current: 1});
};
this.setState({phone: phone, email: email, username: res.data.name, name: res.data.name});
if (phone !== "" && email === "") {
@ -116,19 +120,15 @@ class ForgetPage extends React.Component {
switch (res.data2) {
case "email":
this.setState({isFixed: true, fixedContent: email, verifyType: "email"});
this.setState({isFixed: true, fixedContent: email, verifyType: "email"}, () => {saveFields();});
break;
case "phone":
this.setState({isFixed: true, fixedContent: phone, verifyType: "phone"});
this.setState({isFixed: true, fixedContent: phone, verifyType: "phone"}, () => {saveFields();});
break;
default:
saveFields();
break;
}
if (this.state.isFixed) {
forms.step2.setFieldsValue({email: this.state.fixedContent});
this.setState({username: this.state.fixedContent});
}
this.setState({current: 1});
} else {
Setting.showMessage("error", i18next.t(`signup:${res.msg}`));
}
@ -136,26 +136,28 @@ class ForgetPage extends React.Component {
break;
case "step2":
const oAuthParams = Util.getOAuthGetParameters();
const login = () => {
AuthBackend.login({
application: forms.step2.getFieldValue("application"),
organization: forms.step2.getFieldValue("organization"),
username: this.state.username,
name: this.state.name,
code: forms.step2.getFieldValue("emailCode"),
phonePrefix: this.state.application?.organizationObj.phonePrefix,
type: "login",
}, oAuthParams).then(res => {
if (res.status === "ok") {
this.setState({current: 2, userId: res.data, username: res.data.split("/")[1]});
} else {
Setting.showMessage("error", i18next.t(`signup:${res.msg}`));
}
});
};
if (this.state.verifyType === "email") {
this.setState({username: this.state.email});
this.setState({username: this.state.email}, () => {login();});
} else if (this.state.verifyType === "phone") {
this.setState({username: this.state.phone});
this.setState({username: this.state.phone}, () => {login();});
}
AuthBackend.login({
application: forms.step2.getFieldValue("application"),
organization: forms.step2.getFieldValue("organization"),
username: this.state.username,
name: this.state.name,
code: forms.step2.getFieldValue("emailCode"),
phonePrefix: this.state.application?.organizationObj.phonePrefix,
type: "login",
}, oAuthParams).then(res => {
if (res.status === "ok") {
this.setState({current: 2, userId: res.data, username: res.data.split("/")[1]});
} else {
Setting.showMessage("error", i18next.t(`signup:${res.msg}`));
}
});
break;
default:
break;
@ -490,7 +492,7 @@ class ForgetPage extends React.Component {
return (
<div className="loginBackground" style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${application.formBackgroundUrl})`}}>
<CustomGithubCorner />
<div className="login-content forget-content" style={{padding: Setting.isMobile() ? "0" : null, boxShadow: Setting.isMobile() ? "none" : null}}>
<div className="forget-content" style={{padding: Setting.isMobile() ? "0" : null, boxShadow: Setting.isMobile() ? "none" : null}}>
<Row>
<Col span={24} style={{justifyContent: "center"}}>
<Row>

View File

@ -28,11 +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;
class LoginPage extends React.Component {
constructor(props) {
@ -69,7 +65,7 @@ class LoginPage extends React.Component {
} else if (this.state.type === "saml") {
this.getSamlApplication();
} else {
Util.showMessage("error", `Unknown authentication type: ${this.state.type}`);
Setting.showMessage("error", `Unknown authentication type: ${this.state.type}`);
}
}
@ -94,7 +90,7 @@ class LoginPage extends React.Component {
application: res.data,
});
} else {
// Util.showMessage("error", res.msg);
// Setting.showMessage("error", res.msg);
this.setState({
application: res.data,
msg: res.msg,
@ -124,7 +120,7 @@ class LoginPage extends React.Component {
applicationName: res.data.name,
});
} else {
Util.showMessage("error", res.msg);
Setting.showMessage("error", res.msg);
}
});
}
@ -271,7 +267,7 @@ class LoginPage extends React.Component {
// If service was not specified, Casdoor must display a message notifying the client that it has successfully initiated a single sign-on session.
msg += "Now you can visit apps protected by Casdoor.";
}
Util.showMessage("success", msg);
Setting.showMessage("success", msg);
this.setState({openCaptchaModal: false});
@ -283,7 +279,7 @@ class LoginPage extends React.Component {
}
} else {
this.setState({openCaptchaModal: false});
Util.showMessage("error", `Failed to log in: ${res.msg}`);
Setting.showMessage("error", `Failed to log in: ${res.msg}`);
}
});
} else {
@ -297,13 +293,13 @@ class LoginPage extends React.Component {
const responseType = values["type"];
if (responseType === "login") {
Util.showMessage("success", "Logged in successfully");
Setting.showMessage("success", "Logged in successfully");
const link = Setting.getFromLink();
Setting.goToLink(link);
} else if (responseType === "code") {
this.postCodeLoginAction(res);
// Util.showMessage("success", `Authorization code: ${res.data}`);
// Setting.showMessage("success", `Authorization code: ${res.data}`);
} else if (responseType === "token" || responseType === "id_token") {
const accessToken = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}#${responseType}=${accessToken}?state=${oAuthParams.state}&token_type=bearer`);
@ -314,7 +310,7 @@ class LoginPage extends React.Component {
}
} else {
this.setState({openCaptchaModal: false});
Util.showMessage("error", `Failed to log in: ${res.msg}`);
Setting.showMessage("error", `Failed to log in: ${res.msg}`);
}
});
}
@ -337,8 +333,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)}>
{
@ -365,23 +361,23 @@ class LoginPage extends React.Component {
size="large"
>
<Form.Item
style={{height: 0, visibility: "hidden"}}
hidden={true}
name="application"
rules={[
{
required: true,
message: "Please input your application!",
message: i18next.t("application:Please input your application!"),
},
]}
>
</Form.Item>
<Form.Item
style={{height: 0, visibility: "hidden"}}
hidden={true}
name="organization"
rules={[
{
required: true,
message: "Please input your organization!",
message: i18next.t("application:Please input your organization!"),
},
]}
>
@ -483,7 +479,7 @@ class LoginPage extends React.Component {
{application.displayName}
</span>
</a>
:
:
</div>
<br />
{
@ -675,7 +671,7 @@ class LoginPage extends React.Component {
}),
})
.then(res => res.json()).then((res) => {
if (res.msg === "") {
if (res.status === "ok") {
const responseType = values["type"];
if (responseType === "code") {
this.postCodeLoginAction(res);
@ -683,7 +679,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 +687,7 @@ class LoginPage extends React.Component {
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}${error}`);
});
});
}
@ -736,21 +732,16 @@ class LoginPage extends React.Component {
renderMethodChoiceBox() {
const application = this.getApplicationObj();
const items = [
{label: i18next.t("login:Password"), key: "password"},
];
application.enableCodeSignin ? items.push({label: i18next.t("login:Verification Code"), key: "verificationCode"}) : null;
application.enableWebAuthn ? items.push({label: i18next.t("login:WebAuthn"), key: "webAuthn"}) : null;
if (application.enableCodeSignin || application.enableWebAuthn) {
return (
<div>
<Tabs size={"small"} defaultActiveKey="password" onChange={(key) => {this.setState({loginMethod: key});}} centered>
<TabPane tab={i18next.t("login:Password")} key="password" />
{
!application.enableCodeSignin ? null : (
<TabPane tab={i18next.t("login:Verification Code")} key="verificationCode" />
)
}
{
!application.enableWebAuthn ? null : (
<TabPane tab={i18next.t("login:WebAuthn")} key="webAuthn" />
)
}
<Tabs items={items} size={"small"} defaultActiveKey="password" onChange={(key) => {this.setState({loginMethod: key});}} centered>
</Tabs>
</div>
);
@ -783,7 +774,7 @@ class LoginPage extends React.Component {
<div className="loginBackground" style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${application.formBackgroundUrl})`}}>
<CustomGithubCorner />
<div className="login-content" style={{margin: this.parseOffset(application.formOffset)}}>
{Setting.inIframe() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
{Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
<div className="login-panel">
<div className="side-image" style={{display: application.formOffset !== 4 ? "none" : null}}>
<div dangerouslySetInnerHTML={{__html: application.formSideHtml}} />
@ -800,7 +791,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 +808,4 @@ class LoginPage extends React.Component {
}
}
export default withTranslation()(withRouter(LoginPage));
export default LoginPage;

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