Compare commits

..

23 Commits

Author SHA1 Message Date
Lai Zn
d986a4a9e0 feat: fix bug that initialize group children as empty array instead of empty string (#3620) 2025-02-26 08:50:09 +08:00
DacongDA
2df3878c15 feat: fix bug that group.HaveChildren is never set to false bug Something isn't working (#3609) 2025-02-22 01:46:35 +08:00
DacongDA
24ab8880cc feat: fix bug that organization might be nil in some case and cause nil point error (#3608) 2025-02-21 23:43:30 +08:00
ners
f26b4853c5 feat: bump Go version to go 1.18 (#3599) 2025-02-21 13:10:17 +08:00
DacongDA
d78e8e9776 feat: fix LDAP filter condition will return nil if error happened (#3604) 2025-02-21 13:09:39 +08:00
WindSpiritSR
d61f9a1856 feat: update antd from 5.2.3 to 5.24.1 (#3593) 2025-02-18 20:54:10 +08:00
WindSpiritSR
aa52af02b3 feat: fix style props of Editor (#3590) 2025-02-17 13:39:49 +08:00
WindSpiritSR
2a5722e45b feat: add detail sidebar for record list page, improve token list page (#3589) 2025-02-16 22:01:25 +08:00
Mayank
26718bc4a1 feat: update signinUrl storage to include pathname and query parameters only to prevent new tab popup after password reset (#3587) 2025-02-14 20:31:36 +08:00
Yang Luo
f8d44e2dca feat: set default CountryCode for user 2025-02-14 16:54:25 +08:00
Yang Luo
26eea501be feat: don't use organization.MasterVerificationCode when sending 2025-02-14 16:54:25 +08:00
Mayank
63b8e857bc feat: update signinUrl storage to include path and query parameters in forced reset password flow (#3583) 2025-02-14 01:32:10 +08:00
WindSpiritSR
81b336b37a feat: replace react-codemirror2 with @uiw/react-codemirror (#3577)
Signed-off-by: WindSpiritSR <simon343riley@gmail.com>
2025-02-14 00:10:33 +08:00
DacongDA
9c39179849 feat: fix bug that user forbidden check will be skipped in OAuth login (#3580) 2025-02-13 13:14:44 +08:00
Bui Le Anh Nguyen
37d93a5eea feat: update SendgridEmailProvider to support dynamic host/path, add From name field (#3576)
* feat: add fields into UI FromName, Host, Endpoint

* feat: update SendgridEmailProvider support dynamic host/path client init, code convention
2025-02-13 00:51:31 +08:00
Yang Luo
e926a07c58 feat: add "User type" to user list page 2025-02-12 21:29:18 +08:00
Yang Luo
9c46344e68 feat: improve default org passwordOptions handling 2025-02-12 21:20:32 +08:00
WindSpiritSR
c0ec73dfd3 feat: fix tableNamePrefix doesn't work bug in /get-dashboard API (#3572) 2025-02-11 17:20:45 +08:00
Brian Yu
b1b6ebe692 feat(jwt): add azp claim to ID token (#3570)
Added the `azp` (Authorized Party) claim to various JWT token structures
including Claims, ClaimsShort, ClaimsWithoutThirdIdp, and ClaimsStandard.
Updated the generateJwtToken and getClaimsCustom functions to handle the
new claim. This change aligns with the OpenID Connect specification.
2025-02-10 20:44:44 +08:00
Yang Luo
a0931e4597 feat: add userTypes to Organization 2025-02-09 17:12:13 +08:00
DacongDA
c181006661 feat: cache theme in signup page (#3568) 2025-02-09 15:12:35 +08:00
Cutsin
2e83e49492 feat: fix bug due to null characters in descriptor when creating a payment intent (#3567) 2025-02-08 19:35:51 +08:00
Coki
5661942175 feat: add CLI version cache and proxy support (#3565)
* feat: add CLI version cache mechanism

* feat: add /api/refresh-engines to allowed endpoints in demo mode

* feat: add proxy support for cli downloader

* feat: add SafeGoroutine for CLIDownloader initialization

* refactor: optimize code structure
2025-02-08 19:34:19 +08:00
56 changed files with 2351 additions and 2592 deletions

View File

@@ -157,7 +157,7 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
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/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/verify-code" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") || urlPath == "/api/webhook" || urlPath == "/api/get-qrcode" {
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/verify-code" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") || urlPath == "/api/webhook" || urlPath == "/api/get-qrcode" || urlPath == "/api/refresh-engines" {
return true
} else if urlPath == "/api/update-user" {
// Allow ordinary users to update their own information

View File

@@ -458,6 +458,10 @@ func (c *ApiController) GetAccount() {
return
}
if organization != nil && len(organization.CountryCodes) == 1 && u != nil && u.CountryCode == "" {
u.CountryCode = organization.CountryCodes[0]
}
accessToken := c.GetSessionToken()
if accessToken == "" {
accessToken, err = object.GetAccessTokenByUser(user, c.Ctx.Request.Host)

View File

@@ -54,6 +54,11 @@ func tokenToResponse(token *object.Token) *Response {
// HandleLoggedIn ...
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *form.AuthForm) (resp *Response) {
if user.IsForbidden {
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
return
}
userId := user.GetId()
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
@@ -678,10 +683,6 @@ func (c *ApiController) Login() {
if user != nil && !user.IsDeleted {
// Sign in via OAuth (want to sign up but already have account)
if user.IsForbidden {
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
}
// sync info from 3rd-party if possible
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
if err != nil {

View File

@@ -23,9 +23,68 @@ import (
"os/exec"
"sort"
"strings"
"sync"
"time"
)
type CLIVersionInfo struct {
Version string
BinaryPath string
BinaryTime time.Time
}
var (
cliVersionCache = make(map[string]*CLIVersionInfo)
cliVersionMutex sync.RWMutex
)
// getCLIVersion
// @Title getCLIVersion
// @Description Get CLI version with cache mechanism
// @Param language string The language of CLI (go/java/rust etc.)
// @Return string The version string of CLI
// @Return error Error if CLI execution fails
func getCLIVersion(language string) (string, error) {
binaryName := fmt.Sprintf("casbin-%s-cli", language)
binaryPath, err := exec.LookPath(binaryName)
if err != nil {
return "", fmt.Errorf("executable file not found: %v", err)
}
fileInfo, err := os.Stat(binaryPath)
if err != nil {
return "", fmt.Errorf("failed to get binary info: %v", err)
}
cliVersionMutex.RLock()
if info, exists := cliVersionCache[language]; exists {
if info.BinaryPath == binaryPath && info.BinaryTime == fileInfo.ModTime() {
cliVersionMutex.RUnlock()
return info.Version, nil
}
}
cliVersionMutex.RUnlock()
cmd := exec.Command(binaryName, "--version")
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("failed to get CLI version: %v", err)
}
version := strings.TrimSpace(string(output))
cliVersionMutex.Lock()
cliVersionCache[language] = &CLIVersionInfo{
Version: version,
BinaryPath: binaryPath,
BinaryTime: fileInfo.ModTime(),
}
cliVersionMutex.Unlock()
return version, nil
}
func processArgsToTempFiles(args []string) ([]string, []string, error) {
tempFiles := []string{}
newArgs := []string{}
@@ -93,6 +152,16 @@ func (c *ApiController) RunCasbinCommand() {
return
}
if len(args) > 0 && args[0] == "--version" {
version, err := getCLIVersion(language)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(version)
return
}
tempFiles, processedArgs, err := processArgsToTempFiles(args)
defer func() {
for _, file := range tempFiles {

View File

@@ -9,7 +9,6 @@ import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
@@ -17,6 +16,8 @@ import (
"time"
"github.com/beego/beego"
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/util"
)
const (
@@ -108,9 +109,10 @@ func getFinalBinaryName(lang string) string {
// @Param language string true "Language type"
// @Success 200 {string} string "Download URL and version"
func getLatestCLIURL(repoURL string, language string) (string, string, error) {
resp, err := http.Get(repoURL)
client := proxy.GetHttpClient(repoURL)
resp, err := client.Get(repoURL)
if err != nil {
return "", "", err
return "", "", fmt.Errorf("failed to fetch release info: %v", err)
}
defer resp.Body.Close()
@@ -346,7 +348,8 @@ func downloadCLI() error {
originalPath := filepath.Join(downloadFolder, getBinaryNames()[lang])
fmt.Printf("downloading %s CLI: %s\n", lang, cliURL)
resp, err := http.Get(cliURL)
client := proxy.GetHttpClient(cliURL)
resp, err := client.Get(cliURL)
if err != nil {
fmt.Printf("failed to download %s CLI: %v\n", lang, err)
continue
@@ -354,15 +357,27 @@ func downloadCLI() error {
func() {
defer resp.Body.Close()
out, err := os.Create(originalPath)
if err != nil {
fmt.Printf("failed to create %s CLI file: %v\n", lang, err)
if err := os.MkdirAll(filepath.Dir(originalPath), 0o755); err != nil {
fmt.Printf("failed to create directory for %s CLI: %v\n", lang, err)
return
}
defer out.Close()
if _, err = io.Copy(out, resp.Body); err != nil {
fmt.Printf("failed to save %s CLI: %v\n", lang, err)
tmpFile := originalPath + ".tmp"
out, err := os.Create(tmpFile)
if err != nil {
fmt.Printf("failed to create or write %s CLI: %v\n", lang, err)
return
}
defer func() {
out.Close()
os.Remove(tmpFile)
}()
if _, err = io.Copy(out, resp.Body); err != nil ||
out.Close() != nil ||
os.Rename(tmpFile, originalPath) != nil {
fmt.Printf("failed to download %s CLI: %v\n", lang, err)
return
}
}()
@@ -493,10 +508,12 @@ func InitCLIDownloader() {
return
}
err := DownloadCLI()
if err != nil {
fmt.Printf("failed to initialize CLI downloader: %v\n", err)
}
util.SafeGoroutine(func() {
err := DownloadCLI()
if err != nil {
fmt.Printf("failed to initialize CLI downloader: %v\n", err)
}
go ScheduleCLIUpdater()
ScheduleCLIUpdater()
})
}

View File

@@ -24,7 +24,7 @@ func GetEmailProvider(typ string, clientId string, clientSecret string, host str
} else if typ == "Custom HTTP Email" {
return NewHttpEmailProvider(endpoint, method)
} else if typ == "SendGrid" {
return NewSendgridEmailProvider(clientSecret)
return NewSendgridEmailProvider(clientSecret, host, endpoint)
} else {
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl)
}

View File

@@ -17,14 +17,16 @@ package email
import (
"encoding/json"
"fmt"
"strings"
"net/http"
"github.com/sendgrid/sendgrid-go"
"github.com/sendgrid/sendgrid-go/helpers/mail"
)
type SendgridEmailProvider struct {
ApiKey string
ApiKey string
Host string
Endpoint string
}
type SendgridResponseBody struct {
@@ -35,23 +37,25 @@ type SendgridResponseBody struct {
} `json:"errors"`
}
func NewSendgridEmailProvider(apiKey string) *SendgridEmailProvider {
return &SendgridEmailProvider{ApiKey: apiKey}
func NewSendgridEmailProvider(apiKey string, host string, endpoint string) *SendgridEmailProvider {
return &SendgridEmailProvider{ApiKey: apiKey, Host: host, Endpoint: endpoint}
}
func (s *SendgridEmailProvider) Send(fromAddress string, fromName, toAddress string, subject string, content string) error {
func (s *SendgridEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
client := s.initSendgridClient()
from := mail.NewEmail(fromName, fromAddress)
to := mail.NewEmail("", toAddress)
message := mail.NewSingleEmail(from, subject, to, "", content)
client := sendgrid.NewSendClient(s.ApiKey)
response, err := client.Send(message)
resp, err := client.Send(message)
if err != nil {
return err
}
if response.StatusCode >= 300 {
if resp.StatusCode >= 300 {
var responseBody SendgridResponseBody
err = json.Unmarshal([]byte(response.Body), &responseBody)
err = json.Unmarshal([]byte(resp.Body), &responseBody)
if err != nil {
return err
}
@@ -61,8 +65,23 @@ func (s *SendgridEmailProvider) Send(fromAddress string, fromName, toAddress str
messages = append(messages, sendgridError.Message)
}
return fmt.Errorf("SendGrid status code: %d, error message: %s", response.StatusCode, strings.Join(messages, " | "))
return fmt.Errorf("status code: %d, error message: %s", resp.StatusCode, messages)
}
if resp.StatusCode != http.StatusAccepted {
return fmt.Errorf("status code: %d", resp.StatusCode)
}
return nil
}
func (s *SendgridEmailProvider) initSendgridClient() *sendgrid.Client {
if s.Host == "" || s.Endpoint == "" {
return sendgrid.NewSendClient(s.ApiKey)
}
request := sendgrid.GetRequest(s.ApiKey, s.Endpoint, s.Host)
request.Method = "POST"
return &sendgrid.Client{Request: request}
}

171
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/casdoor/casdoor
go 1.16
go 1.18
require (
github.com/Masterminds/squirrel v1.5.3
@@ -18,7 +18,6 @@ require (
github.com/casvisor/casvisor-go-sdk v1.4.0
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/denisenkom/go-mssqldb v0.9.0
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3
github.com/fogleman/gg v1.3.0
github.com/go-asn1-ber/asn1-ber v1.5.5
@@ -46,7 +45,6 @@ require (
github.com/russellhaering/gosaml2 v0.9.0
github.com/russellhaering/goxmldsig v1.2.0
github.com/sendgrid/sendgrid-go v3.14.0+incompatible
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
@@ -54,20 +52,179 @@ require (
github.com/stripe/stripe-go/v74 v74.29.0
github.com/tealeg/xlsx v1.0.5
github.com/thanhpk/randstr v1.0.4
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/xorm-io/builder v0.3.13
github.com/xorm-io/core v0.7.4
github.com/xorm-io/xorm v1.1.6
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/crypto v0.32.0
golang.org/x/net v0.34.0
golang.org/x/oauth2 v0.17.0
golang.org/x/text v0.21.0
google.golang.org/api v0.150.0
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68
maunium.net/go/mautrix v0.16.0
modernc.org/sqlite v1.18.2
)
require (
cloud.google.com/go v0.110.8 // indirect
cloud.google.com/go/compute v1.23.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.3 // indirect
cloud.google.com/go/storage v1.35.1 // indirect
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
github.com/Azure/azure-storage-blob-go v0.15.0 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20221121042443-a3fd332d56d9 // indirect
github.com/SherClockHolmes/webpush-go v1.2.0 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.62.545 // indirect
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible // indirect
github.com/apistd/uni-go-sdk v0.0.2 // indirect
github.com/atc0005/go-teams-notify/v2 v2.6.1 // indirect
github.com/baidubce/bce-sdk-go v0.9.156 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blinkbean/dingtalk v0.0.0-20210905093040-7d935c0f7e19 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/bwmarrin/discordgo v0.27.1 // indirect
github.com/casdoor/casdoor-go-sdk v0.50.0 // indirect
github.com/casdoor/go-reddit/v2 v2.1.0 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/dghubble/oauth1 v0.7.2 // indirect
github.com/dghubble/sling v1.4.0 // indirect
github.com/di-wu/parser v0.2.2 // indirect
github.com/di-wu/xsd-datetime v1.0.0 // indirect
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 // indirect
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/go-lark/lark v1.9.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-webauthn/revoke v0.1.6 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gomodule/redigo v2.0.0+incompatible // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-tpm v0.3.3 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/gregdel/pushover v1.2.1 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
github.com/markbates/going v1.0.0 // indirect
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-ieproxy v0.0.1 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mileusna/viber v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect
github.com/pingcap/log v0.0.0-20210625125904-98ed8e2eb1c7 // indirect
github.com/pingcap/tidb/parser v0.0.0-20221126021158-6b02a5d8ba7d // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/common v0.30.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/qiniu/go-sdk/v7 v7.12.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rs/zerolog v1.30.0 // indirect
github.com/scim2/filter-parser/v2 v2.2.0 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/skeema/knownhosts v1.2.1 // indirect
github.com/slack-go/slack v0.12.3 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.744 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.744 // indirect
github.com/tidwall/gjson v1.16.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/tklauser/numcpus v0.4.0 // indirect
github.com/twilio/twilio-go v1.13.0 // indirect
github.com/ucloud/ucloud-sdk-go v0.22.5 // indirect
github.com/utahta/go-linenotify v0.5.0 // indirect
github.com/volcengine/volc-sdk-golang v1.0.117 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.mau.fi/util v0.0.0-20230805171708-199bf3eec776 // indirect
go.opencensus.io v0.24.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.7.0 // indirect
go.uber.org/zap v1.19.1 // indirect
golang.org/x/exp v0.0.0-20230810033253-352e893a4cad // indirect
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.1.1 // indirect
maunium.net/go/maulogger/v2 v2.4.1 // indirect
modernc.org/cc/v3 v3.37.0 // indirect
modernc.org/ccgo/v3 v3.16.9 // indirect
modernc.org/libc v1.18.0 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.3.0 // indirect
modernc.org/opt v0.1.1 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
)

1632
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -434,7 +434,7 @@
"isTopGroup": true,
"title": "",
"key": "",
"children": "",
"children": [],
"isEnabled": true
}
],

View File

@@ -185,12 +185,9 @@ func buildUserFilterCondition(filter interface{}) (builder.Cond, error) {
attr := string(f.AttributeDesc())
if attr == ldapMemberOfAttr {
groupId := string(f.AssertionValue())
users, err := object.GetGroupUsers(groupId)
if err != nil {
return nil, err
}
var names []string
groupId := string(f.AssertionValue())
users := object.GetGroupUsersWithoutError(groupId)
for _, user := range users {
names = append(names, user.Name)
}
@@ -249,7 +246,7 @@ func buildSafeCondition(filter interface{}) builder.Cond {
condition, err := buildUserFilterCondition(filter)
if err != nil {
log.Printf("err = %v", err.Error())
return nil
return builder.And(builder.Expr("1 != 1"))
}
return condition
}

View File

@@ -46,7 +46,7 @@ func main() {
object.InitCasvisorConfig()
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
controllers.InitCLIDownloader()
util.SafeGoroutine(func() { controllers.InitCLIDownloader() })
// beego.DelStaticPath("/static")
// beego.SetStaticPath("/static", "web/build/static")

View File

@@ -252,7 +252,7 @@ func CheckPassword(user *User, password string, lang string, options ...bool) er
credManager := cred.GetCredManager(passwordType)
if credManager != nil {
if organization.MasterPassword != "" {
if credManager.IsPasswordCorrect(password, organization.MasterPassword, "", organization.PasswordSalt) {
if password == organization.MasterPassword || credManager.IsPasswordCorrect(password, organization.MasterPassword, "", organization.PasswordSalt) {
return resetUserSigninErrorTimes(user)
}
}

View File

@@ -74,7 +74,7 @@ func checkPasswordComplexity(password string, options []string) string {
}
if len(options) == 0 {
options = []string{"AtLeast6"}
return ""
}
checkers := map[string]ValidatorFunc{

View File

@@ -17,6 +17,8 @@ package object
import (
"sync"
"time"
"github.com/casdoor/casdoor/conf"
)
type DashboardDateItem struct {
@@ -40,11 +42,12 @@ func GetDashboard(owner string) (*map[string][]int64, error) {
time30day := time.Now().AddDate(0, 0, -30)
var wg sync.WaitGroup
var err error
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
wg.Add(len(tableNames))
ch := make(chan error, len(tableNames))
for _, tableName := range tableNames {
dashboard[tableName+"Counts"] = make([]int64, 31)
tableName := tableName
tableFullName := tableNamePrefix + tableName
go func(ch chan error) {
defer wg.Done()
dashboardDateItems := []DashboardDateItem{}
@@ -58,16 +61,16 @@ func GetDashboard(owner string) (*map[string][]int64, error) {
dbQueryBefore = dbQueryBefore.And("owner = ?", owner)
}
if countResult, err = dbQueryBefore.And("created_time < ?", time30day).Table(tableName).Count(); err != nil {
if countResult, err = dbQueryBefore.And("created_time < ?", time30day).Table(tableFullName).Count(); err != nil {
ch <- err
return
}
if err = dbQueryAfter.And("created_time >= ?", time30day).Table(tableName).Find(&dashboardDateItems); err != nil {
if err = dbQueryAfter.And("created_time >= ?", time30day).Table(tableFullName).Find(&dashboardDateItems); err != nil {
ch <- err
return
}
dashboardMap.Store(tableName, DashboardMapItem{
dashboardMap.Store(tableFullName, DashboardMapItem{
dashboardDateItems: dashboardDateItems,
itemCount: countResult,
})

View File

@@ -83,19 +83,23 @@ func GetPaginationGroups(owner string, offset, limit int, field, value, sortFiel
func GetGroupsHaveChildrenMap(groups []*Group) (map[string]*Group, error) {
groupsHaveChildren := []*Group{}
resultMap := make(map[string]*Group)
groupMap := map[string]*Group{}
groupIds := []string{}
for _, group := range groups {
groupMap[group.Name] = group
groupIds = append(groupIds, group.Name)
groupIds = append(groupIds, group.ParentId)
if !group.IsTopGroup {
groupIds = append(groupIds, group.ParentId)
}
}
err := ormer.Engine.Cols("owner", "name", "parent_id", "display_name").Distinct("parent_id").In("parent_id", groupIds).Find(&groupsHaveChildren)
if err != nil {
return nil, err
}
for _, group := range groups {
resultMap[group.Name] = group
for _, group := range groupsHaveChildren {
resultMap[group.ParentId] = groupMap[group.ParentId]
}
return resultMap, nil
}
@@ -302,7 +306,10 @@ func GetPaginationGroupUsers(groupId string, offset, limit int, field, value, so
func GetGroupUsers(groupId string) ([]*User, error) {
users := []*User{}
owner, _ := util.GetOwnerAndNameFromId(groupId)
owner, _, err := util.GetOwnerAndNameFromIdWithError(groupId)
if err != nil {
return nil, err
}
names, err := userEnforcer.GetUserNamesByGroupName(groupId)
if err != nil {
return nil, err
@@ -314,6 +321,11 @@ func GetGroupUsers(groupId string) ([]*User, error) {
return users, nil
}
func GetGroupUsersWithoutError(groupId string) []*User {
users, _ := GetGroupUsers(groupId)
return users
}
func ExtendGroupWithUsers(group *Group) error {
if group == nil {
return nil

View File

@@ -103,6 +103,7 @@ func initBuiltInOrganization() bool {
PasswordOptions: []string{"AtLeast6"},
CountryCodes: []string{"US", "ES", "FR", "DE", "GB", "CN", "JP", "KR", "VN", "ID", "SG", "IN"},
DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
UserTypes: []string{},
Tags: []string{},
Languages: []string{"en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vi", "pt"},
InitScore: 2000,

View File

@@ -66,6 +66,7 @@ type Organization struct {
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
UserTypes []string `xorm:"mediumtext" json:"userTypes"`
Tags []string `xorm:"mediumtext" json:"tags"`
Languages []string `xorm:"varchar(255)" json:"languages"`
ThemeData *ThemeData `xorm:"json" json:"themeData"`

View File

@@ -30,6 +30,8 @@ type Claims struct {
Nonce string `json:"nonce,omitempty"`
Tag string `json:"tag"`
Scope string `json:"scope,omitempty"`
// the `azp` (Authorized Party) claim. Optional. See https://openid.net/specs/openid-connect-core-1_0.html#IDToken
Azp string `json:"azp,omitempty"`
jwt.RegisteredClaims
}
@@ -137,6 +139,7 @@ type ClaimsShort struct {
TokenType string `json:"tokenType,omitempty"`
Nonce string `json:"nonce,omitempty"`
Scope string `json:"scope,omitempty"`
Azp string `json:"azp,omitempty"`
jwt.RegisteredClaims
}
@@ -155,6 +158,7 @@ type ClaimsWithoutThirdIdp struct {
Nonce string `json:"nonce,omitempty"`
Tag string `json:"tag"`
Scope string `json:"scope,omitempty"`
Azp string `json:"azp,omitempty"`
jwt.RegisteredClaims
}
@@ -269,6 +273,7 @@ func getShortClaims(claims Claims) ClaimsShort {
Nonce: claims.Nonce,
Scope: claims.Scope,
RegisteredClaims: claims.RegisteredClaims,
Azp: claims.Azp,
}
return res
}
@@ -281,6 +286,7 @@ func getClaimsWithoutThirdIdp(claims Claims) ClaimsWithoutThirdIdp {
Tag: claims.Tag,
Scope: claims.Scope,
RegisteredClaims: claims.RegisteredClaims,
Azp: claims.Azp,
}
return res
}
@@ -301,6 +307,7 @@ func getClaimsCustom(claims Claims, tokenField []string) jwt.MapClaims {
res["nonce"] = claims.Nonce
res["tag"] = claims.Tag
res["scope"] = claims.Scope
res["azp"] = claims.Azp
for _, field := range tokenField {
userField := userValue.FieldByName(field)
@@ -357,6 +364,7 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
// FIXME: A workaround for custom claim by reusing `tag` in user info
Tag: user.Tag,
Scope: scope,
Azp: application.ClientId,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: originBackend,
Subject: user.Id,

View File

@@ -32,6 +32,7 @@ type ClaimsStandard struct {
Nonce string `json:"nonce,omitempty"`
Scope string `json:"scope,omitempty"`
Address OIDCAddress `json:"address,omitempty"`
Azp string `json:"azp,omitempty"`
jwt.RegisteredClaims
}
@@ -52,6 +53,7 @@ func getStandardClaims(claims Claims) ClaimsStandard {
Nonce: claims.Nonce,
Scope: claims.Scope,
RegisteredClaims: claims.RegisteredClaims,
Azp: claims.Azp,
}
res.Phone = ""

View File

@@ -86,9 +86,9 @@ func SendVerificationCodeToEmail(organization *Organization, user *User, provide
title := provider.Title
code := getRandomCode(6)
if organization.MasterVerificationCode != "" {
code = organization.MasterVerificationCode
}
// if organization.MasterVerificationCode != "" {
// code = organization.MasterVerificationCode
// }
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
content := strings.Replace(provider.Content, "%s", code, 1)
@@ -124,9 +124,9 @@ func SendVerificationCodeToPhone(organization *Organization, user *User, provide
}
code := getRandomCode(6)
if organization.MasterVerificationCode != "" {
code = organization.MasterVerificationCode
}
// if organization.MasterVerificationCode != "" {
// code = organization.MasterVerificationCode
// }
err = SendSms(provider, code, dest)
if err != nil {

View File

@@ -203,7 +203,7 @@ func (c *AirwallexClient) CreateIntent(r *PayReq) (*AirWallexIntentResp, error)
"amount": r.Price,
"merchant_order_id": orderId,
"request_id": orderId,
"descriptor": string([]rune(description)[:32]), // display to the customer.
"descriptor": strings.ReplaceAll(string([]rune(description)[:32]), "\x00", ""),
"metadata": map[string]interface{}{"description": description},
"order": map[string]interface{}{"products": []map[string]interface{}{{"name": r.ProductDisplayName, "quantity": 1, "desc": r.ProductDescription, "image_url": r.ProductImage}}},
"customer": map[string]interface{}{"merchant_customer_id": r.PayerId, "email": r.PayerEmail, "first_name": r.PayerName, "last_name": r.PayerName},

View File

@@ -47,12 +47,12 @@ func getOrganizationThemeCookieFromUrlPath(ctx *context.Context, urlPath string)
var application *object.Application
var organization *object.Organization
var err error
if urlPath == "/login" {
if urlPath == "/login" || urlPath == "/signup" {
application, err = object.GetDefaultApplication(fmt.Sprintf("admin/built-in"))
if err != nil {
return nil, err
}
} else if strings.HasPrefix(urlPath, "/login/oauth/authorize") {
} else if strings.HasSuffix(urlPath, "/oauth/authorize") {
clientId := ctx.Input.Query("client_id")
if clientId == "" {
return nil, nil
@@ -76,6 +76,15 @@ func getOrganizationThemeCookieFromUrlPath(ctx *context.Context, urlPath string)
if err != nil {
return nil, err
}
} else if strings.HasPrefix(urlPath, "/signup/") {
owner, _ := strings.CutPrefix(urlPath, "/signup/")
if owner == "undefined" || strings.Count(owner, "/") > 0 {
return nil, nil
}
application, err = object.GetDefaultApplication(fmt.Sprintf("admin/%s", owner))
if err != nil {
return nil, err
}
} else if strings.HasPrefix(urlPath, "/cas/") && strings.HasSuffix(urlPath, "/login") {
owner, _ := strings.CutPrefix(urlPath, "/cas/")
owner, _ = strings.CutSuffix(owner, "/login")
@@ -97,11 +106,14 @@ func getOrganizationThemeCookieFromUrlPath(ctx *context.Context, urlPath string)
}
organizationThemeCookie := &OrganizationThemeCookie{
application.ThemeData,
application.Logo,
application.FooterHtml,
organization.Favicon,
organization.DisplayName,
ThemeData: application.ThemeData,
LogoUrl: application.Logo,
FooterHtml: application.FooterHtml,
}
if organization != nil {
organizationThemeCookie.Favicon = organization.Favicon
organizationThemeCookie.DisplayName = organization.DisplayName
}
return organizationThemeCookie, nil

View File

@@ -3,13 +3,16 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/cssinjs": "^1.10.1",
"@ant-design/icons": "^4.7.0",
"@ant-design/cssinjs": "^1.23.0",
"@ant-design/icons": "^5.6.1",
"@craco/craco": "^6.4.5",
"@crowdin/cli": "^3.7.10",
"@ctrl/tinycolor": "^3.5.0",
"@emotion/react": "^11.10.5",
"@metamask/eth-sig-util": "^6.0.0",
"@uiw/codemirror-extensions-langs": "^4.23.8",
"@uiw/codemirror-theme-material": "^4.23.8",
"@uiw/react-codemirror": "^4.23.8",
"@web3-onboard/coinbase": "^2.2.5",
"@web3-onboard/core": "^2.20.5",
"@web3-onboard/frontier": "^2.0.4",
@@ -20,10 +23,10 @@
"@web3-onboard/sequence": "^2.0.8",
"@web3-onboard/taho": "^2.0.5",
"@web3-onboard/trust": "^2.0.4",
"antd": "5.2.3",
"antd-token-previewer": "^1.1.0-22",
"antd": "5.24.1",
"antd-token-previewer": "^2.0.8",
"buffer": "^6.0.3",
"codemirror": "^5.61.1",
"codemirror": "^6.0.1",
"copy-to-clipboard": "^3.3.1",
"core-js": "^3.25.0",
"craco-less": "^2.0.0",
@@ -40,7 +43,6 @@
"qrcode.react": "^3.1.0",
"react": "^18.2.0",
"react-app-polyfill": "^3.0.0",
"react-codemirror2": "^7.2.1",
"react-cropper": "^2.1.7",
"react-device-detect": "^2.2.2",
"react-dom": "^18.2.0",

View File

@@ -327,7 +327,7 @@ class App extends Component {
isAiAssistantOpen: false,
});
}}
visible={this.state.isAiAssistantOpen}
open={this.state.isAiAssistantOpen}
>
<iframe id="iframeHelper" title={"iframeHelper"} src={`${Conf.AiAssistantUrl}/?isRaw=1`} width="100%" height="100%" scrolling="no" frameBorder="no" />
</Drawer>

View File

@@ -58,6 +58,16 @@ img {
}
}
.org-select {
display: flex;
position: relative;
transform: translateY(50%);
margin: 0 10px !important;
float: right;
min-width: 120px;
max-width: 180px;
}
.rightDropDown {
display: flex;
align-items: center;

View File

@@ -34,14 +34,8 @@ import PromptPage from "./auth/PromptPage";
import copy from "copy-to-clipboard";
import ThemeEditor from "./common/theme/ThemeEditor";
import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css";
import SigninTable from "./table/SigninTable";
require("codemirror/theme/material-darker.css");
require("codemirror/mode/htmlmixed/htmlmixed");
require("codemirror/mode/xml/xml");
require("codemirror/mode/css/css");
import Editor from "./common/Editor";
const {Option} = Select;
@@ -629,13 +623,9 @@ class ApplicationEditPage extends React.Component {
<Col span={22} >
<Popover placement="right" content={
<div style={{width: "900px", height: "300px"}} >
<CodeMirror
value={this.state.application.signupHtml}
options={{mode: "htmlmixed", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
this.updateApplicationField("signupHtml", value);
}}
/>
<Editor value={this.state.application.signupHtml} lang="html" fillHeight dark onChange={value => {
this.updateApplicationField("signupHtml", value);
}} />
</div>
} title={i18next.t("provider:Signup HTML - Edit")} trigger="click">
<Input value={this.state.application.signupHtml} style={{marginBottom: "10px"}} onChange={e => {
@@ -651,13 +641,9 @@ class ApplicationEditPage extends React.Component {
<Col span={22} >
<Popover placement="right" content={
<div style={{width: "900px", height: "300px"}} >
<CodeMirror
value={this.state.application.signinHtml}
options={{mode: "htmlmixed", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
this.updateApplicationField("signinHtml", value);
}}
/>
<Editor value={this.state.application.signinHtml} lang="html" fillHeight dark onChange={value => {
this.updateApplicationField("signinHtml", value);
}} />
</div>
} title={i18next.t("provider:Signin HTML - Edit")} trigger="click">
<Input value={this.state.application.signinHtml} style={{marginBottom: "10px"}} onChange={e => {
@@ -758,11 +744,7 @@ class ApplicationEditPage extends React.Component {
{Setting.getLabel(i18next.t("application:SAML metadata"), i18next.t("application:SAML metadata - Tooltip"))} :
</Col>
<Col span={22}>
<CodeMirror
value={this.state.samlMetadata}
options={{mode: "xml", theme: "default"}}
onBeforeChange={(editor, data, value) => {}}
/>
<Editor value={this.state.samlMetadata?.toString() ?? ""} lang="xml" readOnly />
<br />
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&enablePostBinding=${this.state.application.enableSamlPostBinding}`);
@@ -829,9 +811,12 @@ class ApplicationEditPage extends React.Component {
<Col span={22}>
<Popover placement="right" content={
<div style={{width: "900px", height: "300px"}} >
<CodeMirror value={this.state.application.formCss === "" ? template : this.state.application.formCss}
options={{mode: "css", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
<Editor
value={this.state.application.formCss === "" ? template : this.state.application.formCss}
lang="css"
fillHeight
dark
onChange={value => {
this.updateApplicationField("formCss", value);
}}
/>
@@ -850,9 +835,12 @@ class ApplicationEditPage extends React.Component {
<Col span={22}>
<Popover placement="right" content={
<div style={{width: "900px", height: "300px"}} >
<CodeMirror value={this.state.application.formCssMobile === "" ? template : this.state.application.formCssMobile}
options={{mode: "css", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
<Editor
value={this.state.application.formCssMobile === "" ? template : this.state.application.formCssMobile}
lang="css"
fillHeight
dark
onChange={value => {
this.updateApplicationField("formCssMobile", value);
}}
/>
@@ -887,9 +875,12 @@ class ApplicationEditPage extends React.Component {
<Col span={21} >
<Popover placement="right" content={
<div style={{width: "900px", height: "300px"}} >
<CodeMirror value={this.state.application.formSideHtml === "" ? sideTemplate : this.state.application.formSideHtml}
options={{mode: "htmlmixed", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
<Editor
value={this.state.application.formSideHtml === "" ? sideTemplate : this.state.application.formSideHtml}
lang="html"
fillHeight
dark
onChange={value => {
this.updateApplicationField("formSideHtml", value);
}}
/>
@@ -936,10 +927,12 @@ class ApplicationEditPage extends React.Component {
<Col span={22} >
<Popover placement="right" content={
<div style={{width: "900px", height: "300px"}} >
<CodeMirror
<Editor
value={this.state.application.headerHtml}
options={{mode: "htmlmixed", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
lang="html"
fillHeight
dark
onChange={value => {
this.updateApplicationField("headerHtml", value);
}}
/>
@@ -958,10 +951,12 @@ class ApplicationEditPage extends React.Component {
<Col span={22} >
<Popover placement="right" content={
<div style={{width: "900px", height: "300px"}} >
<CodeMirror
<Editor
value={this.state.application.footerHtml}
options={{mode: "htmlmixed", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
lang="html"
fillHeight
dark
onChange={value => {
this.updateApplicationField("footerHtml", value);
}}
/>

View File

@@ -73,7 +73,7 @@ class BaseListPage extends React.Component {
this.fetch({pagination});
}
getColumnSearchProps = dataIndex => ({
getColumnSearchProps = (dataIndex, customRender = null) => ({
filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}) => (
<div style={{padding: 8}}>
<Input
@@ -121,13 +121,15 @@ class BaseListPage extends React.Component {
record[dataIndex]
? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase())
: "",
onFilterDropdownOpenChange: visible => {
if (visible) {
setTimeout(() => this.searchInput.select(), 100);
}
filterDropdownProps: {
onOpenChange: visible => {
if (visible) {
setTimeout(() => this.searchInput.select(), 100);
}
},
},
render: text =>
this.state.searchedColumn === dataIndex ? (
render: (text, record, index) => {
const highlightContent = this.state.searchedColumn === dataIndex ? (
<Highlighter
highlightStyle={{backgroundColor: "#ffc069", padding: 0}}
searchWords={[this.state.searchText]}
@@ -136,7 +138,10 @@ class BaseListPage extends React.Component {
/>
) : (
text
),
);
return customRender ? customRender({text, record, index}, highlightContent) : highlightContent;
},
});
handleSearch = (selectedKeys, confirm, dataIndex) => {
@@ -170,7 +175,7 @@ class BaseListPage extends React.Component {
const steps = TourConfig.getSteps();
steps.map((item, index) => {
if (!index) {
item.target = () => document.querySelector("table");
item.target = () => document.querySelector(".ant-table");
} else {
item.target = () => document.getElementById(item.id) || null;
}

View File

@@ -13,15 +13,11 @@
// limitations under the License.
import React, {useCallback, useEffect, useRef, useState} from "react";
import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css";
import "codemirror/mode/properties/properties";
import * as Setting from "./Setting";
import IframeEditor from "./IframeEditor";
import {Tabs} from "antd";
import i18next from "i18next";
const {TabPane} = Tabs;
import Editor from "./common/Editor";
const CasbinEditor = ({model, onModelTextChange}) => {
const [activeKey, setActiveKey] = useState("advanced");
@@ -68,10 +64,15 @@ const CasbinEditor = ({model, onModelTextChange}) => {
return (
<div style={{height: "100%", width: "100%", display: "flex", flexDirection: "column"}}>
<Tabs activeKey={activeKey} onChange={handleTabChange} style={{flex: "0 0 auto", marginTop: "-10px"}}>
<TabPane tab={i18next.t("model:Basic Editor")} key="basic" />
<TabPane tab={i18next.t("model:Advanced Editor")} key="advanced" />
</Tabs>
<Tabs
activeKey={activeKey}
onChange={handleTabChange}
style={{flex: "0 0 auto", marginTop: "-10px"}}
items={[
{key: "basic", label: i18next.t("model:Basic Editor")},
{key: "advanced", label: i18next.t("model:Advanced Editor")},
]}
/>
<div style={{flex: "1 1 auto", overflow: "hidden"}}>
{activeKey === "advanced" ? (
<IframeEditor
@@ -81,11 +82,10 @@ const CasbinEditor = ({model, onModelTextChange}) => {
style={{width: "100%", height: "100%"}}
/>
) : (
<CodeMirror
<Editor
value={localModelText}
className="full-height-editor no-horizontal-scroll-editor"
options={{mode: "properties", theme: "default"}}
onBeforeChange={(editor, data, value) => {
readOnly={Setting.builtInObject(model)}
onChange={value => {
handleModelTextChange(value);
}}
/>

View File

@@ -14,7 +14,7 @@
import React from "react";
import {Link} from "react-router-dom";
import {Button, Table} from "antd";
import {Button, Table, Tooltip} from "antd";
import moment from "moment";
import * as Setting from "./Setting";
import * as GroupBackend from "./backend/GroupBackend";
@@ -202,12 +202,16 @@ class GroupListPage extends BaseListPage {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/groups/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal
disabled={record.haveChildren}
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteGroup(index)}
>
</PopconfirmModal>
{
record.haveChildren ? <Tooltip placement="topLeft" title={i18next.t("group:You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page")}>
<Button disabled type="primary" danger>{i18next.t("general:Delete")}</Button>
</Tooltip> :
<PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteGroup(index)}
>
</PopconfirmModal>
}
</div>
);
},

View File

@@ -206,11 +206,11 @@ function ManagementPage(props) {
<OrganizationSelect
initValue={Setting.getOrganization()}
withAll={true}
style={{marginRight: "20px", width: "180px", display: !Setting.isMobile() ? "flex" : "none"}}
className="org-select"
style={{display: Setting.isMobile() ? "none" : "flex"}}
onChange={(value) => {
Setting.setOrganization(value);
}}
className="select-box"
/>
}
</React.Fragment>
@@ -459,7 +459,7 @@ function ManagementPage(props) {
<Header style={{padding: "0", marginBottom: "3px", backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}} >
{props.requiredEnableMfa || (Setting.isMobile() ?
<React.Fragment>
<Drawer title={i18next.t("general:Close")} placement="left" visible={menuVisible} onClose={onClose}>
<Drawer title={i18next.t("general:Close")} placement="left" open={menuVisible} onClose={onClose}>
<Menu
items={getMenuItems()}
mode={"inline"}

View File

@@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Controlled as CodeMirror} from "react-codemirror2";
import {Link} from "react-router-dom";
import {Button, Popover, Table} from "antd";
import moment from "moment";
@@ -22,6 +22,7 @@ import * as ModelBackend from "./backend/ModelBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./common/modal/PopconfirmModal";
import Editor from "./common/Editor";
const rbacModel = `[request_definition]
r = sub, obj, act
@@ -148,11 +149,7 @@ class ModelListPage extends BaseListPage {
return (
<Popover placement="topRight" content={() => {
return (
<CodeMirror
value={text}
options={{mode: "properties", theme: "default"}}
onBeforeChange={(editor, data, value) => {}}
/>
<Editor value={text} />
);
}} title="" trigger="hover">
{

View File

@@ -421,6 +421,18 @@ class OrganizationEditPage extends React.Component {
} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:User types"), i18next.t("organization:User types - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.organization.userTypes} onChange={(value => {this.updateOrganizationField("userTypes", value);})}>
{
this.state.organization.userTypes?.map((item, index) => <Option key={index} value={item}>{item}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Tags"), i18next.t("organization:Tags - Tooltip"))} :

View File

@@ -34,7 +34,7 @@ class OrganizationListPage extends BaseListPage {
favicon: `${Setting.StaticBaseUrl}/img/favicon.png`,
passwordType: "plain",
PasswordSalt: "",
passwordOptions: [],
passwordOptions: ["AtLeast6"],
passwordObfuscatorType: "Plain",
passwordObfuscatorKey: "",
passwordExpireDays: 0,

View File

@@ -113,8 +113,8 @@ class PermissionListPage extends BaseListPage {
return (
<Upload {...props}>
<Button id="upload-button" type="primary" size="small">
<UploadOutlined /> {i18next.t("user:Upload (.xlsx)")}
<Button icon={<UploadOutlined />} id="upload-button" type="primary" size="small">
{i18next.t("user:Upload (.xlsx)")}
</Button></Upload>
);
}

View File

@@ -28,14 +28,7 @@ import copy from "copy-to-clipboard";
import {CaptchaPreview} from "./common/CaptchaPreview";
import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
import * as Web3Auth from "./auth/Web3Auth";
import {Controlled as CodeMirror} from "react-codemirror2";
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");
import Editor from "./common/Editor";
const {Option} = Select;
const {TextArea} = Input;
@@ -822,7 +815,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
{
(this.state.provider.type === "WeChat Pay" || this.state.provider.type === "CUCloud") || (this.state.provider.category === "Email" && (this.state.provider.type === "Azure ACS" || this.state.provider.type === "SendGrid")) ? null : (
(this.state.provider.type === "WeChat Pay" || this.state.provider.type === "CUCloud") || (this.state.provider.category === "Email" && (this.state.provider.type === "Azure ACS")) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientSecret2Label(this.state.provider)} :
@@ -908,7 +901,7 @@ class ProviderEditPage extends React.Component {
</Row>
)
}
{this.state.provider.category === "Storage" || ["Custom HTTP SMS", "Custom HTTP Email", "CUCloud"].includes(this.state.provider.type) ? (
{this.state.provider.category === "Storage" || ["Custom HTTP SMS", "Custom HTTP Email", "SendGrid", "CUCloud"].includes(this.state.provider.type) ? (
<div>
{["Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
@@ -922,7 +915,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
)}
{["Custom HTTP SMS", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor", "CUCloud"].includes(this.state.provider.type) ? null : (
{["Custom HTTP SMS", "SendGrid", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor", "CUCloud"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
@@ -934,7 +927,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
)}
{["Custom HTTP SMS", "Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
{["Custom HTTP SMS", "SendGrid", "Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{["Casdoor"].includes(this.state.provider.type) ?
@@ -948,7 +941,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
)}
{["Custom HTTP SMS", "CUCloud"].includes(this.state.provider.type) ? null : (
{["Custom HTTP SMS", "SendGrid", "CUCloud"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} :
@@ -960,7 +953,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
)}
{["Custom HTTP SMS", "Synology", "Casdoor", "CUCloud"].includes(this.state.provider.type) ? null : (
{["Custom HTTP SMS", "SendGrid", "Synology", "Casdoor", "CUCloud"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
@@ -1067,18 +1060,16 @@ class ProviderEditPage extends React.Component {
</React.Fragment>
) : this.state.provider.category === "Email" ? (
<React.Fragment>
{["SendGrid"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.provider.host} onChange={e => {
this.updateProviderField("host", e.target.value);
}} />
</Col>
</Row>
)}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.provider.host} onChange={e => {
this.updateProviderField("host", e.target.value);
}} />
</Col>
</Row>
{["Azure ACS", "SendGrid"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
@@ -1129,10 +1120,12 @@ class ProviderEditPage extends React.Component {
<Row>
<Col span={Setting.isMobile() ? 22 : 11}>
<div style={{height: "300px", margin: "10px"}}>
<CodeMirror
<Editor
value={this.state.provider.content}
options={{mode: "htmlmixed", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
fillHeight
dark
lang="html"
onChange={value => {
this.updateProviderField("content", value);
}}
/>

View File

@@ -14,12 +14,12 @@
import React from "react";
import {Link} from "react-router-dom";
import {Switch, Table} from "antd";
import {Button, Descriptions, Drawer, Switch, Table, Tooltip} from "antd";
import * as Setting from "./Setting";
import * as RecordBackend from "./backend/RecordBackend";
import i18next from "i18next";
import moment from "moment";
import BaseListPage from "./BaseListPage";
import Editor from "./common/Editor";
class RecordListPage extends BaseListPage {
UNSAFE_componentWillMount() {
@@ -28,21 +28,6 @@ class RecordListPage extends BaseListPage {
this.fetch({pagination});
}
newRecord() {
return {
owner: "built-in",
name: "1234",
id: "1234",
clientIp: "::1",
timestamp: moment().format(),
organization: "built-in",
username: "admin",
requestUri: "/api/get-account",
action: "login",
isTriggered: false,
};
}
renderTable(records) {
let columns = [
{
@@ -65,16 +50,13 @@ class RecordListPage extends BaseListPage {
title: i18next.t("general:Client IP"),
dataIndex: "clientIp",
key: "clientIp",
width: "100px",
width: "120px",
sorter: true,
...this.getColumnSearchProps("clientIp"),
render: (text, record, index) => {
return (
<a target="_blank" rel="noreferrer" href={`https://db-ip.com/${text}`}>
{text}
</a>
);
},
...this.getColumnSearchProps("clientIp", (row, highlightContent) => (
<a target="_blank" rel="noreferrer" href={`https://db-ip.com/${row.text}`}>
{highlightContent}
</a>
)),
},
{
title: i18next.t("general:Timestamp"),
@@ -120,28 +102,28 @@ class RecordListPage extends BaseListPage {
title: i18next.t("general:Method"),
dataIndex: "method",
key: "method",
width: "110px",
width: "100px",
sorter: true,
filterMultiple: false,
filters: [
{text: "GET", value: "GET"},
{text: "HEAD", value: "HEAD"},
{text: "POST", value: "POST"},
{text: "PUT", value: "PUT"},
{text: "DELETE", value: "DELETE"},
{text: "CONNECT", value: "CONNECT"},
{text: "OPTIONS", value: "OPTIONS"},
{text: "TRACE", value: "TRACE"},
{text: "PATCH", value: "PATCH"},
],
"GET", "HEAD", "POST", "PUT", "DELETE",
"CONNECT", "OPTIONS", "TRACE", "PATCH",
].map(el => ({text: el, value: el})),
},
{
title: i18next.t("general:Request URI"),
dataIndex: "requestUri",
key: "requestUri",
// width: "300px",
width: "200px",
sorter: true,
...this.getColumnSearchProps("requestUri"),
ellipsis: {
showTitle: false,
},
...this.getColumnSearchProps("requestUri", (row, highlightContent) => (
<Tooltip placement="topLeft" title={row.text}>
{highlightContent}
</Tooltip>
)),
},
{
title: i18next.t("user:Language"),
@@ -155,7 +137,7 @@ class RecordListPage extends BaseListPage {
title: i18next.t("record:Status code"),
dataIndex: "statusCode",
key: "statusCode",
width: "90px",
width: "120px",
sorter: true,
...this.getColumnSearchProps("statusCode"),
},
@@ -163,16 +145,26 @@ class RecordListPage extends BaseListPage {
title: i18next.t("record:Response"),
dataIndex: "response",
key: "response",
width: "90px",
width: "220px",
sorter: true,
...this.getColumnSearchProps("response"),
ellipsis: {
showTitle: false,
},
...this.getColumnSearchProps("response", (row, highlightContent) => (
<Tooltip placement="topLeft" title={row.text}>
{highlightContent}
</Tooltip>
)),
},
{
title: i18next.t("record:Object"),
dataIndex: "object",
key: "object",
width: "90px",
width: "200px",
sorter: true,
ellipsis: {
showTitle: false,
},
...this.getColumnSearchProps("object"),
},
{
@@ -191,7 +183,7 @@ class RecordListPage extends BaseListPage {
title: i18next.t("record:Is triggered"),
dataIndex: "isTriggered",
key: "isTriggered",
width: "140px",
width: "120px",
sorter: true,
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
@@ -204,6 +196,24 @@ class RecordListPage extends BaseListPage {
);
},
},
{
title: i18next.t("general:Action"),
dataIndex: "action",
key: "action",
width: "80px",
sorter: true,
fixed: "right",
render: (text, record, index) => (
<Button type="link" onClick={() => {
this.setState({
detailRecord: record,
detailShow: true,
});
}}>
{i18next.t("general:Detail")}
</Button>
),
},
];
if (Setting.isLocalAdminUser(this.props.account)) {
@@ -220,7 +230,7 @@ class RecordListPage extends BaseListPage {
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={records} rowKey="id" size="middle" bordered pagination={paginationProps}
<Table scroll={{x: "100%"}} columns={columns} dataSource={records} rowKey="id" size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Records")}&nbsp;&nbsp;&nbsp;&nbsp;
@@ -229,10 +239,79 @@ class RecordListPage extends BaseListPage {
loading={this.state.loading}
onChange={this.handleTableChange}
/>
{/* TODO: Should be packaged as a component after confirm it run correctly.*/}
<Drawer
title={i18next.t("general:Detail")}
width={Setting.isMobile() ? "100%" : 640}
placement="right"
destroyOnClose
onClose={() => this.setState({detailShow: false})}
open={this.state.detailShow}
>
<Descriptions bordered size="small" column={1} layout={Setting.isMobile() ? "vertical" : "horizontal"} style={{padding: "12px", height: "100%", overflowY: "auto"}}>
<Descriptions.Item label={i18next.t("general:ID")}>{this.getDetailField("id")}</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Client IP")}>{this.getDetailField("clientIp")}</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Timestamp")}>{this.getDetailField("createdTime")}</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Organization")}>
<Link to={`/organizations/${this.getDetailField("organization")}`}>
{this.getDetailField("organization")}
</Link>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:User")}>
<Link to={`/users/${this.getDetailField("organization")}/${this.getDetailField("user")}`}>
{this.getDetailField("user")}
</Link>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Method")}>{this.getDetailField("method")}</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Request URI")}>{this.getDetailField("requestUri")}</Descriptions.Item>
<Descriptions.Item label={i18next.t("user:Language")}>{this.getDetailField("language")}</Descriptions.Item>
<Descriptions.Item label={i18next.t("record:Status code")}>{this.getDetailField("statusCode")}</Descriptions.Item>
<Descriptions.Item label={i18next.t("general:Action")}>{this.getDetailField("action")}</Descriptions.Item>
<Descriptions.Item label={i18next.t("record:Response")}>
<Editor
value={this.getDetailField("response")}
fillHeight
fillWidth
maxWidth={this.getEditorMaxWidth()}
dark
readOnly
/>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("record:Object")}>
<Editor
value={this.jsonStrFormatter(this.getDetailField("object"))}
lang="json"
fillHeight
fillWidth
maxWidth={this.getEditorMaxWidth()}
dark
readOnly
/>
</Descriptions.Item>
</Descriptions>
</Drawer>
</div>
);
}
getEditorMaxWidth = () => {
return Setting.isMobile() ? window.innerWidth - 60 : 475;
};
jsonStrFormatter = str => {
try {
return JSON.stringify(JSON.parse(str), null, 2);
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
return str;
}
};
getDetailField = dataIndex => {
return this.state.detailRecord ? this.state.detailRecord?.[dataIndex] ?? "" : "";
};
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
const sortField = params.sortField, sortOrder = params.sortOrder;
@@ -255,6 +334,8 @@ class RecordListPage extends BaseListPage {
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
detailShow: false,
detailRecord: null,
});
} else {
if (res.data.includes("Please login first")) {

View File

@@ -106,8 +106,8 @@ class RoleListPage extends BaseListPage {
return (
<Upload {...props}>
<Button type="primary" size="small">
<UploadOutlined /> {i18next.t("user:Upload (.xlsx)")}
<Button icon={<UploadOutlined />} type="primary" size="small">
{i18next.t("user:Upload (.xlsx)")}
</Button>
</Upload>
);

View File

@@ -21,11 +21,8 @@ import * as Setting from "./Setting";
import i18next from "i18next";
import SyncerTableColumnTable from "./table/SyncerTableColumnTable";
import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css";
import * as CertBackend from "./backend/CertBackend";
require("codemirror/theme/material-darker.css");
require("codemirror/mode/javascript/javascript");
import Editor from "./common/Editor";
const {Option} = Select;
@@ -512,10 +509,13 @@ class SyncerEditPage extends React.Component {
</Col>
<Col span={22} >
<div style={{width: "100%", height: "300px"}} >
<CodeMirror
<Editor
value={this.state.syncer.errorText}
options={{mode: "javascript", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
fillHeight
readOnly
dark
lang="js"
onChange={value => {
this.updateSyncerField("errorText", value);
}}
/>

View File

@@ -153,7 +153,7 @@ class TokenListPage extends BaseListPage {
title: i18next.t("token:Authorization code"),
dataIndex: "code",
key: "code",
// width: '150px',
width: "180px",
sorter: true,
...this.getColumnSearchProps("code"),
render: (text, record, index) => {
@@ -164,7 +164,7 @@ class TokenListPage extends BaseListPage {
title: i18next.t("token:Access token"),
dataIndex: "accessToken",
key: "accessToken",
// width: '150px',
width: "220px",
sorter: true,
ellipsis: true,
...this.getColumnSearchProps("accessToken"),
@@ -225,7 +225,7 @@ class TokenListPage extends BaseListPage {
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={tokens} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
<Table scroll={{x: "100%"}} columns={columns} dataSource={tokens} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Tokens")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@@ -397,6 +397,12 @@ class UserEditPage extends React.Component {
</Row>
);
} else if (accountItem.name === "User type") {
let userTypes = ["normal-user", "paid-user"];
const organization = this.getUserOrganization();
if (organization && organization.userTypes && organization.userTypes.length > 0) {
userTypes = organization.userTypes;
}
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
@@ -404,7 +410,7 @@ class UserEditPage extends React.Component {
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.user.type} onChange={(value => {this.updateUserField("type", value);})}
options={["normal-user", "paid-user"].map(item => Setting.getOption(item, item))}
options={userTypes.map(item => Setting.getOption(item, item))}
/>
</Col>
</Row>

View File

@@ -188,8 +188,8 @@ class UserListPage extends BaseListPage {
return (
<Upload {...props}>
<Button id="upload-button" type="primary" size="small">
<UploadOutlined /> {i18next.t("user:Upload (.xlsx)")}
<Button icon={<UploadOutlined />} id="upload-button" type="primary" size="small">
{i18next.t("user:Upload (.xlsx)")}
</Button>
</Upload>
);
@@ -318,6 +318,14 @@ class UserListPage extends BaseListPage {
return Setting.initCountries().getName(record.region, Setting.getLanguage(), {select: "official"});
},
},
{
title: i18next.t("general:User type"),
dataIndex: "type",
key: "type",
width: "120px",
sorter: true,
...this.getColumnSearchProps("type"),
},
{
title: i18next.t("user:Tag"),
dataIndex: "tag",
@@ -343,7 +351,7 @@ class UserListPage extends BaseListPage {
title: i18next.t("user:Is admin"),
dataIndex: "isAdmin",
key: "isAdmin",
width: "110px",
width: "120px",
sorter: true,
render: (text, record, index) => {
return (

View File

@@ -21,10 +21,7 @@ import * as Setting from "./Setting";
import i18next from "i18next";
import WebhookHeaderTable from "./table/WebhookHeaderTable";
import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css";
require("codemirror/theme/material-darker.css");
require("codemirror/mode/javascript/javascript");
import Editor from "./common/Editor";
const {Option} = Select;
@@ -304,11 +301,7 @@ class WebhookEditPage extends React.Component {
</Col>
<Col span={22} >
<div style={{width: "900px", height: "300px"}} >
<CodeMirror
value={previewText}
options={{mode: "javascript", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {}}
/>
<Editor value={previewText} lang="js" fillHeight readOnly dark />
</div>
</Col>
</Row>

View File

@@ -471,9 +471,12 @@ class ForgetPage extends React.Component {
<React.Fragment>
<CustomGithubCorner />
<div className="forget-content" style={{padding: Setting.isMobile() ? "0" : null, boxShadow: Setting.isMobile() ? "none" : null}}>
<Button type="text" style={{position: "relative", left: Setting.isMobile() ? "10px" : "-90px", top: 0}} size={"large"} onClick={() => {this.stepBack();}}>
<ArrowLeftOutlined style={{fontSize: "24px"}} />
</Button>
<Button type="text"
style={{position: "relative", left: Setting.isMobile() ? "10px" : "-90px", top: 0}}
icon={<ArrowLeftOutlined style={{fontSize: "24px"}} />}
size={"large"}
onClick={() => {this.stepBack();}}
/>
<Row>
<Col span={24} style={{justifyContent: "center"}}>
<Row>

View File

@@ -68,7 +68,7 @@ class LoginPage extends React.Component {
this.state.applicationName = props.match?.params?.casApplicationName;
}
localStorage.setItem("signinUrl", window.location.href);
localStorage.setItem("signinUrl", window.location.pathname + window.location.search);
this.form = React.createRef();
}
@@ -314,7 +314,7 @@ class LoginPage extends React.Component {
}
if (resp.data2) {
sessionStorage.setItem("signinUrl", window.location.href);
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
Setting.goToLinkSoft(ths, `/forget/${application.name}`);
return;
}
@@ -454,7 +454,7 @@ class LoginPage extends React.Component {
if (responseType === "login") {
if (res.data2) {
sessionStorage.setItem("signinUrl", window.location.href);
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
Setting.goToLink(this, `/forget/${this.state.applicationName}`);
}
Setting.showMessage("success", i18next.t("application:Logged in successfully"));
@@ -463,7 +463,7 @@ class LoginPage extends React.Component {
this.postCodeLoginAction(res);
} else if (responseType === "token" || responseType === "id_token") {
if (res.data2) {
sessionStorage.setItem("signinUrl", window.location.href);
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
Setting.goToLink(this, `/forget/${this.state.applicationName}`);
}
const amendatoryResponseType = responseType === "token" ? "access_token" : responseType;
@@ -475,7 +475,7 @@ class LoginPage extends React.Component {
return;
}
if (res.data2.needUpdatePassword) {
sessionStorage.setItem("signinUrl", window.location.href);
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
Setting.goToLink(this, `/forget/${this.state.applicationName}`);
}
if (res.data2.method === "POST") {
@@ -530,9 +530,11 @@ class LoginPage extends React.Component {
return null;
}
const resultItemKey = `${application.organization}_${application.name}_${signinItem.name}`;
if (signinItem.name === "Logo") {
return (
<div className="login-logo-box">
<div key={resultItemKey} className="login-logo-box">
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
{
Setting.renderHelmet(application)
@@ -544,7 +546,7 @@ class LoginPage extends React.Component {
);
} else if (signinItem.name === "Back button") {
return (
<div className="back-button">
<div key={resultItemKey} className="back-button">
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
{
this.renderBackButton()
@@ -562,14 +564,14 @@ class LoginPage extends React.Component {
}
return (
<div className="login-languages">
<div key={resultItemKey} className="login-languages">
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
<LanguageSelect languages={application.organizationObj.languages} />
</div>
);
} else if (signinItem.name === "Signin methods") {
return (
<div>
<div key={resultItemKey}>
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
{this.renderMethodChoiceBox()}
</div>
@@ -577,7 +579,7 @@ class LoginPage extends React.Component {
;
} else if (signinItem.name === "Username") {
return (
<div>
<div key={resultItemKey}>
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
<Form.Item
name="username"
@@ -654,14 +656,14 @@ class LoginPage extends React.Component {
);
} else if (signinItem.name === "Password") {
return (
<div>
<div key={resultItemKey}>
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
{this.renderPasswordOrCodeInput(signinItem)}
</div>
);
} else if (signinItem.name === "Forgot password?") {
return (
<div>
<div key={resultItemKey}>
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
<div className="login-forget-password">
<Form.Item name="autoSignin" valuePropName="checked" noStyle>
@@ -679,7 +681,7 @@ class LoginPage extends React.Component {
return AgreementModal.isAgreementRequired(application) ? AgreementModal.renderAgreementFormItem(application, true, {}, this) : null;
} else if (signinItem.name === "Login button") {
return (
<Form.Item className="login-button-box">
<Form.Item key={resultItemKey} className="login-button-box">
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
<Button
type="primary"
@@ -723,13 +725,13 @@ class LoginPage extends React.Component {
}
return (
<div>
<div key={resultItemKey}>
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
<Form.Item>
{
application.providers.filter(providerItem => this.isProviderVisible(providerItem)).map((providerItem, id) => {
return (
<span key ={id} onClick={(e) => {
<span key={id} onClick={(e) => {
const agreementChecked = this.form.current.getFieldValue("agreement");
if (agreementChecked !== undefined && typeof agreementChecked === "boolean" && !agreementChecked) {
@@ -752,11 +754,11 @@ class LoginPage extends React.Component {
);
} else if (signinItem.name.startsWith("Text ") || signinItem?.isCustom) {
return (
<div dangerouslySetInnerHTML={{__html: signinItem.customCss}} />
<div key={resultItemKey} dangerouslySetInnerHTML={{__html: signinItem.customCss}} />
);
} else if (signinItem.name === "Signup link") {
return (
<div style={{width: "100%"}} className="login-signup-link">
<div key={resultItemKey} style={{width: "100%"}} className="login-signup-link">
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
{this.renderFooter(application, signinItem)}
</div>

View File

@@ -167,25 +167,35 @@ const Dashboard = (props) => {
};
myChart.setOption(option);
const cardStyles = {
body: {
width: Setting.isMobile() ? "340px" : "100%",
height: Setting.isMobile() ? "100px" : "150px",
display: "flex",
alignItems: "center",
justifyContent: "center",
},
};
return (
<Row id="statistic" gutter={80} justify={"center"}>
<Col span={50} style={{marginBottom: "10px"}}>
<Card bordered={false} bodyStyle={{width: Setting.isMobile() ? "340px" : "100%", height: Setting.isMobile() ? "100px" : "150px", display: "flex", alignItems: "center", justifyContent: "center"}}>
<Card variant="borderless" styles={cardStyles}>
<Statistic title={i18next.t("home:Total users")} fontSize="100px" value={dashboardData.userCounts[30]} valueStyle={{fontSize: "30px"}} style={{width: "200px", paddingLeft: "10px"}} />
</Card>
</Col>
<Col span={50} style={{marginBottom: "10px"}}>
<Card bordered={false} bodyStyle={{width: Setting.isMobile() ? "340px" : "100%", height: Setting.isMobile() ? "100px" : "150px", display: "flex", alignItems: "center", justifyContent: "center"}}>
<Card variant="borderless" styles={cardStyles}>
<Statistic title={i18next.t("home:New users today")} fontSize="100px" value={dashboardData.userCounts[30] - dashboardData.userCounts[30 - 1]} valueStyle={{fontSize: "30px"}} prefix={<ArrowUpOutlined />} style={{width: "200px", paddingLeft: "10px"}} />
</Card>
</Col>
<Col span={50} style={{marginBottom: "10px"}}>
<Card bordered={false} bodyStyle={{width: Setting.isMobile() ? "340px" : "100%", height: Setting.isMobile() ? "100px" : "150px", display: "flex", alignItems: "center", justifyContent: "center"}}>
<Card variant="borderless" styles={cardStyles}>
<Statistic title={i18next.t("home:New users past 7 days")} value={dashboardData.userCounts[30] - dashboardData.userCounts[30 - 7]} valueStyle={{fontSize: "30px"}} prefix={<ArrowUpOutlined />} style={{width: "200px", paddingLeft: "10px"}} />
</Card>
</Col>
<Col span={50} style={{marginBottom: "10px"}}>
<Card bordered={false} bodyStyle={{width: Setting.isMobile() ? "340px" : "100%", height: Setting.isMobile() ? "100px" : "150px", display: "flex", alignItems: "center", justifyContent: "center"}}>
<Card variant="borderless" styles={cardStyles}>
<Statistic title={i18next.t("home:New users past 30 days")} value={dashboardData.userCounts[30] - dashboardData.userCounts[30 - 30]} valueStyle={{fontSize: "30px"}} prefix={<ArrowUpOutlined />} style={{width: "200px", paddingLeft: "10px"}} />
</Card>
</Col>

View File

@@ -31,7 +31,7 @@ const GridCards = (props) => {
return (
Setting.isMobile() ? (
<Card bodyStyle={{padding: 0}}>
<Card styles={{body: {padding: 0}}}>
{items.map(item => <SingleCard key={item.link} logo={item.logo} link={item.link} title={item.name} desc={item.description} isSingle={items.length === 1} />)}
</Card>
) : (

83
web/src/common/Editor.js Normal file
View File

@@ -0,0 +1,83 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import CodeMirror from "@uiw/react-codemirror";
import {materialDark} from "@uiw/codemirror-theme-material";
import {langs} from "@uiw/codemirror-extensions-langs";
export const Editor = (props) => {
let style = {};
let height = props.height;
let width = props.width;
const copy2StyleProps = [
"width", "maxWidth", "minWidth",
"height", "maxHeight", "minHeight",
];
if (props.fillHeight) {
height = "100%";
style = {...style, height: "100%"};
}
if (props.fillWidth) {
width = "100%";
style = {...style, width: "100%"};
}
/**
* @uiw/react-codemirror style props sucha as "height" "width"
* may need to be configured with "style" in some scenarios to take effect
*/
copy2StyleProps.forEach(el => {
if (["number", "string"].includes(typeof props[el])) {
style = {...style, [el]: props[el]};
}
});
if (props.style) {
style = {...style, ...props.style};
}
let extensions = [];
switch (props.lang) {
case "javascript":
case "js":
extensions = [langs.javascript()];
break;
case "html":
extensions = [langs.html()];
break;
case "css":
extensions = [langs.css()];
break;
case "xml":
extensions = [langs.xml()];
break;
case "json":
extensions = [langs.json()];
break;
}
return (
<CodeMirror
value={props.value}
{...props}
width={width}
height={height}
style={style}
readOnly={props.readOnly}
theme={props.dark ? materialDark : "light"}
extensions={extensions}
onChange={props.onChange}
/>
);
};
export default Editor;

View File

@@ -58,7 +58,7 @@ export function checkPasswordComplexity(password, options) {
}
if (!options || options.length === 0) {
options = ["AtLeast6"];
return "";
}
const checkers = {

View File

@@ -70,6 +70,7 @@ function OrganizationSelect(props) {
<Select
options={getOrganizationItems()}
virtual={false}
popupMatchSelectWidth={false}
placeholder={i18next.t("login:Please select an organization")}
value={value}
onChange={handleOnChange}

View File

@@ -241,6 +241,7 @@
"Delete": "Delete",
"Description": "Description",
"Description - Tooltip": "Detailed description information for reference, Casdoor itself will not use it",
"Detail": "Detail",
"Disable": "Disable",
"Display name": "Display name",
"Display name - Tooltip": "A user-friendly, easily readable name displayed publicly in the UI",

View File

@@ -241,6 +241,7 @@
"Delete": "删除",
"Description": "描述信息",
"Description - Tooltip": "供人参考的详细描述信息Casdoor平台本身不会使用",
"Detail": "详情",
"Disable": "关闭",
"Display name": "显示名称",
"Display name - Tooltip": "在界面里公开显示的、易读的名称",

View File

@@ -147,7 +147,7 @@ class PricingPage extends React.Component {
if (Setting.isMobile()) {
return (
<Card style={{border: "none"}} bodyStyle={{padding: 0}}>
<Card style={{border: "none"}} styles={{body: {padding: 0}}}>
{
this.state.plans.map(item => {
return item.period === this.state.selectedPeriod ? (

View File

@@ -17,12 +17,7 @@ import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Input, Popover, Row, Select, Space, Switch, Table, Tooltip} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css";
require("codemirror/theme/material-darker.css");
require("codemirror/mode/htmlmixed/htmlmixed");
import Editor from "../common/Editor";
const {Option} = Select;
@@ -175,12 +170,9 @@ class SigninTable extends React.Component {
return (
<Popover placement="right" content={
<div style={{width: "900px", height: "300px"}} >
<CodeMirror value={text}
options={{mode: "htmlmixed", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
this.updateField(table, index, "label", value);
}}
/>
<Editor value={text} lang="html" fillHeight dark onChange={value => {
this.updateField(table, index, "label", value);
}} />
</div>
} title={i18next.t("signup:Label HTML")} trigger="click">
<Input value={text} style={{marginBottom: "10px"}} onChange={e => {
@@ -206,9 +198,12 @@ class SigninTable extends React.Component {
return (
<Popover placement="right" content={
<div style={{width: "900px", height: "300px"}} >
<CodeMirror value={text?.replaceAll("<style>", "").replaceAll("</style>", "")}
options={{mode: "css", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
<Editor
value={text?.replaceAll("<style>", "").replaceAll("</style>", "")}
lang="css"
fillHeight
dark
onChange={value => {
this.updateField(table, index, "customCss", value);
}}
/>

View File

@@ -17,12 +17,7 @@ import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Input, Popover, Row, Select, Switch, Table, Tooltip} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css";
require("codemirror/theme/material-darker.css");
require("codemirror/mode/htmlmixed/htmlmixed");
import Editor from "../common/Editor";
const EmailCss = ".signup-email{}\n.signup-email-input{}\n.signup-email-code{}\n.signup-email-code-input{}\n";
const PhoneCss = ".signup-phone{}\n.signup-phone-input{}\n.phone-code{}\n.signup-phone-code-input{}";
@@ -234,12 +229,9 @@ class SignupTable extends React.Component {
return (
<Popover placement="right" content={
<div style={{width: "900px", height: "300px"}} >
<CodeMirror value={text}
options={{mode: "htmlmixed", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
this.updateField(table, index, "label", value);
}}
/>
<Editor value={text} lang="html" fillHeight dark onChange={value => {
this.updateField(table, index, "label", value);
}} />
</div>
} title={i18next.t("signup:Label HTML")} trigger="click">
<Input value={text} style={{marginBottom: "10px"}} onChange={e => {
@@ -265,9 +257,12 @@ class SignupTable extends React.Component {
return (
<Popover placement="right" content={
<div style={{width: "900px", height: "300px"}}>
<CodeMirror value={text ? text : SignupTableDefaultCssMap[record.name]}
options={{mode: "css", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
<Editor
value={text ? text : SignupTableDefaultCssMap[record.name]}
lang="css"
fillHeight
dark
onChange={value => {
this.updateField(table, index, "customCss", value ? value : SignupTableDefaultCssMap[record.name]);
}}
/>

File diff suppressed because it is too large Load Diff