Compare commits

...

40 Commits

Author SHA1 Message Date
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
69 changed files with 4911 additions and 5701 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)
}
@ -108,6 +110,7 @@ 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)
@ -128,6 +131,12 @@ p, *, *, GET, /api/get-release, *, *
}
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)
@ -135,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

@ -269,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

@ -121,3 +121,23 @@ func (c *ApiController) DeleteOrganization() {
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

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

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

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

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

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

@ -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"},
@ -105,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{},
@ -135,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",
@ -156,6 +157,7 @@ func initBuiltInApplication() {
},
RedirectUris: []string{},
ExpireInHours: 168,
FormOffset: 8,
}
AddApplication(application)
}
@ -239,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 {
@ -250,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,6 +41,7 @@ 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"`
@ -216,3 +217,32 @@ 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
}
for _, application := range applications {
if application.EnableSignUp {
return application
}
}
return applications[0]
}

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

@ -488,7 +488,7 @@ func AddUsers(users []*User) bool {
}
func AddUsersInBatch(users []*User) bool {
batchSize := 1000
batchSize := conf.GetConfigBatchSize()
if len(users) == 0 {
return false
@ -527,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,
@ -566,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,7 +47,7 @@ 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)
@ -63,7 +63,7 @@ func SendVerificationCodeToPhone(organization *Organization, user *User, provide
return errors.New("please set a SMS provider first")
}
code := getRandomCode(5)
code := getRandomCode(6)
if err := SendSms(provider, code, dest); err != nil {
return err
}

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

View File

@ -19,14 +19,14 @@ import (
"os"
"strings"
"github.com/astaxie/beego"
"github.com/astaxie/beego/context"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
)
var (
oldStaticBaseUrl = "https://cdn.casbin.org"
newStaticBaseUrl = beego.AppConfig.String("staticBaseUrl")
newStaticBaseUrl = conf.GetConfigString("staticBaseUrl")
)
func StaticFilter(ctx *context.Context) {

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))]
}

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

