Compare commits

...

48 Commits

Author SHA1 Message Date
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
462a82a3d5 fix: Add distinctions between access_token and refresh_token (#1280) 2022-11-13 13:00:25 +08:00
262aeba7e2 fix(RoleEditPage): Fix a crash when the sample role domains is null (#1268) 2022-11-13 12:28:24 +08:00
61c2fd5412 feat: fix the issue of jumping back to the login page after resetting password (#1288)
* fix: redirect to login page

* fix: front end router

* fix: front end router

* fix: signup page router

* fix: redirect to login page
2022-11-13 12:16:49 +08:00
d542208eb8 feat: fix select language box overlay (#1289)
* fix: select language box overlay

* fix: select language box position

* fix: select language box position

* fix: select language box position
2022-11-13 10:52:22 +08:00
f818200c95 feat: fix empty organization in adapter edit page (#1274) 2022-11-08 21:03:15 +08:00
5bc2e91344 fix: fix typo (#1264) 2022-11-06 21:14:26 +08:00
295f732b18 Show tag in i18n 2022-11-06 20:19:31 +08:00
770ae47471 feat: fix memory leak problem (#1257) 2022-11-06 01:43:27 +08:00
2ce4f96355 fix: forget page mobile view (#1263) 2022-11-05 22:54:22 +08:00
07ed834b27 fix: system info mobile view (#1261) 2022-11-05 22:46:52 +08:00
8d686411ee feat: support add providers inside the Organization scope (#1250)
* feat: support add providers inside the Organization scope

Signed-off-by: magicwind <2814461814@qq.com>

* Update ProviderListPage.js

* fix: gloabal admin can see all providers

* fix: table fixed column warning

* fix: edit application page can get all providers

Signed-off-by: magicwind <2814461814@qq.com>
Co-authored-by: hsluoyz <hsluoyz@qq.com>
2022-11-04 21:31:08 +08:00
ce722897f1 feat: support prefix path for storage files (#1258) 2022-11-04 21:08:39 +08:00
a8381e875b feat: change all occurrences when a object name is changed (#1252) 2022-11-02 00:17:38 +08:00
4c81fd7d16 feat: fix generating wrong x.509 private key file header (#1253)
According to the [official x509 documentation](https://pkg.go.dev/crypto/x509#MarshalPKCS1PrivateKey), the private key generated using `x509.MarshalPKCS1PrivateKey` starts with `-----BEGIN RSA PRIVATE KEY-----` instead of `-----BEGIN PRIVATE KEY-----`. Otherwise, it will not be parsed by most tools (like OpenSSL, [jwt.io](https://jwt.io/), etc.) because it does not conform to the specification.
2022-11-01 22:19:38 +08:00
25ee4226d3 feat: clear the session of a signin but non-existent user (#1246) 2022-10-29 20:18:02 +08:00
9d5b019243 fix: nil error if init data is empty (#1247) 2022-10-29 20:04:43 +08:00
6bb7b545b4 feat: restrict DingTalk user log in who is under the DingTalk Org(which ClientId belong) (#1241)
* feat: fix bug in GetAcceptLanguage()

* feat: add appName when logging in with DingTalk

* fix review problems

* format code

* delete useless printf

* modify display name

Co-authored-by: Gucheng Wang <nomeguy@qq.com>
2022-10-28 22:14:05 +08:00
25d56ee8d5 feat: allow captcha to be enabled when logging in (#1211)
* Fix bug in GetAcceptLanguage()

* feat: allow captcha to be enabled when logging in

* feat: when the login password is wrong, enable captcha

* feat: Restrict captcha from frontend

* fix: modify CaptchaModal component

* fix: modify the words of i18n

* Update data.json

Co-authored-by: Gucheng Wang <nomeguy@qq.com>
Co-authored-by: hsluoyz <hsluoyz@qq.com>
2022-10-28 13:38:14 +08:00
7e5952c804 fix: login / signin frontend router (#1244)
* fix: go to link

* fix: remove gotologin

* fix: redirect to login page

* fix: redirect to login page

* remove comments

* fix: formats

* fix: formats

* Update Setting.js

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2022-10-28 02:23:57 +08:00
130 changed files with 3397 additions and 1138 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, *, *
@ -142,7 +146,7 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
userId := fmt.Sprintf("%s/%s", subOwner, subName)
user := object.GetUser(userId)
if user != nil && user.IsAdmin && subOwner == objOwner {
if user != nil && user.IsAdmin && (subOwner == objOwner || (objOwner == "admin" && subOwner == objName)) {
return true
}

View File

@ -14,6 +14,8 @@
package captcha
import "fmt"
type CaptchaProvider interface {
VerifyCaptcha(token, clientSecret string) (bool, error)
}
@ -29,6 +31,17 @@ func GetCaptchaProvider(captchaType string) CaptchaProvider {
return NewAliyunCaptchaProvider()
} else if captchaType == "GEETEST" {
return NewGEETESTCaptchaProvider()
} else if captchaType == "Cloudflare Turnstile" {
return NewCloudflareTurnstileProvider()
}
return nil
}
func VerifyCaptchaByCaptchaType(captchaType, token, clientSecret string) (bool, error) {
provider := GetCaptchaProvider(captchaType)
if provider == nil {
return false, fmt.Errorf("invalid captcha provider: %s", captchaType)
}
return provider.VerifyCaptcha(token, clientSecret)
}

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

@ -64,6 +64,10 @@ type RequestForm struct {
RelayState string `json:"relayState"`
SamlRequest string `json:"samlRequest"`
SamlResponse string `json:"samlResponse"`
CaptchaType string `json:"captchaType"`
CaptchaToken string `json:"captchaToken"`
ClientSecret string `json:"clientSecret"`
}
type Response struct {
@ -241,8 +245,7 @@ func (c *ApiController) Logout() {
util.LogInfo(c.Ctx, "API: [%s] logged out", user)
application := c.GetSessionApplication()
c.SetSessionUsername("")
c.SetSessionData(nil)
c.ClearUserSession()
if application == nil || application.Name == "app-built-in" || application.HomepageUrl == "" {
c.ResponseOk(user)

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,12 +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"
@ -31,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}
@ -251,6 +260,25 @@ func (c *ApiController) Login() {
return
}
} else {
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
if application == nil {
c.ResponseError(fmt.Sprintf("The application: %s does not exist", form.Application))
return
}
if object.CheckToEnableCaptcha(application) {
isHuman, err := captcha.VerifyCaptchaByCaptchaType(form.CaptchaType, form.CaptchaToken, form.ClientSecret)
if err != nil {
c.ResponseError(err.Error())
return
}
if !isHuman {
c.ResponseError("Turing test failed.")
return
}
}
password := form.Password
user, msg = object.CheckUserPassword(form.Organization, form.Username, password, c.GetAcceptLanguage())
}
@ -279,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))
@ -510,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

@ -63,8 +63,7 @@ func (c *ApiController) GetSessionUsername() string {
if sessionData != nil &&
sessionData.ExpireTime != 0 &&
sessionData.ExpireTime < time.Now().Unix() {
c.SetSessionUsername("")
c.SetSessionData(nil)
c.ClearUserSession()
return ""
}
@ -85,13 +84,17 @@ func (c *ApiController) GetSessionApplication() *object.Application {
return application
}
func (c *ApiController) ClearUserSession() {
c.SetSessionUsername("")
c.SetSessionData(nil)
}
func (c *ApiController) GetSessionOidc() (string, string) {
sessionData := c.GetSessionData()
if sessionData != nil &&
sessionData.ExpireTime != 0 &&
sessionData.ExpireTime < time.Now().Unix() {
c.SetSessionUsername("")
c.SetSessionData(nil)
c.ClearUserSession()
return "", ""
}
scopeValue := c.GetSession("scope")

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

@ -48,6 +48,30 @@ func (c *ApiController) GetProviders() {
}
}
// GetGlobalProviders
// @Title GetGlobalProviders
// @Tag Provider API
// @Description get Global providers
// @Success 200 {array} object.Provider The Response object
// @router /get-global-providers [get]
func (c *ApiController) GetGlobalProviders() {
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 limit == "" || page == "" {
c.Data["json"] = object.GetMaskedProviders(object.GetGlobalProviders())
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetGlobalProviderCount(field, value)))
providers := object.GetMaskedProviders(object.GetPaginationGlobalProviders(paginator.Offset(), limit, field, value, sortField, sortOrder))
c.ResponseOk(providers, paginator.Nums())
}
}
// GetProvider
// @Title GetProvider
// @Tag Provider API
@ -57,7 +81,6 @@ func (c *ApiController) GetProviders() {
// @router /get-provider [get]
func (c *ApiController) GetProvider() {
id := c.Input().Get("id")
c.Data["json"] = object.GetMaskedProvider(object.GetProvider(id))
c.ServeJSON()
}
@ -99,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]
@ -98,6 +99,7 @@ func (c *ApiController) RequireSignedInUser() (*object.User, bool) {
user := object.GetUser(userId)
if user == nil {
c.ClearUserSession()
c.ResponseError(fmt.Sprintf(c.T("UserErr.DoNotExist"), userId))
return nil, false
}
@ -124,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
@ -151,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

@ -15,9 +15,12 @@
package idp
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"strings"
"time"
@ -167,7 +170,10 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
Email: dtUserInfo.Email,
AvatarUrl: dtUserInfo.AvatarUrl,
}
isUserInOrg, err := idp.isUserInOrg(userInfo.UnionId)
if !isUserInOrg {
return nil, err
}
return &userInfo, nil
}
@ -194,3 +200,62 @@ func (idp *DingTalkIdProvider) postWithBody(body interface{}, url string) ([]byt
return data, nil
}
func (idp *DingTalkIdProvider) getInnerAppAccessToken() string {
appKey := idp.Config.ClientID
appSecret := idp.Config.ClientSecret
body := make(map[string]string)
body["appKey"] = appKey
body["appSecret"] = appSecret
bodyData, err := json.Marshal(body)
if err != nil {
log.Println(err.Error())
}
reader := bytes.NewReader(bodyData)
request, err := http.NewRequest("POST", "https://api.dingtalk.com/v1.0/oauth2/accessToken", reader)
request.Header.Set("Content-Type", "application/json;charset=UTF-8")
resp, err := idp.Client.Do(request)
respBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println(err.Error())
}
var data struct {
ExpireIn int `json:"expireIn"`
AccessToken string `json:"accessToken"`
}
err = json.Unmarshal(respBytes, &data)
if err != nil {
log.Println(err.Error())
}
return data.AccessToken
}
func (idp *DingTalkIdProvider) isUserInOrg(unionId string) (bool, error) {
body := make(map[string]string)
body["unionid"] = unionId
bodyData, err := json.Marshal(body)
if err != nil {
log.Println(err.Error())
}
reader := bytes.NewReader(bodyData)
accessToken := idp.getInnerAppAccessToken()
request, _ := http.NewRequest("POST", "https://oapi.dingtalk.com/topapi/user/getbyunionid?access_token="+accessToken, reader)
request.Header.Set("Content-Type", "application/json;charset=UTF-8")
resp, err := idp.Client.Do(request)
respBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println(err.Error())
}
var data struct {
ErrCode int `json:"errcode"`
ErrMessage string `json:"errmsg"`
}
err = json.Unmarshal(respBytes, &data)
if err != nil {
log.Println(err.Error())
}
if data.ErrCode == 60121 {
return false, fmt.Errorf("the user is not found in the organization where clientId and clientSecret belong")
}
return true, nil
}

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
@ -270,6 +294,13 @@ func UpdateApplication(id string, application *Application) bool {
application.Name = name
}
if name != application.Name {
err := applicationChangeTrigger(name, application.Name)
if err != nil {
return false
}
}
for _, providerItem := range application.Providers {
providerItem.Provider = nil
}
@ -371,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 {
@ -400,3 +431,55 @@ func ExtendManagedAccountsWithUser(user *User) *User {
return user
}
func applicationChangeTrigger(oldName string, newName string) error {
session := adapter.Engine.NewSession()
defer session.Close()
err := session.Begin()
if err != nil {
return err
}
organization := new(Organization)
organization.DefaultApplication = newName
_, err = session.Where("default_application=?", oldName).Update(organization)
if err != nil {
return err
}
user := new(User)
user.SignupApplication = newName
_, err = session.Where("signup_application=?", oldName).Update(user)
if err != nil {
return err
}
resource := new(Resource)
resource.Application = newName
_, err = session.Where("application=?", oldName).Update(resource)
if err != nil {
return err
}
var permissions []*Permission
err = adapter.Engine.Find(&permissions)
if err != nil {
return err
}
for i := 0; i < len(permissions); i++ {
permissionResoureces := permissions[i].Resources
for j := 0; j < len(permissionResoureces); j++ {
if permissionResoureces[j] == oldName {
permissionResoureces[j] = newName
}
}
permissions[i].Resources = permissionResoureces
_, err = session.Where("name=?", permissions[i].Name).Update(permissions[i])
if err != nil {
return err
}
}
return session.Commit()
}

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

@ -114,6 +114,12 @@ func UpdateCert(id string, cert *Cert) bool {
return false
}
if name != cert.Name {
err := certChangeTrigger(name, cert.Name)
if err != nil {
return false
}
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(cert)
if err != nil {
panic(err)
@ -161,3 +167,22 @@ func getCertByApplication(application *Application) *Cert {
func GetDefaultCert() *Cert {
return getCert("admin", "cert-built-in")
}
func certChangeTrigger(oldName string, newName string) error {
session := adapter.Engine.NewSession()
defer session.Close()
err := session.Begin()
if err != nil {
return err
}
application := new(Application)
application.Cert = newName
_, err = session.Where("cert=?", oldName).Update(application)
if err != nil {
return err
}
return session.Commit()
}

View File

@ -340,3 +340,20 @@ func CheckUsername(username string, lang string) string {
return ""
}
func CheckToEnableCaptcha(application *Application) bool {
if len(application.Providers) == 0 {
return false
}
for _, providerItem := range application.Providers {
if providerItem.Provider == nil {
continue
}
if providerItem.Provider.Category == "Captcha" && providerItem.Provider.Type == "Default" {
return providerItem.Rule == "Always"
}
}
return false
}

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"},
@ -143,7 +144,7 @@ func initBuiltInApplication() {
EnablePassword: true,
EnableSignUp: true,
Providers: []*ProviderItem{
{Name: "provider_captcha_default", CanSignUp: false, CanSignIn: false, CanUnlink: false, Prompted: false, AlertType: "None", Provider: nil},
{Name: "provider_captcha_default", CanSignUp: false, CanSignIn: false, CanUnlink: false, Prompted: false, AlertType: "None", Rule: "None", Provider: nil},
},
SignupItems: []*SignupItem{
{Name: "ID", Visible: false, Required: true, Prompted: false, Rule: "Random"},
@ -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

@ -56,12 +56,40 @@ func readInitDataFromFile(filePath string) *InitData {
s := util.ReadStringFromPath(filePath)
data := &InitData{}
data := &InitData{
Organizations: []*Organization{},
Applications: []*Application{},
Users: []*User{},
Certs: []*Cert{},
Providers: []*Provider{},
Ldaps: []*Ldap{},
}
err := util.JsonToStruct(s, data)
if err != nil {
panic(err)
}
// transform nil slice to empty slice
for _, organization := range data.Organizations {
if organization.Tags == nil {
organization.Tags = []string{}
}
}
for _, application := range data.Applications {
if application.Providers == nil {
application.Providers = []*ProviderItem{}
}
if application.SignupItems == nil {
application.SignupItems = []*SignupItem{}
}
if application.GrantTypes == nil {
application.GrantTypes = []string{}
}
if application.RedirectUris == nil {
application.RedirectUris = []string{}
}
}
return data
}
@ -140,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

@ -92,6 +92,12 @@ func UpdateModel(id string, modelObj *Model) bool {
return false
}
if name != modelObj.Name {
err := modelChangeTrigger(name, modelObj.Name)
if err != nil {
return false
}
}
// check model grammar
_, err := model.NewModelFromString(modelObj.ModelText)
if err != nil {
@ -127,3 +133,22 @@ func DeleteModel(model *Model) bool {
func (model *Model) GetId() string {
return fmt.Sprintf("%s/%s", model.Owner, model.Name)
}
func modelChangeTrigger(oldName string, newName string) error {
session := adapter.Engine.NewSession()
defer session.Close()
err := session.Begin()
if err != nil {
return err
}
permission := new(Permission)
permission.Model = newName
_, err = session.Where("model=?", oldName).Update(permission)
if err != nil {
return err
}
return session.Commit()
}

View File

@ -16,6 +16,7 @@ package object
import (
"fmt"
"strings"
"github.com/casdoor/casdoor/cred"
"github.com/casdoor/casdoor/i18n"
@ -44,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"`
@ -134,15 +136,10 @@ func UpdateOrganization(id string, organization *Organization) bool {
}
if name != organization.Name {
go func() {
application := new(Application)
application.Organization = organization.Name
_, _ = adapter.Engine.Where("organization=?", name).Update(application)
user := new(User)
user.Owner = organization.Name
_, _ = adapter.Engine.Where("owner=?", name).Update(user)
}()
err := organizationChangeTrigger(name, organization.Name)
if err != nil {
return false
}
}
if organization.MasterPassword != "" && organization.MasterPassword != "***" {
@ -226,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{}
@ -252,3 +254,148 @@ func GetDefaultApplication(id string) (*Application, error) {
return defaultApplication, nil
}
func organizationChangeTrigger(oldName string, newName string) error {
session := adapter.Engine.NewSession()
defer session.Close()
err := session.Begin()
if err != nil {
return err
}
application := new(Application)
application.Organization = newName
_, err = session.Where("organization=?", oldName).Update(application)
if err != nil {
return err
}
user := new(User)
user.Owner = newName
_, err = session.Where("owner=?", oldName).Update(user)
if err != nil {
return err
}
role := new(Role)
_, err = adapter.Engine.Where("owner=?", oldName).Get(role)
if err != nil {
return err
}
for i, u := range role.Users {
// u = organization/username
split := strings.Split(u, "/")
if split[0] == oldName {
split[0] = newName
role.Users[i] = split[0] + "/" + split[1]
}
}
for i, u := range role.Roles {
// u = organization/username
split := strings.Split(u, "/")
if split[0] == oldName {
split[0] = newName
role.Roles[i] = split[0] + "/" + split[1]
}
}
role.Owner = newName
_, err = session.Where("owner=?", oldName).Update(role)
if err != nil {
return err
}
permission := new(Permission)
_, err = adapter.Engine.Where("owner=?", oldName).Get(permission)
if err != nil {
return err
}
for i, u := range permission.Users {
// u = organization/username
split := strings.Split(u, "/")
if split[0] == oldName {
split[0] = newName
permission.Users[i] = split[0] + "/" + split[1]
}
}
for i, u := range permission.Roles {
// u = organization/username
split := strings.Split(u, "/")
if split[0] == oldName {
split[0] = newName
permission.Roles[i] = split[0] + "/" + split[1]
}
}
permission.Owner = newName
_, err = session.Where("owner=?", oldName).Update(permission)
if err != nil {
return err
}
casbinAdapter := new(CasbinAdapter)
casbinAdapter.Owner = newName
casbinAdapter.Organization = newName
_, err = session.Where("owner=?", oldName).Update(casbinAdapter)
if err != nil {
return err
}
ldap := new(Ldap)
ldap.Owner = newName
_, err = session.Where("owner=?", oldName).Update(ldap)
if err != nil {
return err
}
model := new(Model)
model.Owner = newName
_, err = session.Where("owner=?", oldName).Update(model)
if err != nil {
return err
}
payment := new(Payment)
payment.Organization = newName
_, err = session.Where("organization=?", oldName).Update(payment)
if err != nil {
return err
}
record := new(Record)
record.Owner = newName
record.Organization = newName
_, err = session.Where("organization=?", oldName).Update(record)
if err != nil {
return err
}
resource := new(Resource)
resource.Owner = newName
_, err = session.Where("owner=?", oldName).Update(resource)
if err != nil {
return err
}
syncer := new(Syncer)
syncer.Organization = newName
_, err = session.Where("organization=?", oldName).Update(syncer)
if err != nil {
return err
}
token := new(Token)
token.Organization = newName
_, err = session.Where("organization=?", oldName).Update(token)
if err != nil {
return err
}
webhook := new(Webhook)
webhook.Organization = newName
_, err = session.Where("organization=?", oldName).Update(webhook)
if err != nil {
return err
}
return session.Commit()
}

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)
}
@ -152,20 +154,20 @@ func removePolicies(permission *Permission) {
}
}
func Enforce(userId string, permissionRule *PermissionRule) bool {
func Enforce(permissionRule *PermissionRule) bool {
permission := GetPermission(permissionRule.Id)
enforcer := getEnforcer(permission)
allow, err := enforcer.Enforce(userId, permissionRule.V1, permissionRule.V2)
allow, err := enforcer.Enforce(permissionRule.V0, permissionRule.V1, permissionRule.V2)
if err != nil {
panic(err)
}
return allow
}
func BatchEnforce(userId string, permissionRules []PermissionRule) []bool {
func BatchEnforce(permissionRules []PermissionRule) []bool {
var requests [][]interface{}
for _, permissionRule := range permissionRules {
requests = append(requests, []interface{}{userId, permissionRule.V1, permissionRule.V2})
requests = append(requests, []interface{}{permissionRule.V0, permissionRule.V1, permissionRule.V2})
}
permission := GetPermission(permissionRules[0].Id)
enforcer := getEnforcer(permission)

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"`
@ -60,6 +60,7 @@ type Provider struct {
IntranetEndpoint string `xorm:"varchar(100)" json:"intranetEndpoint"`
Domain string `xorm:"varchar(100)" json:"domain"`
Bucket string `xorm:"varchar(100)" json:"bucket"`
PathPrefix string `xorm:"varchar(100)" json:"pathPrefix"`
Metadata string `xorm:"mediumtext" json:"metadata"`
IdP string `xorm:"mediumtext" json:"idP"`
@ -92,7 +93,17 @@ func GetMaskedProviders(providers []*Provider) []*Provider {
}
func GetProviderCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
session := GetSession("", -1, -1, field, value, "", "")
count, err := session.Where("owner = ? or owner = ? ", "admin", owner).Count(&Provider{})
if err != nil {
panic(err)
}
return int(count)
}
func GetGlobalProviderCount(field, value string) int {
session := GetSession("", -1, -1, field, value, "", "")
count, err := session.Count(&Provider{})
if err != nil {
panic(err)
@ -103,7 +114,17 @@ func GetProviderCount(owner, 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)
}
return providers
}
func GetGlobalProviders() []*Provider {
providers := []*Provider{}
err := adapter.Engine.Desc("created_time").Find(&providers)
if err != nil {
panic(err)
}
@ -113,7 +134,18 @@ func GetProviders(owner string) []*Provider {
func GetPaginationProviders(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Provider {
providers := []*Provider{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
session := GetSession("", offset, limit, field, value, sortField, sortOrder)
err := session.Where("owner = ? or owner = ? ", "admin", owner).Find(&providers)
if err != nil {
panic(err)
}
return providers
}
func GetPaginationGlobalProviders(offset, limit int, field, value, sortField, sortOrder string) []*Provider {
providers := []*Provider{}
session := GetSession("", offset, limit, field, value, sortField, sortOrder)
err := session.Find(&providers)
if err != nil {
panic(err)
@ -127,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)
@ -175,6 +207,13 @@ func UpdateProvider(id string, provider *Provider) bool {
return false
}
if name != provider.Name {
err := providerChangeTrigger(name, provider.Name)
if err != nil {
return false
}
}
session := adapter.Engine.ID(core.PK{owner, name}).AllCols()
if provider.ClientSecret == "***" {
session = session.Omit("client_secret")
@ -262,3 +301,41 @@ func GetCaptchaProviderByApplication(applicationId, isCurrentProvider, lang stri
}
return nil, nil
}
func providerChangeTrigger(oldName string, newName string) error {
session := adapter.Engine.NewSession()
defer session.Close()
err := session.Begin()
if err != nil {
return err
}
var applications []*Application
err = adapter.Engine.Find(&applications)
if err != nil {
return err
}
for i := 0; i < len(applications); i++ {
providers := applications[i].Providers
for j := 0; j < len(providers); j++ {
if providers[j].Name == oldName {
providers[j].Name = newName
}
}
applications[i].Providers = providers
_, err = session.Where("name=?", applications[i].Name).Update(applications[i])
if err != nil {
return err
}
}
resource := new(Resource)
resource.Provider = newName
_, err = session.Where("provider=?", oldName).Update(resource)
if err != nil {
return err
}
return session.Commit()
}

View File

@ -15,12 +15,15 @@
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"`
Prompted bool `json:"prompted"`
AlertType string `json:"alertType"`
Rule string `json:"rule"`
Provider *Provider `json:"provider"`
}

View File

@ -16,6 +16,7 @@ package object
import (
"fmt"
"strings"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
@ -94,6 +95,13 @@ func UpdateRole(id string, role *Role) bool {
return false
}
if name != role.Name {
err := roleChangeTrigger(name, role.Name)
if err != nil {
return false
}
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(role)
if err != nil {
panic(err)
@ -133,3 +141,54 @@ func GetRolesByUser(userId string) []*Role {
return roles
}
func roleChangeTrigger(oldName string, newName string) error {
session := adapter.Engine.NewSession()
defer session.Close()
err := session.Begin()
if err != nil {
return err
}
var roles []*Role
err = adapter.Engine.Find(&roles)
if err != nil {
return err
}
for _, role := range roles {
for j, u := range role.Roles {
split := strings.Split(u, "/")
if split[1] == oldName {
split[1] = newName
role.Roles[j] = split[0] + "/" + split[1]
}
}
_, err = session.Where("name=?", role.Name).Update(role)
if err != nil {
return err
}
}
var permissions []*Permission
err = adapter.Engine.Find(&permissions)
if err != nil {
return err
}
for _, permission := range permissions {
for j, u := range permission.Roles {
// u = organization/username
split := strings.Split(u, "/")
if split[1] == oldName {
split[1] = newName
permission.Roles[j] = split[0] + "/" + split[1]
}
}
_, err = session.Where("name=?", permission.Name).Update(permission)
if err != nil {
return err
}
}
return session.Commit()
}

View File

@ -54,8 +54,8 @@ func escapePath(path string) string {
return res
}
func getUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool) (string, string) {
escapedPath := escapePath(fullFilePath)
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)
host := ""
@ -70,7 +70,7 @@ func getUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool
host = util.UrlJoin(provider.Domain, "/files")
}
if provider.Type == "Azure Blob" {
host = fmt.Sprintf("%s/%s", host, provider.Bucket)
host = util.UrlJoin(host, provider.Bucket)
}
fileUrl := util.UrlJoin(host, escapePath(objectKey))
@ -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

@ -37,7 +37,16 @@ func (syncer *Syncer) getOriginalUsers() ([]*OriginalUser, error) {
return nil, err
}
return syncer.getOriginalUsersFromMap(results), nil
// Memory leak problem handling
// https://github.com/casdoor/casdoor/issues/1256
users := syncer.getOriginalUsersFromMap(results)
for _, m := range results {
for k := range m {
delete(m, k)
}
}
return users, nil
}
func (syncer *Syncer) getOriginalUserMap() ([]*OriginalUser, map[string]*OriginalUser, error) {

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

@ -24,9 +24,10 @@ import (
type Claims struct {
*User
Nonce string `json:"nonce,omitempty"`
Tag string `json:"tag,omitempty"`
Scope string `json:"scope,omitempty"`
TokenType string `json:"tokenType,omitempty"`
Nonce string `json:"nonce,omitempty"`
Tag string `json:"tag,omitempty"`
Scope string `json:"scope,omitempty"`
jwt.RegisteredClaims
}
@ -37,8 +38,9 @@ type UserShort struct {
type ClaimsShort struct {
*UserShort
Nonce string `json:"nonce,omitempty"`
Scope string `json:"scope,omitempty"`
TokenType string `json:"tokenType,omitempty"`
Nonce string `json:"nonce,omitempty"`
Scope string `json:"scope,omitempty"`
jwt.RegisteredClaims
}
@ -53,6 +55,7 @@ func getShortUser(user *User) *UserShort {
func getShortClaims(claims Claims) ClaimsShort {
res := ClaimsShort{
UserShort: getShortUser(claims.User),
TokenType: claims.TokenType,
Nonce: claims.Nonce,
Scope: claims.Scope,
RegisteredClaims: claims.RegisteredClaims,
@ -72,8 +75,9 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
jti := fmt.Sprintf("%s/%s", application.Owner, name)
claims := Claims{
User: user,
Nonce: nonce,
User: user,
TokenType: "access-token",
Nonce: nonce,
// FIXME: A workaround for custom claim by reusing `tag` in user info
Tag: user.Tag,
Scope: scope,
@ -97,10 +101,12 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
token = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsShort)
claimsShort.ExpiresAt = jwt.NewNumericDate(refreshExpireTime)
claimsShort.TokenType = "refresh-token"
refreshToken = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsShort)
} else {
token = jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
claims.ExpiresAt = jwt.NewNumericDate(refreshExpireTime)
claims.TokenType = "refresh-token"
refreshToken = jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
}

View File

@ -37,7 +37,7 @@ func generateRsaKeys(bitSize int, expireInYears int, commonName string, organiza
// Encode private key to PKCS#1 ASN.1 PEM.
privateKeyPem := pem.EncodeToMemory(
&pem.Block{
Type: "PRIVATE KEY",
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
},
)

View File

@ -380,6 +380,13 @@ func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) boo
return false
}
if name != user.Name {
err := userChangeTrigger(name, user.Name)
if err != nil {
return false
}
}
if user.Password == "***" {
user.Password = oldUser.Password
}
@ -416,6 +423,13 @@ func UpdateUserForAllFields(id string, user *User) bool {
return false
}
if name != user.Name {
err := userChangeTrigger(name, user.Name)
if err != nil {
return false
}
}
user.UpdateUserHash()
if user.Avatar != oldUser.Avatar && user.Avatar != "" {
@ -567,3 +581,62 @@ func ExtendUserWithRolesAndPermissions(user *User) {
user.Roles = GetRolesByUser(user.GetId())
user.Permissions = GetPermissionsByUser(user.GetId())
}
func userChangeTrigger(oldName string, newName string) error {
session := adapter.Engine.NewSession()
defer session.Close()
err := session.Begin()
if err != nil {
return err
}
var roles []*Role
err = adapter.Engine.Find(&roles)
if err != nil {
return err
}
for _, role := range roles {
for j, u := range role.Users {
// u = organization/username
split := strings.Split(u, "/")
if split[1] == oldName {
split[1] = newName
role.Users[j] = split[0] + "/" + split[1]
}
}
_, err = session.Where("name=?", role.Name).Update(role)
if err != nil {
return err
}
}
var permissions []*Permission
err = adapter.Engine.Find(&permissions)
if err != nil {
return err
}
for _, permission := range permissions {
for j, u := range permission.Users {
// u = organization/username
split := strings.Split(u, "/")
if split[1] == oldName {
split[1] = newName
permission.Users[j] = split[0] + "/" + split[1]
}
}
_, err = session.Where("name=?", permission.Name).Update(permission)
if err != nil {
return err
}
}
resource := new(Resource)
resource.User = newName
_, err = session.Where("user=?", oldName).Update(resource)
if err != nil {
return err
}
return session.Commit()
}

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")
@ -123,6 +129,7 @@ func initAPI() {
beego.Router("/api/get-providers", &controllers.ApiController{}, "GET:GetProviders")
beego.Router("/api/get-provider", &controllers.ApiController{}, "GET:GetProvider")
beego.Router("/api/get-global-providers", &controllers.ApiController{}, "GET:GetGlobalProviders")
beego.Router("/api/update-provider", &controllers.ApiController{}, "POST:UpdateProvider")
beego.Router("/api/add-provider", &controllers.ApiController{}, "POST:AddProvider")
beego.Router("/api/delete-provider", &controllers.ApiController{}, "POST:DeleteProvider")

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

@ -120,7 +120,7 @@ class AccountTable extends React.Component {
},
},
{
title: i18next.t("provider:visible"),
title: i18next.t("organization:Visible"),
dataIndex: "visible",
key: "visible",
width: "120px",
@ -133,7 +133,7 @@ class AccountTable extends React.Component {
},
},
{
title: i18next.t("organization:viewRule"),
title: i18next.t("organization:View rule"),
dataIndex: "viewRule",
key: "viewRule",
width: "155px",
@ -160,7 +160,7 @@ class AccountTable extends React.Component {
},
},
{
title: i18next.t("organization:modifyRule"),
title: i18next.t("organization:Modify rule"),
dataIndex: "modifyRule",
key: "modifyRule",
width: "155px",

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,
@ -59,7 +58,7 @@ class AdapterEditPage extends React.Component {
}
getOrganizations() {
OrganizationBackend.getOrganizations(this.state.organizationName)
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
@ -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={
@ -195,7 +107,7 @@ class AdapterEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.adapter.organization} onChange={(value => {this.updateadapterField("organization", value);})}>
<Select virtual={false} style={{width: "100%"}} value={this.state.adapter.organization} onChange={(value => {this.updateAdapterField("organization", value);})}>
{
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
}
@ -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,46 +45,38 @@ 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}`);
});
}
renderTable(adapters) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
width: "120px",
sorter: true,
...this.getColumnSearchProps("organization"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
@ -101,6 +93,21 @@ class AdapterListPage extends BaseListPage {
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
width: "120px",
sorter: true,
...this.getColumnSearchProps("organization"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",

View File

@ -420,13 +420,8 @@ class App extends Component {
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/providers">
<Link to="/providers">
{i18next.t("general:Providers")}
</Link>
</Menu.Item>
);
}
if (Setting.isLocalAdminUser(this.state.account)) {
res.push(
<Menu.Item key="/applications">
<Link to="/applications">
@ -434,9 +429,13 @@ class App extends Component {
</Link>
</Menu.Item>
);
}
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">
@ -565,9 +564,9 @@ class App extends Component {
<Route exact path="/adapters" render={(props) => this.renderLoginIfNotLoggedIn(<AdapterListPage account={this.state.account} {...props} />)} />
<Route exact path="/adapters/:organizationName/:adapterName" render={(props) => this.renderLoginIfNotLoggedIn(<AdapterEditPage account={this.state.account} {...props} />)} />
<Route exact path="/providers" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderListPage account={this.state.account} {...props} />)} />
<Route exact path="/providers/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)} />
<Route exact path="/providers/:organizationName/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)} />
<Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)} />
<Route exact path="/applications/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationEditPage account={this.state.account} {...props} />)} />
<Route exact path="/applications/:organizationName/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationEditPage account={this.state.account} {...props} />)} />
<Route exact path="/resources" render={(props) => this.renderLoginIfNotLoggedIn(<ResourceListPage account={this.state.account} {...props} />)} />
{/* <Route exact path="/resources/:resourceName" render={(props) => this.renderLoginIfNotLoggedIn(<ResourceEditPage account={this.state.account} {...props} />)}/>*/}
<Route exact path="/ldap/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapEditPage account={this.state.account} {...props} />)} />
@ -635,7 +634,7 @@ class App extends Component {
{
this.renderAccount()
}
<SelectLanguageBox />
{this.state.account && <SelectLanguageBox languages={this.state.account.organization.languages} />}
</div>
</Header>
<Layout style={{backgroundColor: "#f5f5f5", alignItems: "stretch"}}>
@ -679,7 +678,7 @@ class App extends Component {
{
this.renderAccount()
}
<SelectLanguageBox />
{this.state.account && <SelectLanguageBox languages={this.state.account.organization.languages} />}
</div>
</Header>
{

View File

@ -59,12 +59,6 @@
height: 70px; /* Footer height */
}
#language-box-corner {
position: absolute;
top: 75px;
right: 0;
}
.language-box {
background: url("@{StaticBaseUrl}/img/muti_language.svg");
background-size: 25px, 25px;
@ -123,7 +117,7 @@
.login-form {
text-align: center;
padding: 10px;
padding: 30px;
}
.login-content {

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,6 +94,7 @@ class ApplicationEditPage extends React.Component {
super(props);
this.state = {
classes: props,
owner: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
applicationName: props.match.params.applicationName,
application: null,
organizations: [],
@ -141,12 +145,11 @@ class ApplicationEditPage extends React.Component {
}
getProviders() {
ProviderBackend.getProviders("admin")
.then((res) => {
this.setState({
providers: res,
});
ProviderBackend.getProviders(this.state.owner).then((res => {
this.setState({
providers: res,
});
}));
}
getSamlMetadata() {
@ -188,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});
@ -277,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>)
}
@ -720,7 +723,7 @@ class ApplicationEditPage extends React.Component {
return (
<React.Fragment>
<Col span={11}>
<Col span={previewGrid}>
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}${signUpUrl}`);
Setting.showMessage("success", i18next.t("application:Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
@ -729,7 +732,7 @@ class ApplicationEditPage extends React.Component {
{i18next.t("application:Copy signup page URL")}
</Button>
<br />
<div style={{position: "relative", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
<div style={{position: "relative", width: previewWidth, border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
{
this.state.application.enablePassword ? (
<SignupPage application={this.state.application} />
@ -740,8 +743,8 @@ class ApplicationEditPage extends React.Component {
<div style={maskStyle} />
</div>
</Col>
<Col span={11}>
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
<Col span={previewGrid}>
<Button style={{marginBottom: "10px", marginTop: Setting.isMobile() ? "15px" : "0"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}${signInUrl}`);
Setting.showMessage("success", i18next.t("application:Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
}}
@ -749,7 +752,7 @@ class ApplicationEditPage extends React.Component {
{i18next.t("application:Copy signin page URL")}
</Button>
<br />
<div style={{position: "relative", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
<div style={{position: "relative", width: previewWidth, border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
<LoginPage type={"login"} mode={"signin"} application={this.state.application} />
<div style={maskStyle} />
</div>
@ -762,7 +765,7 @@ class ApplicationEditPage extends React.Component {
const promptUrl = `/prompt/${this.state.application.name}`;
const maskStyle = {position: "absolute", top: "0px", left: "0px", zIndex: 10, height: "100%", width: "100%", background: "rgba(0,0,0,0.4)"};
return (
<Col span={11}>
<Col span={previewGrid}>
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}${promptUrl}`);
Setting.showMessage("success", i18next.t("application:Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
@ -771,7 +774,7 @@ class ApplicationEditPage extends React.Component {
{i18next.t("application:Copy prompt page URL")}
</Button>
<br />
<div style={{position: "relative", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", flexDirection: "column", flex: "auto"}}>
<div style={{position: "relative", width: previewWidth, border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", flexDirection: "column", flex: "auto"}}>
<PromptPage application={this.state.application} account={this.props.account} />
<div style={maskStyle} />
</div>
@ -781,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,
});
@ -792,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`,
@ -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,7 +237,7 @@ class ApplicationListPage extends BaseListPage {
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/applications/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/applications/${record.organization}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete application: ${record.name} ?`}
onConfirm={() => this.deleteApplication(index)}
@ -254,7 +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

@ -217,8 +217,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 +229,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}`);
});
}

View File

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

View File

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

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,46 +38,38 @@ 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}`);
});
}
renderTable(models) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
@ -94,6 +86,21 @@ class ModelListPage extends BaseListPage {
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",

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}`);
});
}

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}`);
});
}
@ -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,31 +51,71 @@ 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}`);
});
}
renderTable(payments) {
const columns = [
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "180px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
return (
<Link to={`/payments/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Provider"),
dataIndex: "provider",
key: "provider",
width: "150px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("provider"),
render: (text, record, index) => {
return (
<Link to={`/providers/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
@ -106,22 +146,7 @@ class PaymentListPage extends BaseListPage {
);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "180px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
return (
<Link to={`/payments/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
@ -140,22 +165,6 @@ class PaymentListPage extends BaseListPage {
// sorter: true,
// ...this.getColumnSearchProps('displayName'),
// },
{
title: i18next.t("general:Provider"),
dataIndex: "provider",
key: "provider",
width: "150px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("provider"),
render: (text, record, index) => {
return (
<Link to={`/providers/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("payment:Type"),
dataIndex: "type",

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,50 +48,39 @@ 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}`);
});
}
renderTable(permissions) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
// https://github.com/ant-design/ant-design/issues/22184
{
title: i18next.t("general:Name"),
dataIndex: "name",
@ -108,6 +97,21 @@ class PermissionListPage extends BaseListPage {
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",

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}`);
});
}

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;
@ -32,17 +33,20 @@ class ProviderEditPage extends React.Component {
this.state = {
classes: props,
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();
}
getProvider() {
ProviderBackend.getProvider("admin", this.state.providerName)
ProviderBackend.getProvider(this.state.owner, this.state.providerName)
.then((provider) => {
this.setState({
provider: provider,
@ -50,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);
@ -190,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"))} :
@ -229,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>
@ -252,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>
@ -423,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"}} >
@ -469,6 +515,16 @@ class ProviderEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:The prefix path of the file - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.pathPrefix} onChange={e => {
this.updateProviderField("pathPrefix", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
@ -735,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

@ -23,10 +23,25 @@ import i18next from "i18next";
import BaseListPage from "./BaseListPage";
class ProviderListPage extends BaseListPage {
constructor(props) {
super(props);
this.state = {
classes: props,
owner: Setting.isAdminUser(props.account) ? "admin" : props.account.owner,
data: [],
pagination: {
current: 1,
pageSize: 10,
},
loading: false,
searchText: "",
searchedColumn: "",
};
}
newProvider() {
const randomName = Setting.getRandomName();
return {
owner: "admin", // this.props.account.providername,
owner: this.state.owner,
name: `provider_${randomName}`,
createdTime: moment().format(),
displayName: `New Provider - ${randomName}`,
@ -46,26 +61,33 @@ class ProviderListPage extends BaseListPage {
const newProvider = this.newProvider();
ProviderBackend.addProvider(newProvider)
.then((res) => {
this.props.history.push({pathname: `/providers/${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}`);
});
}
@ -81,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",
@ -177,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.name}`)}>{i18next.t("general:Edit")}</Button>
<Button disabled={!Setting.isAdminUser(this.props.account) && (record.owner !== this.props.account.owner)} style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/providers/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete provider: ${record.name} ?`}
onConfirm={() => this.deleteProvider(index)}
>
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
<Button disabled={!Setting.isAdminUser(this.props.account) && (record.owner !== this.props.account.owner)} style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);
@ -224,7 +254,8 @@ class ProviderListPage extends BaseListPage {
value = params.type;
}
this.setState({loading: true});
ProviderBackend.getProviders("admin", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
(Setting.isAdminUser(this.props.account) ? ProviderBackend.getGlobalProviders(params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
: ProviderBackend.getProviders(this.state.owner, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder))
.then((res) => {
if (res.status === "ok") {
this.setState({

View File

@ -39,7 +39,7 @@ class ProviderTable extends React.Component {
}
addRow(table) {
const row = {name: Setting.getNewRowNameForTable(table, "Please select a provider"), canSignUp: true, canSignIn: true, canUnlink: true, alertType: "None"};
const row = {name: Setting.getNewRowNameForTable(table, "Please select a provider"), canSignUp: true, canSignIn: true, canUnlink: true, alertType: "None", rule: "None"};
if (table === undefined) {
table = [];
}
@ -105,7 +105,7 @@ class ProviderTable extends React.Component {
},
},
{
title: i18next.t("provider:canSignUp"),
title: i18next.t("provider:Can signup"),
dataIndex: "canSignUp",
key: "canSignUp",
width: "120px",
@ -122,7 +122,7 @@ class ProviderTable extends React.Component {
},
},
{
title: i18next.t("provider:canSignIn"),
title: i18next.t("provider:Can signin"),
dataIndex: "canSignIn",
key: "canSignIn",
width: "120px",
@ -139,7 +139,7 @@ class ProviderTable extends React.Component {
},
},
{
title: i18next.t("provider:canUnlink"),
title: i18next.t("provider:Can unlink"),
dataIndex: "canUnlink",
key: "canUnlink",
width: "120px",
@ -156,7 +156,7 @@ class ProviderTable extends React.Component {
},
},
{
title: i18next.t("provider:prompted"),
title: i18next.t("provider:Prompted"),
dataIndex: "prompted",
key: "prompted",
width: "120px",
@ -193,6 +193,28 @@ class ProviderTable extends React.Component {
// )
// }
// },
{
title: i18next.t("application:Rule"),
dataIndex: "rule",
key: "rule",
width: "100px",
render: (text, record, index) => {
if (record.provider?.category !== "Captcha") {
return null;
}
return (
<Select virtual={false} style={{width: "100%"}}
value={text}
defaultValue="None"
onChange={value => {
this.updateField(table, index, "rule", value);
}} >
<Option key="None" value="None">{i18next.t("application:None")}</Option>
<Option key="Always" value="Always">{i18next.t("application:Always")}</Option>
</Select>
);
},
},
{
title: i18next.t("general:Action"),
key: "action",

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>
);
}
},

View File

@ -173,7 +173,7 @@ class RoleEditPage extends React.Component {
this.updateRoleField("domains", value);
})}>
{
this.state.role.domains.map((domain, index) => <Option key={index} value={domain}>{domain}</Option>)
this.state.role.domains?.map((domain, index) => <Option key={index} value={domain}>{domain}</Option>)
}
</Select>
</Col>
@ -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,46 +40,38 @@ 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}`);
});
}
renderTable(roles) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
@ -96,6 +88,21 @@ class RoleListPage extends BaseListPage {
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",

View File

@ -28,28 +28,45 @@ class SelectLanguageBox extends React.Component {
super(props);
this.state = {
classes: props,
languages: props.languages ?? ["en", "zh", "es", "fr", "de", "ja", "ko", "ru"],
};
}
items = [
this.getItem("English", "en", flagIcon("US", "English")),
this.getItem("简体中文", "zh", flagIcon("CN", "简体中文")),
this.getItem("Español", "es", flagIcon("ES", "Español")),
this.getItem("Français", "fr", flagIcon("FR", "Français")),
this.getItem("Deutsch", "de", flagIcon("DE", "Deutsch")),
this.getItem("日本語", "ja", flagIcon("JP", "日本語")),
this.getItem("한국어", "ko", flagIcon("KR", "한국어")),
this.getItem("Русский", "ru", flagIcon("RU", "Русский")),
];
getOrganizationLanguages(languages) {
const select = [];
for (const language of languages) {
this.items.map((item, index) => item.key === language ? select.push(item) : null);
}
return select;
}
getItem(label, key, icon) {
return {key, icon, label};
}
render() {
const languageItems = this.getOrganizationLanguages(this.state.languages);
const menu = (
<Menu onClick={(e) => {
Setting.changeLanguage(e.key);
<Menu items={languageItems} onClick={(e) => {
Setting.setLanguage(e.key);
}}>
<Menu.Item key="en" icon={flagIcon("US", "English")}>English</Menu.Item>
<Menu.Item key="zh" icon={flagIcon("CN", "简体中文")}>简体中文</Menu.Item>
<Menu.Item key="es" icon={flagIcon("ES", "Español")}>Español</Menu.Item>
<Menu.Item key="fr" icon={flagIcon("FR", "Français")}>Français</Menu.Item>
<Menu.Item key="de" icon={flagIcon("DE", "Deutsch")}>Deutsch</Menu.Item>
<Menu.Item key="ja" icon={flagIcon("JP", "日本語")}>日本語</Menu.Item>
<Menu.Item key="ko" icon={flagIcon("KR", "한국어")}>한국어</Menu.Item>
<Menu.Item key="ru" icon={flagIcon("RU", "Русский")}>Русский</Menu.Item>
</Menu>
);
return (
<Dropdown overlay={menu} >
<div className="language-box" id={this.props.id} style={this.props.style} />
<div className="language-box" style={{display: languageItems.length === 0 ? "none" : null, ...this.props.style}} />
</Dropdown>
);
}

View File

@ -12,9 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Tag, Tooltip, message} from "antd";
import {QuestionCircleTwoTone} from "@ant-design/icons";
import React from "react";
import {isMobile as isMobileDevice} from "react-device-detect";
import "./i18n";
import i18next from "i18next";
@ -22,7 +23,6 @@ import copy from "copy-to-clipboard";
import {authConfig} from "./auth/Auth";
import {Helmet} from "react-helmet";
import * as Conf from "./Conf";
import {Link} from "react-router-dom";
import * as path from "path-browserify";
export const ServerUrl = "";
@ -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 [];
@ -743,26 +742,30 @@ export function renderLogo(application) {
}
}
export function goToLogin(ths, application) {
export function getLoginLink(application) {
let url;
if (application === null) {
return;
}
if (!application.enablePassword && window.location.pathname.includes("/auto-signup/oauth/authorize")) {
const link = window.location.href.replace("/auto-signup/oauth/authorize", "/login/oauth/authorize");
goToLink(link);
return;
}
if (authConfig.appName === application.name) {
goToLinkSoft(ths, "/login");
url = null;
} else if (!application.enablePassword && window.location.pathname.includes("/auto-signup/oauth/authorize")) {
url = window.location.href.replace("/auto-signup/oauth/authorize", "/login/oauth/authorize");
} else if (authConfig.appName === application.name) {
url = "/login";
} else if (application.signinUrl === "") {
url = path.join(application.homepageUrl, "/login");
} else {
if (application.signinUrl === "") {
goToLink(path.join(application.homepageUrl, "login"));
} else {
goToLink(application.signinUrl);
}
url = application.signinUrl;
}
return url;
}
export function renderLoginLink(application, text) {
const url = getLoginLink(application);
return renderLink(url, text, null);
}
export function redirectToLoginPage(application, history) {
const loginLink = getLoginLink(application);
history.push(loginLink);
}
function renderLink(url, text, onClick) {

View File

@ -104,7 +104,7 @@ class SignupTable extends React.Component {
},
},
{
title: i18next.t("provider:visible"),
title: i18next.t("provider:Visible"),
dataIndex: "visible",
key: "visible",
width: "120px",
@ -126,7 +126,7 @@ class SignupTable extends React.Component {
},
},
{
title: i18next.t("provider:required"),
title: i18next.t("provider:Required"),
dataIndex: "required",
key: "required",
width: "120px",
@ -143,7 +143,7 @@ class SignupTable extends React.Component {
},
},
{
title: i18next.t("provider:prompted"),
title: i18next.t("provider:Prompted"),
dataIndex: "prompted",
key: "prompted",
width: "120px",
@ -164,7 +164,7 @@ class SignupTable extends React.Component {
},
},
{
title: i18next.t("application:rule"),
title: i18next.t("application:Rule"),
dataIndex: "rule",
key: "rule",
width: "155px",

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}`);
});
}
@ -88,21 +95,6 @@ class SyncerListPage extends BaseListPage {
renderTable(syncers) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
width: "120px",
sorter: true,
...this.getColumnSearchProps("organization"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
@ -119,6 +111,21 @@ class SyncerListPage extends BaseListPage {
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
width: "120px",
sorter: true,
...this.getColumnSearchProps("organization"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",

View File

@ -90,37 +90,66 @@ class SystemInfo extends React.Component {
</div> : i18next.t("system:Get Memory Usage Failed")
);
return (
<Row>
<Col span={6}></Col>
<Col span={12}>
<Row gutter={[10, 10]}>
<Col span={12}>
<Card title={i18next.t("system:CPU Usage")} bordered={true} style={{textAlign: "center"}}>
{this.state.loading ? <Spin size="large" /> : CPUInfo}
</Card>
</Col>
<Col span={12}>
<Card title={i18next.t("system:Memory Usage")} bordered={true} style={{textAlign: "center"}}>
{this.state.loading ? <Spin size="large" /> : MemInfo}
</Card>
</Col>
</Row>
<Divider />
<Card title={i18next.t("system:About Casdoor")} bordered={true} style={{textAlign: "center"}}>
<div>{i18next.t("system:An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS")}</div>
GitHub: <a href="https://github.com/casdoor/casdoor">casdoor</a>
<br />
{i18next.t("system:Version")}: <a href={this.state.href}>{this.state.latestVersion}</a>
<br />
{i18next.t("system:Official Website")}: <a href="https://casdoor.org/">casdoor.org</a>
<br />
{i18next.t("system:Community")}: <a href="https://casdoor.org/#:~:text=Casdoor%20API-,Community,-GitHub">contact us</a>
</Card>
</Col>
<Col span={6}></Col>
</Row>
);
if (!Setting.isMobile()) {
return (
<Row>
<Col span={6}></Col>
<Col span={12}>
<Row gutter={[10, 10]}>
<Col span={12}>
<Card title={i18next.t("system:CPU Usage")} bordered={true} style={{textAlign: "center", height: "100%"}}>
{this.state.loading ? <Spin size="large" /> : CPUInfo}
</Card>
</Col>
<Col span={12}>
<Card title={i18next.t("system:Memory Usage")} bordered={true} style={{textAlign: "center", height: "100%"}}>
{this.state.loading ? <Spin size="large" /> : MemInfo}
</Card>
</Col>
</Row>
<Divider />
<Card title={i18next.t("system:About Casdoor")} bordered={true} style={{textAlign: "center"}}>
<div>{i18next.t("system:An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS")}</div>
GitHub: <a href="https://github.com/casdoor/casdoor">casdoor</a>
<br />
{i18next.t("system:Version")}: <a href={this.state.href}>{this.state.latestVersion}</a>
<br />
{i18next.t("system:Official Website")}: <a href="https://casdoor.org/">casdoor.org</a>
<br />
{i18next.t("system:Community")}: <a href="https://casdoor.org/#:~:text=Casdoor%20API-,Community,-GitHub">contact us</a>
</Card>
</Col>
<Col span={6}></Col>
</Row>
);
} else {
return (
<Row gutter={[16, 0]}>
<Col span={24}>
<Card title={i18next.t("system:CPU Usage")} bordered={true} style={{textAlign: "center", width: "100%"}}>
{this.state.loading ? <Spin size="large" /> : CPUInfo}
</Card>
</Col>
<Col span={24}>
<Card title={i18next.t("system:Memory Usage")} bordered={true} style={{textAlign: "center", width: "100%"}}>
{this.state.loading ? <Spin size="large" /> : MemInfo}
</Card>
</Col>
<Col span={24}>
<Card title={i18next.t("system:About Casdoor")} bordered={true} style={{textAlign: "center"}}>
<div>{i18next.t("system:An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS")}</div>
GitHub: <a href="https://github.com/casdoor/casdoor">casdoor</a>
<br />
{i18next.t("system:Version")}: <a href={this.state.href}>{this.state.latestVersion}</a>
<br />
{i18next.t("system:Official Website")}: <a href="https://casdoor.org/">casdoor.org</a>
<br />
{i18next.t("system:Community")}: <a href="https://casdoor.org/#:~:text=Casdoor%20API-,Community,-GitHub">contact us</a>
</Card>
</Col>
</Row>
);
}
}
}

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

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}`);
});
}

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