mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-09 01:13:41 +08:00
Compare commits
40 Commits
Author | SHA1 | Date | |
---|---|---|---|
5707e38912 | |||
ed959bd8c7 | |||
b6cdc46023 | |||
c661a57cb2 | |||
8456b7f7c4 | |||
e8d2906e3c | |||
1edb91b3a3 | |||
94b6eb803d | |||
cfce5289ed | |||
10f1c37730 | |||
6035b98653 | |||
e158b58ffa | |||
a399184cfc | |||
2f9f946c87 | |||
d8b60f838e | |||
7599e2715a | |||
35676455bc | |||
8128671c8c | |||
ee54dec3b3 | |||
d278bc9651 | |||
b23bd0b189 | |||
409be85264 | |||
0395b7e1a9 | |||
4536fd0636 | |||
af9ae7dbb7 | |||
e266696b32 | |||
e108d26ec7 | |||
349ce7f1d4 | |||
8da50b7893 | |||
2394c8e2b4 | |||
c62983d734 | |||
5948782cdd | |||
674d1619dd | |||
11b8b65ca0 | |||
411d76798d | |||
7b0b426a76 | |||
a383af0ebc | |||
f02875e1b1 | |||
e2921419b9 | |||
42864700ec |
21
README.md
21
README.md
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
4
build.sh
4
build.sh
@ -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 .
|
||||
|
@ -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
|
||||
|
47
conf/conf.go
47
conf/conf.go
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
70
deployment/deploy.go
Normal 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
29
deployment/deploy_test.go
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//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)
|
||||
}
|
@ -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,
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ type UserInfo struct {
|
||||
Id string
|
||||
Username string
|
||||
DisplayName string
|
||||
UnionId string
|
||||
Email string
|
||||
AvatarUrl string
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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"},
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"`
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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{
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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) {
|
||||
|
18
util/path.go
18
util/path.go
@ -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))]
|
||||
}
|
||||
|
@ -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
6
web/.stylelintrc.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": [
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-recommended-less"
|
||||
]
|
||||
}
|
17
web/babel.config.json
Normal file
17
web/babel.config.json
Normal 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"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
@ -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": {
|
||||
|
@ -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} />
|
||||
|
@ -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%;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -53,6 +53,7 @@ class ApplicationListPage extends BaseListPage {
|
||||
redirectUris: ["http://localhost:9000/callback"],
|
||||
tokenFormat: "JWT",
|
||||
expireInHours: 24 * 7,
|
||||
formOffset: 8,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>)
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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"))} :
|
||||
|
@ -35,6 +35,7 @@ class OrganizationListPage extends BaseListPage {
|
||||
PasswordSalt: "",
|
||||
phonePrefix: "86",
|
||||
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
|
||||
defaultApplication: "",
|
||||
tags: [],
|
||||
masterPassword: "",
|
||||
enableSoftDeletion: false,
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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":
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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}`;
|
||||
|
@ -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 {
|
||||
{i18next.t("signup:Have account?")}
|
||||
<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 />
|
||||
|
||||
<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)
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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": "削除",
|
||||
|
@ -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",
|
||||
|
@ -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": "Удалить",
|
||||
|
@ -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": "删除",
|
||||
|
9191
web/yarn.lock
9191
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user