@ -388,16 +388,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">
@ -527,7 +526,7 @@ class App extends Component {
}
renderRouter() {
return(
return (
<div>
<Switch>
<Route exact path="/result" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...props} />)} />
@ -595,7 +594,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()
@ -618,7 +617,7 @@ class App extends Component {
</div>
);
} else {
return(
return (
<div>
<Header style={{padding: "0", marginBottom: "3px"}}>
{
@ -746,6 +745,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

@ -38,7 +38,7 @@ class ManagedAccountTable extends React.Component {
}
addRow(table) {
const row = {application: "", username: "", password: "", signinUrl: ""};
const row = {application: "", username: "", password: ""};
if (table === undefined || table === null) {
table = [];
}
@ -69,16 +69,11 @@ class ManagedAccountTable extends React.Component {
key: "application",
render: (text, record, index) => {
const items = this.props.applications;
const signinUrlMap = new Map();
for (const application of items) {
signinUrlMap.set(application.name, application.signinUrl);
}
return (
<Select virtual={false} style={{width: "100%"}}
value={text}
onChange={value => {
this.updateField(table, index, "application", value);
this.updateField(table, index, "signinUrl", signinUrlMap.get(value));
}} >
{
items.map((item, index) => <Option key={index} value={item.name}>{item.name}</Option>)

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

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

@ -82,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/",
},
},
@ -354,6 +354,14 @@ export function isPromptAnswered(user, application) {
return true;
}
export function parseObject(s) {
try {
return eval("(" + s + ")");
} catch (e) {
return null;
}
}
export function parseJson(s) {
if (s === "") {
return null;
@ -450,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;
}
@ -500,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++) {
@ -510,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() {
@ -633,6 +641,7 @@ export function getProviderTypeOptions(category) {
return (
[
{id: "Default", name: "Default"},
{id: "SUBMAIL", name: "SUBMAIL"},
]
);
} else if (category === "SMS") {
@ -706,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"}} />
);
}
}
@ -912,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

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

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

View File

@ -54,3 +54,10 @@ export function deleteOrganization(organization) {
body: JSON.stringify(newOrganization),
}).then(res => res.json());
}
export function getDefaultApplication(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-default-application?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include",
}).then(res => res.json());
}

View File

@ -17,13 +17,12 @@ import React from "react";
import i18next from "i18next";
import * as UserBackend from "../backend/UserBackend";
import {SafetyOutlined} from "@ant-design/icons";
import {authConfig} from "../auth/Auth";
import {CaptchaWidget} from "./CaptchaWidget";
const {Search} = Input;
export const CountDownInput = (props) => {
const {disabled, textBefore, onChange, onButtonClickArgs} = props;
const {disabled, textBefore, onChange, onButtonClickArgs, application} = props;
const [visible, setVisible] = React.useState(false);
const [key, setKey] = React.useState("");
const [captchaImg, setCaptchaImg] = React.useState("");
@ -69,7 +68,7 @@ export const CountDownInput = (props) => {
};
const loadCaptcha = () => {
UserBackend.getCaptcha("admin", authConfig.appName, false).then(res => {
UserBackend.getCaptcha(application.owner, application.name, false).then(res => {
if (res.type === "none") {
UserBackend.sendCode("none", "", "", ...onButtonClickArgs).then(res => {
if (res) {

View File

@ -1,14 +1,28 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
font-family:
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Fira Sans",
"Droid Sans",
"Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
font-family:
source-code-pro,
Menlo,
Monaco,
Consolas,
"Courier New",
monospace;
}
@ -17,12 +31,14 @@ code {
background-size: 130px, 27px;
width: 130px;
height: 27px;
/*background: rgba(0, 0, 0, 0.2);*/
margin: 17px 10px 16px 20px;
margin: 17px 0 16px 15px;
float: left;
}
.ant-table.ant-table-middle .ant-table-title, .ant-table.ant-table-middle .ant-table-footer, .ant-table.ant-table-middle thead > tr > th, .ant-table.ant-table-middle tbody > tr > td {
.ant-table.ant-table-middle .ant-table-title,
.ant-table.ant-table-middle .ant-table-footer,
.ant-table.ant-table-middle thead > tr > th,
.ant-table.ant-table-middle tbody > tr > td {
padding: 1px 8px !important;
}

View File

@ -16,18 +16,19 @@ import "core-js/es";
import "react-app-polyfill/ie9";
import "react-app-polyfill/stable";
import React from "react";
import ReactDOM from "react-dom";
import {createRoot} from "react-dom/client";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import {BrowserRouter} from "react-router-dom";
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
const container = document.getElementById("root");
const app = createRoot(container);
app.render(<BrowserRouter>
<App />
</BrowserRouter>);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.

View File

@ -6,6 +6,8 @@
"Sign Up": "Registrieren"
},
"application": {
"Background URL": "Background URL",
"Background URL - Tooltip": "Background URL - Tooltip",
"Copy SAML metadata URL": "Copy SAML metadata URL",
"Copy prompt page URL": "Copy prompt page URL",
"Copy signin page URL": "Copy signin page URL",
@ -21,6 +23,11 @@
"Enable signup": "Anmeldung aktivieren",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "Datei erfolgreich hochgeladen",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
"From position": "From position",
"From position - Tooltip": "From position - Tooltip",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
@ -117,6 +124,8 @@
"Click to Upload": "Click to Upload",
"Client IP": "Client-IP",
"Created time": "Erstellte Zeit",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Standard Avatar",
"Default avatar - Tooltip": "default avatar",
"Delete": "Löschen",

View File

@ -6,6 +6,8 @@
"Sign Up": "Sign Up"
},
"application": {
"Background URL": "Background URL",
"Background URL - Tooltip": "Background URL - Tooltip",
"Copy SAML metadata URL": "Copy SAML metadata URL",
"Copy prompt page URL": "Copy prompt page URL",
"Copy signin page URL": "Copy signin page URL",
@ -21,6 +23,11 @@
"Enable signup": "Enable signup",
"Enable signup - Tooltip": "Enable signup - Tooltip",
"File uploaded successfully": "File uploaded successfully",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
"From position": "From position",
"From position - Tooltip": "From position - Tooltip",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
@ -117,6 +124,8 @@
"Click to Upload": "Click to Upload",
"Client IP": "Client IP",
"Created time": "Created time",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Default avatar",
"Default avatar - Tooltip": "Default avatar - Tooltip",
"Delete": "Delete",

View File

@ -6,6 +6,8 @@
"Sign Up": "S'inscrire"
},
"application": {
"Background URL": "Background URL",
"Background URL - Tooltip": "Background URL - Tooltip",
"Copy SAML metadata URL": "Copy SAML metadata URL",
"Copy prompt page URL": "Copy prompt page URL",
"Copy signin page URL": "Copy signin page URL",
@ -21,6 +23,11 @@
"Enable signup": "Activer l'inscription",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "Fichier téléchargé avec succès",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
"From position": "From position",
"From position - Tooltip": "From position - Tooltip",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
@ -117,6 +124,8 @@
"Click to Upload": "Click to Upload",
"Client IP": "IP du client",
"Created time": "Date de création",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Avatar par défaut",
"Default avatar - Tooltip": "default avatar",
"Delete": "Supprimez",

View File

@ -6,6 +6,8 @@
"Sign Up": "新規登録"
},
"application": {
"Background URL": "Background URL",
"Background URL - Tooltip": "Background URL - Tooltip",
"Copy SAML metadata URL": "Copy SAML metadata URL",
"Copy prompt page URL": "Copy prompt page URL",
"Copy signin page URL": "Copy signin page URL",
@ -21,6 +23,11 @@
"Enable signup": "サインアップを有効にする",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "ファイルが正常にアップロードされました",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
"From position": "From position",
"From position - Tooltip": "From position - Tooltip",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
@ -117,6 +124,8 @@
"Click to Upload": "Click to Upload",
"Client IP": "クライアント IP",
"Created time": "作成日時",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "デフォルトのアバター",
"Default avatar - Tooltip": "default avatar",
"Delete": "削除",

View File

@ -6,6 +6,8 @@
"Sign Up": "Sign Up"
},
"application": {
"Background URL": "Background URL",
"Background URL - Tooltip": "Background URL - Tooltip",
"Copy SAML metadata URL": "Copy SAML metadata URL",
"Copy prompt page URL": "Copy prompt page URL",
"Copy signin page URL": "Copy signin page URL",
@ -21,6 +23,11 @@
"Enable signup": "Enable signup",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "File uploaded successfully",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
"From position": "From position",
"From position - Tooltip": "From position - Tooltip",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
@ -117,6 +124,8 @@
"Click to Upload": "Click to Upload",
"Client IP": "Client IP",
"Created time": "Created time",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Default avatar",
"Default avatar - Tooltip": "default avatar",
"Delete": "Delete",

View File

@ -6,6 +6,8 @@
"Sign Up": "Регистрация"
},
"application": {
"Background URL": "Background URL",
"Background URL - Tooltip": "Background URL - Tooltip",
"Copy SAML metadata URL": "Копировать адрес метаданных SAML",
"Copy prompt page URL": "Скопировать URL-адрес страницы запроса",
"Copy signin page URL": "Скопировать URL-адрес страницы входа",
@ -21,6 +23,11 @@
"Enable signup": "Включить регистрацию",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "Файл успешно загружен",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
"From position": "From position",
"From position - Tooltip": "From position - Tooltip",
"Grant types": "Виды грантов",
"Grant types - Tooltip": "Виды грантов - Подсказка",
"New Application": "Новое приложение",
@ -117,6 +124,8 @@
"Click to Upload": "Нажмите здесь, чтобы загрузить",
"Client IP": "IP клиента",
"Created time": "Время создания",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Аватар по умолчанию",
"Default avatar - Tooltip": "default avatar",
"Delete": "Удалить",

View File

@ -6,6 +6,8 @@
"Sign Up": "注册"
},
"application": {
"Background URL": "背景图URL",
"Background URL - Tooltip": "登录页背景图的链接",
"Copy SAML metadata URL": "复制SAML元数据URL",
"Copy prompt page URL": "复制提醒页面URL",
"Copy signin page URL": "复制登录页面URL",
@ -21,6 +23,11 @@
"Enable signup": "启用注册",
"Enable signup - Tooltip": "是否允许用户注册",
"File uploaded successfully": "文件上传成功",
"Form CSS": "表单CSS",
"Form CSS - Edit": "编辑表单CSS",
"Form CSS - Tooltip": "表单的CSS样式(增加边框和阴影)",
"From position": "面板的位置",
"From position - Tooltip": "登录和注册面板的位置",
"Grant types": "OAuth授权类型",
"Grant types - Tooltip": "选择允许哪些OAuth协议中的Grant types",
"New Application": "添加应用",
@ -117,6 +124,8 @@
"Click to Upload": "点击上传",
"Client IP": "客户端IP",
"Created time": "创建时间",
"Default application": "默认应用",
"Default application - Tooltip": "默认应用",
"Default avatar": "默认头像",
"Default avatar - Tooltip": "默认头像",
"Delete": "删除",

File diff suppressed because it is too large Load Diff