Compare commits

...

23 Commits

Author SHA1 Message Date
a79a2e7e55 Revert "feat: don't send verification code if failed signin limit is reached …"
This reverts commit f2e3037bc5.
2025-03-01 12:57:11 +08:00
41d9422687 feat: increase username limit to 255 chars 2025-03-01 00:44:34 +08:00
3297db688b feat: support shared cert in GetCert() API 2025-02-28 23:02:13 +08:00
cc82d292f0 feat: set frontend origin to 7001 if in dev mode (#3615) 2025-02-26 22:35:50 +08:00
f2e3037bc5 feat: don't send verification code if failed signin limit is reached (#3616) 2025-02-26 22:34:14 +08:00
d986a4a9e0 feat: fix bug that initialize group children as empty array instead of empty string (#3620) 2025-02-26 08:50:09 +08:00
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
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
f26b4853c5 feat: bump Go version to go 1.18 (#3599) 2025-02-21 13:10:17 +08:00
d78e8e9776 feat: fix LDAP filter condition will return nil if error happened (#3604) 2025-02-21 13:09:39 +08:00
d61f9a1856 feat: update antd from 5.2.3 to 5.24.1 (#3593) 2025-02-18 20:54:10 +08:00
aa52af02b3 feat: fix style props of Editor (#3590) 2025-02-17 13:39:49 +08:00
2a5722e45b feat: add detail sidebar for record list page, improve token list page (#3589) 2025-02-16 22:01:25 +08:00
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
f8d44e2dca feat: set default CountryCode for user 2025-02-14 16:54:25 +08:00
26eea501be feat: don't use organization.MasterVerificationCode when sending 2025-02-14 16:54:25 +08:00
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
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
9c39179849 feat: fix bug that user forbidden check will be skipped in OAuth login (#3580) 2025-02-13 13:14:44 +08:00
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
e926a07c58 feat: add "User type" to user list page 2025-02-12 21:29:18 +08:00
9c46344e68 feat: improve default org passwordOptions handling 2025-02-12 21:20:32 +08:00
c0ec73dfd3 feat: fix tableNamePrefix doesn't work bug in /get-dashboard API (#3572) 2025-02-11 17:20:45 +08:00
73 changed files with 2243 additions and 2603 deletions

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

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

@ -67,7 +67,7 @@
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Uživatelské jméno nemůže být emailová adresa",
"Username cannot contain white spaces": "Uživatelské jméno nemůže obsahovat mezery",
"Username cannot start with a digit": "Uživatelské jméno nemůže začínat číslicí",
"Username is too long (maximum is 39 characters).": "Uživatelské jméno je příliš dlouhé (maximálně 39 znaků).",
"Username is too long (maximum is 255 characters).": "Uživatelské jméno je příliš dlouhé (maximálně 255 znaků).",
"Username must have at least 2 characters": "Uživatelské jméno musí mít alespoň 2 znaky",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Zadali jste špatné heslo nebo kód příliš mnohokrát, prosím počkejte %d minut a zkuste to znovu",
"Your region is not allow to signup by phone": "Vaše oblast neumožňuje registraci pomocí telefonu",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Benutzername kann keine E-Mail-Adresse sein",
"Username cannot contain white spaces": "Benutzername darf keine Leerzeichen enthalten",
"Username cannot start with a digit": "Benutzername darf nicht mit einer Ziffer beginnen",
"Username is too long (maximum is 39 characters).": "Benutzername ist zu lang (das Maximum beträgt 39 Zeichen).",
"Username is too long (maximum is 255 characters).": "Benutzername ist zu lang (das Maximum beträgt 255 Zeichen).",
"Username must have at least 2 characters": "Benutzername muss mindestens 2 Zeichen lang sein",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Sie haben zu oft das falsche Passwort oder den falschen Code eingegeben. Bitte warten Sie %d Minuten und versuchen Sie es erneut",
"Your region is not allow to signup by phone": "Ihre Region ist nicht berechtigt, sich telefonisch anzumelden",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Nombre de usuario no puede ser una dirección de correo electrónico",
"Username cannot contain white spaces": "Nombre de usuario no puede contener espacios en blanco",
"Username cannot start with a digit": "El nombre de usuario no puede empezar con un dígito",
"Username is too long (maximum is 39 characters).": "El nombre de usuario es demasiado largo (el máximo es de 39 caracteres).",
"Username is too long (maximum is 255 characters).": "El nombre de usuario es demasiado largo (el máximo es de 255 caracteres).",
"Username must have at least 2 characters": "Nombre de usuario debe tener al menos 2 caracteres",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Has ingresado la contraseña o código incorrecto demasiadas veces, por favor espera %d minutos e intenta de nuevo",
"Your region is not allow to signup by phone": "Tu región no está permitida para registrarse por teléfono",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "نام کاربری نمی‌تواند یک آدرس ایمیل باشد",
"Username cannot contain white spaces": "نام کاربری نمی‌تواند حاوی فاصله باشد",
"Username cannot start with a digit": "نام کاربری نمی‌تواند با یک رقم شروع شود",
"Username is too long (maximum is 39 characters).": "نام کاربری بیش از حد طولانی است (حداکثر ۳۹ کاراکتر).",
"Username is too long (maximum is 255 characters).": "نام کاربری بیش از حد طولانی است (حداکثر ۳۹ کاراکتر).",
"Username must have at least 2 characters": "نام کاربری باید حداقل ۲ کاراکتر داشته باشد",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "شما رمز عبور یا کد اشتباه را بیش از حد وارد کرده‌اید، لطفاً %d دقیقه صبر کنید و دوباره تلاش کنید",
"Your region is not allow to signup by phone": "منطقه شما اجازه ثبت‌نام با تلفن را ندارد",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Nom d'utilisateur ne peut pas être une adresse e-mail",
"Username cannot contain white spaces": "Nom d'utilisateur ne peut pas contenir d'espaces blancs",
"Username cannot start with a digit": "Nom d'utilisateur ne peut pas commencer par un chiffre",
"Username is too long (maximum is 39 characters).": "Nom d'utilisateur est trop long (maximum de 39 caractères).",
"Username is too long (maximum is 255 characters).": "Nom d'utilisateur est trop long (maximum de 255 caractères).",
"Username must have at least 2 characters": "Le nom d'utilisateur doit comporter au moins 2 caractères",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Vous avez entré le mauvais mot de passe ou code plusieurs fois, veuillez attendre %d minutes et réessayer",
"Your region is not allow to signup by phone": "Votre région n'est pas autorisée à s'inscrire par téléphone",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Username tidak bisa menjadi alamat email",
"Username cannot contain white spaces": "Username tidak boleh mengandung spasi",
"Username cannot start with a digit": "Username tidak dapat dimulai dengan angka",
"Username is too long (maximum is 39 characters).": "Nama pengguna terlalu panjang (maksimum 39 karakter).",
"Username is too long (maximum is 255 characters).": "Nama pengguna terlalu panjang (maksimum 255 karakter).",
"Username must have at least 2 characters": "Nama pengguna harus memiliki setidaknya 2 karakter",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Anda telah memasukkan kata sandi atau kode yang salah terlalu banyak kali, mohon tunggu selama %d menit dan coba lagi",
"Your region is not allow to signup by phone": "Wilayah Anda tidak diizinkan untuk mendaftar melalui telepon",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "ユーザー名には電子メールアドレスを使用できません",
"Username cannot contain white spaces": "ユーザ名にはスペースを含めることはできません",
"Username cannot start with a digit": "ユーザー名は数字で始めることはできません",
"Username is too long (maximum is 39 characters).": "ユーザー名が長すぎます(最大39文字)。",
"Username is too long (maximum is 255 characters).": "ユーザー名が長すぎます(最大255文字)。",
"Username must have at least 2 characters": "ユーザー名は少なくとも2文字必要です",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "あなたは間違ったパスワードまたはコードを何度も入力しました。%d 分間待ってから再度お試しください",
"Your region is not allow to signup by phone": "あなたの地域は電話でサインアップすることができません",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "사용자 이름은 이메일 주소가 될 수 없습니다",
"Username cannot contain white spaces": "사용자 이름에는 공백이 포함될 수 없습니다",
"Username cannot start with a digit": "사용자 이름은 숫자로 시작할 수 없습니다",
"Username is too long (maximum is 39 characters).": "사용자 이름이 너무 깁니다 (최대 39자).",
"Username is too long (maximum is 255 characters).": "사용자 이름이 너무 깁니다 (최대 255자).",
"Username must have at least 2 characters": "사용자 이름은 적어도 2개의 문자가 있어야 합니다",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "올바르지 않은 비밀번호나 코드를 여러 번 입력했습니다. %d분 동안 기다리신 후 다시 시도해주세요",
"Your region is not allow to signup by phone": "당신의 지역은 전화로 가입할 수 없습니다",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "O nome de usuário não pode começar com um dígito",
"Username is too long (maximum is 39 characters).": "Nome de usuário é muito longo (máximo é 39 caracteres).",
"Username is too long (maximum is 255 characters).": "Nome de usuário é muito longo (máximo é 255 caracteres).",
"Username must have at least 2 characters": "Nome de usuário deve ter pelo menos 2 caracteres",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Имя пользователя не может быть адресом электронной почты",
"Username cannot contain white spaces": "Имя пользователя не может содержать пробелы",
"Username cannot start with a digit": "Имя пользователя не может начинаться с цифры",
"Username is too long (maximum is 39 characters).": "Имя пользователя слишком длинное (максимальная длина - 39 символов).",
"Username is too long (maximum is 255 characters).": "Имя пользователя слишком длинное (максимальная длина - 255 символов).",
"Username must have at least 2 characters": "Имя пользователя должно содержать не менее 2 символов",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Вы ввели неправильный пароль или код слишком много раз, пожалуйста, подождите %d минут и попробуйте снова",
"Your region is not allow to signup by phone": "Ваш регион не разрешает регистрацию по телефону",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Používateľské meno nemôže byť e-mailová adresa",
"Username cannot contain white spaces": "Používateľské meno nemôže obsahovať medzery",
"Username cannot start with a digit": "Používateľské meno nemôže začínať číslicou",
"Username is too long (maximum is 39 characters).": "Používateľské meno je príliš dlhé (maximum je 39 znakov).",
"Username is too long (maximum is 255 characters).": "Používateľské meno je príliš dlhé (maximum je 255 znakov).",
"Username must have at least 2 characters": "Používateľské meno musí mať aspoň 2 znaky",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Zadali ste nesprávne heslo alebo kód príliš veľa krát, prosím, počkajte %d minút a skúste to znova",
"Your region is not allow to signup by phone": "Váš región neumožňuje registráciu cez telefón",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Kullanıcı adı bir e-mail adresi olamaz",
"Username cannot contain white spaces": "Kullanıcı adı boşluk karakteri içeremez",
"Username cannot start with a digit": "Kullanıcı adı rakamla başlayamaz",
"Username is too long (maximum is 39 characters).": "Kullanıcı adı çok uzun (en fazla 39 karakter olmalı).",
"Username is too long (maximum is 255 characters).": "Kullanıcı adı çok uzun (en fazla 255 karakter olmalı).",
"Username must have at least 2 characters": "Kullanıcı adı en az iki karakterden oluşmalı",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Çok fazla hatalı şifre denemesi yaptınız. %d dakika kadar bekleyip yeniden giriş yapmayı deneyebilirsiniz.",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "Tên người dùng không thể là địa chỉ email",
"Username cannot contain white spaces": "Tên người dùng không thể chứa khoảng trắng",
"Username cannot start with a digit": "Tên người dùng không thể bắt đầu bằng chữ số",
"Username is too long (maximum is 39 characters).": "Tên đăng nhập quá dài (tối đa là 39 ký tự).",
"Username is too long (maximum is 255 characters).": "Tên đăng nhập quá dài (tối đa là 255 ký tự).",
"Username must have at least 2 characters": "Tên đăng nhập phải có ít nhất 2 ký tự",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Bạn đã nhập sai mật khẩu hoặc mã quá nhiều lần, vui lòng đợi %d phút và thử lại",
"Your region is not allow to signup by phone": "Vùng của bạn không được phép đăng ký bằng điện thoại",

View File

@ -67,7 +67,7 @@
"Username cannot be an email address": "用户名不可以是邮箱地址",
"Username cannot contain white spaces": "用户名禁止包含空格",
"Username cannot start with a digit": "用户名禁止使用数字开头",
"Username is too long (maximum is 39 characters).": "用户名过长(最大允许长度为39个字符)",
"Username is too long (maximum is 255 characters).": "用户名过长(最大允许长度为255个字符)",
"Username must have at least 2 characters": "用户名至少要有2个字符",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "密码错误次数已达上限,请在 %d 分后重试",
"Your region is not allow to signup by phone": "所在地区不支持手机号注册",

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

@ -146,7 +146,12 @@ func getCertByName(name string) (*Cert, error) {
func GetCert(id string) (*Cert, error) {
owner, name := util.GetOwnerAndNameFromId(id)
return getCert(owner, name)
cert, err := getCert(owner, name)
if cert == nil && owner != "admin" {
return getCert("admin", name)
} else {
return cert, err
}
}
func UpdateCert(id string, cert *Cert) (bool, error) {

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)
}
}
@ -517,8 +517,8 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
func CheckUsername(username string, lang string) string {
if username == "" {
return i18n.Translate(lang, "check:Empty username.")
} else if len(username) > 39 {
return i18n.Translate(lang, "check:Username is too long (maximum is 39 characters).")
} else if len(username) > 255 {
return i18n.Translate(lang, "check:Username is too long (maximum is 255 characters).")
}
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex
@ -533,8 +533,8 @@ func CheckUsername(username string, lang string) string {
func CheckUsernameWithEmail(username string, lang string) string {
if username == "" {
return i18n.Translate(lang, "check:Empty username.")
} else if len(username) > 39 {
return i18n.Translate(lang, "check:Username is too long (maximum is 39 characters).")
} else if len(username) > 255 {
return i18n.Translate(lang, "check:Username is too long (maximum is 255 characters).")
}
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex

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

@ -77,6 +77,7 @@ func getOriginFromHostInternal(host string) (string, string) {
return origin, origin
}
isDev := conf.GetConfigString("runmode") == "dev"
// "door.casdoor.com"
protocol := "https://"
if !strings.Contains(host, ".") {
@ -87,7 +88,7 @@ func getOriginFromHostInternal(host string) (string, string) {
protocol = "http://"
}
if host == "localhost:8000" {
if host == "localhost:8000" && isDev {
return fmt.Sprintf("%s%s", protocol, "localhost:7001"), fmt.Sprintf("%s%s", protocol, "localhost:8000")
} else {
return fmt.Sprintf("%s%s", protocol, host), fmt.Sprintf("%s%s", protocol, host)

View File

@ -48,7 +48,7 @@ func InitUserManager() {
type User struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
Name string `xorm:"varchar(255) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100) index" json:"createdTime"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
DeletedTime string `xorm:"varchar(100)" json:"deletedTime"`

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

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

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

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