Compare commits

...

63 Commits

Author SHA1 Message Date
604033aa02 feat: use Casdoor to manage Casbin DB table (#1100)
* feat: use Casdoor to manage Casbin DB table

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* fix: remove trivial codes

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* chore: go fmt

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* feat: support role definition

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* fix: i18n

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* fix: i18n

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-09-14 22:14:13 +08:00
729c20393c fix: missing providers and org in GetDefaultApplication (#1123)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-09-13 22:54:05 +08:00
a90b27b74a Fix admin UI issues 2022-09-13 21:32:18 +08:00
5707e38912 feat: add batchSize to conf (#1120) 2022-09-13 20:31:22 +08:00
ed959bd8c7 feat: improve login page style (#1119)
Signed-off-by: magicwind <2814461814@qq.com>

Signed-off-by: magicwind <2814461814@qq.com>
2022-09-12 00:01:18 +08:00
b6cdc46023 feat: add defaultApplication for Orgnization (#1111)
* feat: add defaultApplication for Orgnization

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* fix: remove redundant codes

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* fix: don't use app-built-in

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* fix: add query param

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* Update organization.go

* Update organization.go

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-09-10 20:41:45 +08:00
c661a57cb2 Support regex in CheckRedirectUriValid() 2022-09-10 13:12:36 +08:00
8456b7f7c4 fix: with error pq: column "DingTalk" of relation "user" does not exist (#1116)
* feat: add dingtalk union_id

* fix: with pg, column Dingtalk of relation user table does not exist.

* Update user_util.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-09-10 13:08:37 +08:00
e8d2906e3c Fix bug in form CSS 2022-09-10 01:33:44 +08:00
1edb91b3a3 feat: custom login form and background (#1107)
* feat: custom login form and background

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

* feat: costom login form border

* chore: update i18

* Update ApplicationEditPage.js

* Update LoginPage.js

* Update SignupPage.js

* Update LoginPage.js

* Update ApplicationEditPage.js

Signed-off-by: magicwind <2814461814@qq.com>
Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-09-10 00:56:37 +08:00
94b6eb803d Fix WeChat MP login "state too long" bug 2022-09-09 11:43:54 +08:00
cfce5289ed Rename getStateFromQueryParams() and getQueryParamsFromState() 2022-09-09 02:02:32 +08:00
10f1c37730 Fix 403 bug for /api/login/* APIs 2022-09-09 01:54:05 +08:00
6035b98653 feat: add dingtalk union_id (#1110) 2022-09-08 14:44:06 +08:00
e158b58ffa fix: add hidden signal to support chrome extension to auto-signin (#1109)
* feat: add hiden applicationName(support chrome extension to auto recognize applicationName)

* feat: add hiden applicationName for all page

* fix typo

* delete unuseful code

* remove hidden applicationName from login page

* prevent crash if signupApplication is null

* Update App.js

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-09-07 17:02:28 +08:00
a399184cfc fix: correct edit URL in model list (#1108)
Co-authored-by: Mario Fischer <mario.fischer@inmanet.de>
2022-09-07 00:54:27 +08:00
2f9f946c87 feat: fix GOPROXY bug by exporting environment variable (#1106) 2022-09-05 23:17:39 +08:00
d8b60f838e fix: fix bugs about 3rd-party login in cas flow (#1096) 2022-09-05 23:02:25 +08:00
7599e2715a feat: add demo mode (#1097)
* feat: add demo mode

* feat: add demo mode

* Update app.conf

* Update authz.go

* Update authz.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-09-04 21:20:19 +08:00
35676455bc chore(style): add keyword spacing rule (#1098) 2022-09-04 19:40:30 +08:00
8128671c8c Improve email code 2022-09-04 12:15:07 +08:00
ee54dec3b3 feat: add support for mysubmail (#1095)
* feat: add support for mysubmail

* Update email.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-09-04 12:09:50 +08:00
d278bc9651 Add receiver for Email provider 2022-09-04 11:37:36 +08:00
b23bd0b189 Support SUBMAIL email provider 2022-09-04 11:21:20 +08:00
409be85264 Fix placeholder typo 2022-09-03 18:52:35 +08:00
0395b7e1a9 feat: migrate permission data (#1083)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-09-03 16:45:58 +08:00
4536fd0636 Use app.conf value in getOriginFromHost() 2022-09-03 15:12:34 +08:00
af9ae7dbb7 feat: buildx failed with: EROR: failed to solve: executor failed running [/bin/sh -c ./build.sh]: exit code: 127 (#1089) 2022-09-02 14:50:27 +08:00
e266696b32 feat: add default permission to built-in group (#1087)
* fix: add default permission

* fix: add default permission

* fix: add default permission
2022-09-02 12:03:13 +08:00
e108d26ec7 fix: recover header logo && add styleint check (#1084)
* fix: fix header logo not show

* feat: update lint-staged

feat: add stylelint
2022-08-31 23:26:58 +08:00
349ce7f1d4 fix: refactor build.sh #1081 (#1082)
* fix: Add default access permission for new built-in group users

* fix: Add default access permission for new built-in group users

* fix: File is not `gofumpt`-ed (gofumpt)

* fix: refactor build.sh #1081

* fix: rollback

* fix: newline

* fix: refactor build.sh rename var #1081
2022-08-31 16:08:10 +08:00
8da50b7893 feat: extend managed accounts for get-account api (#1068)
* feat: add get-extend-account api

* feat: extend managed accounts for get-account api

* fix go-linter err

* Use GetApplicationsByOrganizationName
2022-08-30 00:57:27 +08:00
2394c8e2b4 Make sure newStaticBaseUrl is not empty 2022-08-29 21:27:47 +08:00
c62983d734 Use conf.GetConfigString() 2022-08-29 21:26:00 +08:00
5948782cdd fix: fix eslint error in webstorm (#1073) 2022-08-29 15:23:51 +08:00
674d1619dd fix: fix hot update error #1071 (#1072) 2022-08-29 13:45:31 +08:00
11b8b65ca0 feat: update antd and react to latest (#1069) 2022-08-28 23:14:04 +08:00
411d76798d fix: fix upload file security issue (#1063)
* fix: fix upload file security issue

* fix: fix
2022-08-25 11:34:09 +08:00
7b0b426a76 feat: check model grammar when saving and provide a ACL model as init data (#1062)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-08-24 17:21:05 +08:00
a383af0ebc feat: fix token info not contains roles and permissions (#1060)
* fix: fix token info not contains roles and permissions

feat: remove repeated code for obtaining roles and permissions in user controller

* Update user.go

* Update user.go

* Update token.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-08-24 01:41:26 +08:00
f02875e1b1 fix: enable captcha in the application which is not built-in (#1061) 2022-08-23 23:30:45 +08:00
e2921419b9 Add TestDeployStaticFiles() 2022-08-23 21:17:58 +08:00
42864700ec chore: update badges (#1058) 2022-08-23 13:11:42 +08:00
c1fe547939 fix: not start countdown if SMS fails to send (#1057) 2022-08-22 23:06:41 +08:00
267833d9f9 feat: fix the application edit page bug due to this.props.location.search (#1055) 2022-08-22 15:13:58 +08:00
2d3d1167bb Fix HasPromptPage() for signup items 2022-08-22 11:51:20 +08:00
ef5abdfa8f feat: rollback to fix ci (#1051) 2022-08-22 11:14:51 +08:00
580d43101e fix(i18n): add spanish translations (#1043)
* feat(i18n): add spanish translations

* feat(i18n): add missing translations

* feat(i18n): use new icon flag set

* use document protocol

* fix(i18n): use our static flags
2022-08-22 09:42:28 +08:00
fdf2b880cb feat: click on the app card to log in automatically (#1049) 2022-08-22 01:17:18 +08:00
80a2263b18 fix: fix ci bug: "/go/src/casdoor/.git/refs/heads" not found (#1050) 2022-08-22 01:02:57 +08:00
1f11d22c1c fix: add managed account table for supporting Chrome extension to auto login (#1030)
* feat: add manage accounts table(support chrome extension to auto login)

* fix go lint err

* rename manageAccounts to managedAccounts

* expand up&down buttom column width

* rename ManagedAccountsTable to ManagedAccountTable
2022-08-22 00:25:39 +08:00
b6988286b5 Improve i18n for permission page 2022-08-21 23:17:14 +08:00
64f787fab5 feat: can modify static resource url by app.conf (#1045)
* feat: can modify static resource url by app.conf

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

* Update static_filter.go

Signed-off-by: magicwind <2814461814@qq.com>
Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-08-21 21:40:27 +08:00
39c6bd5850 fix: country/region prompted page not show when signin (#1047) 2022-08-21 17:41:07 +08:00
7312c5ce3c Don't check domain for submitPermissionEdit() 2022-08-21 15:28:19 +08:00
0bc5b90218 fix: add country/region selectbox in prompt page (#1022) 2022-08-21 11:12:23 +08:00
f3b3376a3c fix: fix get version error (#1044)
* feat: fix get version error

* feat: more safe

* fix
2022-08-21 10:47:36 +08:00
feec6abd88 fix: fix translations for system info page (#1042) 2022-08-20 23:00:37 +08:00
c50042c85a feat: fix the go.sum error (#1040) 2022-08-20 22:04:37 +08:00
ef4c3833a4 feat: add system info page (#1033)
* feat: add system info page

* feat: add some code

* fix
2022-08-20 21:22:46 +08:00
67a5adf585 feat: replace panic with details json error payload. (#1039)
Signed-off-by: 疯魔慕薇 <kfanjian@gmail.com>

Signed-off-by: 疯魔慕薇 <kfanjian@gmail.com>
2022-08-20 21:09:32 +08:00
08a1e7ae32 fix: keep phone/email unique. (#1038)
Signed-off-by: 疯魔慕薇 <kfanjian@gmail.com>

Signed-off-by: 疯魔慕薇 <kfanjian@gmail.com>
2022-08-20 12:14:08 +08:00
7d979cbaf0 feat(storage): add support for min.io storage (#1037)
* feat(storage): add support for min.io storage

* fix(minio): use doublequote

* fix(storage): change storage name to MinIO
2022-08-20 11:30:13 +08:00
117 changed files with 7906 additions and 5846 deletions

View File

@ -8,7 +8,7 @@
<img alt="docker pull casbin/casdoor" src="https://img.shields.io/docker/pulls/casbin/casdoor.svg">
</a>
<a href="https://github.com/casdoor/casdoor/actions/workflows/build.yml">
<img alt="GitHub Workflow Status (branch)" src="https://github.com/casbin/jcasbin/workflows/build/badge.svg?style=flat-square">
<img alt="GitHub Workflow Status (branch)" src="https://github.com/casdoor/casdoor/workflows/Build/badge.svg?style=flat-square">
</a>
<a href="https://github.com/casdoor/casdoor/releases/latest">
<img alt="GitHub Release" src="https://img.shields.io/github/v/release/casbin/casdoor.svg">
@ -42,65 +42,48 @@
</a>
</p>
## Online demo
- International: https://door.casdoor.org (read-only)
- Asian mirror: https://door.casdoor.com (read-only)
- Asian mirror: https://demo.casdoor.com (read-write, will restore for every 5 minutes)
## Documentation
- International: https://casdoor.org
- Asian mirror: https://docs.casdoor.cn
## Install
- By source code: https://casdoor.org/docs/basic/server-installation
- By Docker: https://casdoor.org/docs/basic/try-with-docker
## How to connect to Casdoor?
https://casdoor.org/docs/how-to-connect/overview
## Casdoor Public API
- Docs: https://casdoor.org/docs/basic/public-api
- Swagger: https://door.casdoor.com/swagger
## Integrations
https://casdoor.org/docs/integration/apisix
## How to contact?
- Gitter: https://gitter.im/casbin/casdoor
- Forum: https://forum.casbin.com
- Contact: https://tawk.to/chat/623352fea34c2456412b8c51/1fuc7od6e
## Contribute
For casdoor, if you have any questions, you can give Issues, or you can also directly start Pull Requests(but we recommend giving issues first to communicate with the community).
### I18n translation
If you are contributing to casdoor, please note that we use [Crowdin](https://crowdin.com/project/casdoor-site) as translating platform and i18next as translating tool. When you add some words using i18next in the ```web/``` directory, please remember to add what you have added to the ```web/src/locales/en/data.json``` file.
If you are contributing to casdoor, please note that we use [Crowdin](https://crowdin.com/project/casdoor-site) as translating platform and i18next as translating tool. When you add some words using i18next in the `web/` directory, please remember to add what you have added to the `web/src/locales/en/data.json` file.
## License

View File

@ -15,6 +15,8 @@
package authz
import (
"strings"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
xormadapter "github.com/casbin/xorm-adapter/v2"
@ -28,7 +30,7 @@ func InitAuthz() {
var err error
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
a, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetBeegoConfDataSourceName()+conf.GetConfigString("dbName"), "casbin_rule", tableNamePrefix, true)
a, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName()+conf.GetConfigString("dbName"), "casbin_rule", tableNamePrefix, true)
if err != nil {
panic(err)
}
@ -107,6 +109,8 @@ p, *, *, POST, /api/acs, *, *
p, *, *, GET, /api/saml/metadata, *, *
p, *, *, *, /cas, *, *
p, *, *, *, /api/webauthn, *, *
p, *, *, GET, /api/get-release, *, *
p, *, *, GET, /api/get-default-application, *, *
`
sa := stringadapter.NewAdapter(ruleText)
@ -127,6 +131,12 @@ p, *, *, *, /api/webauthn, *, *
}
func IsAllowed(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
if conf.IsDemoMode() {
if !isAllowedInDemoMode(subOwner, subName, method, urlPath, objOwner, objName) {
return false
}
}
res, err := Enforcer.Enforce(subOwner, subName, method, urlPath, objOwner, objName)
if err != nil {
panic(err)
@ -134,3 +144,22 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
return res
}
func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
if method == "POST" {
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/send-verification-code" {
return true
} else if urlPath == "/api/update-user" {
// Allow ordinary users to update their own information
if subOwner == objOwner && subName == objName && !(subOwner == "built-in" && subName == "admin") {
return true
}
return false
} else {
return false
}
}
// If method equals GET
return true
}

View File

@ -4,8 +4,8 @@ curl www.google.com -o /dev/null --connect-timeout 5 2 > /dev/null
if [ $? == 0 ]
then
echo "Successfully connected to Google, no need to use Go proxy"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server .
else
echo "Google is blocked, Go proxy is enabled: GOPROXY=https://goproxy.cn,direct"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPROXY=https://goproxy.cn,direct go build -ldflags="-w -s" -o server .
export GOPROXY="https://goproxy.cn,direct"
fi
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server .

View File

@ -16,4 +16,6 @@ verificationCodeTimeout = 10
initScore = 2000
logPostOnly = true
origin =
staticBaseUrl = "https://cdn.casbin.org"
staticBaseUrl = "https://cdn.casbin.org"
isDemoMode = false
batchSize = 100

View File

@ -24,11 +24,32 @@ import (
"github.com/astaxie/beego"
)
func init() {
// this array contains the beego configuration items that may be modified via env
presetConfigItems := []string{"httpport", "appname"}
for _, key := range presetConfigItems {
if value, ok := os.LookupEnv(key); ok {
err := beego.AppConfig.Set(key, value)
if err != nil {
panic(err)
}
}
}
}
func GetConfigString(key string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return beego.AppConfig.String(key)
res := beego.AppConfig.String(key)
if res == "" {
if key == "staticBaseUrl" {
res = "https://cdn.casbin.org"
}
}
return res
}
func GetConfigBool(key string) (bool, error) {
@ -47,17 +68,7 @@ func GetConfigInt64(key string) (int64, error) {
return num, err
}
func init() {
// this array contains the beego configuration items that may be modified via env
presetConfigItems := []string{"httpport", "appname"}
for _, key := range presetConfigItems {
if value, ok := os.LookupEnv(key); ok {
beego.AppConfig.Set(key, value)
}
}
}
func GetBeegoConfDataSourceName() string {
func GetConfigDataSourceName() string {
dataSourceName := GetConfigString("dataSourceName")
runningInDocker := os.Getenv("RUNNING_IN_DOCKER")
@ -72,3 +83,15 @@ func GetBeegoConfDataSourceName() string {
return dataSourceName
}
func IsDemoMode() bool {
return strings.ToLower(GetConfigString("isDemoMode")) == "true"
}
func GetConfigBatchSize() int {
res, err := strconv.Atoi(GetConfigString("batchSize"))
if err != nil {
res = 100
}
return res
}

View File

@ -105,7 +105,8 @@ func (c *ApiController) Signup() {
var form RequestForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
@ -156,6 +157,12 @@ func (c *ApiController) Signup() {
username = id
}
initScore, err := getInitScore()
if err != nil {
c.ResponseError(fmt.Errorf("get init score failed, error: %w", err).Error())
return
}
user := &object.User{
Owner: form.Organization,
Name: username,
@ -171,7 +178,7 @@ func (c *ApiController) Signup() {
Affiliation: form.Affiliation,
IdCard: form.IdCard,
Region: form.Region,
Score: getInitScore(),
Score: initScore,
IsAdmin: false,
IsGlobalAdmin: false,
IsForbidden: false,
@ -262,6 +269,11 @@ func (c *ApiController) GetAccount() {
return
}
managedAccounts := c.Input().Get("managedAccounts")
if managedAccounts == "1" {
user = object.ExtendManagedAccountsWithUser(user)
}
organization := object.GetMaskedOrganization(object.GetOrganizationByUser(user))
resp := Response{
Status: "ok",

View File

@ -111,8 +111,7 @@ func (c *ApiController) GetOrganizationApplications() {
return
}
var applications []*object.Application
applications = object.GetApplicationsByOrganizationName(owner, organization)
applications := object.GetApplicationsByOrganizationName(owner, organization)
c.Data["json"] = object.GetMaskedApplications(applications, userId)
c.ServeJSON()
}
@ -131,7 +130,8 @@ func (c *ApiController) UpdateApplication() {
var application object.Application
err := json.Unmarshal(c.Ctx.Input.RequestBody, &application)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateApplication(id, &application))
@ -149,7 +149,8 @@ func (c *ApiController) AddApplication() {
var application object.Application
err := json.Unmarshal(c.Ctx.Input.RequestBody, &application)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddApplication(&application))
@ -167,7 +168,8 @@ func (c *ApiController) DeleteApplication() {
var application object.Application
err := json.Unmarshal(c.Ctx.Input.RequestBody, &application)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteApplication(&application))

View File

@ -344,7 +344,7 @@ func (c *ApiController) Login() {
user = object.GetUserByField(application.Organization, provider.Type, userInfo.Id)
}
if user != nil && user.IsDeleted == false {
if user != nil && !user.IsDeleted {
// Sign in via OAuth (want to sign up but already have account)
if user.IsForbidden {
@ -384,6 +384,12 @@ func (c *ApiController) Login() {
properties := map[string]string{}
properties["no"] = strconv.Itoa(len(object.GetUsers(application.Organization)) + 2)
initScore, err := getInitScore()
if err != nil {
c.ResponseError(fmt.Errorf("get init score failed, error: %w", err).Error())
return
}
user = &object.User{
Owner: application.Organization,
Name: userInfo.Username,
@ -394,7 +400,7 @@ func (c *ApiController) Login() {
Avatar: userInfo.AvatarUrl,
Address: []string{},
Email: userInfo.Email,
Score: getInitScore(),
Score: initScore,
IsAdmin: false,
IsGlobalAdmin: false,
IsForbidden: false,

View File

@ -19,6 +19,7 @@ import (
"time"
"github.com/astaxie/beego"
"github.com/astaxie/beego/logs"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
@ -58,6 +59,7 @@ func (c *ApiController) IsGlobalAdmin() bool {
func (c *ApiController) GetSessionUsername() string {
// check if user session expired
sessionData := c.GetSessionData()
if sessionData != nil &&
sessionData.ExpireTime != 0 &&
sessionData.ExpireTime < time.Now().Unix() {
@ -120,7 +122,8 @@ func (c *ApiController) GetSessionData() *SessionData {
sessionData := &SessionData{}
err := util.JsonToStruct(session.(string), sessionData)
if err != nil {
panic(err)
logs.Error("GetSessionData failed, error: %s", err)
return nil
}
return sessionData

View File

@ -0,0 +1,94 @@
// 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 controllers
import (
"encoding/json"
"github.com/astaxie/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
func (c *ApiController) GetCasbinAdapters() {
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 limit == "" || page == "" {
c.Data["json"] = object.GetCasbinAdapters(owner)
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetCasbinAdapterCount(owner, field, value)))
adapters := object.GetPaginationCasbinAdapters(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
c.ResponseOk(adapters, paginator.Nums())
}
}
func (c *ApiController) GetCasbinAdapter() {
id := c.Input().Get("id")
c.Data["json"] = object.GetCasbinAdapter(id)
c.ServeJSON()
}
func (c *ApiController) UpdateCasbinAdapter() {
id := c.Input().Get("id")
var casbinAdapter object.CasbinAdapter
err := json.Unmarshal(c.Ctx.Input.RequestBody, &casbinAdapter)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateCasbinAdapter(id, &casbinAdapter))
c.ServeJSON()
}
func (c *ApiController) AddCasbinAdapter() {
var casbinAdapter object.CasbinAdapter
err := json.Unmarshal(c.Ctx.Input.RequestBody, &casbinAdapter)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddCasbinAdapter(&casbinAdapter))
c.ServeJSON()
}
func (c *ApiController) DeleteCasbinAdapter() {
var casbinAdapter object.CasbinAdapter
err := json.Unmarshal(c.Ctx.Input.RequestBody, &casbinAdapter)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteCasbinAdapter(&casbinAdapter))
c.ServeJSON()
}
func (c *ApiController) SyncPolicies() {
id := c.Input().Get("id")
adapter := object.GetCasbinAdapter(id)
c.Data["json"] = object.SyncPolicies(adapter)
c.ServeJSON()
}

View File

@ -76,7 +76,8 @@ func (c *ApiController) UpdateCert() {
var cert object.Cert
err := json.Unmarshal(c.Ctx.Input.RequestBody, &cert)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateCert(id, &cert))
@ -94,7 +95,8 @@ func (c *ApiController) AddCert() {
var cert object.Cert
err := json.Unmarshal(c.Ctx.Input.RequestBody, &cert)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddCert(&cert))
@ -112,7 +114,8 @@ func (c *ApiController) DeleteCert() {
var cert object.Cert
err := json.Unmarshal(c.Ctx.Input.RequestBody, &cert)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteCert(&cert))

View File

@ -30,7 +30,8 @@ func (c *ApiController) Enforce() {
var permissionRule object.PermissionRule
err := json.Unmarshal(c.Ctx.Input.RequestBody, &permissionRule)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = object.Enforce(userId, &permissionRule)
@ -47,7 +48,8 @@ func (c *ApiController) BatchEnforce() {
var permissionRules []object.PermissionRule
err := json.Unmarshal(c.Ctx.Input.RequestBody, &permissionRules)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = object.BatchEnforce(userId, permissionRules)

View File

@ -199,7 +199,8 @@ func (c *ApiController) DeleteLdap() {
var ldap object.Ldap
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ldap)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
object.GetLdapAutoSynchronizer().StopAutoSync(ldap.Id)
@ -217,7 +218,8 @@ func (c *ApiController) SyncLdapUsers() {
var users []object.LdapRespUser
err := json.Unmarshal(c.Ctx.Input.RequestBody, &users)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
object.UpdateLdapSyncTime(ldapId)
@ -239,7 +241,8 @@ func (c *ApiController) CheckLdapUsersExist() {
var uuids []string
err := json.Unmarshal(c.Ctx.Input.RequestBody, &uuids)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
exist := object.CheckLdapUuidExist(owner, uuids)

View File

@ -37,7 +37,8 @@ func (c *ApiController) Unlink() {
var form LinkForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
providerType := form.ProviderType

View File

@ -76,7 +76,8 @@ func (c *ApiController) UpdateModel() {
var model object.Model
err := json.Unmarshal(c.Ctx.Input.RequestBody, &model)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateModel(id, &model))
@ -94,7 +95,8 @@ func (c *ApiController) AddModel() {
var model object.Model
err := json.Unmarshal(c.Ctx.Input.RequestBody, &model)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddModel(&model))
@ -112,7 +114,8 @@ func (c *ApiController) DeleteModel() {
var model object.Model
err := json.Unmarshal(c.Ctx.Input.RequestBody, &model)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteModel(&model))

View File

@ -76,7 +76,8 @@ func (c *ApiController) UpdateOrganization() {
var organization object.Organization
err := json.Unmarshal(c.Ctx.Input.RequestBody, &organization)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization))
@ -94,7 +95,8 @@ func (c *ApiController) AddOrganization() {
var organization object.Organization
err := json.Unmarshal(c.Ctx.Input.RequestBody, &organization)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddOrganization(&organization))
@ -112,9 +114,30 @@ func (c *ApiController) DeleteOrganization() {
var organization object.Organization
err := json.Unmarshal(c.Ctx.Input.RequestBody, &organization)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteOrganization(&organization))
c.ServeJSON()
}
// GetDefaultApplication ...
// @Title GetDefaultApplication
// @Tag Organization API
// @Description get default application
// @Param id query string true "organization id"
// @Success 200 {object} Response The Response object
// @router /get-default-application [get]
func (c *ApiController) GetDefaultApplication() {
userId := c.GetSessionUsername()
id := c.Input().Get("id")
application := object.GetMaskedApplication(object.GetDefaultApplication(id), userId)
if application == nil {
c.ResponseError("Please set a default application for this organization")
return
}
c.ResponseOk(application)
}

View File

@ -95,7 +95,8 @@ func (c *ApiController) UpdatePayment() {
var payment object.Payment
err := json.Unmarshal(c.Ctx.Input.RequestBody, &payment)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdatePayment(id, &payment))
@ -113,7 +114,8 @@ func (c *ApiController) AddPayment() {
var payment object.Payment
err := json.Unmarshal(c.Ctx.Input.RequestBody, &payment)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddPayment(&payment))
@ -131,7 +133,8 @@ func (c *ApiController) DeletePayment() {
var payment object.Payment
err := json.Unmarshal(c.Ctx.Input.RequestBody, &payment)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeletePayment(&payment))
@ -157,7 +160,8 @@ func (c *ApiController) NotifyPayment() {
if ok {
_, err := c.Ctx.ResponseWriter.Write([]byte("success"))
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
} else {
panic(fmt.Errorf("NotifyPayment() failed: %v", ok))

View File

@ -94,7 +94,8 @@ func (c *ApiController) UpdatePermission() {
var permission object.Permission
err := json.Unmarshal(c.Ctx.Input.RequestBody, &permission)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdatePermission(id, &permission))
@ -112,7 +113,8 @@ func (c *ApiController) AddPermission() {
var permission object.Permission
err := json.Unmarshal(c.Ctx.Input.RequestBody, &permission)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddPermission(&permission))
@ -130,7 +132,8 @@ func (c *ApiController) DeletePermission() {
var permission object.Permission
err := json.Unmarshal(c.Ctx.Input.RequestBody, &permission)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeletePermission(&permission))

View File

@ -80,7 +80,8 @@ func (c *ApiController) UpdateProduct() {
var product object.Product
err := json.Unmarshal(c.Ctx.Input.RequestBody, &product)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateProduct(id, &product))
@ -98,7 +99,8 @@ func (c *ApiController) AddProduct() {
var product object.Product
err := json.Unmarshal(c.Ctx.Input.RequestBody, &product)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddProduct(&product))
@ -116,7 +118,8 @@ func (c *ApiController) DeleteProduct() {
var product object.Product
err := json.Unmarshal(c.Ctx.Input.RequestBody, &product)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteProduct(&product))

View File

@ -76,7 +76,8 @@ func (c *ApiController) UpdateProvider() {
var provider object.Provider
err := json.Unmarshal(c.Ctx.Input.RequestBody, &provider)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateProvider(id, &provider))
@ -94,7 +95,8 @@ func (c *ApiController) AddProvider() {
var provider object.Provider
err := json.Unmarshal(c.Ctx.Input.RequestBody, &provider)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddProvider(&provider))
@ -112,7 +114,8 @@ func (c *ApiController) DeleteProvider() {
var provider object.Provider
err := json.Unmarshal(c.Ctx.Input.RequestBody, &provider)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteProvider(&provider))

View File

@ -59,7 +59,8 @@ func (c *ApiController) GetRecordsByFilter() {
record := &object.Record{}
err := util.JsonToStruct(body, record)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = object.GetRecordsByField(record)

View File

@ -72,7 +72,8 @@ func (c *ApiController) UpdateResource() {
var resource object.Resource
err := json.Unmarshal(c.Ctx.Input.RequestBody, &resource)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateResource(id, &resource))
@ -87,7 +88,8 @@ func (c *ApiController) AddResource() {
var resource object.Resource
err := json.Unmarshal(c.Ctx.Input.RequestBody, &resource)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddResource(&resource))
@ -102,7 +104,8 @@ func (c *ApiController) DeleteResource() {
var resource object.Resource
err := json.Unmarshal(c.Ctx.Input.RequestBody, &resource)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
provider, _, ok := c.GetProviderFromContext("Storage")

View File

@ -76,7 +76,8 @@ func (c *ApiController) UpdateRole() {
var role object.Role
err := json.Unmarshal(c.Ctx.Input.RequestBody, &role)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateRole(id, &role))
@ -94,7 +95,8 @@ func (c *ApiController) AddRole() {
var role object.Role
err := json.Unmarshal(c.Ctx.Input.RequestBody, &role)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddRole(&role))
@ -112,7 +114,8 @@ func (c *ApiController) DeleteRole() {
var role object.Role
err := json.Unmarshal(c.Ctx.Input.RequestBody, &role)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteRole(&role))

View File

@ -76,7 +76,8 @@ func (c *ApiController) UpdateSyncer() {
var syncer object.Syncer
err := json.Unmarshal(c.Ctx.Input.RequestBody, &syncer)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateSyncer(id, &syncer))
@ -94,7 +95,8 @@ func (c *ApiController) AddSyncer() {
var syncer object.Syncer
err := json.Unmarshal(c.Ctx.Input.RequestBody, &syncer)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddSyncer(&syncer))
@ -112,7 +114,8 @@ func (c *ApiController) DeleteSyncer() {
var syncer object.Syncer
err := json.Unmarshal(c.Ctx.Input.RequestBody, &syncer)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteSyncer(&syncer))

View File

@ -0,0 +1,82 @@
// 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 controllers
import (
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
type SystemInfo struct {
MemoryUsed uint64 `json:"memory_used"`
MemoryTotal uint64 `json:"memory_total"`
CpuUsage []float64 `json:"cpu_usage"`
}
// GetSystemInfo
// @Title GetSystemInfo
// @Tag System API
// @Description get user's system info
// @Param id query string true "The id of the user"
// @Success 200 {object} object.SystemInfo The Response object
// @router /get-system-info [get]
func (c *ApiController) GetSystemInfo() {
id := c.GetString("id")
if id == "" {
id = c.GetSessionUsername()
}
user := object.GetUser(id)
if user == nil || !user.IsGlobalAdmin {
c.ResponseError("You are not authorized to access this resource")
return
}
cpuUsage, err := util.GetCpuUsage()
if err != nil {
c.ResponseError(err.Error())
return
}
memoryUsed, memoryTotal, err := util.GetMemoryUsage()
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = SystemInfo{
CpuUsage: cpuUsage,
MemoryUsed: memoryUsed,
MemoryTotal: memoryTotal,
}
c.ServeJSON()
}
// GitRepoVersion
// @Title GitRepoVersion
// @Tag System API
// @Description get local github repo's latest release version info
// @Success 200 {string} local latest version hash of casdoor
// @router /get-release [get]
func (c *ApiController) GitRepoVersion() {
version, err := util.GetGitRepoVersion()
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = version
c.ServeJSON()
}

View File

@ -79,7 +79,8 @@ func (c *ApiController) UpdateToken() {
var token object.Token
err := json.Unmarshal(c.Ctx.Input.RequestBody, &token)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateToken(id, &token))
@ -97,7 +98,8 @@ func (c *ApiController) AddToken() {
var token object.Token
err := json.Unmarshal(c.Ctx.Input.RequestBody, &token)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddToken(&token))
@ -115,7 +117,8 @@ func (c *ApiController) DeleteToken() {
var token object.Token
err := json.Unmarshal(c.Ctx.Input.RequestBody, &token)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteToken(&token))

View File

@ -119,12 +119,7 @@ func (c *ApiController) GetUser() {
user = object.GetUser(id)
}
if user != nil {
roles := object.GetRolesByUser(user.GetId())
user.Roles = roles
permissions := object.GetPermissionsByUser(user.GetId())
user.Permissions = permissions
}
object.ExtendUserWithRolesAndPermissions(user)
c.Data["json"] = object.GetMaskedUser(user)
c.ServeJSON()
@ -149,7 +144,8 @@ func (c *ApiController) UpdateUser() {
var user object.User
err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
if user.DisplayName == "" {
@ -183,7 +179,8 @@ func (c *ApiController) AddUser() {
var user object.User
err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddUser(&user))
@ -201,7 +198,8 @@ func (c *ApiController) DeleteUser() {
var user object.User
err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteUser(&user))
@ -220,7 +218,8 @@ func (c *ApiController) GetEmailAndPhone() {
var form RequestForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
user := object.GetUserByFields(form.Organization, form.Username)
@ -306,7 +305,8 @@ func (c *ApiController) CheckUserPassword() {
var user object.User
err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
_, msg := object.CheckUserPassword(user.Owner, user.Name, user.Password)

View File

@ -24,17 +24,18 @@ import (
"github.com/casdoor/casdoor/util"
)
func saveFile(path string, file *multipart.File) {
func saveFile(path string, file *multipart.File) (err error) {
f, err := os.Create(path)
if err != nil {
panic(err)
return err
}
defer f.Close()
_, err = io.Copy(f, *file)
if err != nil {
panic(err)
return err
}
return nil
}
func (c *ApiController) UploadUsers() {
@ -43,13 +44,18 @@ func (c *ApiController) UploadUsers() {
file, header, err := c.Ctx.Request.FormFile("file")
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
fileId := fmt.Sprintf("%s_%s_%s", owner, user, util.RemoveExt(header.Filename))
path := util.GetUploadXlsxPath(fileId)
util.EnsureFileFolderExists(path)
saveFile(path, &file)
err = saveFile(path, &file)
if err != nil {
c.ResponseError(err.Error())
return
}
affected := object.UploadUsers(owner, fileId)
if affected {

View File

@ -23,9 +23,8 @@ import (
"github.com/casdoor/casdoor/util"
)
// ResponseOk ...
func (c *ApiController) ResponseOk(data ...interface{}) {
resp := Response{Status: "ok"}
// ResponseJsonData ...
func (c *ApiController) ResponseJsonData(resp *Response, data ...interface{}) {
switch len(data) {
case 2:
resp.Data2 = data[1]
@ -37,18 +36,16 @@ func (c *ApiController) ResponseOk(data ...interface{}) {
c.ServeJSON()
}
// ResponseOk ...
func (c *ApiController) ResponseOk(data ...interface{}) {
resp := &Response{Status: "ok"}
c.ResponseJsonData(resp, data...)
}
// ResponseError ...
func (c *ApiController) ResponseError(error string, data ...interface{}) {
resp := Response{Status: "error", Msg: error}
switch len(data) {
case 2:
resp.Data2 = data[1]
fallthrough
case 1:
resp.Data = data[0]
}
c.Data["json"] = resp
c.ServeJSON()
resp := &Response{Status: "error", Msg: error}
c.ResponseJsonData(resp, data...)
}
// SetTokenErrorHttpStatus ...
@ -78,13 +75,8 @@ func (c *ApiController) RequireSignedIn() (string, bool) {
return userId, true
}
func getInitScore() int {
score, err := strconv.Atoi(conf.GetConfigString("initScore"))
if err != nil {
panic(err)
}
return score
func getInitScore() (int, error) {
return strconv.Atoi(conf.GetConfigString("initScore"))
}
func (c *ApiController) GetProviderFromContext(category string) (*object.Provider, *object.User, bool) {

View File

@ -76,7 +76,8 @@ func (c *ApiController) UpdateWebhook() {
var webhook object.Webhook
err := json.Unmarshal(c.Ctx.Input.RequestBody, &webhook)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateWebhook(id, &webhook))
@ -94,7 +95,8 @@ func (c *ApiController) AddWebhook() {
var webhook object.Webhook
err := json.Unmarshal(c.Ctx.Input.RequestBody, &webhook)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddWebhook(&webhook))
@ -112,7 +114,8 @@ func (c *ApiController) DeleteWebhook() {
var webhook object.Webhook
err := json.Unmarshal(c.Ctx.Input.RequestBody, &webhook)
if err != nil {
panic(err)
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteWebhook(&webhook))

70
deployment/deploy.go Normal file
View File

@ -0,0 +1,70 @@
// 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 deployment
import (
"fmt"
"os"
"strings"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/storage"
"github.com/casdoor/casdoor/util"
"github.com/casdoor/oss"
)
func deployStaticFiles(provider *object.Provider) {
storageProvider := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, provider.Endpoint)
if storageProvider == nil {
panic(fmt.Sprintf("the provider type: %s is not supported", provider.Type))
}
uploadFolder(storageProvider, "js")
uploadFolder(storageProvider, "css")
updateHtml(provider.Domain)
}
func uploadFolder(storageProvider oss.StorageInterface, folder string) {
path := fmt.Sprintf("../web/build/static/%s/", folder)
filenames := util.ListFiles(path)
for _, filename := range filenames {
if !strings.HasSuffix(filename, folder) {
continue
}
file, err := os.Open(path + filename)
if err != nil {
panic(err)
}
objectKey := fmt.Sprintf("static/%s/%s", folder, filename)
_, err = storageProvider.Put(objectKey, file)
if err != nil {
panic(err)
}
fmt.Printf("Uploaded [%s] to [%s]\n", path, objectKey)
}
}
func updateHtml(domainPath string) {
htmlPath := "../web/build/index.html"
html := util.ReadStringFromPath(htmlPath)
html = strings.Replace(html, "\"/static/", fmt.Sprintf("\"%s", domainPath), -1)
util.WriteStringToPath(html, htmlPath)
fmt.Printf("Updated HTML to [%s]\n", html)
}

29
deployment/deploy_test.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.
//go:build !skipCi
// +build !skipCi
package deployment
import (
"testing"
"github.com/casdoor/casdoor/object"
)
func TestDeployStaticFiles(t *testing.T) {
provider := object.GetProvider("admin/provider_storage_aliyun_oss")
deployStaticFiles(provider)
}

7
go.mod
View File

@ -21,6 +21,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/google/go-cmp v0.5.8 // indirect
github.com/google/uuid v1.2.0
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/lestrrat-go/jwx v0.9.0
@ -31,13 +32,17 @@ require (
github.com/russellhaering/gosaml2 v0.6.0
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/smartystreets/goconvey v1.6.4 // indirect
github.com/stretchr/testify v1.7.0
github.com/stretchr/testify v1.8.0
github.com/tealeg/xlsx v1.0.5
github.com/thanhpk/randstr v1.0.4
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/crypto v0.0.0-20220208233918-bba287dce954
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gopkg.in/ini.v1 v1.62.0 // indirect

28
go.sum
View File

@ -156,6 +156,8 @@ github.com/go-ldap/ldap/v3 v3.3.0 h1:lwx+SJpgOHd8tG6SumBQZXCmNX51zM8B1cfxJ5gv4tQ
github.com/go-ldap/ldap/v3 v3.3.0/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-pay/gopay v1.5.72 h1:3zm64xMBhJBa8rXbm//q5UiGgOa4WO5XYEnU394N2Zw=
github.com/go-pay/gopay v1.5.72/go.mod h1:0qOGIJuFW7PKDOjmecwKyW0mgsVImgwB9yPJj0ilpn8=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@ -219,8 +221,9 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -376,6 +379,8 @@ github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 h1:X+yvsM2yrEktyI+b2qND5gpH8YhURn0k8OCaeRnkINo=
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
github.com/siddontang/goredis v0.0.0-20150324035039-760763f78400/go.mod h1:DDcKzU3qCuvj/tPnimWSsZZzvk9qvkvrIL5naVBPh5s=
github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA=
@ -389,13 +394,15 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9
github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
@ -406,6 +413,10 @@ github.com/tencentcloud/tencentcloud-sdk-go v1.0.154 h1:THBgwGwUQtsw6L53cSSA2wwL
github.com/tencentcloud/tencentcloud-sdk-go v1.0.154/go.mod h1:asUz5BPXxgoPGaRgZaVm1iGcUAuHyYUo1nXqKa83cvI=
github.com/thanhpk/randstr v1.0.4 h1:IN78qu/bR+My+gHCvMEXhR/i5oriVHcTB/BJJIRTsNo=
github.com/thanhpk/randstr v1.0.4/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U=
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o=
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
github.com/volcengine/volc-sdk-golang v1.0.19 h1:jJp+aJgK0e//rZ9I0K2Y7ufJwvuZRo/AQsYDynXMNgA=
github.com/volcengine/volc-sdk-golang v1.0.19/go.mod h1:+GGi447k4p1I5PNdbpG2GLaF0Ui9vIInTojMM0IfSS4=
@ -417,6 +428,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/gopher-lua v0.0.0-20171031051903-609c9cd26973/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
@ -537,6 +550,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -563,8 +577,10 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211020174200-9d6173849985/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -626,7 +642,6 @@ golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4X
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@ -739,8 +754,9 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -121,6 +121,7 @@ func (idp *DingTalkIdProvider) GetToken(code string) (*oauth2.Token, error) {
type DingTalkUserResponse struct {
Nick string `json:"nick"`
OpenId string `json:"openId"`
UnionId string `json:"unionId"`
AvatarUrl string `json:"avatarUrl"`
Email string `json:"email"`
Errmsg string `json:"message"`
@ -162,6 +163,7 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
Id: dtUserInfo.OpenId,
Username: dtUserInfo.Nick,
DisplayName: dtUserInfo.Nick,
UnionId: dtUserInfo.UnionId,
Email: dtUserInfo.Email,
AvatarUrl: dtUserInfo.AvatarUrl,
}

View File

@ -25,6 +25,7 @@ type UserInfo struct {
Id string
Username string
DisplayName string
UnionId string
Email string
AvatarUrl string
}

View File

@ -45,7 +45,8 @@ func main() {
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
// beego.DelStaticPath("/static")
beego.SetStaticPath("/static", "web/build/static")
// beego.SetStaticPath("/static", "web/build/static")
beego.BConfig.WebConfig.DirectoryIndex = true
beego.SetStaticPath("/swagger", "swagger")
beego.SetStaticPath("/files", "files")

View File

@ -43,7 +43,7 @@ func InitConfig() {
}
func InitAdapter(createDatabase bool) {
adapter = NewAdapter(conf.GetConfigString("driverName"), conf.GetBeegoConfDataSourceName(), conf.GetConfigString("dbName"))
adapter = NewAdapter(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName(), conf.GetConfigString("dbName"))
if createDatabase {
adapter.CreateDatabase()
}
@ -145,6 +145,11 @@ func (a *Adapter) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(CasbinAdapter))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Provider))
if err != nil {
panic(err)

View File

@ -17,6 +17,7 @@ package object
import (
"fmt"
"net/url"
"regexp"
"strings"
"github.com/casdoor/casdoor/util"
@ -66,6 +67,9 @@ type Application struct {
TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"`
SignupHtml string `xorm:"mediumtext" json:"signupHtml"`
SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
FormCss string `xorm:"text" json:"formCss"`
FormOffset int `json:"formOffset"`
FormBackgroundUrl string `xorm:"varchar(200)" json:"formBackgroundUrl"`
}
func GetApplicationCount(owner, field, value string) int {
@ -319,7 +323,8 @@ func (application *Application) GetId() string {
func CheckRedirectUriValid(application *Application, redirectUri string) bool {
validUri := false
for _, tmpUri := range application.RedirectUris {
if strings.Contains(redirectUri, tmpUri) {
tmpUriRegex := regexp.MustCompile(tmpUri)
if tmpUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, tmpUri) {
validUri = true
break
}
@ -362,3 +367,34 @@ func IsAllowOrigin(origin string) bool {
return allowOrigin
}
func getApplicationMap(organization string) map[string]*Application {
applications := GetApplicationsByOrganizationName("admin", organization)
applicationMap := make(map[string]*Application)
for _, application := range applications {
applicationMap[application.Name] = application
}
return applicationMap
}
func ExtendManagedAccountsWithUser(user *User) *User {
if user.ManagedAccounts == nil || len(user.ManagedAccounts) == 0 {
return user
}
applicationMap := getApplicationMap(user.Owner)
var managedAccounts []ManagedAccount
for _, managedAccount := range user.ManagedAccounts {
application := applicationMap[managedAccount.Application]
if application != nil {
managedAccount.SigninUrl = application.SigninUrl
managedAccounts = append(managedAccounts, managedAccount)
}
}
user.ManagedAccounts = managedAccounts
return user
}

View File

@ -73,6 +73,10 @@ func (application *Application) IsSignupItemRequired(itemName string) bool {
return signupItem.Required
}
func (si *SignupItem) isSignupItemPrompted() bool {
return si.Visible && si.Prompted
}
func (application *Application) GetSignupItemRule(itemName string) string {
signupItem := application.getSignupItem(itemName)
if signupItem == nil {
@ -92,6 +96,16 @@ func (application *Application) getAllPromptedProviderItems() []*ProviderItem {
return res
}
func (application *Application) getAllPromptedSignupItems() []*SignupItem {
res := []*SignupItem{}
for _, signupItem := range application.SignupItems {
if signupItem.isSignupItemPrompted() {
res = append(res, signupItem)
}
}
return res
}
func (application *Application) isAffiliationPrompted() bool {
signupItem := application.getSignupItem("Affiliation")
if signupItem == nil {
@ -107,5 +121,10 @@ func (application *Application) HasPromptPage() bool {
return true
}
signupItems := application.getAllPromptedSignupItems()
if len(signupItems) != 0 {
return true
}
return application.isAffiliationPrompted()
}

217
object/casbin_adapter.go Normal file
View File

@ -0,0 +1,217 @@
// 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 object
import (
"fmt"
"strings"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
xormadapter "github.com/casbin/xorm-adapter/v2"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
type CasbinAdapter struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
Organization string `xorm:"varchar(100)" json:"organization"`
Type string `xorm:"varchar(100)" json:"type"`
Model string `xorm:"varchar(100)" json:"model"`
Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"`
User string `xorm:"varchar(100)" json:"user"`
Password string `xorm:"varchar(100)" json:"password"`
DatabaseType string `xorm:"varchar(100)" json:"databaseType"`
Database string `xorm:"varchar(100)" json:"database"`
Table string `xorm:"varchar(100)" json:"table"`
IsEnabled bool `json:"isEnabled"`
Adapter *xormadapter.Adapter `xorm:"-" json:"-"`
}
func GetCasbinAdapterCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&CasbinAdapter{})
if err != nil {
panic(err)
}
return int(count)
}
func GetCasbinAdapters(owner string) []*CasbinAdapter {
adapters := []*CasbinAdapter{}
err := adapter.Engine.Where("owner = ?", owner).Find(&adapters)
if err != nil {
panic(err)
}
return adapters
}
func GetPaginationCasbinAdapters(owner string, page, limit int, field, value, sort, order string) []*CasbinAdapter {
session := GetSession(owner, page, limit, field, value, sort, order)
adapters := []*CasbinAdapter{}
err := session.Find(&adapters)
if err != nil {
panic(err)
}
return adapters
}
func getCasbinAdapter(owner, name string) *CasbinAdapter {
if owner == "" || name == "" {
return nil
}
casbinAdapter := CasbinAdapter{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&casbinAdapter)
if err != nil {
panic(err)
}
if existed {
return &casbinAdapter
} else {
return nil
}
}
func GetCasbinAdapter(id string) *CasbinAdapter {
owner, name := util.GetOwnerAndNameFromId(id)
return getCasbinAdapter(owner, name)
}
func UpdateCasbinAdapter(id string, casbinAdapter *CasbinAdapter) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getCasbinAdapter(owner, name) == nil {
return false
}
session := adapter.Engine.ID(core.PK{owner, name}).AllCols()
if casbinAdapter.Password == "***" {
session.Omit("password")
}
affected, err := session.Update(casbinAdapter)
if err != nil {
panic(err)
}
return affected != 0
}
func AddCasbinAdapter(casbinAdapter *CasbinAdapter) bool {
affected, err := adapter.Engine.Insert(casbinAdapter)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteCasbinAdapter(casbinAdapter *CasbinAdapter) bool {
affected, err := adapter.Engine.ID(core.PK{casbinAdapter.Owner, casbinAdapter.Name}).Delete(&CasbinAdapter{})
if err != nil {
panic(err)
}
return affected != 0
}
func (casbinAdapter *CasbinAdapter) GetId() string {
return fmt.Sprintf("%s/%s", casbinAdapter.Owner, casbinAdapter.Name)
}
func (casbinAdapter *CasbinAdapter) getTable() string {
if casbinAdapter.DatabaseType == "mssql" {
return fmt.Sprintf("[%s]", casbinAdapter.Table)
} else {
return casbinAdapter.Table
}
}
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 {
// init Adapter
if casbinAdapter.Adapter == nil {
var dataSourceName string
if casbinAdapter.DatabaseType == "mssql" {
dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", casbinAdapter.User, casbinAdapter.Password, casbinAdapter.Host, casbinAdapter.Port, casbinAdapter.Database)
} else if casbinAdapter.DatabaseType == "postgres" {
dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=disable dbname=%s", casbinAdapter.User, casbinAdapter.Password, casbinAdapter.Host, casbinAdapter.Port, casbinAdapter.Database)
} else {
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", casbinAdapter.User, casbinAdapter.Password, casbinAdapter.Host, casbinAdapter.Port)
}
if !isCloudIntranet {
dataSourceName = strings.ReplaceAll(dataSourceName, "dbi.", "db.")
}
casbinAdapter.Adapter, _ = xormadapter.NewAdapterByEngineWithTableName(NewAdapter(casbinAdapter.DatabaseType, dataSourceName, casbinAdapter.Database).Engine, casbinAdapter.getTable(), "")
}
// init Model
modelObj := getModel(casbinAdapter.Owner, casbinAdapter.Model)
m, err := model.NewModelFromString(modelObj.ModelText)
if err != nil {
panic(err)
}
// init Enforcer
enforcer, err := casbin.NewEnforcer(m, casbinAdapter.Adapter)
if err != nil {
panic(err)
}
policies := matrixToCasbinRules("p", enforcer.GetPolicy())
if strings.Contains(modelObj.ModelText, "[role_definition]") {
policies = append(policies, matrixToCasbinRules("g", enforcer.GetGroupingPolicy())...)
}
return policies
}

View File

@ -62,10 +62,10 @@ func CheckUserSignup(application *Application, organization *Organization, usern
if HasUserByField(organization.Name, "name", username) {
return "username already exists"
}
if HasUserByField(organization.Name, "email", username) {
if HasUserByField(organization.Name, "email", email) {
return "email already exists"
}
if HasUserByField(organization.Name, "phone", username) {
if HasUserByField(organization.Name, "phone", phone) {
return "phone already exists"
}
}
@ -302,6 +302,10 @@ func CheckAccessPermission(userId string, application *Application) (bool, error
}
if isHit {
containsAsterisk := ContainsAsterisk(userId, permission.Users)
if containsAsterisk {
return true, err
}
enforcer := getEnforcer(permission)
allowed, err = enforcer.Enforce(userId, application.Name, "read")
break

View File

@ -16,10 +16,28 @@
package object
import "github.com/go-gomail/gomail"
import (
"crypto/tls"
"github.com/go-gomail/gomail"
)
func getDialer(provider *Provider) *gomail.Dialer {
dialer := &gomail.Dialer{}
if provider.Type == "SUBMAIL" {
dialer = gomail.NewDialer(provider.Host, provider.Port, provider.AppId, provider.ClientSecret)
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
} else {
dialer = gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
}
dialer.SSL = !provider.DisableSsl
return dialer
}
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
dialer := gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
dialer := getDialer(provider)
message := gomail.NewMessage()
message.SetAddressHeader("From", provider.ClientId, sender)
@ -32,8 +50,7 @@ func SendEmail(provider *Provider, title string, content string, dest string, se
// DailSmtpServer Dail Smtp server
func DailSmtpServer(provider *Provider) error {
dialer := gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
dialer.SSL = !provider.DisableSsl
dialer := getDialer(provider)
sender, err := dialer.Dial()
if err != nil {

View File

@ -19,14 +19,17 @@ import (
"fmt"
"os"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
"github.com/duo-labs/webauthn/webauthn"
)
func InitDb() {
MigratePermissionRule()
existed := initBuiltInOrganization()
if !existed {
initBuiltInModel()
initBuiltInPermission()
initBuiltInProvider()
initBuiltInUser()
@ -38,8 +41,6 @@ func InitDb() {
initWebAuthn()
}
var staticBaseUrl = beego.AppConfig.String("staticBaseUrl")
func initBuiltInOrganization() bool {
organization := getOrganization("admin", "built-in")
if organization != nil {
@ -52,10 +53,10 @@ func initBuiltInOrganization() bool {
CreatedTime: util.GetCurrentTime(),
DisplayName: "Built-in Organization",
WebsiteUrl: "https://example.com",
Favicon: fmt.Sprintf("%s/img/casbin/favicon.ico", staticBaseUrl),
Favicon: fmt.Sprintf("%s/img/casbin/favicon.ico", conf.GetConfigString("staticBaseUrl")),
PasswordType: "plain",
PhonePrefix: "86",
DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", staticBaseUrl),
DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
Tags: []string{},
AccountItems: []*AccountItem{
{Name: "Organization", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
@ -84,6 +85,7 @@ func initBuiltInOrganization() bool {
{Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
},
}
AddOrganization(organization)
@ -104,7 +106,7 @@ func initBuiltInUser() {
Type: "normal-user",
Password: "123",
DisplayName: "Admin",
Avatar: fmt.Sprintf("%s/img/casbin.svg", staticBaseUrl),
Avatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
Email: "admin@example.com",
Phone: "12345678910",
Address: []string{},
@ -134,7 +136,7 @@ func initBuiltInApplication() {
Name: "app-built-in",
CreatedTime: util.GetCurrentTime(),
DisplayName: "Casdoor",
Logo: fmt.Sprintf("%s/img/casdoor-logo_1185x256.png", staticBaseUrl),
Logo: fmt.Sprintf("%s/img/casdoor-logo_1185x256.png", conf.GetConfigString("staticBaseUrl")),
HomepageUrl: "https://casdoor.org",
Organization: "built-in",
Cert: "cert-built-in",
@ -155,6 +157,7 @@ func initBuiltInApplication() {
},
RedirectUris: []string{},
ExpireInHours: 168,
FormOffset: 8,
}
AddApplication(application)
}
@ -238,6 +241,33 @@ func initWebAuthn() {
gob.Register(webauthn.SessionData{})
}
func initBuiltInModel() {
model := GetModel("built-in/model-built-in")
if model != nil {
return
}
model = &Model{
Owner: "built-in",
Name: "model-built-in",
CreatedTime: util.GetCurrentTime(),
DisplayName: "Built-in Model",
IsEnabled: true,
ModelText: `[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act`,
}
AddModel(model)
}
func initBuiltInPermission() {
permission := GetPermission("built-in/permission-built-in")
if permission != nil {
@ -249,9 +279,10 @@ func initBuiltInPermission() {
Name: "permission-built-in",
CreatedTime: util.GetCurrentTime(),
DisplayName: "Built-in Permission",
Users: []string{"built-in/admin"},
Users: []string{"built-in/*"},
Roles: []string{},
Domains: []string{},
Model: "model-built-in",
ResourceType: "Application",
Resources: []string{"app-built-in"},
Actions: []string{"Read", "Write", "Admin"},

View File

@ -17,6 +17,7 @@ package object
import (
"fmt"
"github.com/casbin/casbin/v2/model"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
@ -85,13 +86,19 @@ func GetModel(id string) *Model {
return getModel(owner, name)
}
func UpdateModel(id string, model *Model) bool {
func UpdateModel(id string, modelObj *Model) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getModel(owner, name) == nil {
return false
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(model)
// check model grammar
_, err := model.NewModelFromString(modelObj.ModelText)
if err != nil {
panic(err)
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(modelObj)
if err != nil {
panic(err)
}

View File

@ -43,6 +43,11 @@ type OidcDiscovery struct {
}
func getOriginFromHost(host string) (string, string) {
origin := conf.GetConfigString("origin")
if origin != "" {
return origin, origin
}
protocol := "https://"
if strings.HasPrefix(host, "localhost") {
protocol = "http://"
@ -58,12 +63,6 @@ func getOriginFromHost(host string) (string, string) {
func GetOidcDiscovery(host string) OidcDiscovery {
originFrontend, originBackend := getOriginFromHost(host)
origin := conf.GetConfigString("origin")
if origin != "" {
originFrontend = origin
originBackend = origin
}
// Examples:
// https://login.okta.com/.well-known/openid-configuration
// https://auth0.auth0.com/.well-known/openid-configuration

View File

@ -41,12 +41,13 @@ type Organization struct {
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PhonePrefix string `xorm:"varchar(10)" json:"phonePrefix"`
DefaultAvatar string `xorm:"varchar(100)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
Tags []string `xorm:"mediumtext" json:"tags"`
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
EnableSoftDeletion bool `json:"enableSoftDeletion"`
IsProfilePublic bool `json:"isProfilePublic"`
AccountItems []*AccountItem `xorm:"varchar(2000)" json:"accountItems"`
AccountItems []*AccountItem `xorm:"varchar(3000)" json:"accountItems"`
}
func GetOrganizationCount(owner, field, value string) int {
@ -216,3 +217,37 @@ func CheckAccountItemModifyRule(accountItem *AccountItem, user *User) (bool, str
}
return true, ""
}
func GetDefaultApplication(id string) *Application {
organization := GetOrganization(id)
if organization == nil {
return nil
}
if organization.DefaultApplication != "" {
return getApplication("admin", organization.DefaultApplication)
}
applications := []*Application{}
err := adapter.Engine.Asc("created_time").Find(&applications, &Application{Organization: organization.Name})
if err != nil {
panic(err)
}
if len(applications) == 0 {
return nil
}
defaultApplication := applications[0]
for _, application := range applications {
if application.EnableSignUp {
defaultApplication = application
break
}
}
extendApplicationWithProviders(defaultApplication)
extendApplicationWithOrg(defaultApplication)
return defaultApplication
}

View File

@ -16,6 +16,7 @@ package object
import (
"fmt"
"strings"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
@ -207,3 +208,44 @@ func GetPermissionsBySubmitter(owner string, submitter string) []*Permission {
return permissions
}
func MigratePermissionRule() {
models := []*Model{}
err := adapter.Engine.Find(&models, &Model{})
if err != nil {
panic(err)
}
isHit := false
for _, model := range models {
if strings.Contains(model.ModelText, "permission") {
// update model table
model.ModelText = strings.Replace(model.ModelText, "permission,", "", -1)
UpdateModel(model.GetId(), model)
isHit = true
}
}
if isHit {
// update permission_rule table
sql := "UPDATE `permission_rule`SET V0 = V1, V1 = V2, V2 = V3, V3 = V4, V4 = V5 WHERE V0 IN (SELECT CONCAT(owner, '/', name) AS permission_id FROM `permission`)"
_, err = adapter.Engine.Exec(sql)
if err != nil {
return
}
}
}
func ContainsAsterisk(userId string, users []string) bool {
containsAsterisk := false
group, _ := util.GetOwnerAndNameFromId(userId)
for _, user := range users {
permissionGroup, permissionUserName := util.GetOwnerAndNameFromId(user)
if permissionGroup == group && permissionUserName == "*" {
containsAsterisk = true
break
}
}
return containsAsterisk
}

View File

@ -29,7 +29,7 @@ func getEnforcer(permission *Permission) *casbin.Enforcer {
tableName = permission.Adapter
}
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
adapter, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetBeegoConfDataSourceName()+conf.GetConfigString("dbName"), tableName, tableNamePrefix, true)
adapter, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName()+conf.GetConfigString("dbName"), tableName, tableNamePrefix, true)
if err != nil {
panic(err)
}

View File

@ -48,6 +48,7 @@ type Provider struct {
DisableSsl bool `json:"disableSsl"`
Title string `xorm:"varchar(100)" json:"title"`
Content string `xorm:"varchar(1000)" json:"content"`
Receiver string `xorm:"varchar(100)" json:"receiver"`
RegionId string `xorm:"varchar(100)" json:"regionId"`
SignName string `xorm:"varchar(100)" json:"signName"`

View File

@ -28,7 +28,6 @@ import (
"time"
"github.com/RobotsAndPencils/go-saml"
"github.com/astaxie/beego"
"github.com/beevik/etree"
"github.com/golang-jwt/jwt/v4"
dsig "github.com/russellhaering/goxmldsig"
@ -176,16 +175,12 @@ type Attribute struct {
}
func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, error) {
//_, originBackend := getOriginFromHost(host)
cert := getCertByApplication(application)
block, _ := pem.Decode([]byte(cert.Certificate))
certificate := base64.StdEncoding.EncodeToString(block.Bytes)
origin := beego.AppConfig.String("origin")
originFrontend, originBackend := getOriginFromHost(host)
if origin != "" {
originBackend = origin
}
d := IdpEntityDescriptor{
XMLName: xml.Name{
Local: "md:EntityDescriptor",

View File

@ -70,10 +70,12 @@ func GenerateSamlLoginUrl(id, relayState string) (string, string, error) {
}
func buildSp(provider *Provider, samlResponse string) (*saml2.SAMLServiceProvider, error) {
origin := conf.GetConfigString("origin")
certStore := dsig.MemoryX509CertificateStore{
Roots: []*x509.Certificate{},
}
origin := conf.GetConfigString("origin")
certEncodedData := ""
if samlResponse != "" {
certEncodedData = parseSamlResponse(samlResponse, provider.Type)

View File

@ -103,6 +103,11 @@ func uploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffe
}
func UploadFileSafe(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffer) (string, string, error) {
// check fullFilePath is there security issue
if strings.Contains(fullFilePath, "..") {
return "", "", fmt.Errorf("the fullFilePath: %s is not allowed", fullFilePath)
}
var fileUrl string
var objectKey string
var err error

View File

@ -287,6 +287,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
}
}
ExtendUserWithRolesAndPermissions(user)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, nonce, scope, host)
if err != nil {
panic(err)
@ -421,6 +422,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
}
}
ExtendUserWithRolesAndPermissions(user)
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
if err != nil {
return &TokenError{
@ -571,6 +573,7 @@ func GetPasswordToken(application *Application, username string, password string
}
}
ExtendUserWithRolesAndPermissions(user)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
if err != nil {
return nil, &TokenError{
@ -640,6 +643,7 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
// GetTokenByUser
// Implicit flow
func GetTokenByUser(application *Application, user *User, scope string, host string) (*Token, error) {
ExtendUserWithRolesAndPermissions(user)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
if err != nil {
return nil, err
@ -726,6 +730,7 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin
AddUser(user)
}
ExtendUserWithRolesAndPermissions(user)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", host)
if err != nil {
return nil, &TokenError{

View File

@ -18,7 +18,6 @@ import (
"fmt"
"time"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
"github.com/golang-jwt/jwt/v4"
)
@ -67,11 +66,7 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour)
user.Password = ""
origin := conf.GetConfigString("origin")
_, originBackend := getOriginFromHost(host)
if origin != "" {
originBackend = origin
}
name := util.GenerateId()
jti := fmt.Sprintf("%s/%s", application.Owner, name)

View File

@ -114,6 +114,8 @@ type User struct {
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
SigninWrongTimes int `json:"signinWrongTimes"`
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
}
type Userinfo struct {
@ -128,6 +130,13 @@ type Userinfo struct {
Phone string `json:"phone,omitempty"`
}
type ManagedAccount struct {
Application string `xorm:"varchar(100)" json:"application"`
Username string `xorm:"varchar(100)" json:"username"`
Password string `xorm:"varchar(100)" json:"password"`
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
}
func GetGlobalUserCount(field, value string) int {
session := GetSession("", -1, -1, field, value, "", "")
count, err := session.Count(&User{})
@ -334,6 +343,12 @@ func GetMaskedUser(user *User) *User {
if user.Password != "" {
user.Password = "***"
}
if user.ManagedAccounts != nil {
for _, manageAccount := range user.ManagedAccounts {
manageAccount.Password = "***"
}
}
return user
}
@ -378,7 +393,7 @@ func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) boo
columns = []string{
"owner", "display_name", "avatar",
"location", "address", "region", "language", "affiliation", "title", "homepage", "bio", "score", "tag", "signup_application",
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials",
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts",
"signin_wrong_times", "last_signin_wrong_time",
}
}
@ -473,7 +488,7 @@ func AddUsers(users []*User) bool {
}
func AddUsersInBatch(users []*User) bool {
batchSize := 1000
batchSize := conf.GetConfigBatchSize()
if len(users) == 0 {
return false
@ -512,11 +527,8 @@ func GetUserInfo(userId string, scope string, aud string, host string) (*Userinf
if user == nil {
return nil, fmt.Errorf("the user: %s doesn't exist", userId)
}
origin := conf.GetConfigString("origin")
_, originBackend := getOriginFromHost(host)
if origin != "" {
originBackend = origin
}
resp := Userinfo{
Sub: user.Id,
@ -551,3 +563,12 @@ func (user *User) GetId() string {
func isUserIdGlobalAdmin(userId string) bool {
return strings.HasPrefix(userId, "built-in/")
}
func ExtendUserWithRolesAndPermissions(user *User) {
if user == nil {
return
}
user.Roles = GetRolesByUser(user.GetId())
user.Permissions = GetPermissionsByUser(user.GetId())
}

View File

@ -80,7 +80,7 @@ func SetUserField(user *User, field string, value string) bool {
value = user.Password
}
affected, err := adapter.Engine.Table(user).ID(core.PK{user.Owner, user.Name}).Update(map[string]interface{}{field: value})
affected, err := adapter.Engine.Table(user).ID(core.PK{user.Owner, user.Name}).Update(map[string]interface{}{strings.ToLower(field): value})
if err != nil {
panic(err)
}
@ -137,6 +137,12 @@ func SetUserOAuthProperties(organization *Organization, user *User, providerType
user.Email = userInfo.Email
}
}
if userInfo.UnionId != "" {
propertyName := fmt.Sprintf("oauth_%s_unionId", providerType)
setUserProperty(user, propertyName, userInfo.UnionId)
}
if userInfo.AvatarUrl != "" {
propertyName := fmt.Sprintf("oauth_%s_avatarUrl", providerType)
setUserProperty(user, propertyName, userInfo.AvatarUrl)

View File

@ -19,7 +19,7 @@ import (
"net/url"
"strings"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor/conf"
"github.com/duo-labs/webauthn/protocol"
"github.com/duo-labs/webauthn/webauthn"
)
@ -27,20 +27,17 @@ import (
func GetWebAuthnObject(host string) *webauthn.WebAuthn {
var err error
origin := beego.AppConfig.String("origin")
if origin == "" {
_, origin = getOriginFromHost(host)
}
_, originBackend := getOriginFromHost(host)
localUrl, err := url.Parse(origin)
localUrl, err := url.Parse(originBackend)
if err != nil {
panic("error when parsing origin:" + err.Error())
}
webAuthn, err := webauthn.New(&webauthn.Config{
RPDisplayName: beego.AppConfig.String("appname"), // Display Name for your site
RPDisplayName: conf.GetConfigString("appname"), // Display Name for your site
RPID: strings.Split(localUrl.Host, ":")[0], // Generally the domain name for your site, it's ok because splits cannot return empty array
RPOrigin: origin, // The origin URL for WebAuthn requests
RPOrigin: originBackend, // The origin URL for WebAuthn requests
// RPIcon: "https://duo.com/logo.png", // Optional icon URL for your site
})
if err != nil {

View File

@ -47,15 +47,15 @@ func SendVerificationCodeToEmail(organization *Organization, user *User, provide
sender := organization.DisplayName
title := provider.Title
code := getRandomCode(5)
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 := AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code); err != nil {
if err := SendEmail(provider, title, content, dest, sender); err != nil {
return err
}
return SendEmail(provider, title, content, dest, sender)
return AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code)
}
func SendVerificationCodeToPhone(organization *Organization, user *User, provider *Provider, remoteAddr string, dest string) error {
@ -63,12 +63,12 @@ func SendVerificationCodeToPhone(organization *Organization, user *User, provide
return errors.New("please set a SMS provider first")
}
code := getRandomCode(5)
if err := AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code); err != nil {
code := getRandomCode(6)
if err := SendSms(provider, code, dest); err != nil {
return err
}
return SendSms(provider, code, dest)
return AddToVerificationRecord(user, provider, remoteAddr, provider.Category, dest, code)
}
func AddToVerificationRecord(user *User, provider *Provider, remoteAddr, recordType, dest, code string) error {

View File

@ -60,6 +60,7 @@ func initAPI() {
beego.Router("/api/update-organization", &controllers.ApiController{}, "POST:UpdateOrganization")
beego.Router("/api/add-organization", &controllers.ApiController{}, "POST:AddOrganization")
beego.Router("/api/delete-organization", &controllers.ApiController{}, "POST:DeleteOrganization")
beego.Router("/api/get-default-application", &controllers.ApiController{}, "GET:GetDefaultApplication")
beego.Router("/api/get-global-users", &controllers.ApiController{}, "GET:GetGlobalUsers")
beego.Router("/api/get-users", &controllers.ApiController{}, "GET:GetUsers")
@ -96,6 +97,13 @@ func initAPI() {
beego.Router("/api/add-model", &controllers.ApiController{}, "POST:AddModel")
beego.Router("/api/delete-model", &controllers.ApiController{}, "POST:DeleteModel")
beego.Router("/api/get-adapters", &controllers.ApiController{}, "GET:GetCasbinAdapters")
beego.Router("/api/get-adapter", &controllers.ApiController{}, "GET:GetCasbinAdapter")
beego.Router("/api/update-adapter", &controllers.ApiController{}, "POST:UpdateCasbinAdapter")
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/set-password", &controllers.ApiController{}, "POST:SetPassword")
beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword")
beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "POST:GetEmailAndPhone")
@ -202,4 +210,7 @@ func initAPI() {
beego.Router("/api/webauthn/signup/finish", &controllers.ApiController{}, "Post:WebAuthnSignupFinish")
beego.Router("/api/webauthn/signin/begin", &controllers.ApiController{}, "Get:WebAuthnSigninBegin")
beego.Router("/api/webauthn/signin/finish", &controllers.ApiController{}, "Post:WebAuthnSigninFinish")
beego.Router("/api/get-system-info", &controllers.ApiController{}, "GET:GetSystemInfo")
beego.Router("/api/get-release", &controllers.ApiController{}, "GET:GitRepoVersion")
}

View File

@ -16,12 +16,19 @@ package routers
import (
"net/http"
"os"
"strings"
"github.com/astaxie/beego/context"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
)
var (
oldStaticBaseUrl = "https://cdn.casbin.org"
newStaticBaseUrl = conf.GetConfigString("staticBaseUrl")
)
func StaticFilter(ctx *context.Context) {
urlPath := ctx.Request.URL.Path
if strings.HasPrefix(urlPath, "/api/") || strings.HasPrefix(urlPath, "/.well-known/") {
@ -38,9 +45,35 @@ func StaticFilter(ctx *context.Context) {
path += urlPath
}
if util.FileExist(path) {
if !util.FileExist(path) {
path = "web/build/index.html"
}
if oldStaticBaseUrl == newStaticBaseUrl {
http.ServeFile(ctx.ResponseWriter, ctx.Request, path)
} else {
http.ServeFile(ctx.ResponseWriter, ctx.Request, "web/build/index.html")
serveFileWithReplace(ctx.ResponseWriter, ctx.Request, path, oldStaticBaseUrl, newStaticBaseUrl)
}
}
func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string, old string, new string) {
f, err := os.Open(name)
if err != nil {
panic(err)
}
defer f.Close()
d, err := f.Stat()
if err != nil {
panic(err)
}
oldContent := util.ReadStringFromPath(name)
newContent := strings.ReplaceAll(oldContent, old, new)
http.ServeContent(w, r, d.Name(), d.ModTime(), strings.NewReader(newContent))
_, err = w.Write([]byte(newContent))
if err != nil {
panic(err)
}
}

36
storage/minio_s3.go Normal file
View File

@ -0,0 +1,36 @@
// Copyright 2021 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 storage
import (
awss3 "github.com/aws/aws-sdk-go/service/s3"
"github.com/casdoor/oss"
"github.com/casdoor/oss/s3"
)
func NewMinIOS3StorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface {
sp := s3.New(&s3.Config{
AccessID: clientId,
AccessKey: clientSecret,
Region: region,
Bucket: bucket,
Endpoint: endpoint,
S3Endpoint: endpoint,
ACL: awss3.BucketCannedACLPublicRead,
S3ForcePathStyle: true,
})
return sp
}

View File

@ -22,6 +22,8 @@ func GetStorageProvider(providerType string, clientId string, clientSecret strin
return NewLocalFileSystemStorageProvider(clientId, clientSecret, region, bucket, endpoint)
case "AWS S3":
return NewAwsS3StorageProvider(clientId, clientSecret, region, bucket, endpoint)
case "MinIO":
return NewMinIOS3StorageProvider(clientId, clientSecret, region, bucket, endpoint)
case "Aliyun OSS":
return NewAliyunOssStorageProvider(clientId, clientSecret, region, bucket, endpoint)
case "Tencent Cloud COS":

View File

@ -16,6 +16,7 @@ package util
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
@ -43,6 +44,23 @@ func EnsureFileFolderExists(path string) {
}
}
func ListFiles(path string) []string {
res := []string{}
files, err := ioutil.ReadDir(path)
if err != nil {
panic(err)
}
for _, f := range files {
if !f.IsDir() {
res = append(res, f.Name())
}
}
return res
}
func RemoveExt(filename string) string {
return filename[:len(filename)-len(filepath.Ext(filename))]
}

78
util/system.go Normal file
View File

@ -0,0 +1,78 @@
// 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 (
"io/ioutil"
"os"
"runtime"
"strings"
"time"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/mem"
)
// get cpu usage
func GetCpuUsage() ([]float64, error) {
usage, err := cpu.Percent(time.Second, true)
return usage, err
}
var fileDate, version string
// get memory usage
func GetMemoryUsage() (uint64, uint64, error) {
virtualMem, err := mem.VirtualMemory()
if err != nil {
return 0, 0, err
}
var m runtime.MemStats
runtime.ReadMemStats(&m)
return m.TotalAlloc, virtualMem.Total, nil
}
// get github repo release version
func GetGitRepoVersion() (string, error) {
pwd, err := os.Getwd()
if err != nil {
return "", err
}
fileInfos, err := ioutil.ReadDir(pwd + "/.git/refs/heads")
for _, v := range fileInfos {
if v.Name() == "master" {
if v.ModTime().String() == fileDate {
return version, nil
} else {
fileDate = v.ModTime().String()
break
}
}
}
content, err := ioutil.ReadFile(pwd + "/.git/refs/heads/master")
if err != nil {
return "", err
}
// Convert to full length
temp := string(content)
version = strings.ReplaceAll(temp, "\n", "")
return version, nil
}

33
util/sysytem_test.go Normal file
View File

@ -0,0 +1,33 @@
// 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 (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetCpuUsage(t *testing.T) {
usage, err := GetCpuUsage()
assert.Nil(t, err)
t.Log(usage)
}
func TestGetMemoryUsage(t *testing.T) {
used, total, err := GetMemoryUsage()
assert.Nil(t, err)
t.Log(used, total)
}

View File

@ -4,12 +4,18 @@
"es6": true,
"node": true
},
"parser": "babel-eslint",
"parser": "@babel/eslint-parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
},
"requireConfigFile": false,
"babelOptions": {
"babelrc": false,
"configFile": false,
"presets": ["@babel/preset-react"]
}
},
"settings": {
@ -91,6 +97,7 @@
"react/jsx-key": "error",
"no-console": "error",
"eqeqeq": "error",
"keyword-spacing": "error",
"react/prop-types": "off",
"react/display-name": "off",

6
web/.stylelintrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": [
"stylelint-config-standard",
"stylelint-config-recommended-less"
]
}

17
web/babel.config.json Normal file
View File

@ -0,0 +1,17 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1"
},
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
]
]
}

View File

@ -3,35 +3,35 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/icons": "^4.6.2",
"@craco/craco": "^6.1.1",
"@crowdin/cli": "^3.6.4",
"@ant-design/icons": "^4.7.0",
"@craco/craco": "^6.4.5",
"@crowdin/cli": "^3.7.10",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"antd": "^4.15.5",
"antd": "^4.22.8",
"codemirror": "^5.61.1",
"copy-to-clipboard": "^3.3.1",
"core-js": "^3.21.1",
"craco-less": "^1.17.1",
"core-js": "^3.25.0",
"craco-less": "^2.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"file-saver": "^2.0.5",
"i18n-iso-countries": "^7.0.0",
"i18next": "^19.8.9",
"moment": "^2.29.1",
"qs": "^6.10.2",
"react": "^17.0.2",
"react": "^18.2.0",
"react-app-polyfill": "^3.0.0",
"react-codemirror2": "^7.2.1",
"react-cropper": "^2.1.7",
"react-device-detect": "^1.14.0",
"react-dom": "^17.0.2",
"react-device-detect": "^2.2.2",
"react-dom": "^18.2.0",
"react-github-corner": "^2.5.0",
"react-helmet": "^6.1.0",
"react-highlight-words": "^0.17.0",
"react-highlight-words": "^0.18.0",
"react-i18next": "^11.8.7",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-router-dom": "^5.3.3",
"react-scripts": "5.0.1",
"react-social-login-buttons": "^3.4.0"
},
"scripts": {
@ -41,7 +41,8 @@
"eject": "craco eject",
"crowdin:sync": "crowdin upload && crowdin download",
"preinstall": "node -e \"if (process.env.npm_execpath.indexOf('yarn') === -1) throw new Error('Use yarn for installing: https://yarnpkg.com/en/docs/install')\"",
"fix": "eslint --fix ."
"fix": "eslint --fix src/**/*.{js,jsx,ts,tsx}",
"lint:css": "stylelint src/**/*.{css,less} --fix"
},
"eslintConfig": {
"extends": "react-app"
@ -61,15 +62,24 @@
]
},
"devDependencies": {
"@babel/core": "^7.18.13",
"@babel/eslint-parser": "^7.18.9",
"@babel/preset-react": "^7.18.6",
"cross-env": "^7.0.3",
"eslint": "^7.11.0",
"eslint-plugin-react": "^7.30.1",
"eslint": "8.22.0",
"eslint-plugin-react": "^7.31.1",
"husky": "^4.3.8",
"lint-staged": "^13.0.3"
"lint-staged": "^13.0.3",
"stylelint": "^14.11.0",
"stylelint-config-recommended-less": "^1.0.4",
"stylelint-config-standard": "^28.0.0"
},
"lint-staged": {
"src/**/*.{js,jsx,css,sass,ts,tsx}": [
"yarn fix"
"src/**/*.{css,less}": [
"stylelint --fix"
],
"src/**/*.{js,jsx,ts,tsx}": [
"eslint --fix"
]
},
"husky": {

View File

@ -19,7 +19,7 @@
name="description"
content="Casdoor - An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS"
/>
<link rel="apple-touch-icon" href="https://cdn.casdoor.com/static/favicon.png" />
<link rel="apple-touch-icon" href="https://cdn.casbin.org/img/favicon.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/

View File

@ -95,6 +95,7 @@ class AccountTable extends React.Component {
{name: "Is forbidden", displayName: i18next.t("user:Is forbidden")},
{name: "Is deleted", displayName: i18next.t("user:Is deleted")},
{name: "WebAuthn credentials", displayName: i18next.t("user:WebAuthn credentials")},
{name: "Managed accounts", displayName: i18next.t("user:Managed accounts")},
];
const getItemDisplayName = (text) => {

412
web/src/AdapterEditPage.js Normal file
View File

@ -0,0 +1,412 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select, Switch, Table, Tooltip} from "antd";
import * as AdapterBackend from "./backend/AdapterBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import "codemirror/lib/codemirror.css";
import * as ModelBackend from "./backend/ModelBackend";
import {EditOutlined, MinusOutlined} from "@ant-design/icons";
require("codemirror/theme/material-darker.css");
require("codemirror/mode/javascript/javascript");
const {Option} = Select;
class AdapterEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
organizationName: 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",
};
}
UNSAFE_componentWillMount() {
this.getAdapter();
this.getOrganizations();
}
getAdapter() {
AdapterBackend.getAdapter(this.state.organizationName, this.state.adapterName)
.then((adapter) => {
this.setState({
adapter: adapter,
});
this.getModels(adapter.owner);
});
}
getOrganizations() {
OrganizationBackend.getOrganizations(this.state.organizationName)
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
});
});
}
getModels(organizationName) {
ModelBackend.getModels(organizationName)
.then((res) => {
this.setState({
models: res,
});
});
}
parseAdapterField(key, value) {
if (["port"].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateAdapterField(key, value) {
value = this.parseAdapterField(key, value);
const adapter = this.state.adapter;
adapter[key] = value;
this.setState({
adapter: adapter,
});
}
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={
<div>
{this.state.mode === "add" ? i18next.t("adapter:New Adapter") : i18next.t("adapter:Edit Adapter")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitAdapterEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitAdapterEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteAdapter()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
<Row style={{marginTop: "10px"}} >
<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%"}} 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>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.adapter.name} onChange={e => {
this.updateAdapterField("name", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("provider:Type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.adapter.type} onChange={(value => {
this.updateAdapterField("type", value);
const adapter = this.state.adapter;
// adapter["tableColumns"] = Setting.getAdapterTableColumns(this.state.adapter);
this.setState({
adapter: adapter,
});
})}>
{
["Database"]
.map((item, index) => <Option key={index} value={item}>{item}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.adapter.host} onChange={e => {
this.updateAdapterField("host", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.adapter.port} onChange={value => {
this.updateAdapterField("port", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.adapter.user} onChange={e => {
this.updateAdapterField("user", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.adapter.password} onChange={e => {
this.updateAdapterField("password", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Database type"), i18next.t("syncer:Database type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.adapter.databaseType} onChange={(value => {this.updateAdapterField("databaseType", value);})}>
{
[
{id: "mysql", name: "MySQL"},
{id: "postgres", name: "PostgreSQL"},
{id: "mssql", name: "SQL Server"},
{id: "oracle", name: "Oracle"},
{id: "sqlite3", name: "Sqlite 3"},
].map((databaseType, index) => <Option key={index} value={databaseType.id}>{databaseType.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.adapter.database} onChange={e => {
this.updateAdapterField("database", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("syncer:Table"), i18next.t("syncer:Table - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.adapter.table}
disabled={this.state.adapter.type === "Keycloak"} onChange={e => {
this.updateAdapterField("table", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Model"), i18next.t("general:Model - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.adapter.model} onChange={(model => {
this.updateAdapterField("model", model);
})}>
{
this.state.models.map((model, index) => <Option key={index} value={model.name}>{model.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<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>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.adapter.isEnabled} onChange={checked => {
this.updateAdapterField("isEnabled", checked);
}} />
</Col>
</Row>
</Card>
);
}
submitAdapterEdit(willExist) {
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");
this.setState({
adapterName: this.state.adapter.name,
});
if (willExist) {
this.props.history.push("/adapters");
} else {
this.props.history.push(`/adapters/${this.state.adapter.name}`);
}
} else {
Setting.showMessage("error", res.msg);
this.updateAdapterField("name", this.state.adapterName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
}
deleteAdapter() {
AdapterBackend.deleteAdapter(this.state.adapter)
.then(() => {
this.props.history.push("/adapters");
})
.catch(error => {
Setting.showMessage("error", `adapter failed to delete: ${error}`);
});
}
render() {
return (
<div>
{
this.state.adapter !== null ? this.renderAdapter() : null
}
<div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitAdapterEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitAdapterEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteAdapter()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);
}
}
export default AdapterEditPage;

261
web/src/AdapterListPage.js Normal file
View File

@ -0,0 +1,261 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Popconfirm, Switch, Table} from "antd";
import moment from "moment";
import * as Setting from "./Setting";
import * as AdapterBackend from "./backend/AdapterBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
class AdapterListPage extends BaseListPage {
newAdapter() {
const randomName = Setting.getRandomName();
return {
owner: "built-in",
name: `adapter_${randomName}`,
createdTime: moment().format(),
organization: "built-in",
type: "Database",
host: "localhost",
port: 3306,
user: "root",
password: "123456",
databaseType: "mysql",
database: "dbName",
table: "tableName",
isEnabled: false,
};
}
addAdapter() {
const newAdapter = this.newAdapter();
AdapterBackend.addAdapter(newAdapter)
.then((res) => {
this.props.history.push({pathname: `/adapters/${newAdapter.owner}/${newAdapter.name}`, mode: "add"});
}
)
.catch(error => {
Setting.showMessage("error", `Adapter failed to add: ${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},
});
}
)
.catch(error => {
Setting.showMessage("error", `Adapter failed to delete: ${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",
key: "name",
width: "150px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
return (
<Link to={`/adapters/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
key: "createdTime",
width: "160px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("provider:Type"),
dataIndex: "type",
key: "type",
width: "100px",
sorter: true,
filterMultiple: false,
filters: [
{text: "Database", value: "Database"},
],
},
{
title: i18next.t("provider:Host"),
dataIndex: "host",
key: "host",
width: "120px",
sorter: true,
...this.getColumnSearchProps("host"),
},
{
title: i18next.t("provider:Port"),
dataIndex: "port",
key: "port",
width: "100px",
sorter: true,
...this.getColumnSearchProps("port"),
},
{
title: i18next.t("general:User"),
dataIndex: "user",
key: "user",
width: "120px",
sorter: true,
...this.getColumnSearchProps("user"),
},
{
title: i18next.t("general:Password"),
dataIndex: "password",
key: "password",
width: "120px",
sorter: true,
...this.getColumnSearchProps("password"),
},
{
title: i18next.t("syncer:Database type"),
dataIndex: "databaseType",
key: "databaseType",
width: "120px",
sorter: (a, b) => a.databaseType.localeCompare(b.databaseType),
},
{
title: i18next.t("syncer:Database"),
dataIndex: "database",
key: "database",
width: "120px",
sorter: true,
},
{
title: i18next.t("syncer:Table"),
dataIndex: "table",
key: "table",
width: "120px",
sorter: true,
},
{
title: i18next.t("general:Is enabled"),
dataIndex: "isEnabled",
key: "isEnabled",
width: "120px",
sorter: true,
render: (text, record, index) => {
return (
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
);
},
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "170px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/adapters/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete adapter: ${record.name} ?`}
onConfirm={() => this.deleteAdapter(index)}
>
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);
},
},
];
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={adapters} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Adapters")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addAdapter.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
const sortField = params.sortField, sortOrder = params.sortOrder;
if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({loading: true});
AdapterBackend.getAdapters("", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
}
});
};
}
export default AdapterListPage;

View File

@ -71,6 +71,9 @@ import SamlCallback from "./auth/SamlCallback";
import CasLogout from "./auth/CasLogout";
import ModelListPage from "./ModelListPage";
import ModelEditPage from "./ModelEditPage";
import SystemInfo from "./SystemInfo";
import AdapterListPage from "./AdapterListPage";
import AdapterEditPage from "./AdapterEditPage";
const {Header, Footer} = Layout;
@ -122,6 +125,8 @@ class App extends Component {
this.setState({selectedMenuKey: "/permissions"});
} else if (uri.includes("/models")) {
this.setState({selectedMenuKey: "/models"});
} else if (uri.includes("/adapters")) {
this.setState({selectedMenuKey: "/adapters"});
} else if (uri.includes("/providers")) {
this.setState({selectedMenuKey: "/providers"});
} else if (uri.includes("/applications")) {
@ -148,6 +153,8 @@ class App extends Component {
this.setState({selectedMenuKey: "/login"});
} else if (uri.includes("/result")) {
this.setState({selectedMenuKey: "/result"});
} else if (uri.includes("/sysinfo")) {
this.setState({selectedMenuKey: "/sysinfo"});
} else {
this.setState({selectedMenuKey: -1});
}
@ -385,16 +392,15 @@ class App extends Component {
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/permissions">
<Link to="/permissions">
{i18next.t("general:Permissions")}
</Link>
</Menu.Item>
);
}
res.push(
<Menu.Item key="/permissions">
<Link to="/permissions">
{i18next.t("general:Permissions")}
</Link>
</Menu.Item>
);
if (Setting.isAdminUser(this.state.account)) {
res.push(
<Menu.Item key="/models">
@ -403,6 +409,13 @@ class App extends Component {
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/adapters">
<Link to="/adapters">
{i18next.t("general:Adapters")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/providers">
<Link to="/providers">
@ -478,8 +491,14 @@ class App extends Component {
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/sysinfo">
<Link to="/sysinfo">
{i18next.t("general:SysInfo")}
</Link>
</Menu.Item>
);
}
res.push(
<Menu.Item key="/swagger">
<a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>
@ -518,7 +537,7 @@ class App extends Component {
}
renderRouter() {
return(
return (
<div>
<Switch>
<Route exact path="/result" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...props} />)} />
@ -536,6 +555,8 @@ class App extends Component {
<Route exact path="/permissions/:organizationName/:permissionName" render={(props) => this.renderLoginIfNotLoggedIn(<PermissionEditPage account={this.state.account} {...props} />)} />
<Route exact path="/models" render={(props) => this.renderLoginIfNotLoggedIn(<ModelListPage account={this.state.account} {...props} />)} />
<Route exact path="/models/:organizationName/:modelName" render={(props) => this.renderLoginIfNotLoggedIn(<ModelEditPage account={this.state.account} {...props} />)} />
<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="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)} />
@ -560,6 +581,7 @@ class App extends Component {
<Route exact path="/payments/:paymentName/result" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentResultPage account={this.state.account} {...props} />)} />
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)} />
<Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />} />
<Route exact path="/sysinfo" render={(props) => this.renderLoginIfNotLoggedIn(<SystemInfo account={this.state.account} {...props} />)} />
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
</Switch>
@ -585,7 +607,7 @@ class App extends Component {
// theme="dark"
mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"}
selectedKeys={[`${this.state.selectedMenuKey}`]}
style={{lineHeight: "64px", width: "80%", position: "absolute"}}
style={{lineHeight: "64px", width: "80%", position: "absolute", left: "145px"}}
>
{
this.renderMenu()
@ -608,7 +630,7 @@ class App extends Component {
</div>
);
} else {
return(
return (
<div>
<Header style={{padding: "0", marginBottom: "3px"}}>
{
@ -691,6 +713,7 @@ class App extends Component {
<Route exact path="/forget/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ForgetPage {...props} />)} />
<Route exact path="/prompt" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage account={this.state.account} {...props} />)} />
<Route exact path="/prompt/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage account={this.state.account} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} {...props} />)} />
<Route exact path="/sysinfo" render={(props) => this.renderLoginIfNotLoggedIn(<SystemInfo {...props} />)} />
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
</Switch>
@ -735,6 +758,7 @@ class App extends Component {
const organization = this.state.account.organization;
return (
<React.Fragment>
<div style={{display: "none"}} id="CasdoorApplicationName" value={this.state.account.signupApplication} />
<Helmet>
<title>{organization.displayName}</title>
<link rel="icon" href={organization.favicon} />

View File

@ -1,6 +1,8 @@
@import '~antd/dist/antd.less';
/* stylelint-disable at-rule-name-case */
/* stylelint-disable selector-class-pattern */
@import "~antd/dist/antd.less";
@StaticBaseUrl:"https://cdn.casbin.org";
@StaticBaseUrl: "https://cdn.casbin.org";
.App {
text-align: center;
@ -69,8 +71,13 @@
}
.content-warp-card {
box-shadow: 0 1px 5px 0 rgba(51, 51, 51, 0.14);
margin: 5px 5px 5px 5px;
box-shadow: 0 1px 5px 0 rgb(51 51 51 / 14%);
margin: 5px;
flex: 1;
align-items: stretch;
}
.loginBackground {
background: #ffffff no-repeat;
background-size: 100% 100%;
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, Popover, Row, Select, Switch, Upload} from "antd";
import {Button, Card, Col, Input, Popover, Radio, Row, Select, Switch, Upload} from "antd";
import {CopyOutlined, LinkOutlined, UploadOutlined} from "@ant-design/icons";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as CertBackend from "./backend/CertBackend";
@ -35,9 +35,18 @@ import "codemirror/lib/codemirror.css";
require("codemirror/theme/material-darker.css");
require("codemirror/mode/htmlmixed/htmlmixed");
require("codemirror/mode/xml/xml");
require("codemirror/mode/css/css");
const {Option} = Select;
const template = {
padding: "30px",
border: "2px solid #ffffff",
borderRadius: "7px",
backgroundColor: "#ffffff",
boxShadow: " 0px 0px 20px rgba(0, 0, 0, 0.20)",
};
class ApplicationEditPage extends React.Component {
constructor(props) {
super(props);
@ -111,7 +120,7 @@ class ApplicationEditPage extends React.Component {
}
parseApplicationField(key, value) {
if (["expireInHours", "refreshExpireInHours"].includes(key)) {
if (["expireInHours", "refreshExpireInHours", "offset"].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
@ -148,6 +157,7 @@ class ApplicationEditPage extends React.Component {
}
renderApplication() {
const preview = JSON.stringify(template, null, 2);
return (
<Card size="small" title={
<div>
@ -536,6 +546,66 @@ class ApplicationEditPage extends React.Component {
this.renderSignupSigninPreview()
}
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Background URL"), i18next.t("application:Background URL - Tooltip"))} :
</Col>
<Col span={22} style={(Setting.isMobile()) ? {maxWidth: "100%"} : {}}>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col>
<Col span={23} >
<Input prefix={<LinkOutlined />} value={this.state.application.formBackgroundUrl} onChange={e => {
this.updateApplicationField("formBackgroundUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("general:Preview")}:
</Col>
<Col span={22} >
<a target="_blank" rel="noreferrer" href={this.state.application.formBackgroundUrl}>
<img src={this.state.application.formBackgroundUrl} alt={this.state.application.formBackgroundUrl} height={90} style={{marginBottom: "20px"}} />
</a>
</Col>
</Row>
</Col>
</Row>
<Row>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Form CSS"), i18next.t("application:Form CSS - Tooltip"))} :
</Col>
<Col span={22}>
<Popover placement="right" content={
<div style={{width: "900px", height: "300px"}} >
<CodeMirror value={this.state.application.formCss === "" ? preview : this.state.application.formCss}
options={{mode: "css", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
this.updateApplicationField("formCss", value);
}}
/>
</div>
} title={i18next.t("application:Form CSS - Edit")} trigger="click">
<Input value={this.state.application.formCss} style={{marginBottom: "10px"}} onChange={e => {
this.updateApplicationField("formCss", e.target.value);
}} />
</Popover>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:From position"), i18next.t("application:From position - Tooltip"))} :
</Col>
<Col span={22} >
<Radio.Group onChange={e => {this.updateApplicationField("formOffset", e.target.value);}} value={this.state.application.formOffset !== 0 ? this.state.application.formOffset : 8}>
<Radio.Button value={2}>left</Radio.Button>
<Radio.Button value={8}>center</Radio.Button>
<Radio.Button value={14}>right</Radio.Button>
</Radio.Group>
</Col>
</Row>
{
!this.state.application.enableSignUp ? null : (
<Row style={{marginTop: "20px"}} >
@ -591,7 +661,7 @@ class ApplicationEditPage extends React.Component {
<LoginPage type={"login"} mode={"signup"} application={this.state.application} />
)
}
<div style={maskStyle}></div>
<div style={maskStyle} />
</div>
</Col>
<Col span={11}>
@ -605,7 +675,7 @@ class ApplicationEditPage extends React.Component {
<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"}}>
<LoginPage type={"login"} mode={"signin"} application={this.state.application} />
<div style={maskStyle}></div>
<div style={maskStyle} />
</div>
</Col>
</React.Fragment>

View File

@ -53,6 +53,7 @@ class ApplicationListPage extends BaseListPage {
redirectUris: ["http://localhost:9000/callback"],
tokenFormat: "JWT",
expireInHours: 24 * 7,
formOffset: 8,
};
}

View File

@ -39,101 +39,102 @@ class BaseListPage extends React.Component {
this.fetch({pagination});
}
getColumnSearchProps = dataIndex => ({
filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}) => (
<div style={{padding: 8}}>
<Input
ref={node => {
this.searchInput = node;
}}
placeholder={`Search ${dataIndex}`}
value={selectedKeys[0]}
onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
style={{marginBottom: 8, display: "block"}}
/>
<Space>
<Button
type="primary"
onClick={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
icon={<SearchOutlined />}
size="small"
style={{width: 90}}
>
getColumnSearchProps = dataIndex => ({
filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}) => (
<div style={{padding: 8}}>
<Input
ref={node => {
this.searchInput = node;
}}
placeholder={`Search ${dataIndex}`}
value={selectedKeys[0]}
onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
style={{marginBottom: 8, display: "block"}}
/>
<Space>
<Button
type="primary"
onClick={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
icon={<SearchOutlined />}
size="small"
style={{width: 90}}
>
Search
</Button>
<Button onClick={() => this.handleReset(clearFilters)} size="small" style={{width: 90}}>
</Button>
<Button onClick={() => this.handleReset(clearFilters)} size="small" style={{width: 90}}>
Reset
</Button>
<Button
type="link"
size="small"
onClick={() => {
confirm({closeDropdown: false});
this.setState({
searchText: selectedKeys[0],
searchedColumn: dataIndex,
});
}}
>
</Button>
<Button
type="link"
size="small"
onClick={() => {
confirm({closeDropdown: false});
this.setState({
searchText: selectedKeys[0],
searchedColumn: dataIndex,
});
}}
>
Filter
</Button>
</Space>
</div>
</Button>
</Space>
</div>
),
filterIcon: filtered => <SearchOutlined style={{color: filtered ? "#1890ff" : undefined}} />,
onFilter: (value, record) =>
record[dataIndex]
? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase())
: "",
onFilterDropdownVisibleChange: visible => {
if (visible) {
setTimeout(() => this.searchInput.select(), 100);
}
},
render: text =>
this.state.searchedColumn === dataIndex ? (
<Highlighter
highlightStyle={{backgroundColor: "#ffc069", padding: 0}}
searchWords={[this.state.searchText]}
autoEscape
textToHighlight={text ? text.toString() : ""}
/>
) : (
text
),
filterIcon: filtered => <SearchOutlined style={{color: filtered ? "#1890ff" : undefined}} />,
onFilter: (value, record) =>
record[dataIndex]
? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase())
: "",
onFilterDropdownVisibleChange: visible => {
if (visible) {
setTimeout(() => this.searchInput.select(), 100);
}
},
render: text =>
this.state.searchedColumn === dataIndex ? (
<Highlighter
highlightStyle={{backgroundColor: "#ffc069", padding: 0}}
searchWords={[this.state.searchText]}
autoEscape
textToHighlight={text ? text.toString() : ""}
/>
) : (
text
),
});
handleSearch = (selectedKeys, confirm, dataIndex) => {
this.fetch({searchText: selectedKeys[0], searchedColumn: dataIndex, pagination: this.state.pagination});
};
handleReset = clearFilters => {
clearFilters();
const {pagination} = this.state;
this.fetch({pagination});
};
handleTableChange = (pagination, filters, sorter) => {
this.fetch({
sortField: sorter.field,
sortOrder: sorter.order,
pagination,
...filters,
searchText: this.state.searchText,
searchedColumn: this.state.searchedColumn,
});
};
handleSearch = (selectedKeys, confirm, dataIndex) => {
this.fetch({searchText: selectedKeys[0], searchedColumn: dataIndex, pagination: this.state.pagination});
};
handleReset = clearFilters => {
clearFilters();
const {pagination} = this.state;
this.fetch({pagination});
};
handleTableChange = (pagination, filters, sorter) => {
this.fetch({
sortField: sorter.field,
sortOrder: sorter.order,
pagination,
...filters,
searchText: this.state.searchText,
searchedColumn: this.state.searchedColumn,
});
};
render() {
return (
<div>
{
this.renderTable(this.state.data)
}
</div>
);
}
render() {
return (
<div>
{
this.renderTable(this.state.data)
}
</div>
);
}
}
export default BaseListPage;

View File

@ -0,0 +1,160 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Input, Row, Select, Table, Tooltip} from "antd";
import * as Setting from "./Setting";
import i18next from "i18next";
const {Option} = Select;
class ManagedAccountTable extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
};
}
updateTable(table) {
this.props.onUpdateTable(table);
}
updateField(table, index, key, value) {
table[index][key] = value;
this.updateTable(table);
}
addRow(table) {
const row = {application: "", username: "", password: ""};
if (table === undefined || table === null) {
table = [];
}
table = Setting.addRow(table, row);
this.updateTable(table);
}
deleteRow(table, i) {
table = Setting.deleteRow(table, i);
this.updateTable(table);
}
upRow(table, i) {
table = Setting.swapRow(table, i - 1, i);
this.updateTable(table);
}
downRow(table, i) {
table = Setting.swapRow(table, i, i + 1);
this.updateTable(table);
}
renderTable(table) {
const columns = [
{
title: i18next.t("general:Application"),
dataIndex: "application",
key: "application",
render: (text, record, index) => {
const items = this.props.applications;
return (
<Select virtual={false} style={{width: "100%"}}
value={text}
onChange={value => {
this.updateField(table, index, "application", value);
}} >
{
items.map((item, index) => <Option key={index} value={item.name}>{item.name}</Option>)
}
</Select>
);
},
},
{
title: i18next.t("signup:Username"),
dataIndex: "username",
key: "username",
width: "420px",
render: (text, record, index) => {
return (
<Input defaultValue={text} onChange={e => {
this.updateField(table, index, "username", e.target.value);
}} />
);
},
},
{
title: i18next.t("general:Password"),
dataIndex: "password",
key: "password",
width: "420px",
render: (text, record, index) => {
return (
<Input defaultValue={text} onChange={e => {
this.updateField(table, index, "password", e.target.value);
}} />
);
},
},
{
title: i18next.t("general:Action"),
key: "action",
width: "100px",
render: (text, record, index) => {
return (
<div>
<Tooltip placement="bottomLeft" title={i18next.t("general:Up")}>
<Button style={{marginRight: "5px"}} disabled={index === 0} icon={<UpOutlined />} size="small" onClick={() => this.upRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={i18next.t("general:Down")}>
<Button style={{marginRight: "5px"}} disabled={index === table.length - 1} icon={<DownOutlined />} size="small" onClick={() => this.downRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={i18next.t("general:Delete")}>
<Button icon={<DeleteOutlined />} size="small" onClick={() => this.deleteRow(table, index)} />
</Tooltip>
</div>
);
},
},
];
return (
<Table scroll={{x: "max-content"}} rowKey="name" columns={columns} dataSource={table} size="middle" bordered pagination={false}
title={() => (
<div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
</div>
)}
/>
);
}
render() {
return (
<div>
<Row style={{marginTop: "20px"}} >
<Col span={24}>
{
this.renderTable(this.props.table)
}
</Col>
</Row>
</div>
);
}
}
export default ManagedAccountTable;

View File

@ -88,7 +88,7 @@ class ModelListPage extends BaseListPage {
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
return (
<Link to={`/models/${text}`}>
<Link to={`/models/${record.owner}/${text}`}>
{text}
</Link>
);

View File

@ -15,6 +15,7 @@
import React from "react";
import {Button, Card, Col, Input, Row, Select, Switch} from "antd";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as LdapBackend from "./backend/LdapBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
@ -31,6 +32,7 @@ class OrganizationEditPage extends React.Component {
classes: props,
organizationName: props.match.params.organizationName,
organization: null,
applications: [],
ldaps: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
@ -38,6 +40,7 @@ class OrganizationEditPage extends React.Component {
UNSAFE_componentWillMount() {
this.getOrganization();
this.getApplications();
this.getLdaps();
}
@ -50,6 +53,15 @@ class OrganizationEditPage extends React.Component {
});
}
getApplications() {
ApplicationBackend.getApplicationsByOrganization("admin", this.state.organizationName)
.then((applications) => {
this.setState({
applications: applications,
});
});
}
getLdaps() {
LdapBackend.getLdaps(this.state.organizationName)
.then(res => {
@ -209,6 +221,18 @@ class OrganizationEditPage extends React.Component {
</Row>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Default application"), i18next.t("general:Default application - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.defaultApplication} onChange={(value => {this.updateOrganizationField("defaultApplication", value);})}>
{
this.state.applications?.map((item, index) => <Option key={index} value={item.name}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Tags"), i18next.t("organization:Tags - Tooltip"))} :

View File

@ -35,6 +35,7 @@ class OrganizationListPage extends BaseListPage {
PasswordSalt: "",
phonePrefix: "86",
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
defaultApplication: "",
tags: [],
masterPassword: "",
enableSoftDeletion: false,

View File

@ -245,7 +245,8 @@ class PermissionEditPage extends React.Component {
})}>
{
[
{id: "Application", name: "Application"},
{id: "Application", name: i18next.t("general:Application")},
{id: "TreeNode", name: i18next.t("permission:TreeNode")},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
@ -273,9 +274,9 @@ class PermissionEditPage extends React.Component {
})}>
{
[
{id: "Read", name: "Read"},
{id: "Write", name: "Write"},
{id: "Admin", name: "Admin"},
{id: "Read", name: i18next.t("permission:Read")},
{id: "Write", name: i18next.t("permission:Write")},
{id: "Admin", name: i18next.t("permission:Admin")},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
@ -291,8 +292,8 @@ class PermissionEditPage extends React.Component {
})}>
{
[
{id: "Allow", name: "Allow"},
{id: "Deny", name: "Deny"},
{id: "Allow", name: i18next.t("permission:Allow")},
{id: "Deny", name: i18next.t("permission:Deny")},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
@ -358,8 +359,8 @@ class PermissionEditPage extends React.Component {
})}>
{
[
{id: "Approved", name: "Approved"},
{id: "Pending", name: "Pending"},
{id: "Approved", name: i18next.t("permission:Approved")},
{id: "Pending", name: i18next.t("permission:Pending")},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
@ -374,10 +375,10 @@ class PermissionEditPage extends React.Component {
Setting.showMessage("error", "The users and roles cannot be empty at the same time");
return;
}
if (this.state.permission.domains.length === 0) {
Setting.showMessage("error", "The domains cannot be empty");
return;
}
// if (this.state.permission.domains.length === 0) {
// Setting.showMessage("error", "The domains cannot be empty");
// return;
// }
if (this.state.permission.resources.length === 0) {
Setting.showMessage("error", "The resources cannot be empty");
return;

View File

@ -188,7 +188,19 @@ class PermissionListPage extends BaseListPage {
sorter: true,
...this.getColumnSearchProps("actions"),
render: (text, record, index) => {
return Setting.getTags(text);
const tags = text.map((tag, i) => {
switch (tag) {
case "Read":
return i18next.t("permission:Read");
case "Write":
return i18next.t("permission:Write");
case "Admin":
return i18next.t("permission:Admin");
default:
return null;
}
});
return Setting.getTags(tags);
},
},
{
@ -197,11 +209,21 @@ class PermissionListPage extends BaseListPage {
key: "effect",
filterMultiple: false,
filters: [
{text: "Allow", value: "Allow"},
{text: "Deny", value: "Deny"},
{text: i18next.t("permission:Allow"), value: "Allow"},
{text: i18next.t("permission:Deny"), value: "Deny"},
],
width: "120px",
sorter: true,
render: (text, record, index) => {
switch (text) {
case "Allow":
return Setting.getTag("success", i18next.t("permission:Allow"));
case "Deny":
return Setting.getTag("error", i18next.t("permission:Deny"));
default:
return null;
}
},
},
{
title: i18next.t("general:Is enabled"),
@ -248,11 +270,21 @@ class PermissionListPage extends BaseListPage {
key: "state",
filterMultiple: false,
filters: [
{text: "Approved", value: "Approved"},
{text: "Pending", value: "Pending"},
{text: i18next.t("permission:Approved"), value: "Approved"},
{text: i18next.t("permission:Pending"), value: "Pending"},
],
width: "120px",
sorter: true,
render: (text, record, index) => {
switch (text) {
case "Approved":
return Setting.getTag("success", i18next.t("permission:Approved"));
case "Pending":
return Setting.getTag("error", i18next.t("permission:Pending"));
default:
return null;
}
},
},
{
title: i18next.t("general:Action"),

View File

@ -34,7 +34,6 @@ class ProviderEditPage extends React.Component {
providerName: props.match.params.providerName,
provider: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
testEmail: this.props.account["email"] !== undefined ? this.props.account["email"] : "",
};
}
@ -131,6 +130,9 @@ class ProviderEditPage extends React.Component {
} else if (this.state.provider.category === "SMS" && this.state.provider.type === "Huawei Cloud SMS") {
text = i18next.t("provider:Channel No.");
tooltip = i18next.t("provider:Channel No. - Tooltip");
} else if (this.state.provider.category === "Email" && this.state.provider.type === "SUBMAIL") {
text = i18next.t("provider:App ID");
tooltip = i18next.t("provider:App ID - Tooltip");
} else {
return null;
}
@ -199,9 +201,12 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("type", "GitHub");
} else if (value === "Email") {
this.updateProviderField("type", "Default");
this.updateProviderField("host", "smtp.example.com");
this.updateProviderField("port", 465);
this.updateProviderField("disableSsl", false);
this.updateProviderField("title", "Casdoor Verification Code");
this.updateProviderField("content", "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes.");
this.updateProviderField("receiver", this.props.account.email);
} else if (value === "SMS") {
this.updateProviderField("type", "Aliyun SMS");
} else if (value === "Storage") {
@ -474,7 +479,7 @@ class ProviderEditPage extends React.Component {
}} />
</Col>
</Row>
{this.state.provider.type === "AWS S3" || this.state.provider.type === "Tencent Cloud COS" ? (
{["AWS S3", "MinIO", "Tencent Cloud COS"].includes(this.state.provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Region ID"), i18next.t("provider:Region ID - Tooltip"))} :
@ -536,7 +541,7 @@ class ProviderEditPage extends React.Component {
{Setting.getLabel(i18next.t("provider:Email Content"), i18next.t("provider:Email Content - Tooltip"))} :
</Col>
<Col span={22} >
<TextArea autoSize={{minRows: 1, maxRows: 100}} value={this.state.provider.content} onChange={e => {
<TextArea autoSize={{minRows: 3, maxRows: 100}} value={this.state.provider.content} onChange={e => {
this.updateProviderField("content", e.target.value);
}} />
</Col>
@ -546,19 +551,16 @@ class ProviderEditPage extends React.Component {
{Setting.getLabel(i18next.t("provider:Test Email"), i18next.t("provider:Test Email - Tooltip"))} :
</Col>
<Col span={4} >
<Input value={this.state.testEmail}
placeHolder = {i18next.t("user:Input your email")}
onChange={e => {
this.setState({testEmail: e.target.value});
}} />
<Input value={this.state.provider.receiver} placeholder = {i18next.t("user:Input your email")} onChange={e => {
this.updateProviderField("receiver", e.target.value);
}} />
</Col>
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary"
onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} >
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} >
{i18next.t("provider:Test Connection")}
</Button>
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary"
disabled={!Setting.isValidEmail(this.state.testEmail)}
onClick={() => ProviderEditTestEmail.sendTestEmail(this.state.provider, this.state.testEmail)} >
disabled={!Setting.isValidEmail(this.state.provider.receiver)}
onClick={() => ProviderEditTestEmail.sendTestEmail(this.state.provider, this.state.provider.receiver)} >
{i18next.t("provider:Send Test Email")}
</Button>
</Row>

View File

@ -56,8 +56,12 @@ export const ResetModal = (props) => {
});
};
let placeHolder = "";
if (destType === "email") {placeHolder = i18next.t("user:Input your email");} else if (destType === "phone") {placeHolder = i18next.t("user:Input your phone number");}
let placeholder = "";
if (destType === "email") {
placeholder = i18next.t("user:Input your email");
} else if (destType === "phone") {
placeholder = i18next.t("user:Input your phone number");
}
return (
<Row>
@ -80,7 +84,7 @@ export const ResetModal = (props) => {
<Input
addonBefore={destType === "email" ? i18next.t("user:New Email") : i18next.t("user:New phone")}
prefix={destType === "email" ? <MailOutlined /> : <PhoneOutlined />}
placeholder={placeHolder}
placeholder={placeholder}
onChange={e => setDest(e.target.value)}
/>
</Row>
@ -89,6 +93,7 @@ export const ResetModal = (props) => {
textBefore={i18next.t("code:Code You Received")}
onChange={setCode}
onButtonClickArgs={[dest, destType, Setting.getApplicationName(application)]}
application={application}
/>
</Row>
</Col>

View File

@ -15,12 +15,13 @@
import React from "react";
import * as Setting from "./Setting";
import {Dropdown, Menu} from "antd";
import {createFromIconfontCN} from "@ant-design/icons";
import "./App.less";
const IconFont = createFromIconfontCN({
scriptUrl: "//at.alicdn.com/t/font_2680620_ffij16fkwdg.js",
});
function flagIcon(country, alt) {
return (
<img width={24} alt={alt} src={`${Setting.StaticBaseUrl}/flag-icons/${country}.svg`} />
);
}
class SelectLanguageBox extends React.Component {
constructor(props) {
@ -35,13 +36,14 @@ class SelectLanguageBox extends React.Component {
<Menu onClick={(e) => {
Setting.changeLanguage(e.key);
}}>
<Menu.Item key="en" icon={<IconFont type="icon-en" />}>English</Menu.Item>
<Menu.Item key="zh" icon={<IconFont type="icon-zh" />}>简体中文</Menu.Item>
<Menu.Item key="fr" icon={<IconFont type="icon-fr" />}>Français</Menu.Item>
<Menu.Item key="de" icon={<IconFont type="icon-de" />}>Deutsch</Menu.Item>
<Menu.Item key="ja" icon={<IconFont type="icon-ja" />}>日本語</Menu.Item>
<Menu.Item key="ko" icon={<IconFont type="icon-ko" />}>한국어</Menu.Item>
<Menu.Item key="ru" icon={<IconFont type="icon-ru" />}>Русский</Menu.Item>
<Menu.Item key="en" icon={flagIcon("US", "English")}>English</Menu.Item>
<Menu.Item key="es" icon={flagIcon("ES", "Español")}>Español</Menu.Item>
<Menu.Item key="zh" icon={flagIcon("CN", "简体中文")}>简体中文</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>
);

View File

@ -69,6 +69,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_aws.png`,
url: "https://aws.amazon.com/s3",
},
"MinIO": {
logo: "https://min.io/resources/img/logo.svg",
url: "https://min.io/",
},
"Aliyun OSS": {
logo: `${StaticBaseUrl}/img/social_aliyun.png`,
url: "https://aliyun.com/product/oss",
@ -78,7 +82,7 @@ export const OtherProviderInfo = {
url: "https://cloud.tencent.com/product/cos",
},
"Azure Blob": {
logo: `${StaticBaseUrl}/img/social_azure.jpg`,
logo: `${StaticBaseUrl}/img/social_azure.png`,
url: "https://azure.microsoft.com/en-us/services/storage/blobs/",
},
},
@ -204,10 +208,18 @@ export function isProviderPrompted(providerItem) {
return isProviderVisible(providerItem) && providerItem.prompted;
}
export function isSignupItemPrompted(signupItem) {
return signupItem.visible && signupItem.prompted;
}
export function getAllPromptedProviderItems(application) {
return application.providers.filter(providerItem => isProviderPrompted(providerItem));
}
export function getAllPromptedSignupItems(application) {
return application.signupItems.filter(signupItem => isSignupItemPrompted(signupItem));
}
export function getSignupItem(application, itemName) {
const signupItems = application.signupItems?.filter(signupItem => signupItem.name === itemName);
if (signupItems.length === 0) {
@ -279,6 +291,11 @@ export function hasPromptPage(application) {
return true;
}
const signupItems = getAllPromptedSignupItems(application);
if (signupItems.length !== 0) {
return true;
}
return isAffiliationPrompted(application);
}
@ -303,6 +320,19 @@ function isProviderItemAnswered(user, application, providerItem) {
return linkedValue !== undefined && linkedValue !== "";
}
function isSignupItemAnswered(user, signupItem) {
if (user === null) {
return false;
}
if (signupItem.name !== "Country/Region") {
return true;
}
const value = user["region"];
return value !== undefined && value !== "";
}
export function isPromptAnswered(user, application) {
if (!isAffiliationAnswered(user, application)) {
return false;
@ -314,9 +344,24 @@ export function isPromptAnswered(user, application) {
return false;
}
}
const signupItems = getAllPromptedSignupItems(application);
for (let i = 0; i < signupItems.length; i++) {
if (!isSignupItemAnswered(user, signupItems[i])) {
return false;
}
}
return true;
}
export function parseObject(s) {
try {
return eval("(" + s + ")");
} catch (e) {
return null;
}
}
export function parseJson(s) {
if (s === "") {
return null;
@ -413,9 +458,9 @@ export function trim(str, ch) {
let start = 0;
let end = str.length;
while(start < end && str[start] === ch) {++start;}
while (start < end && str[start] === ch) {++start;}
while(end > start && str[end - 1] === ch) {--end;}
while (end > start && str[end - 1] === ch) {--end;}
return (start > 0 || end < str.length) ? str.substring(start, end) : str;
}
@ -463,7 +508,7 @@ export function getFriendlyFileSize(size) {
return `${num} ${"KMGTPEZY"[i - 1]}B`;
}
function getRandomInt(s) {
function getHashInt(s) {
let hash = 0;
if (s.length !== 0) {
for (let i = 0; i < s.length; i++) {
@ -473,16 +518,16 @@ function getRandomInt(s) {
}
}
if (hash < 0) {
hash = -hash;
}
return hash;
}
export function getAvatarColor(s) {
const colorList = ["#f56a00", "#7265e6", "#ffbf00", "#00a2ae"];
let random = getRandomInt(s);
if (random < 0) {
random = -random;
}
return colorList[random % 4];
const hash = getHashInt(s);
return colorList[hash % 4];
}
export function getLanguage() {
@ -596,6 +641,7 @@ export function getProviderTypeOptions(category) {
return (
[
{id: "Default", name: "Default"},
{id: "SUBMAIL", name: "SUBMAIL"},
]
);
} else if (category === "SMS") {
@ -612,6 +658,7 @@ export function getProviderTypeOptions(category) {
[
{id: "Local File System", name: "Local File System"},
{id: "AWS S3", name: "AWS S3"},
{id: "MinIO", name: "MinIO"},
{id: "Aliyun OSS", name: "Aliyun OSS"},
{id: "Tencent Cloud COS", name: "Tencent Cloud COS"},
{id: "Azure Blob", name: "Azure Blob"},
@ -668,12 +715,12 @@ export function renderLogo(application) {
if (application.homepageUrl !== "") {
return (
<a target="_blank" rel="noreferrer" href={application.homepageUrl}>
<img width={250} src={application.logo} alt={application.displayName} style={{marginBottom: "30px"}} />
<img width={250} src={application.logo} alt={application.displayName} style={{marginBottom: "10px"}} />
</a>
);
} else {
return (
<img width={250} src={application.logo} alt={application.displayName} style={{marginBottom: "30px"}} />
<img width={250} src={application.logo} alt={application.displayName} style={{marginBottom: "10px"}} />
);
}
}
@ -819,7 +866,10 @@ export function getTagColor(s) {
export function getTags(tags) {
const res = [];
if (!tags) {return res;}
if (!tags) {
return res;
}
tags.forEach((tag, i) => {
res.push(
<Tag color={getTagColor(tag)}>
@ -830,6 +880,14 @@ export function getTags(tags) {
return res;
}
export function getTag(color, text) {
return (
<Tag color={color}>
{text}
</Tag>
);
}
export function getApplicationOrgName(application) {
return `${application?.organizationObj.owner}/${application?.organizationObj.name}`;
}
@ -863,6 +921,14 @@ export function scrollToDiv(divId) {
}
}
export function inIframe() {
try {
return window !== window.parent;
} catch (e) {
return true;
}
}
export function getSyncerTableColumns(syncer) {
switch (syncer.type) {
case "Keycloak":

View File

@ -152,7 +152,7 @@ class SignupTable extends React.Component {
return null;
}
if (record.visible) {
if (record.visible && record.name !== "Country/Region") {
return null;
}

127
web/src/SystemInfo.js Normal file
View File

@ -0,0 +1,127 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Card, Col, Divider, Progress, Row, Spin} from "antd";
import * as SystemBackend from "./backend/SystemInfo";
import React from "react";
import * as Setting from "./Setting";
import i18next from "i18next";
class SystemInfo extends React.Component {
constructor(props) {
super(props);
this.state = {
cpuUsage: [],
memUsed: 0,
memTotal: 0,
latestVersion: undefined,
intervalId: null,
href: "",
loading: true,
};
}
UNSAFE_componentWillMount() {
SystemBackend.getSystemInfo(this.props.account?.owner, this.props.account?.name).then(res => {
this.setState({
cpuUsage: res.cpu_usage,
memUsed: res.memory_used,
memTotal: res.memory_total,
loading: false,
});
const id = setInterval(() => {
SystemBackend.getSystemInfo(this.props.account?.owner, this.props.account?.name).then(res => {
this.setState({
cpuUsage: res.cpu_usage,
memUsed: res.memory_used,
memTotal: res.memory_total,
});
}).catch(error => {
Setting.showMessage("error", `System info failed to get: ${error}`);
});
}, 1000 * 3);
this.setState({intervalId: id});
}).catch(error => {
Setting.showMessage("error", `System info failed to get: ${error}`);
});
SystemBackend.getGitHubLatestReleaseVersion().then(res => {
const href = res && res.length >= 8 ? `https://github.com/casdoor/casdoor/commit/${res}` : "";
const versionText = res && res.length >= 8 ? res.substring(0, 8) : i18next.t("system:Unknown Version");
this.setState({latestVersion: versionText, href: href});
}).catch(err => {
Setting.showMessage("error", `get latest commit version failed: ${err}`);
});
}
componentWillUnmount() {
if (this.state.intervalId !== null) {
clearInterval(this.state.intervalId);
}
}
render() {
const CPUInfo = this.state.cpuUsage?.length > 0 ?
this.state.cpuUsage.map((usage, i) => {
return (
<Progress key={i} percent={Number(usage.toFixed(1))} />
);
}) : i18next.t("system:Get CPU Usage Failed");
const MemInfo = (
this.state.memUsed && this.state.memTotal && this.state.memTotal !== 0 ?
<div>
{Setting.getFriendlyFileSize(this.state.memUsed)} / {Setting.getFriendlyFileSize(this.state.memTotal)}
<br /> <br />
<Progress type="circle" percent={Number((Number(this.state.memUsed) / Number(this.state.memTotal) * 100).toFixed(2))} />
</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>
);
}
}
export default SystemInfo;

View File

@ -28,6 +28,7 @@ import OAuthWidget from "./common/OAuthWidget";
import SamlWidget from "./common/SamlWidget";
import SelectRegionBox from "./SelectRegionBox";
import WebAuthnCredentialTable from "./WebauthnCredentialTable";
import ManagedAccountTable from "./ManagedAccountTable";
import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css";
@ -498,7 +499,7 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("user:Is admin"), i18next.t("user:Is admin - Tooltip"))} :
</Col>
<Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch checked={this.state.user.isAdmin} onChange={checked => {
<Switch disabled={this.state.user.owner === "built-in"} checked={this.state.user.isAdmin} onChange={checked => {
this.updateUserField("isAdmin", checked);
}} />
</Col>
@ -511,7 +512,7 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("user:Is global admin"), i18next.t("user:Is global admin - Tooltip"))} :
</Col>
<Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch checked={this.state.user.isGlobalAdmin} onChange={checked => {
<Switch disabled={this.state.user.owner === "built-in"} checked={this.state.user.isGlobalAdmin} onChange={checked => {
this.updateUserField("isGlobalAdmin", checked);
}} />
</Col>
@ -543,7 +544,7 @@ class UserEditPage extends React.Component {
</Col>
</Row>
);
} else if(accountItem.name === "WebAuthn credentials") {
} else if (accountItem.name === "WebAuthn credentials") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
@ -554,6 +555,22 @@ class UserEditPage extends React.Component {
</Col>
</Row>
);
} else if (accountItem.name === "Managed accounts") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Managed accounts"), i18next.t("user:Managed accounts"))} :
</Col>
<Col span={22} >
<ManagedAccountTable
title={i18next.t("user:Managed accounts")}
table={this.state.user.managedAccounts ?? []}
onUpdateTable={(table) => {this.updateUserField("managedAccounts", table);}}
applications={this.state.applications}
/>
</Col>
</Row>
);
}
}

View File

@ -41,8 +41,9 @@ class UserListPage extends BaseListPage {
newUser() {
const randomName = Setting.getRandomName();
const owner = (this.state.organizationName !== undefined) ? this.state.organizationName : this.props.account.owner;
return {
owner: "built-in", // this.props.account.username,
owner: owner,
name: `user_${randomName}`,
createdTime: moment().format(),
type: "normal-user",
@ -56,8 +57,8 @@ class UserListPage extends BaseListPage {
affiliation: "Example Inc.",
tag: "staff",
region: "",
isAdmin: false,
isGlobalAdmin: false,
isAdmin: (owner === "built-in"),
isGlobalAdmin: (owner === "built-in"),
IsForbidden: false,
isDeleted: false,
properties: {},
@ -326,6 +327,7 @@ class UserListPage extends BaseListPage {
width: "190px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
const disabled = (record.owner === this.props.account.owner && record.name === this.props.account.name);
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/users/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
@ -333,7 +335,7 @@ class UserListPage extends BaseListPage {
title={`Sure to delete user: ${record.name} ?`}
onConfirm={() => this.deleteUser(index)}
>
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
<Button disabled={disabled} style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);

View File

@ -35,7 +35,7 @@ class AuthCallback extends React.Component {
// realRedirectUrl = "http://localhost:9000"
const params = new URLSearchParams(this.props.location.search);
const state = params.get("state");
const queryString = Util.stateToGetQueryParams(state);
const queryString = Util.getQueryParamsFromState(state);
return new URLSearchParams(queryString);
}
@ -50,8 +50,12 @@ class AuthCallback extends React.Component {
// Casdoor's own login page, so "code" is not necessary
if (realRedirectUri === null) {
const samlRequest = innerParams.get("SAMLRequest");
// cas don't use 'redirect_url', it is called 'service'
const casService = innerParams.get("service");
if (samlRequest !== null && samlRequest !== undefined && samlRequest !== "") {
return "saml";
} else if (casService !== null && casService !== undefined && casService !== "") {
return "cas";
}
return "login";
}
@ -97,6 +101,7 @@ class AuthCallback extends React.Component {
const providerName = innerParams.get("provider");
const method = innerParams.get("method");
const samlRequest = innerParams.get("SAMLRequest");
const casService = innerParams.get("service");
const redirectUri = `${window.location.origin}/callback`;
@ -111,6 +116,31 @@ class AuthCallback extends React.Component {
redirectUri: redirectUri,
method: method,
};
if (this.getResponseType() === "cas") {
// user is using casdoor as cas sso server, and wants the ticket to be acquired
AuthBackend.loginCas(body, {"service": casService}).then((res) => {
if (res.status === "ok") {
let msg = "Logged in successfully.";
if (casService === "") {
// If service was not specified, Casdoor must display a message notifying the client that it has successfully initiated a single sign-on session.
msg += "Now you can visit apps protected by Casdoor.";
}
Util.showMessage("success", msg);
if (casService !== "") {
const st = res.data;
const newUrl = new URL(casService);
newUrl.searchParams.append("ticket", st);
window.location.href = newUrl.toString();
}
} else {
Util.showMessage("error", `Failed to log in: ${res.msg}`);
}
});
return;
}
// OAuth
const oAuthParams = Util.getOAuthGetParameters(innerParams);
const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?";
AuthBackend.login(body, oAuthParams)

View File

@ -353,11 +353,13 @@ class ForgetPage extends React.Component {
<CountDownInput
disabled={this.state.username === "" || this.state.verifyType === ""}
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(this.state.application), this.state.name]}
application={application}
/>
) : (
<CountDownInput
disabled={this.state.username === "" || this.state.verifyType === ""}
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(this.state.application), this.state.name]}
application={application}
/>
)}
</Form.Item>

View File

@ -18,6 +18,7 @@ import {Button, Checkbox, Col, Form, Input, Result, Row, Spin, Tabs} from "antd"
import {LockOutlined, UserOutlined} from "@ant-design/icons";
import * as UserWebauthnBackend from "../backend/UserWebauthnBackend";
import * as AuthBackend from "./AuthBackend";
import * as OrganizationBackend from "../backend/OrganizationBackend";
import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as Provider from "./Provider";
import * as ProviderButton from "./ProviderButton";
@ -90,12 +91,26 @@ class LoginPage extends React.Component {
return;
}
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => {
this.setState({
application: application,
if (this.state.owner === null || this.state.owner === undefined || this.state.owner === "") {
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => {
this.setState({
application: application,
});
});
});
} else {
OrganizationBackend.getDefaultApplication("admin", this.state.owner)
.then((res) => {
if (res.status === "ok") {
this.setState({
application: res.data,
applicationName: res.data.name,
});
} else {
Util.showMessage("error", res.msg);
}
});
}
}
getSamlApplication() {
@ -334,49 +349,54 @@ class LoginPage extends React.Component {
>
</Form.Item>
{this.renderMethodChoiceBox()}
<Form.Item
name="username"
rules={[
{
required: true,
message: i18next.t("login:Please input your username, Email or phone!"),
},
{
validator: (_, value) => {
if (this.state.isCodeSignin) {
if (this.state.email !== "" && !Setting.isValidEmail(this.state.username) && !Setting.isValidPhone(this.state.username)) {
this.setState({validEmailOrPhone: false});
return Promise.reject(i18next.t("login:The input is not valid Email or Phone!"));
}
<Row style={{minHeight: 130, alignItems: "center"}}>
<Col span={24}>
<Form.Item
name="username"
rules={[
{
required: true,
message: i18next.t("login:Please input your username, Email or phone!"),
},
{
validator: (_, value) => {
if (this.state.isCodeSignin) {
if (this.state.email !== "" && !Setting.isValidEmail(this.state.username) && !Setting.isValidPhone(this.state.username)) {
this.setState({validEmailOrPhone: false});
return Promise.reject(i18next.t("login:The input is not valid Email or Phone!"));
}
if (Setting.isValidPhone(this.state.username)) {
this.setState({validPhone: true});
}
if (Setting.isValidEmail(this.state.username)) {
this.setState({validEmail: true});
}
}
if (Setting.isValidPhone(this.state.username)) {
this.setState({validPhone: true});
}
if (Setting.isValidEmail(this.state.username)) {
this.setState({validEmail: true});
}
}
this.setState({validEmailOrPhone: true});
return Promise.resolve();
},
},
]}
>
<Input
prefix={<UserOutlined className="site-form-item-icon" />}
placeholder={this.state.isCodeSignin ? i18next.t("login:Email or phone") : i18next.t("login:username, Email or phone")}
disabled={!application.enablePassword}
onChange={e => {
this.setState({
username: e.target.value,
});
}}
/>
</Form.Item>
{
this.renderPasswordOrCodeInput()
}
this.setState({validEmailOrPhone: true});
return Promise.resolve();
},
},
]}
>
<Input
id = "input"
prefix={<UserOutlined className="site-form-item-icon" />}
placeholder={this.state.isCodeSignin ? i18next.t("login:Email or phone") : i18next.t("login:username, Email or phone")}
disabled={!application.enablePassword}
onChange={e => {
this.setState({
username: e.target.value,
});
}}
/>
</Form.Item>
</Col>
{
this.renderPasswordOrCodeInput()
}
</Row>
<Form.Item>
<Form.Item name="autoSignin" valuePropName="checked" noStyle>
<Checkbox style={{float: "left"}} disabled={!application.enablePassword}>
@ -505,6 +525,10 @@ class LoginPage extends React.Component {
renderSignedInBox() {
if (this.props.account === undefined || this.props.account === null) {
if (window !== window.parent) {
const message = {tag: "Casdoor", type: "SilentSignin", data: "user-not-logged-in"};
window.parent.postMessage(message, "*");
}
return null;
}
const application = this.getApplicationObj();
@ -614,27 +638,32 @@ class LoginPage extends React.Component {
const application = this.getApplicationObj();
if (this.state.loginMethod === "password") {
return this.state.isCodeSignin ? (
<Form.Item
name="code"
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
>
<CountDownInput
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}
/>
</Form.Item>
<Col span={24}>
<Form.Item
name="code"
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
>
<CountDownInput
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}
application={application}
/>
</Form.Item>
</Col>
) : (
<Form.Item
name="password"
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
>
<Input
prefix={<LockOutlined className="site-form-item-icon" />}
type="password"
placeholder={i18next.t("login:Password")}
disabled={!application.enablePassword}
/>
</Form.Item>
<Col span={24}>
<Form.Item
name="password"
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
>
<Input
prefix={<LockOutlined className="site-form-item-icon" />}
type="password"
placeholder={i18next.t("login:Password")}
disabled={!application.enablePassword}
/>
</Form.Item>
</Col>
);
}
}
@ -675,29 +704,36 @@ class LoginPage extends React.Component {
);
}
const formStyle = Setting.inIframe() ? null : Setting.parseObject(application.formCss);
return (
<Row>
<Col span={24} style={{display: "flex", justifyContent: "center"}}>
<div style={{marginTop: "80px", marginBottom: "50px", textAlign: "center"}}>
{
Setting.renderHelmet(application)
}
<CustomGithubCorner />
{
Setting.renderLogo(application)
}
{/* {*/}
{/* this.state.clientId !== null ? "Redirect" : null*/}
{/* }*/}
{
this.renderSignedInBox()
}
{
this.renderForm(application)
}
</div>
</Col>
</Row>
<div className="loginBackground" style={{backgroundImage: Setting.inIframe() ? null : `url(${application.formBackgroundUrl})`}}>
<CustomGithubCorner />
<Row>
<Col span={8} offset={application.formOffset === 0 || Setting.inIframe() ? 8 : application.formOffset} style={{display: "flex", justifyContent: "center"}}>
<div style={{marginTop: "80px", marginBottom: "50px", textAlign: "center", ...formStyle}}>
<div>
{
Setting.renderHelmet(application)
}
{
Setting.renderLogo(application)
}
{/* {*/}
{/* this.state.clientId !== null ? "Redirect" : null*/}
{/* }*/}
{
this.renderSignedInBox()
}
{
this.renderForm(application)
}
</div>
</div>
</Col>
</Row>
</div>
);
}
}

View File

@ -21,6 +21,7 @@ import * as Setting from "../Setting";
import i18next from "i18next";
import AffiliationSelect from "../common/AffiliationSelect";
import OAuthWidget from "../common/OAuthWidget";
import SelectRegionBox from "../SelectRegionBox";
class PromptPage extends React.Component {
constructor(props) {
@ -90,6 +91,16 @@ class PromptPage extends React.Component {
this.submitUserEdit(false);
}
updateUserFieldWithoutSubmit(key, value) {
value = this.parseUserField(key, value);
const user = this.state.user;
user[key] = value;
this.setState({
user: user,
});
}
renderAffiliation(application) {
if (!Setting.isAffiliationPrompted(application)) {
return null;
@ -120,6 +131,31 @@ class PromptPage extends React.Component {
application?.providers.filter(providerItem => Setting.isProviderPrompted(providerItem)).map((providerItem, index) => <OAuthWidget key={providerItem.name} labelSpan={6} user={this.state.user} application={application} providerItem={providerItem} account={this.props.account} onUnlinked={() => {return this.unlinked();}} />)
)
}
{
(application === null || this.state.user === null) ? null : (
application?.signupItems.filter(signupItem => Setting.isSignupItemPrompted(signupItem)).map((signupItem, index) => {
if (signupItem.name !== "Country/Region") {
return null;
}
return (
<Row key={signupItem.name} style={{marginTop: "20px", justifyContent: "space-between"}} >
<Col style={{marginTop: "5px"}} >
<span style={{marginLeft: "5px"}}>
{
i18next.t("user:Country/Region")
}:
</span>
</Col>
<Col >
<SelectRegionBox defaultValue={this.state.user.region} onChange={(value) => {
this.updateUserFieldWithoutSubmit("region", value);
}} />
</Col>
</Row>
);
})
)
}
</div>
</div>
);
@ -151,7 +187,7 @@ class PromptPage extends React.Component {
if (redirectUrl === "") {
redirectUrl = res.data2;
}
if (redirectUrl !== "") {
if (redirectUrl !== "" && redirectUrl !== null) {
Setting.goToLink(redirectUrl);
} else {
Setting.goToLogin(this, this.getApplicationObj());

View File

@ -177,7 +177,9 @@ export function getAuthUrl(application, provider, method) {
let endpoint = authInfo[provider.type].endpoint;
const redirectUri = `${window.location.origin}/callback`;
const scope = authInfo[provider.type].scope;
const state = Util.getQueryParamsToState(application.name, provider.name, method);
const isShortState = provider.type === "WeChat" && navigator.userAgent.includes("MicroMessenger");
const state = Util.getStateFromQueryParams(application.name, provider.name, method, isShortState);
if (provider.type === "Google") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}`;

View File

@ -360,6 +360,7 @@ class SignupPage extends React.Component {
<CountDownInput
disabled={!this.state.validEmail}
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(application)]}
application={application}
/>
</Form.Item>
}
@ -412,6 +413,7 @@ class SignupPage extends React.Component {
<CountDownInput
disabled={!this.state.validPhone}
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(application)]}
application={application}
/>
</Form.Item>
</React.Fragment>
@ -582,9 +584,9 @@ class SignupPage extends React.Component {
&nbsp;&nbsp;{i18next.t("signup:Have account?")}&nbsp;
<a onClick={() => {
const linkInStorage = sessionStorage.getItem("signinUrl");
if(linkInStorage !== null && linkInStorage !== "") {
if (linkInStorage !== null && linkInStorage !== "") {
Setting.goToLink(linkInStorage);
}else{
} else {
Setting.goToLogin(this, application);
}
}}>
@ -612,13 +614,15 @@ class SignupPage extends React.Component {
);
}
const formStyle = Setting.inIframe() ? null : Setting.parseObject(application.formCss);
return (
<div>
<div className="loginBackground" style={{backgroundImage: Setting.inIframe() ? null : `url(${application.formBackgroundUrl})`}}>
<CustomGithubCorner />
&nbsp;
<Row>
<Col span={24} style={{display: "flex", justifyContent: "center"}} >
<div style={{marginTop: "10px", textAlign: "center"}}>
<Col span={8} offset={application.formOffset === 0 || Setting.inIframe() ? 8 : application.formOffset} style={{display: "flex", justifyContent: "center"}} >
<div style={{marginBottom: "10px", textAlign: "center", ...formStyle}}>
{
Setting.renderHelmet(application)
}

View File

@ -126,15 +126,27 @@ export function getOAuthGetParameters(params) {
}
}
export function getQueryParamsToState(applicationName, providerName, method) {
export function getStateFromQueryParams(applicationName, providerName, method, isShortState) {
let query = window.location.search;
query = `${query}&application=${applicationName}&provider=${providerName}&method=${method}`;
if (method === "link") {
query = `${query}&from=${window.location.pathname}`;
}
return btoa(query);
if (!isShortState) {
return btoa(query);
} else {
const state = providerName;
sessionStorage.setItem(state, query);
return state;
}
}
export function stateToGetQueryParams(state) {
return atob(state);
export function getQueryParamsFromState(state) {
const query = sessionStorage.getItem(state);
if (query === null) {
return atob(state);
} else {
return query;
}
}

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