Compare commits

...

61 Commits

Author SHA1 Message Date
daa7b79915 feat: improve error handling of webauthn login (#3744) 2025-04-24 01:11:24 +08:00
d3a5539dae feat: fix loading status not reset issue when failed to login (#3743) 2025-04-24 00:57:52 +08:00
7d1c614452 feat: use random name as name if user's name is invalid when created by third party provider (#3742) 2025-04-23 21:30:19 +08:00
e2eafa909b feat: fix MODEL_URL in FaceRecognitionModal 2025-04-21 09:10:30 +08:00
56bcef0592 feat: support application.formCss in forget-password page (#3733) 2025-04-19 22:59:21 +08:00
0860cbf343 feat: can specify content type and http body field mapping for Custom HTTP Email provider (#3730) 2025-04-17 01:59:11 +08:00
2f4180b1b6 feat: add missing currencies in plan edit page (#3727) 2025-04-15 16:01:14 +08:00
e3d5619b25 feat: support custom HTTP headers in custom HttpEmailProvider and hide unused fields (#3723) 2025-04-13 23:52:04 +08:00
019fd87b92 feat: fix code comment typos (#3724) 2025-04-13 17:57:37 +08:00
5c41c6c4a5 feat: add BRL currency 2025-04-11 22:24:45 +08:00
b7fafcc62b feat: improve InitFromFile() code order to fix GetOrganizationApplicationCount always returns 0 bug (#3720) 2025-04-11 01:43:54 +08:00
493ceddcd9 feat: improve error handling in system info page 2025-04-11 01:41:27 +08:00
fc618b9bd5 feat: add validation for optional fields in IntrospectionToken for custom token types (#3717) 2025-04-09 22:27:19 +08:00
a00900e405 feat: fix sqlite bug for failed to lookup Client-side Discoverable Credential: user not exist (#3719) 2025-04-09 22:26:47 +08:00
77ef5828dd feat(introspection): return correct active status for expired or revoked tokens (#3716) 2025-04-09 02:00:30 +08:00
c11f013e04 feat: return "Active: false" for expired token in IntrospectToken() (#3714) 2025-04-08 23:20:44 +08:00
b3bafe8402 feat: fix bug that unable to query webauthnCredentials when db is mssql or postgres in GetUserByWebauthID() (#3712) 2025-04-08 17:51:32 +08:00
f04a431d85 feat: Casdoor's LDAP client supports LDAP server's self-signed certificates now (#3709) 2025-04-07 02:02:32 +08:00
952538916d feat: check application existence in object.AddUser() (#3686) 2025-04-05 16:38:20 +08:00
18bb445e71 feat: update github.com/golang-jwt/jwt dependency to v5 (#3708) 2025-04-05 02:05:41 +08:00
cca88e2cb0 feat: fix bug that when email/sms mfa is not preferred, message will send to masked address (#3705) 2025-04-04 01:08:29 +08:00
86c10fe0ab feat: change org.CountryCodes to mediumtext 2025-04-02 20:23:04 +08:00
c1b3bf0f45 feat: set button to loading status immediately after click (#3696) 2025-04-02 01:15:36 +08:00
62bda61af5 feat: can use provider_hint arg to do OAuth redirect automatically (#3698) 2025-04-02 01:15:20 +08:00
b6f943e326 feat: support WebAuthn login without username and upgrade Go to 1.21 (#3695) 2025-04-01 16:35:59 +08:00
2cc5e82d91 feat: support login button loading state (#3694) 2025-04-01 00:57:24 +08:00
e55cd94298 feat: fix issue that user email is still unverified after signup (#3685) 2025-03-29 21:24:01 +08:00
08f7a05e61 feat: fix MFA + LDAP bug in /check-user-password API (#3681) 2025-03-26 22:11:58 +08:00
4bee21f4a3 feat: use StaticBaseUrl in frontend 2025-03-26 21:32:31 +08:00
5417a90223 feat: fix bug that there is already an object named 'casbin_api_rule' in the database (#3680) 2025-03-25 22:24:58 +08:00
131820e34e feat: add application.ForcedRedirectOrigin 2025-03-24 13:42:35 +08:00
2fcbf7cf6c feat: fix apps page grid style (#3679) 2025-03-22 18:19:14 +08:00
14ade8b7e4 feat: fix provider test API's missing owner and name args for auth (#3676) 2025-03-22 17:53:20 +08:00
a11fe59704 feat: support widget items config in org (#3674) 2025-03-21 23:00:07 +08:00
af55d0547f feat: improve frontend i18n strings 2025-03-21 21:03:29 +08:00
81102f8298 feat: fix permission update bug when both org and model are modified (#3671) 2025-03-20 09:05:27 +08:00
141372cb86 feat: support face ID provider (#3666) 2025-03-19 22:57:35 +08:00
15a037ca74 feat: increase frontend build memory to 4096 in Dockerfile (#3672)
297.8 FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
2025-03-19 10:40:34 +08:00
73c680d56f feat: avoid using body in GET requests for AirwallexClient payment provider (#3669) 2025-03-18 20:04:15 +08:00
aafc16e4f4 feat: fix dynamic width of navbar UI (#3664) 2025-03-16 16:12:58 +08:00
7be026dd1f feat: Support for selecting existing users or scanning a QR code when logging into Dingtalk (#3660) 2025-03-13 21:49:07 +08:00
3e7938e5f6 feat: don't panic when provider not found in Login() API (#3659) 2025-03-13 21:35:51 +08:00
30789138e2 feat: fix faceId loop error caused by async (#3651) 2025-03-11 21:03:04 +08:00
9610ce5b8c feat: can add faceId by uploading images (#3641) 2025-03-09 01:29:25 +08:00
a39a311d2f feat: fix webhook bug in RecordEx JSON (#3642) 2025-03-08 00:20:59 +08:00
08e41ab762 feat: can specify user fields in webhook edit page (#3635) 2025-03-04 14:16:16 +08:00
85ca318e2f feat: can assign default group during signup (#3633) 2025-03-02 22:55:51 +08:00
9032865e60 feat: support mobile background for login page (#3629) 2025-03-01 23:01:15 +08:00
5692522ee0 feat: update user language when the language changed on login page (#3628) 2025-03-01 22:28:20 +08:00
cb1882e589 feat: fix MFA bug, revert PR: "feat: don't send verification code if failed signin limit is reached" (#3627) 2025-03-01 12:58:28 +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
139 changed files with 3793 additions and 3079 deletions

View File

@ -1,10 +1,10 @@
FROM --platform=$BUILDPLATFORM node:18.19.0 AS FRONT
WORKDIR /web
COPY ./web .
RUN yarn install --frozen-lockfile --network-timeout 1000000 && yarn run build
RUN yarn install --frozen-lockfile --network-timeout 1000000 && NODE_OPTIONS="--max-old-space-size=4096" yarn run build
FROM --platform=$BUILDPLATFORM golang:1.20.12 AS BACK
FROM --platform=$BUILDPLATFORM golang:1.21.13 AS BACK
WORKDIR /go/src/casdoor
COPY . .
RUN ./build.sh

View File

@ -139,6 +139,8 @@ func (c *ApiController) Signup() {
invitationName = invitation.Name
}
userEmailVerified := false
if application.IsSignupItemVisible("Email") && application.GetSignupItemRule("Email") != "No verification" && authForm.Email != "" {
var checkResult *object.VerifyResult
checkResult, err = object.CheckVerificationCode(authForm.Email, authForm.EmailCode, c.GetAcceptLanguage())
@ -150,6 +152,8 @@ func (c *ApiController) Signup() {
c.ResponseError(checkResult.Msg)
return
}
userEmailVerified = true
}
var checkPhone string
@ -228,6 +232,7 @@ func (c *ApiController) Signup() {
Karma: 0,
Invitation: invitationName,
InvitationCode: authForm.InvitationCode,
EmailVerified: userEmailVerified,
}
if len(organization.Tags) > 0 {
@ -249,6 +254,10 @@ func (c *ApiController) Signup() {
user.Groups = []string{invitation.SignupGroup}
}
if application.DefaultGroup != "" && user.Groups == nil {
user.Groups = []string{application.DefaultGroup}
}
affected, err := object.AddUser(user)
if err != nil {
c.ResponseError(err.Error())

View File

@ -29,6 +29,7 @@ import (
"github.com/casdoor/casdoor/captcha"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/form"
"github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/proxy"
@ -402,11 +403,27 @@ func (c *ApiController) Login() {
return
}
if err := object.CheckFaceId(user, authForm.FaceId, c.GetAcceptLanguage()); err != nil {
c.ResponseError(err.Error(), nil)
return
faceIdProvider, err := object.GetFaceIdProviderByApplication(util.GetId(application.Owner, application.Name), "false", c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
}
if faceIdProvider == nil {
if err := object.CheckFaceId(user, authForm.FaceId, c.GetAcceptLanguage()); err != nil {
c.ResponseError(err.Error(), nil)
return
}
} else {
ok, err := user.CheckUserFace(authForm.FaceIdImage, faceIdProvider)
if err != nil {
c.ResponseError(err.Error(), nil)
}
if !ok {
c.ResponseError(i18n.Translate(c.GetAcceptLanguage(), "check:Face data does not exist, cannot log in"))
return
}
}
} else if authForm.Password == "" {
if user, err = object.GetUserByFields(authForm.Organization, authForm.Username); err != nil {
c.ResponseError(err.Error(), nil)
@ -466,6 +483,14 @@ func (c *ApiController) Login() {
verificationType = "sms"
} else {
verificationType = "email"
if !user.EmailVerified {
user.EmailVerified = true
_, err = object.UpdateUser(user.GetId(), user, []string{"email_verified"}, false)
if err != nil {
c.ResponseError(err.Error(), nil)
return
}
}
}
} else {
var application *object.Application
@ -598,6 +623,9 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error())
return
}
if provider == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s does not exist"), authForm.Provider))
}
providerItem := application.GetProviderItem(provider.Name)
if !providerItem.IsProviderVisible() {
@ -986,6 +1014,18 @@ func (c *ApiController) Login() {
}
}
if authForm.Language != "" {
user := c.getCurrentUser()
if user != nil {
user.Language = authForm.Language
_, err = object.UpdateUser(user.GetId(), user, []string{"language"}, user.IsAdmin)
if err != nil {
c.ResponseError(err.Error())
return
}
}
}
c.Data["json"] = resp
c.ServeJSON()
}

View File

@ -27,10 +27,10 @@ type LdapResp struct {
ExistUuids []string `json:"existUuids"`
}
//type LdapRespGroup struct {
// type LdapRespGroup struct {
// GroupId string
// GroupName string
//}
// }
type LdapSyncResp struct {
Exist []object.LdapUser `json:"exist"`
@ -61,18 +61,18 @@ func (c *ApiController) GetLdapUsers() {
}
defer conn.Close()
//groupsMap, err := conn.GetLdapGroups(ldapServer.BaseDn)
//if err != nil {
// groupsMap, err := conn.GetLdapGroups(ldapServer.BaseDn)
// if err != nil {
// c.ResponseError(err.Error())
// return
//}
// }
//for _, group := range groupsMap {
// for _, group := range groupsMap {
// resp.Groups = append(resp.Groups, LdapRespGroup{
// GroupId: group.GidNumber,
// GroupName: group.Cn,
// })
//}
// }
users, err := conn.GetLdapUsers(ldapServer)
if err != nil {
@ -269,7 +269,11 @@ func (c *ApiController) SyncLdapUsers() {
return
}
exist, failed, _ := object.SyncLdapUsers(owner, users, ldapId)
exist, failed, err := object.SyncLdapUsers(owner, users, ldapId)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(&LdapSyncResp{
Exist: exist,

View File

@ -321,6 +321,11 @@ func (c *ApiController) IntrospectToken() {
return
}
respondWithInactiveToken := func() {
c.Data["json"] = &object.IntrospectionResponse{Active: false}
c.ServeJSON()
}
tokenTypeHint := c.Input().Get("token_type_hint")
var token *object.Token
if tokenTypeHint != "" {
@ -329,7 +334,12 @@ func (c *ApiController) IntrospectToken() {
c.ResponseTokenError(err.Error())
return
}
if token == nil {
if token == nil || token.ExpiresIn <= 0 {
respondWithInactiveToken()
return
}
if token.ExpiresIn <= 0 {
c.Data["json"] = &object.IntrospectionResponse{Active: false}
c.ServeJSON()
return
@ -340,12 +350,11 @@ func (c *ApiController) IntrospectToken() {
if application.TokenFormat == "JWT-Standard" {
jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application)
if err != nil || jwtToken.Valid() != nil {
if err != nil {
// and token revoked case. but we not implement
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
// refs: https://tools.ietf.org/html/rfc7009
c.Data["json"] = &object.IntrospectionResponse{Active: false}
c.ServeJSON()
respondWithInactiveToken()
return
}
@ -365,28 +374,34 @@ func (c *ApiController) IntrospectToken() {
}
} else {
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
if err != nil || jwtToken.Valid() != nil {
if err != nil {
// and token revoked case. but we not implement
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
// refs: https://tools.ietf.org/html/rfc7009
c.Data["json"] = &object.IntrospectionResponse{Active: false}
c.ServeJSON()
respondWithInactiveToken()
return
}
introspectionResponse = object.IntrospectionResponse{
Active: true,
Scope: jwtToken.Scope,
ClientId: clientId,
Username: jwtToken.Name,
TokenType: jwtToken.TokenType,
Exp: jwtToken.ExpiresAt.Unix(),
Iat: jwtToken.IssuedAt.Unix(),
Nbf: jwtToken.NotBefore.Unix(),
Sub: jwtToken.Subject,
Aud: jwtToken.Audience,
Iss: jwtToken.Issuer,
Jti: jwtToken.ID,
Active: true,
ClientId: clientId,
Exp: jwtToken.ExpiresAt.Unix(),
Iat: jwtToken.IssuedAt.Unix(),
Nbf: jwtToken.NotBefore.Unix(),
Sub: jwtToken.Subject,
Aud: jwtToken.Audience,
Iss: jwtToken.Issuer,
Jti: jwtToken.ID,
}
if jwtToken.Scope != "" {
introspectionResponse.Scope = jwtToken.Scope
}
if jwtToken.Name != "" {
introspectionResponse.Username = jwtToken.Name
}
if jwtToken.TokenType != "" {
introspectionResponse.TokenType = jwtToken.TokenType
}
}
@ -396,13 +411,15 @@ func (c *ApiController) IntrospectToken() {
c.ResponseTokenError(err.Error())
return
}
if token == nil {
c.Data["json"] = &object.IntrospectionResponse{Active: false}
c.ServeJSON()
if token == nil || token.ExpiresIn <= 0 {
respondWithInactiveToken()
return
}
}
introspectionResponse.TokenType = token.TokenType
if token != nil {
introspectionResponse.TokenType = token.TokenType
}
c.Data["json"] = introspectionResponse
c.ServeJSON()

View File

@ -457,10 +457,10 @@ func (c *ApiController) SetPassword() {
newPassword := c.Ctx.Request.Form.Get("newPassword")
code := c.Ctx.Request.Form.Get("code")
//if userOwner == "built-in" && userName == "admin" {
// if userOwner == "built-in" && userName == "admin" {
// c.ResponseError(c.T("auth:Unauthorized operation"))
// return
//}
// }
if strings.Contains(newPassword, " ") {
c.ResponseError(c.T("user:New password cannot contain blank space."))
@ -602,7 +602,11 @@ func (c *ApiController) CheckUserPassword() {
return
}
_, err = object.CheckUserPassword(user.Owner, user.Name, user.Password, c.GetAcceptLanguage())
/*
* Verified password with user as subject, if field ldap not empty,
* then `isPasswordWithLdapEnabled` is true
*/
_, err = object.CheckUserPassword(user.Owner, user.Name, user.Password, c.GetAcceptLanguage(), false, false, user.Ldap != "")
if err != nil {
c.ResponseError(err.Error())
} else {

View File

@ -242,7 +242,7 @@ func (c *ApiController) SendVerificationCode() {
} else if vform.Method == ResetVerification {
user = c.getCurrentUser()
} else if vform.Method == MfaAuthVerification {
mfaProps := user.GetPreferredMfaProps(false)
mfaProps := user.GetMfaProps(object.EmailType, false)
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
vform.Dest = mfaProps.Secret
}
@ -281,7 +281,7 @@ func (c *ApiController) SendVerificationCode() {
}
}
} else if vform.Method == MfaAuthVerification {
mfaProps := user.GetPreferredMfaProps(false)
mfaProps := user.GetMfaProps(object.SmsType, false)
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
vform.Dest = mfaProps.Secret
}
@ -436,7 +436,8 @@ func (c *ApiController) ResetEmailOrPhone() {
switch destType {
case object.VerifyTypeEmail:
user.Email = dest
_, err = object.SetUserField(user, "email", user.Email)
user.EmailVerified = true
_, err = object.UpdateUser(user.GetId(), user, []string{"email", "email_verified"}, false)
case object.VerifyTypePhone:
user.Phone = dest
_, err = object.SetUserField(user, "phone", user.Phone)

View File

@ -16,7 +16,7 @@ package controllers
import (
"bytes"
"fmt"
"encoding/base64"
"io"
"github.com/casdoor/casdoor/form"
@ -118,24 +118,7 @@ func (c *ApiController) WebAuthnSigninBegin() {
return
}
userOwner := c.Input().Get("owner")
userName := c.Input().Get("name")
user, err := object.GetUserByFields(userOwner, userName)
if err != nil {
c.ResponseError(err.Error())
return
}
if user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(userOwner, userName)))
return
}
if len(user.WebauthnCredentials) == 0 {
c.ResponseError(c.T("webauthn:Found no credentials for this user"))
return
}
options, sessionData, err := webauthnObj.BeginLogin(user)
options, sessionData, err := webauthnObj.BeginDiscoverableLogin()
if err != nil {
c.ResponseError(err.Error())
return
@ -168,20 +151,23 @@ func (c *ApiController) WebAuthnSigninFinish() {
return
}
c.Ctx.Request.Body = io.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
userId := string(sessionData.UserID)
user, err := object.GetUser(userId)
if err != nil {
c.ResponseError(err.Error())
return
var user *object.User
handler := func(rawID, userHandle []byte) (webauthn.User, error) {
user, err = object.GetUserByWebauthID(base64.StdEncoding.EncodeToString(rawID))
if err != nil {
return nil, err
}
return user, nil
}
_, err = webauthnObj.FinishLogin(user, sessionData, c.Ctx.Request)
_, err = webauthnObj.FinishDiscoverableLogin(handler, sessionData, c.Ctx.Request)
if err != nil {
c.ResponseError(err.Error())
return
}
c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
c.SetSessionUsername(user.GetId())
util.LogInfo(c.Ctx, "API: [%s] signed in", user.GetId())
var application *object.Application

View File

@ -15,6 +15,8 @@
package email
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
@ -24,14 +26,24 @@ import (
)
type HttpEmailProvider struct {
endpoint string
method string
endpoint string
method string
httpHeaders map[string]string
bodyMapping map[string]string
contentType string
}
func NewHttpEmailProvider(endpoint string, method string) *HttpEmailProvider {
func NewHttpEmailProvider(endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string) *HttpEmailProvider {
if contentType == "" {
contentType = "application/x-www-form-urlencoded"
}
client := &HttpEmailProvider{
endpoint: endpoint,
method: method,
endpoint: endpoint,
method: method,
httpHeaders: httpHeaders,
bodyMapping: bodyMapping,
contentType: contentType,
}
return client
}
@ -39,18 +51,52 @@ func NewHttpEmailProvider(endpoint string, method string) *HttpEmailProvider {
func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
var req *http.Request
var err error
if c.method == "POST" {
formValues := url.Values{}
formValues.Set("fromName", fromName)
formValues.Set("toAddress", toAddress)
formValues.Set("subject", subject)
formValues.Set("content", content)
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
fromNameField := "fromName"
toAddressField := "toAddress"
subjectField := "subject"
contentField := "content"
for k, v := range c.bodyMapping {
switch k {
case "fromName":
fromNameField = v
case "toAddress":
toAddressField = v
case "subject":
subjectField = v
case "content":
contentField = v
}
}
if c.method == "POST" || c.method == "PUT" || c.method == "DELETE" {
bodyMap := make(map[string]string)
bodyMap[fromNameField] = fromName
bodyMap[toAddressField] = toAddress
bodyMap[subjectField] = subject
bodyMap[contentField] = content
var fromValueBytes []byte
if c.contentType == "application/json" {
fromValueBytes, err = json.Marshal(bodyMap)
if err != nil {
return err
}
req, err = http.NewRequest(c.method, c.endpoint, bytes.NewBuffer(fromValueBytes))
} else {
formValues := url.Values{}
for k, v := range bodyMap {
formValues.Add(k, v)
}
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
}
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Content-Type", c.contentType)
} else if c.method == "GET" {
req, err = http.NewRequest(c.method, c.endpoint, nil)
if err != nil {
@ -58,15 +104,19 @@ func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress
}
q := req.URL.Query()
q.Add("fromName", fromName)
q.Add("toAddress", toAddress)
q.Add("subject", subject)
q.Add("content", content)
q.Add(fromNameField, fromName)
q.Add(toAddressField, toAddress)
q.Add(subjectField, subject)
q.Add(contentField, content)
req.URL.RawQuery = q.Encode()
} else {
return fmt.Errorf("HttpEmailProvider's Send() error, unsupported method: %s", c.method)
}
for k, v := range c.httpHeaders {
req.Header.Set(k, v)
}
httpClient := proxy.DefaultHttpClient
resp, err := httpClient.Do(req)
if err != nil {

View File

@ -18,11 +18,11 @@ type EmailProvider interface {
Send(fromAddress string, fromName, toAddress string, subject string, content string) error
}
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool, endpoint string, method string) EmailProvider {
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool, endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string) EmailProvider {
if typ == "Azure ACS" {
return NewAzureACSEmailProvider(clientSecret, host)
} else if typ == "Custom HTTP Email" {
return NewHttpEmailProvider(endpoint, method)
return NewHttpEmailProvider(endpoint, method, httpHeaders, bodyMapping, contentType)
} else if typ == "SendGrid" {
return NewSendgridEmailProvider(clientSecret, host, endpoint)
} else {

81
faceId/aliyun.go Normal file
View File

@ -0,0 +1,81 @@
// 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.
package faceId
import (
"strings"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
facebody20191230 "github.com/alibabacloud-go/facebody-20191230/v5/client"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
)
type AliyunFaceIdProvider struct {
AccessKey string
AccessSecret string
Endpoint string
QualityScoreThreshold float32
}
func NewAliyunFaceIdProvider(accessKey string, accessSecret string, endPoint string) *AliyunFaceIdProvider {
return &AliyunFaceIdProvider{
AccessKey: accessKey,
AccessSecret: accessSecret,
Endpoint: endPoint,
QualityScoreThreshold: 0.65,
}
}
func (provider *AliyunFaceIdProvider) Check(base64ImageA string, base64ImageB string) (bool, error) {
config := openapi.Config{
AccessKeyId: tea.String(provider.AccessKey),
AccessKeySecret: tea.String(provider.AccessSecret),
}
config.Endpoint = tea.String(provider.Endpoint)
client, err := facebody20191230.NewClient(&config)
if err != nil {
return false, err
}
compareFaceRequest := &facebody20191230.CompareFaceRequest{
QualityScoreThreshold: tea.Float32(provider.QualityScoreThreshold),
ImageDataA: tea.String(strings.Replace(base64ImageA, "data:image/png;base64,", "", -1)),
ImageDataB: tea.String(strings.Replace(base64ImageB, "data:image/png;base64,", "", -1)),
}
runtime := &util.RuntimeOptions{}
defer func() {
if r := tea.Recover(recover()); r != nil {
err = r
}
}()
result, err := client.CompareFaceWithOptions(compareFaceRequest, runtime)
if err != nil {
return false, err
}
if result == nil {
return false, nil
}
if *result.Body.Data.Thresholds[0] < *result.Body.Data.Confidence {
return true, nil
}
return false, nil
}

23
faceId/provider.go Normal file
View File

@ -0,0 +1,23 @@
// 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.
package faceId
type FaceIdProvider interface {
Check(base64ImageA string, base64ImageB string) (bool, error)
}
func GetFaceIdProvider(typ string, clientId string, clientSecret string, endPoint string) FaceIdProvider {
return NewAliyunFaceIdProvider(clientId, clientSecret, endPoint)
}

View File

@ -34,6 +34,7 @@ type AuthForm struct {
Phone string `json:"phone"`
Affiliation string `json:"affiliation"`
IdCard string `json:"idCard"`
Language string `json:"language"`
Region string `json:"region"`
InvitationCode string `json:"invitationCode"`
@ -67,7 +68,8 @@ type AuthForm struct {
Plan string `json:"plan"`
Pricing string `json:"pricing"`
FaceId []float64 `json:"faceId"`
FaceId []float64 `json:"faceId"`
FaceIdImage []string `json:"faceIdImage"`
}
func GetAuthFormFieldValue(form *AuthForm, fieldName string) (bool, string) {

194
go.mod
View File

@ -1,10 +1,14 @@
module github.com/casdoor/casdoor
go 1.16
go 1.21
require (
github.com/Masterminds/squirrel v1.5.3
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.4
github.com/alibabacloud-go/facebody-20191230/v5 v5.1.2
github.com/alibabacloud-go/tea v1.3.2
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
github.com/aws/aws-sdk-go v1.45.5
github.com/beego/beego v1.12.12
github.com/beevik/etree v1.1.0
@ -18,7 +22,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
@ -28,8 +31,8 @@ require (
github.com/go-pay/gopay v1.5.72
github.com/go-sql-driver/mysql v1.6.0
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/go-webauthn/webauthn v0.6.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/go-webauthn/webauthn v0.10.2
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0
github.com/json-iterator/go v1.1.12
github.com/lestrrat-go/jwx v1.2.29
@ -46,7 +49,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 +56,194 @@ 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/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/darabonba-number v1.0.4 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 // indirect
github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect
github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect
github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect
github.com/alibabacloud-go/tea-utils v1.3.6 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // 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/aliyun/credentials-go v1.3.10 // 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/clbanning/mxj/v2 v2.7.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.6.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/x v0.1.9 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // 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.9.0 // 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/tjfoc/gmsm v1.4.1 // 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
)

1780
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

@ -136,12 +136,12 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
dtUserInfo := &DingTalkUserResponse{}
accessToken := token.AccessToken
reqest, err := http.NewRequest("GET", idp.Config.Endpoint.AuthURL, nil)
request, err := http.NewRequest("GET", idp.Config.Endpoint.AuthURL, nil)
if err != nil {
return nil, err
}
reqest.Header.Add("x-acs-dingtalk-access-token", accessToken)
resp, err := idp.Client.Do(reqest)
request.Header.Add("x-acs-dingtalk-access-token", accessToken)
resp, err := idp.Client.Do(request)
if err != nil {
return nil, err
}

View File

@ -299,12 +299,12 @@ func GetWechatOfficialAccountQRCode(clientId string, clientSecret string, provid
params := fmt.Sprintf(`{"expire_seconds": 3600, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": "%s"}}}`, providerId)
bodyData := bytes.NewReader([]byte(params))
requeset, err := http.NewRequest("POST", qrCodeUrl, bodyData)
request, err := http.NewRequest("POST", qrCodeUrl, bodyData)
if err != nil {
return "", "", err
}
resp, err := client.Do(requeset)
resp, err := client.Do(request)
if err != nil {
return "", "", err
}

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

@ -191,12 +191,7 @@ func (adapter *Adapter) InitAdapter() error {
}
}
var tableName string
if driverName == "mssql" {
tableName = fmt.Sprintf("[%s]", adapter.Table)
} else {
tableName = adapter.Table
}
tableName := adapter.Table
adapter.Adapter, err = xormadapter.NewAdapterByEngineWithTableName(engine, tableName, "")
if err != nil {

View File

@ -71,6 +71,7 @@ type Application struct {
Description string `xorm:"varchar(100)" json:"description"`
Organization string `xorm:"varchar(100)" json:"organization"`
Cert string `xorm:"varchar(100)" json:"cert"`
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
HeaderHtml string `xorm:"mediumtext" json:"headerHtml"`
EnablePassword bool `json:"enablePassword"`
EnableSignUp bool `json:"enableSignUp"`
@ -97,29 +98,31 @@ type Application struct {
IsShared bool `json:"isShared"`
IpRestriction string `json:"ipRestriction"`
ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
TokenSigningMethod string `xorm:"varchar(100)" json:"tokenSigningMethod"`
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
ExpireInHours int `json:"expireInHours"`
RefreshExpireInHours int `json:"refreshExpireInHours"`
SignupUrl string `xorm:"varchar(200)" json:"signupUrl"`
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
ForgetUrl string `xorm:"varchar(200)" json:"forgetUrl"`
AffiliationUrl string `xorm:"varchar(100)" json:"affiliationUrl"`
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"`
SignupHtml string `xorm:"mediumtext" json:"signupHtml"`
SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
ThemeData *ThemeData `xorm:"json" json:"themeData"`
FooterHtml string `xorm:"mediumtext" json:"footerHtml"`
FormCss string `xorm:"text" json:"formCss"`
FormCssMobile string `xorm:"text" json:"formCssMobile"`
FormOffset int `json:"formOffset"`
FormSideHtml string `xorm:"mediumtext" json:"formSideHtml"`
FormBackgroundUrl string `xorm:"varchar(200)" json:"formBackgroundUrl"`
ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
ForcedRedirectOrigin string `xorm:"varchar(100)" json:"forcedRedirectOrigin"`
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
TokenSigningMethod string `xorm:"varchar(100)" json:"tokenSigningMethod"`
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
ExpireInHours int `json:"expireInHours"`
RefreshExpireInHours int `json:"refreshExpireInHours"`
SignupUrl string `xorm:"varchar(200)" json:"signupUrl"`
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
ForgetUrl string `xorm:"varchar(200)" json:"forgetUrl"`
AffiliationUrl string `xorm:"varchar(100)" json:"affiliationUrl"`
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"`
SignupHtml string `xorm:"mediumtext" json:"signupHtml"`
SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
ThemeData *ThemeData `xorm:"json" json:"themeData"`
FooterHtml string `xorm:"mediumtext" json:"footerHtml"`
FormCss string `xorm:"text" json:"formCss"`
FormCssMobile string `xorm:"text" json:"formCssMobile"`
FormOffset int `json:"formOffset"`
FormSideHtml string `xorm:"mediumtext" json:"formSideHtml"`
FormBackgroundUrl string `xorm:"varchar(200)" json:"formBackgroundUrl"`
FormBackgroundUrlMobile string `xorm:"varchar(200)" json:"formBackgroundUrlMobile"`
FailedSigninLimit int `json:"failedSigninLimit"`
FailedSigninFrozenTime int `json:"failedSigninFrozenTime"`
@ -539,7 +542,7 @@ func GetMaskedApplication(application *Application, userId string) *Application
providerItems := []*ProviderItem{}
for _, providerItem := range application.Providers {
if providerItem.Provider != nil && (providerItem.Provider.Category == "OAuth" || providerItem.Provider.Category == "Web3" || providerItem.Provider.Category == "Captcha" || providerItem.Provider.Category == "SAML") {
if providerItem.Provider != nil && (providerItem.Provider.Category == "OAuth" || providerItem.Provider.Category == "Web3" || providerItem.Provider.Category == "Captcha" || providerItem.Provider.Category == "SAML" || providerItem.Provider.Category == "Face ID") {
providerItems = append(providerItems, providerItem)
}
}

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

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

@ -31,7 +31,7 @@ func TestSmtpServer(provider *Provider) error {
}
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl, provider.Endpoint, provider.Method)
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl, provider.Endpoint, provider.Method, provider.HttpHeaders, provider.UserMapping, provider.IssuerUrl)
fromAddress := provider.ClientId2
if fromAddress == "" {

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

@ -70,12 +70,12 @@ func InitFromFile() {
for _, provider := range initData.Providers {
initDefinedProvider(provider)
}
for _, user := range initData.Users {
initDefinedUser(user)
}
for _, application := range initData.Applications {
initDefinedApplication(application)
}
for _, user := range initData.Users {
initDefinedUser(user)
}
for _, cert := range initData.Certs {
initDefinedCert(cert)
}

View File

@ -23,17 +23,18 @@ type Ldap struct {
Owner string `xorm:"varchar(100)" json:"owner"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
ServerName string `xorm:"varchar(100)" json:"serverName"`
Host string `xorm:"varchar(100)" json:"host"`
Port int `xorm:"int" json:"port"`
EnableSsl bool `xorm:"bool" json:"enableSsl"`
Username string `xorm:"varchar(100)" json:"username"`
Password string `xorm:"varchar(100)" json:"password"`
BaseDn string `xorm:"varchar(100)" json:"baseDn"`
Filter string `xorm:"varchar(200)" json:"filter"`
FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
ServerName string `xorm:"varchar(100)" json:"serverName"`
Host string `xorm:"varchar(100)" json:"host"`
Port int `xorm:"int" json:"port"`
EnableSsl bool `xorm:"bool" json:"enableSsl"`
AllowSelfSignedCert bool `xorm:"bool" json:"allowSelfSignedCert"`
Username string `xorm:"varchar(100)" json:"username"`
Password string `xorm:"varchar(100)" json:"password"`
BaseDn string `xorm:"varchar(100)" json:"baseDn"`
Filter string `xorm:"varchar(200)" json:"filter"`
FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
AutoSync int `json:"autoSync"`
LastSync string `xorm:"varchar(100)" json:"lastSync"`
@ -150,7 +151,7 @@ func UpdateLdap(ldap *Ldap) (bool, error) {
}
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type").Update(ldap)
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type", "allow_self_signed_cert").Update(ldap)
if err != nil {
return false, nil
}

View File

@ -106,6 +106,12 @@ func (l *LdapAutoSynchronizer) syncRoutine(ldap *Ldap, stopChan chan struct{}) e
}
existed, failed, err := SyncLdapUsers(ldap.Owner, AutoAdjustLdapUser(users), ldap.Id)
if err != nil {
conn.Close()
logs.Warning(fmt.Sprintf("autoSync failed for %s, error %s", ldap.Id, err))
continue
}
if len(failed) != 0 {
logs.Warning(fmt.Sprintf("ldap autosync,%d new users,but %d user failed during :", len(users)-len(existed)-len(failed), len(failed)), failed)
logs.Warning(err.Error())

View File

@ -16,6 +16,7 @@ package object
import (
"crypto/md5"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
@ -64,8 +65,11 @@ type LdapUser struct {
func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) {
var conn *goldap.Conn
tlsConfig := tls.Config{
InsecureSkipVerify: ldap.AllowSelfSignedCert,
}
if ldap.EnableSsl {
conn, err = goldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port), nil)
conn, err = goldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port), &tlsConfig)
} else {
conn, err = goldap.Dial("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port))
}

View File

@ -60,7 +60,8 @@ func (mfa *SmsMfa) Enable(user *User) error {
columns = append(columns, "mfa_phone_enabled", "phone", "country_code")
} else if mfa.MfaType == EmailType {
user.MfaEmailEnabled = true
columns = append(columns, "mfa_email_enabled", "email")
user.EmailVerified = true
columns = append(columns, "mfa_email_enabled", "email", "email_verified")
}
_, err := UpdateUser(user.GetId(), user, columns, false)

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

@ -63,7 +63,7 @@ type Organization struct {
PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"`
PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"`
PasswordExpireDays int `json:"passwordExpireDays"`
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
CountryCodes []string `xorm:"mediumtext" json:"countryCodes"`
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
UserTypes []string `xorm:"mediumtext" json:"userTypes"`
@ -80,7 +80,8 @@ type Organization struct {
UseEmailAsUsername bool `json:"useEmailAsUsername"`
EnableTour bool `json:"enableTour"`
IpRestriction string `json:"ipRestriction"`
NavItems []string `xorm:"varchar(500)" json:"navItems"`
NavItems []string `xorm:"varchar(1000)" json:"navItems"`
WidgetItems []string `xorm:"varchar(1000)" json:"widgetItems"`
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
@ -227,6 +228,7 @@ func UpdateOrganization(id string, organization *Organization, isGlobalAdmin boo
if !isGlobalAdmin {
organization.NavItems = org.NavItems
organization.WidgetItems = org.WidgetItems
}
session := ormer.Engine.ID(core.PK{owner, name}).AllCols()

View File

@ -157,7 +157,7 @@ func NewAdapter(driverName string, dataSourceName string, dbName string) (*Ormer
return a, nil
}
// NewAdapterFromdb is the constructor for Ormer.
// NewAdapterFromDb is the constructor for Ormer.
func NewAdapterFromDb(driverName string, dataSourceName string, dbName string, db *sql.DB) (*Ormer, error) {
a := &Ormer{}
a.driverName = driverName

View File

@ -148,7 +148,7 @@ func UpdatePermission(id string, permission *Permission) (bool, error) {
}
if permission.ResourceType == "Application" && permission.Model != "" {
model, err := GetModelEx(util.GetId(owner, permission.Model))
model, err := GetModelEx(util.GetId(permission.Owner, permission.Model))
if err != nil {
return false, err
} else if model == nil {

View File

@ -48,6 +48,7 @@ type Provider struct {
CustomLogo string `xorm:"varchar(200)" json:"customLogo"`
Scopes string `xorm:"varchar(100)" json:"scopes"`
UserMapping map[string]string `xorm:"varchar(500)" json:"userMapping"`
HttpHeaders map[string]string `xorm:"varchar(500)" json:"httpHeaders"`
Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"`
@ -384,6 +385,44 @@ func GetCaptchaProviderByApplication(applicationId, isCurrentProvider, lang stri
return nil, nil
}
func GetFaceIdProviderByOwnerName(applicationId, lang string) (*Provider, error) {
owner, name := util.GetOwnerAndNameFromId(applicationId)
provider := Provider{Owner: owner, Name: name, Category: "Face ID"}
existed, err := ormer.Engine.Get(&provider)
if err != nil {
return nil, err
}
if !existed {
return nil, fmt.Errorf(i18n.Translate(lang, "provider:the provider: %s does not exist"), applicationId)
}
return &provider, nil
}
func GetFaceIdProviderByApplication(applicationId, isCurrentProvider, lang string) (*Provider, error) {
if isCurrentProvider == "true" {
return GetFaceIdProviderByOwnerName(applicationId, lang)
}
application, err := GetApplication(applicationId)
if err != nil {
return nil, err
}
if application == nil || len(application.Providers) == 0 {
return nil, fmt.Errorf(i18n.Translate(lang, "provider:Invalid application id"))
}
for _, provider := range application.Providers {
if provider.Provider == nil {
continue
}
if provider.Provider.Category == "Face ID" {
return GetFaceIdProviderByOwnerName(util.GetId(provider.Provider.Owner, provider.Provider.Name), lang)
}
}
return nil, nil
}
func providerChangeTrigger(oldName string, newName string) error {
session := ormer.Engine.NewSession()
defer session.Close()

View File

@ -30,7 +30,7 @@ import (
"time"
"github.com/beevik/etree"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
saml "github.com/russellhaering/gosaml2"
dsig "github.com/russellhaering/goxmldsig"

View File

@ -21,7 +21,7 @@ import (
"time"
"github.com/casdoor/casdoor/util"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {

View File

@ -19,7 +19,7 @@ import (
"strings"
"github.com/casdoor/casdoor/util"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
)
type ClaimsStandard struct {

View File

@ -15,13 +15,17 @@
package object
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"reflect"
"strconv"
"strings"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/faceId"
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/util"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/xorm-io/builder"
@ -48,7 +52,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"`
@ -244,6 +248,7 @@ type MfaAccount struct {
type FaceId struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"`
FaceIdData []float64 `json:"faceIdData"`
ImageUrl string `json:"ImageUrl"`
}
func GetUserFieldStringValue(user *User, fieldName string) (bool, string, error) {
@ -454,6 +459,31 @@ func GetUserByEmail(owner string, email string) (*User, error) {
}
}
func GetUserByWebauthID(webauthId string) (*User, error) {
user := User{}
existed := false
var err error
if ormer.driverName == "postgres" {
existed, err = ormer.Engine.Where(builder.Like{"\"webauthnCredentials\"", webauthId}).Get(&user)
} else if ormer.driverName == "mssql" {
existed, err = ormer.Engine.Where("CAST(webauthnCredentials AS VARCHAR(MAX)) like ?", "%"+webauthId+"%").Get(&user)
} else if ormer.driverName == "sqlite" {
existed, err = ormer.Engine.Where("CAST(webauthnCredentials AS text) like ?", "%"+webauthId+"%").Get(&user)
} else {
existed, err = ormer.Engine.Where("webauthnCredentials like ?", "%"+webauthId+"%").Get(&user)
}
if err != nil {
return nil, err
}
if !existed {
return nil, fmt.Errorf("user not exist")
}
return &user, err
}
func GetUserByEmailOnly(email string) (*User, error) {
if email == "" {
return nil, nil
@ -807,6 +837,10 @@ func AddUser(user *User) (bool, error) {
return false, fmt.Errorf("the user's owner and name should not be empty")
}
if CheckUsername(user.Name, "en") != "" {
user.Name = util.GetRandomName()
}
organization, err := GetOrganizationByUser(user)
if err != nil {
return false, err
@ -815,6 +849,16 @@ func AddUser(user *User) (bool, error) {
return false, fmt.Errorf("the organization: %s is not found", user.Owner)
}
if user.Owner != "built-in" {
applicationCount, err := GetOrganizationApplicationCount(organization.Owner, organization.Name, "", "")
if err != nil {
return false, err
}
if applicationCount == 0 {
return false, fmt.Errorf("The organization: %s should have one application at least", organization.Owner)
}
}
if organization.DefaultPassword != "" && user.Password == "123" {
user.Password = organization.DefaultPassword
}
@ -1179,6 +1223,40 @@ func (user *User) IsGlobalAdmin() bool {
return user.Owner == "built-in"
}
func (user *User) CheckUserFace(faceIdImage []string, provider *Provider) (bool, error) {
faceIdChecker := faceId.GetFaceIdProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Endpoint)
httpClient := proxy.DefaultHttpClient
errList := []error{}
for _, userFaceId := range user.FaceIds {
if userFaceId.ImageUrl != "" {
imgResp, err := httpClient.Get(userFaceId.ImageUrl)
if err != nil {
continue
}
imgByte, err := io.ReadAll(imgResp.Body)
if err != nil {
continue
}
base64Img := base64.StdEncoding.EncodeToString(imgByte)
for _, imgBase64 := range faceIdImage {
isSuccess, err := faceIdChecker.Check(imgBase64, base64Img)
if err != nil {
errList = append(errList, err)
continue
}
if isSuccess {
return true, nil
}
}
}
}
if len(errList) > 0 {
return false, errList[0]
}
return false, nil
}
func GenerateIdForNewUser(application *Application) (string, error) {
if application == nil || application.GetSignupItemRule("ID") != "Incremental" {
return util.GenerateId(), nil

View File

@ -38,6 +38,7 @@ type Webhook struct {
ContentType string `xorm:"varchar(100)" json:"contentType"`
Headers []*Header `xorm:"mediumtext" json:"headers"`
Events []string `xorm:"varchar(1000)" json:"events"`
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
IsUserExtended bool `json:"isUserExtended"`
SingleOrgOnly bool `json:"singleOrgOnly"`
IsEnabled bool `json:"isEnabled"`

View File

@ -17,6 +17,7 @@ package object
import (
"io"
"net/http"
"reflect"
"strings"
"github.com/casdoor/casdoor/util"
@ -25,17 +26,43 @@ import (
func sendWebhook(webhook *Webhook, record *casvisorsdk.Record, extendedUser *User) (int, string, error) {
client := &http.Client{}
userMap := make(map[string]interface{})
var body io.Reader
type RecordEx struct {
casvisorsdk.Record
ExtendedUser *User `xorm:"-" json:"extendedUser"`
}
recordEx := &RecordEx{
Record: *record,
ExtendedUser: extendedUser,
}
if webhook.TokenFields != nil && len(webhook.TokenFields) > 0 && extendedUser != nil {
userValue := reflect.ValueOf(extendedUser).Elem()
body := strings.NewReader(util.StructToJson(recordEx))
for _, field := range webhook.TokenFields {
userField := userValue.FieldByName(field)
if userField.IsValid() {
newfield := util.SnakeToCamel(util.CamelToSnakeCase(field))
userMap[newfield] = userField.Interface()
}
}
type RecordEx struct {
casvisorsdk.Record
ExtendedUser map[string]interface{} `json:"extendedUser"`
}
recordEx := &RecordEx{
Record: *record,
ExtendedUser: userMap,
}
body = strings.NewReader(util.StructToJson(recordEx))
} else {
type RecordEx struct {
casvisorsdk.Record
ExtendedUser *User `xorm:"-" json:"extendedUser"`
}
recordEx := &RecordEx{
Record: *record,
ExtendedUser: extendedUser,
}
body = strings.NewReader(util.StructToJson(recordEx))
}
req, err := http.NewRequest(webhook.Method, webhook.Url, body)
if err != nil {

View File

@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
@ -180,7 +181,11 @@ func (c *AirwallexClient) authRequest(method, url string, body interface{}) (map
return nil, err
}
b, _ := json.Marshal(body)
req, _ := http.NewRequest(method, url, bytes.NewBuffer(b))
var reqBody io.Reader
if method != "GET" {
reqBody = bytes.NewBuffer(b)
}
req, _ := http.NewRequest(method, url, reqBody)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)

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,8 +3,8 @@
"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",
@ -23,8 +23,8 @@
"@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": "^6.0.1",
"copy-to-clipboard": "^3.3.1",

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

@ -13,8 +13,8 @@
// limitations under the License.
import React from "react";
import {Button, Card, Col, ConfigProvider, Input, InputNumber, Popover, Radio, Result, Row, Select, Switch, Upload} from "antd";
import {CopyOutlined, LinkOutlined, UploadOutlined} from "@ant-design/icons";
import {Button, Card, Col, ConfigProvider, Input, InputNumber, Popover, Radio, Result, Row, Select, Space, Switch, Upload} from "antd";
import {CopyOutlined, HolderOutlined, LinkOutlined, UploadOutlined, UsergroupAddOutlined} from "@ant-design/icons";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as CertBackend from "./backend/CertBackend";
import * as Setting from "./Setting";
@ -36,6 +36,7 @@ import ThemeEditor from "./common/theme/ThemeEditor";
import SigninTable from "./table/SigninTable";
import Editor from "./common/Editor";
import * as GroupBackend from "./backend/GroupBackend";
const {Option} = Select;
@ -52,6 +53,14 @@ const template = `<style>
background-color: #333333;
box-shadow: 0 0 30px 20px rgba(255, 255, 255, 0.20);
}
.forget-content {
padding: 10px 100px 20px;
margin: 30px auto;
border: 2px solid #fff;
border-radius: 7px;
background-color: rgb(255 255 255);
box-shadow: 0 0 20px rgb(0 0 0 / 20%);
}
</style>`;
const previewGrid = Setting.isMobile() ? 22 : 11;
@ -85,11 +94,11 @@ const sideTemplate = `<style>
}
</style>
<div class="left-model">
<span class="side-logo"> <img src="https://cdn.casbin.org/img/casdoor-logo_1185x256.png" alt="Casdoor" style="width: 120px">
<span class="side-logo"> <img src="${Setting.StaticBaseUrl}/img/casdoor-logo_1185x256.png" alt="Casdoor" style="width: 120px">
<span>SSO</span>
</span>
<div class="img">
<img src="https://cdn.casbin.org/img/casbin.svg" alt="Casdoor"/>
<img src="${Setting.StaticBaseUrl}/img/casbin.svg" alt="Casdoor"/>
</div>
</div>
`;
@ -116,6 +125,7 @@ class ApplicationEditPage extends React.Component {
UNSAFE_componentWillMount() {
this.getApplication();
this.getOrganizations();
this.getGroups();
}
getApplication() {
@ -167,6 +177,17 @@ class ApplicationEditPage extends React.Component {
});
}
getGroups() {
GroupBackend.getGroups(this.state.organizationName)
.then((res) => {
if (res.status === "ok") {
this.setState({
groups: res.data,
});
}
});
}
getCerts(application) {
let owner = application.organization;
if (application.isShared) {
@ -397,6 +418,16 @@ class ApplicationEditPage extends React.Component {
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Forced redirect origin"), i18next.t("general:Forced redirect origin - Tooltip"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.application.forcedRedirectOrigin} onChange={e => {
this.updateApplicationField("forcedRedirectOrigin", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Token format"), i18next.t("application:Token format - Tooltip"))} :
@ -469,6 +500,31 @@ class ApplicationEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} :
</Col>
<Col span={22}>
<Select virtual={false} style={{width: "100%"}} value={this.state.application.defaultGroup ?? []} onChange={(value => {
this.updateApplicationField("defaultGroup", value);
})}
>
<Option key={""} value={""}>
<Space>
{i18next.t("general:Default")}
</Space>
</Option>
{
this.state.groups?.map((group) => <Option key={group.name} value={`${group.owner}/${group.name}`}>
<Space>
{group.type === "Physical" ? <UsergroupAddOutlined /> : <HolderOutlined />}
{group.displayName}
</Space>
</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Enable signup"), i18next.t("application:Enable signup - Tooltip"))} :
@ -804,6 +860,33 @@ class ApplicationEditPage extends React.Component {
</Row>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Background URL Mobile"), i18next.t("application:Background URL Mobile - Tooltip"))} :
</Col>
<Col span={22} style={(Setting.isMobile()) ? {maxWidth: "100%"} : {}}>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.application.formBackgroundUrlMobile} onChange={e => {
this.updateApplicationField("formBackgroundUrlMobile", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("general:Preview")}:
</Col>
<Col span={22} >
<a target="_blank" rel="noreferrer" href={this.state.application.formBackgroundUrlMobile}>
<img src={this.state.application.formBackgroundUrlMobile} alt={this.state.application.formBackgroundUrlMobile} height={90} style={{marginBottom: "20px"}} />
</a>
</Col>
</Row>
</Col>
</Row>
<Row>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Custom CSS"), i18next.t("application:Custom CSS - Tooltip"))} :

View File

@ -121,10 +121,12 @@ 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, record, index) => {
const highlightContent = this.state.searchedColumn === dataIndex ? (
@ -173,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

@ -19,8 +19,6 @@ import {Tabs} from "antd";
import i18next from "i18next";
import Editor from "./common/Editor";
const {TabPane} = Tabs;
const CasbinEditor = ({model, onModelTextChange}) => {
const [activeKey, setActiveKey] = useState("advanced");
const iframeRef = useRef(null);
@ -66,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

View File

@ -109,7 +109,7 @@ class EntryPage extends React.Component {
<React.Fragment>
<CustomHead headerHtml={this.state.application?.headerHtml} />
<div className={`${isDarkMode ? "loginBackgroundDark" : "loginBackground"}`}
style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}>
style={{backgroundImage: Setting.inIframe() ? null : (Setting.isMobile() ? `url(${this.state.application?.formBackgroundUrlMobile})` : `url(${this.state.application?.formBackgroundUrl})`)}}>
<Spin size="large" spinning={this.state.application === undefined && this.state.pricing === undefined} tip={i18next.t("login:Loading")}
style={{width: "100%", margin: "0 auto", position: "absolute"}} />
<Switch>

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

@ -114,7 +114,7 @@ class InvitationEditPage extends React.Component {
const selectedOrganization = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner);
defaultApplication = selectedOrganization.defaultApplication;
if (!defaultApplication) {
Setting.showMessage("error", i18next.t("invitation:You need to specify a default application for ") + selectedOrganization.name);
Setting.showMessage("error", i18next.t("invitation:You need to first specify a default application for organization: ") + selectedOrganization.name);
return;
}
}

View File

@ -170,6 +170,16 @@ class LdapEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Allow self-signed certificate"), i18next.t("ldap:Allow self-signed certificate - Tooltip"))} :
</Col>
<Col span={21} >
<Switch checked={this.state.ldap.allowSelfSignedCert} onChange={checked => {
this.updateLdapField("allowSelfSignedCert", checked);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Base DN"), i18next.t("ldap:Base DN - Tooltip"))} :

View File

@ -95,8 +95,9 @@ import TransactionEditPage from "./TransactionEditPage";
import VerificationListPage from "./VerificationListPage";
function ManagementPage(props) {
const [menuVisible, setMenuVisible] = useState(false);
const navItems = props.account?.organization?.navItems;
const widgetItems = props.account?.organization?.widgetItems;
function logout() {
AuthBackend.logout()
@ -175,6 +176,35 @@ function ManagementPage(props) {
);
}
function navItemsIsAll() {
return !Array.isArray(navItems) || !!navItems?.includes("all");
}
function widgetItemsIsAll() {
return !Array.isArray(widgetItems) || !!widgetItems?.includes("all");
}
function renderWidgets() {
const widgets = [
Setting.getItem(<ThemeSelect themeAlgorithm={props.themeAlgorithm} onChange={props.setLogoAndThemeAlgorithm} />, "theme"),
Setting.getItem(<LanguageSelect languages={props.account.organization.languages} />, "language"),
Setting.getItem(Conf.AiAssistantUrl?.trim() && (
<Tooltip title="Click to open AI assistant">
<div className="select-box" onClick={props.openAiAssistant}>
<DeploymentUnitOutlined style={{fontSize: "24px"}} />
</div>
</Tooltip>
), "ai-assistant"),
Setting.getItem(<OpenTour />, "tour"),
];
if (widgetItemsIsAll()) {
return widgets.map(item => item.label);
}
return widgets.filter(item => widgetItems.includes(item.key)).map(item => item.label);
}
function renderAccountMenu() {
if (props.account === undefined) {
return null;
@ -188,29 +218,16 @@ function ManagementPage(props) {
return (
<React.Fragment>
{renderRightDropdown()}
<ThemeSelect
themeAlgorithm={props.themeAlgorithm}
onChange={props.setLogoAndThemeAlgorithm} />
<LanguageSelect languages={props.account.organization.languages} />
{
Conf.AiAssistantUrl?.trim() && (
<Tooltip title="Click to open AI assistant">
<div className="select-box" onClick={props.openAiAssistant}>
<DeploymentUnitOutlined style={{fontSize: "24px"}} />
</div>
</Tooltip>
)
}
<OpenTour />
{renderWidgets()}
{Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) &&
<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>
@ -323,13 +340,7 @@ function ManagementPage(props) {
}
}
const navItems = props.account.organization.navItems;
if (!Array.isArray(navItems)) {
return res;
}
if (navItems.includes("all")) {
if (navItemsIsAll()) {
return res;
}
@ -443,8 +454,6 @@ function ManagementPage(props) {
return Setting.isMobile() || window.location.pathname.startsWith("/trees");
}
const menuStyleRight = Setting.isAdminUser(props.account) && !Setting.isMobile() ? "calc(180px + 280px)" : "320px";
const onClose = () => {
setMenuVisible(false);
};
@ -456,34 +465,40 @@ function ManagementPage(props) {
return (
<React.Fragment>
<EnableMfaNotification account={props.account} />
<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}>
<Menu
items={getMenuItems()}
mode={"inline"}
selectedKeys={[props.selectedMenuKey]}
style={{lineHeight: "64px"}}
onClick={onClose}
>
</Menu>
</Drawer>
<Button icon={<BarsOutlined />} onClick={showMenu} type="text">
{i18next.t("general:Menu")}
</Button>
</React.Fragment> :
<Menu
onClick={onClose}
items={getMenuItems()}
mode={"horizontal"}
selectedKeys={[props.selectedMenuKey]}
style={{position: "absolute", left: 0, right: menuStyleRight, backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}}
/>
)}
<Header style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: "0", marginBottom: "4px", backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}} >
{
renderAccountMenu()
props.requiredEnableMfa || (Setting.isMobile() ? (
<React.Fragment>
<Drawer title={i18next.t("general:Close")} placement="left" open={menuVisible} onClose={onClose}>
<Menu
items={getMenuItems()}
mode={"inline"}
selectedKeys={[props.selectedMenuKey]}
style={{lineHeight: "64px"}}
onClick={onClose}
>
</Menu>
</Drawer>
<Button icon={<BarsOutlined />} onClick={showMenu} type="text">
{i18next.t("general:Menu")}
</Button>
</React.Fragment>
) : (
// Padding 1px for Menu Item Highlight border
<div style={{flex: 1, overflow: "hidden", paddingBottom: "1px"}}>
<Menu
onClick={onClose}
items={getMenuItems()}
mode={"horizontal"}
selectedKeys={[props.selectedMenuKey]}
style={{backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}}
/>
</div>
))
}
<div style={{flexShrink: 0}}>
{renderAccountMenu()}
</div>
</Header>
<Content style={{display: "flex", flexDirection: "column"}} >
{isWithoutCard() ?

View File

@ -27,6 +27,7 @@ import AccountTable from "./table/AccountTable";
import ThemeEditor from "./common/theme/ThemeEditor";
import MfaTable from "./table/MfaTable";
import {NavItemTree} from "./common/NavItemTree";
import {WidgetItemTree} from "./common/WidgetItemTree";
const {Option} = Select;
@ -537,7 +538,7 @@ class OrganizationEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Navbar items"), i18next.t("general:Navbar items - Tooltip"))} :
{Setting.getLabel(i18next.t("organization:Navbar items"), i18next.t("organization:Navbar items - Tooltip"))} :
</Col>
<Col span={22} >
<NavItemTree
@ -550,6 +551,21 @@ class OrganizationEditPage extends React.Component {
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Widget items"), i18next.t("organization:Widget items - Tooltip"))} :
</Col>
<Col span={22} >
<WidgetItemTree
disabled={!Setting.isAdminUser(this.props.account)}
checkedKeys={this.state.organization.widgetItems ?? ["all"]}
defaultExpandedKeys={["all"]}
onCheck={(checked, _) => {
this.updateOrganizationField("widgetItems", checked);
}}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :

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

@ -232,6 +232,15 @@ class PlanEditPage extends React.Component {
[
{id: "USD", name: "USD"},
{id: "CNY", name: "CNY"},
{id: "EUR", name: "EUR"},
{id: "JPY", name: "JPY"},
{id: "GBP", name: "GBP"},
{id: "AUD", name: "AUD"},
{id: "CAD", name: "CAD"},
{id: "CHF", name: "CHF"},
{id: "HKD", name: "HKD"},
{id: "SGD", name: "SGD"},
{id: "BRL", name: "BRL"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>

View File

@ -139,6 +139,8 @@ class ProductBuyPage extends React.Component {
return "HK$";
} else if (product?.currency === "SGD") {
return "S$";
} else if (product?.currency === "BRL") {
return "R$";
} else {
return "(Unknown currency)";
}

View File

@ -217,6 +217,7 @@ class ProductEditPage extends React.Component {
{id: "CHF", name: "CHF"},
{id: "HKD", name: "HKD"},
{id: "SGD", name: "SGD"},
{id: "BRL", name: "BRL"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>

View File

@ -29,6 +29,7 @@ import {CaptchaPreview} from "./common/CaptchaPreview";
import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
import * as Web3Auth from "./auth/Web3Auth";
import Editor from "./common/Editor";
import HttpHeaderTable from "./table/HttpHeaderTable";
const {Option} = Select;
const {TextArea} = Input;
@ -41,6 +42,13 @@ const defaultUserMapping = {
avatarUrl: "avatarUrl",
};
const defaultEmailMapping = {
fromName: "fromName",
toAddress: "toAddress",
subject: "subject",
content: "content",
};
class ProviderEditPage extends React.Component {
constructor(props) {
super(props);
@ -71,7 +79,16 @@ class ProviderEditPage extends React.Component {
if (res.status === "ok") {
const provider = res.data;
provider.userMapping = provider.userMapping || defaultUserMapping;
if (provider.type === "Custom HTTP Email") {
if (!provider.userMapping) {
provider.userMapping = provider.userMapping || defaultEmailMapping;
}
if (!provider.userMapping?.fromName) {
provider.userMapping = defaultEmailMapping;
}
} else {
provider.userMapping = provider.userMapping || defaultUserMapping;
}
this.setState({
provider: provider,
});
@ -145,9 +162,16 @@ class ProviderEditPage extends React.Component {
const requiredKeys = ["id", "username", "displayName"];
const provider = this.state.provider;
if (value === "" && requiredKeys.includes(key)) {
Setting.showMessage("error", i18next.t("provider:This field is required"));
return;
if (provider.type === "Custom HTTP Email") {
if (value === "") {
Setting.showMessage("error", i18next.t("provider:This field is required"));
return;
}
} else {
if (value === "" && requiredKeys.includes(key)) {
Setting.showMessage("error", i18next.t("provider:This field is required"));
return;
}
}
provider.userMapping[key] = value;
@ -183,6 +207,30 @@ class ProviderEditPage extends React.Component {
</React.Fragment>
);
}
renderEmailMappingInput() {
return (
<React.Fragment>
{Setting.getLabel(i18next.t("provider:From name"), i18next.t("provider:From name - Tooltip"))} :
<Input value={this.state.provider.userMapping.fromName} onChange={e => {
this.updateUserMappingField("fromName", e.target.value);
}} />
{Setting.getLabel(i18next.t("provider:From address"), i18next.t("provider:From address - Tooltip"))} :
<Input value={this.state.provider.userMapping.toAddress} onChange={e => {
this.updateUserMappingField("toAddress", e.target.value);
}} />
{Setting.getLabel(i18next.t("provider:Subject"), i18next.t("provider:Subject - Tooltip"))} :
<Input value={this.state.provider.userMapping.subject} onChange={e => {
this.updateUserMappingField("subject", e.target.value);
}} />
{Setting.getLabel(i18next.t("provider:Email content"), i18next.t("provider:Email content - Tooltip"))} :
<Input value={this.state.provider.userMapping.content} onChange={e => {
this.updateUserMappingField("content", e.target.value);
}} />
</React.Fragment>
);
}
getClientIdLabel(provider) {
switch (provider.category) {
case "OAuth":
@ -288,10 +336,8 @@ class ProviderEditPage extends React.Component {
default:
if (provider.type === "Aliyun Captcha") {
return Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip"));
} else if (provider.type === "WeChat Pay") {
} else if (provider.type === "WeChat Pay" || provider.type === "CUCloud") {
return Setting.getLabel(i18next.t("provider:App ID"), i18next.t("provider:App ID - Tooltip"));
} else if (provider.type === "CUCloud") {
return Setting.getLabel(i18next.t("provider:Account ID"), i18next.t("provider:Account ID - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"));
}
@ -389,8 +435,8 @@ class ProviderEditPage extends React.Component {
text = i18next.t("provider:App Key");
tooltip = i18next.t("provider:App Key - Tooltip");
} else if (provider.type === "CUCloud") {
text = i18next.t("provider:Topic name");
tooltip = i18next.t("provider:Topic name - Tooltip");
text = "Topic name";
tooltip = "Topic name - Tooltip";
}
}
@ -567,6 +613,8 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("type", "MetaMask");
} else if (value === "Notification") {
this.updateProviderField("type", "Telegram");
} else if (value === "Face ID") {
this.updateProviderField("type", "Alibaba Cloud Facebody");
}
})}>
{
@ -580,6 +628,7 @@ class ProviderEditPage extends React.Component {
{id: "SMS", name: "SMS"},
{id: "Storage", name: "Storage"},
{id: "Web3", name: "Web3"},
{id: "Face ID", name: "Face ID"},
]
.sort((a, b) => a.name.localeCompare(b.name))
.map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
@ -770,6 +819,7 @@ class ProviderEditPage extends React.Component {
(this.state.provider.category === "Web3") ||
(this.state.provider.category === "Storage" && this.state.provider.type === "Local File System") ||
(this.state.provider.category === "SMS" && this.state.provider.type === "Custom HTTP SMS") ||
(this.state.provider.category === "Email" && this.state.provider.type === "Custom HTTP Email") ||
(this.state.provider.category === "Notification" && (this.state.provider.type === "Google Chat" || this.state.provider.type === "Custom HTTP") || this.state.provider.type === "Balance") ? null : (
<React.Fragment>
{
@ -901,7 +951,7 @@ class ProviderEditPage extends React.Component {
</Row>
)
}
{this.state.provider.category === "Storage" || ["Custom HTTP SMS", "Custom HTTP Email", "SendGrid", "CUCloud"].includes(this.state.provider.type) ? (
{["Face ID", "Storage"].includes(this.state.provider.category) || ["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"}} >
@ -915,7 +965,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
)}
{["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 : (
{["Custom HTTP SMS", "Custom HTTP Email", "SendGrid", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor", "CUCloud", "Alibaba Cloud Facebody"].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"))} :
@ -927,7 +977,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
)}
{["Custom HTTP SMS", "SendGrid", "Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
{["Custom HTTP SMS", "Custom HTTP Email", "SendGrid", "Local File System", "CUCloud", "Alibaba Cloud Facebody"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{["Casdoor"].includes(this.state.provider.type) ?
@ -941,7 +991,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
)}
{["Custom HTTP SMS", "SendGrid", "CUCloud"].includes(this.state.provider.type) ? null : (
{["Custom HTTP SMS", "Custom HTTP Email", "SendGrid", "CUCloud", "Alibaba Cloud Facebody"].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"))} :
@ -953,7 +1003,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
)}
{["Custom HTTP SMS", "SendGrid", "Synology", "Casdoor", "CUCloud"].includes(this.state.provider.type) ? null : (
{["Custom HTTP SMS", "Custom HTTP Email", "SendGrid", "Synology", "Casdoor", "CUCloud", "Alibaba Cloud Facebody"].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"))} :
@ -1094,6 +1144,66 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
)}
{
!["Custom HTTP Email"].includes(this.state.provider.type) ? null : (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.method} onChange={value => {
this.updateProviderField("method", value);
}}>
{
[
{id: "GET", name: "GET"},
{id: "POST", name: "POST"},
{id: "PUT", name: "PUT"},
{id: "DELETE", name: "DELETE"},
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
}
</Select>
</Col>
</Row>
{
this.state.provider.method !== "GET" ? (<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("webhook:Content type"), i18next.t("webhook:Content type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.issuerUrl === "" ? "application/x-www-form-urlencoded" : this.state.provider.issuerUrl} onChange={value => {
this.updateProviderField("issuerUrl", value);
}}>
{
[
{id: "application/json", name: "application/json"},
{id: "application/x-www-form-urlencoded", name: "application/x-www-form-urlencoded"},
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
}
</Select>
</Col>
</Row>) : null
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:HTTP header"), i18next.t("provider:HTTP header - Tooltip"))} :
</Col>
<Col span={22} >
<HttpHeaderTable httpHeaders={this.state.provider.httpHeaders} onUpdateTable={(value) => {this.updateProviderField("httpHeaders", value);}} />
</Col>
</Row>
{this.state.provider.method !== "GET" ? <Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:HTTP body mapping"), i18next.t("provider:HTTP body mapping - Tooltip"))} :
</Col>
<Col span={22}>
{this.renderEmailMappingInput()}
</Col>
</Row> : null}
</React.Fragment>
)
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Email title"), i18next.t("provider:Email title - Tooltip"))} :
@ -1162,7 +1272,7 @@ class ProviderEditPage extends React.Component {
</Button>
</Row>
</React.Fragment>
) : this.state.provider.category === "SMS" ? (
) : ["SMS"].includes(this.state.provider.category) ? (
<React.Fragment>
{["Custom HTTP SMS", "Twilio SMS", "Amazon SNS", "Azure ACS", "Msg91 SMS", "Infobip SMS"].includes(this.state.provider.type) ?
null :
@ -1280,7 +1390,7 @@ class ProviderEditPage extends React.Component {
}} />
</Col>
<Col span={16} >
<Button type="primary" loading={this.state.metadataLoading} onClick={() => {this.fetchSamlMetadata();}}>{i18next.t("general:Request")}</Button>
<Button style={{marginLeft: "10px"}} type="primary" loading={this.state.metadataLoading} onClick={() => {this.fetchSamlMetadata();}}>{i18next.t("general:Request")}</Button>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >

View File

@ -272,6 +272,7 @@ class RecordListPage extends BaseListPage {
value={this.getDetailField("response")}
fillHeight
fillWidth
maxWidth={this.getEditorMaxWidth()}
dark
readOnly
/>
@ -282,6 +283,7 @@ class RecordListPage extends BaseListPage {
lang="json"
fillHeight
fillWidth
maxWidth={this.getEditorMaxWidth()}
dark
readOnly
/>
@ -292,6 +294,10 @@ class RecordListPage extends BaseListPage {
);
}
getEditorMaxWidth = () => {
return Setting.isMobile() ? window.innerWidth - 60 : 475;
};
jsonStrFormatter = str => {
try {
return JSON.stringify(JSON.parse(str), null, 2);

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

@ -416,6 +416,12 @@ export const OtherProviderInfo = {
url: "https://www.cucloud.cn/",
},
},
"Face ID": {
"Alibaba Cloud Facebody": {
logo: `${StaticBaseUrl}/img/social_aliyun.png`,
url: "https://vision.aliyun.com/facebody",
},
},
};
export function initCountries() {
@ -1150,6 +1156,10 @@ export function getProviderTypeOptions(category) {
{id: "Viber", name: "Viber"},
{id: "CUCloud", name: "CUCloud"},
]);
} else if (category === "Face ID") {
return ([
{id: "Alibaba Cloud Facebody", name: "Alibaba Cloud Facebody"},
]);
} else {
return [];
}
@ -1522,7 +1532,7 @@ export function getUserCommonFields() {
}
export function getDefaultFooterContent() {
return "Powered by <a target=\"_blank\" href=\"https://casdoor.org\" rel=\"noreferrer\"><img style=\"padding-bottom: 3px\" height=\"20\" alt=\"Casdoor\" src=\"https://cdn.casbin.org/img/casdoor-logo_1185x256.png\"/></a>";
return `Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style="padding-bottom: 3px" height="20" alt="Casdoor" src="${StaticBaseUrl}/img/casdoor-logo_1185x256.png"/></a>`;
}
export function getEmptyFooterContent() {
@ -1554,7 +1564,7 @@ export function getDefaultHtmlEmailContent() {
<div class="email-container">
<div class="header">
<h3>Casbin Organization</h3>
<img src="https://cdn.casbin.org/img/casdoor-logo_1185x256.png" alt="Casdoor Logo" width="300">
<img src="${StaticBaseUrl}/img/casdoor-logo_1185x256.png" alt="Casdoor Logo" width="300">
</div>
<p><strong>%{user.friendlyName}</strong>, here is your verification code</p>
<p>Use this code for your transaction. It's valid for 5 minutes</p>
@ -1593,6 +1603,8 @@ export function getCurrencyText(product) {
return i18next.t("currency:HKD");
} else if (product?.currency === "SGD") {
return i18next.t("currency:SGD");
} else if (product?.currency === "BRL") {
return i18next.t("currency:BRL");
} else {
return "(Unknown currency)";
}

View File

@ -37,17 +37,35 @@ class SystemInfo extends React.Component {
UNSAFE_componentWillMount() {
SystemBackend.getSystemInfo("").then(res => {
this.setState({
systemInfo: res.data,
loading: false,
});
if (res.status === "ok") {
this.setState({
systemInfo: res.data,
});
} else {
Setting.showMessage("error", res.msg);
this.stopTimer();
}
const id = setInterval(() => {
SystemBackend.getSystemInfo("").then(res => {
this.setState({
systemInfo: res.data,
loading: false,
});
if (res.status === "ok") {
this.setState({
systemInfo: res.data,
});
} else {
Setting.showMessage("error", res.msg);
this.stopTimer();
}
}).catch(error => {
Setting.showMessage("error", `System info failed to get: ${error}`);
this.stopTimer();
});
SystemBackend.getPrometheusInfo().then(res => {
this.setState({
@ -55,17 +73,25 @@ class SystemInfo extends React.Component {
});
});
}, 1000 * 2);
this.setState({intervalId: id});
}).catch(error => {
Setting.showMessage("error", `System info failed to get: ${error}`);
this.stopTimer();
});
SystemBackend.getVersionInfo().then(res => {
this.setState({
versionInfo: res.data,
});
if (res.status === "ok") {
this.setState({
versionInfo: res.data,
});
} else {
Setting.showMessage("error", res.msg);
this.stopTimer();
}
}).catch(err => {
Setting.showMessage("error", `Version info failed to get: ${err}`);
this.stopTimer();
});
}
@ -77,10 +103,14 @@ class SystemInfo extends React.Component {
this.setState({isTourVisible: TourConfig.getTourVisible()});
};
componentWillUnmount() {
stopTimer() {
if (this.state.intervalId !== null) {
clearInterval(this.state.intervalId);
}
}
componentWillUnmount() {
this.stopTimer();
window.removeEventListener("storageTourChanged", this.handleTourChange);
}
@ -125,9 +155,9 @@ class SystemInfo extends React.Component {
<br /> <br />
<Progress type="circle" percent={Number((Number(this.state.systemInfo.memoryUsed) / Number(this.state.systemInfo.memoryTotal) * 100).toFixed(2))} />
</div>;
const latencyUi = this.state.prometheusInfo.apiLatency === null || this.state.prometheusInfo.apiLatency?.length <= 0 ? <Spin size="large" /> :
const latencyUi = this.state.prometheusInfo?.apiLatency === null || this.state.prometheusInfo?.apiLatency?.length <= 0 ? <Spin size="large" /> :
<PrometheusInfoTable prometheusInfo={this.state.prometheusInfo} table={"latency"} />;
const throughputUi = this.state.prometheusInfo.apiThroughput === null || this.state.prometheusInfo.apiThroughput?.length <= 0 ? <Spin size="large" /> :
const throughputUi = this.state.prometheusInfo?.apiThroughput === null || this.state.prometheusInfo?.apiThroughput?.length <= 0 ? <Spin size="large" /> :
<PrometheusInfoTable prometheusInfo={this.state.prometheusInfo} table={"throughput"} />;
const link = this.state.versionInfo?.version !== "" ? `https://github.com/casdoor/casdoor/releases/tag/${this.state.versionInfo?.version}` : "";
let versionText = this.state.versionInfo?.version !== "" ? this.state.versionInfo?.version : i18next.t("system:Unknown version");

View File

@ -1,4 +1,5 @@
import React from "react";
import * as Setting from "./Setting";
export const TourObj = {
home: [
@ -8,7 +9,7 @@ export const TourObj = {
cover: (
<img
alt="casdoor.png"
src="https://cdn.casbin.org/img/casdoor-logo_1185x256.png"
src={`${Setting.StaticBaseUrl}/img/casdoor-logo_1185x256.png`}
/>
),
},

View File

@ -1054,6 +1054,7 @@ class UserEditPage extends React.Component {
<FaceIdTable
title={i18next.t("user:Face IDs")}
table={this.state.user.faceIds}
{...this.props}
onUpdateTable={(table) => {this.updateUserField("faceIds", table);}}
/>
</Col>

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

View File

@ -174,7 +174,16 @@ class WebhookEditPage extends React.Component {
renderWebhook() {
const preview = Setting.deepCopy(previewTemplate);
if (this.state.webhook.isUserExtended) {
preview["extendedUser"] = userTemplate;
if (this.state.webhook.tokenFields && this.state.webhook.tokenFields.length !== 0) {
const extendedUser = {};
this.state.webhook.tokenFields.forEach(field => {
const fieldTrans = field.replace(field[0], field[0].toLowerCase());
extendedUser[fieldTrans] = userTemplate[fieldTrans];
});
preview["extendedUser"] = extendedUser;
} else {
preview["extendedUser"] = userTemplate;
}
}
const previewText = JSON.stringify(preview, null, 2);
@ -295,6 +304,18 @@ class WebhookEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("webhook:Extended user fields"), i18next.t("webhook:Extended user fields - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" showSearch style={{width: "100%"}} value={this.state.webhook.tokenFields} onChange={(value => {this.updateWebhookField("tokenFields", value);})}>
{
Setting.getUserCommonFields().map((item, index) => <Option key={index} value={item}>{item}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :

View File

@ -471,9 +471,14 @@ 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>
{Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
{Setting.inIframe() || !Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCssMobile}} />}
<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

@ -37,6 +37,8 @@ import RedirectForm from "../common/RedirectForm";
import {RequiredMfa} from "./mfa/MfaAuthVerifyForm";
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
import * as ProviderButton from "./ProviderButton";
import {goToLink} from "../Setting";
const FaceRecognitionCommonModal = lazy(() => import("../common/modal/FaceRecognitionCommonModal"));
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
class LoginPage extends React.Component {
@ -61,6 +63,8 @@ class LoginPage extends React.Component {
isTermsOfUseVisible: false,
termsOfUseContent: "",
orgChoiceMode: new URLSearchParams(props.location?.search).get("orgChoiceMode") ?? null,
userLang: null,
loginLoading: false,
};
if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) {
@ -262,6 +266,13 @@ class LoginPage extends React.Component {
onUpdateApplication(application) {
this.props.onUpdateApplication(application);
for (const idx in application.providers) {
const provider = application.providers[idx];
if (provider.provider?.category === "Face ID") {
this.setState({haveFaceIdProvider: true});
break;
}
}
}
parseOffset(offset) {
@ -355,6 +366,7 @@ class LoginPage extends React.Component {
}
onFinish(values) {
this.setState({loginLoading: true});
if (this.state.loginMethod === "webAuthn") {
let username = this.state.username;
if (username === null || username === "") {
@ -379,6 +391,9 @@ class LoginPage extends React.Component {
}).then(res => res.json())
.then((res) => {
if (res.status === "error") {
this.setState({
loginLoading: false,
});
Setting.showMessage("error", res.msg);
return;
}
@ -415,6 +430,7 @@ class LoginPage extends React.Component {
login(values) {
// here we are supposed to determine whether Casdoor is working as an OAuth server or CAS server
values["language"] = this.state.userLang ?? "";
if (this.state.type === "cas") {
// CAS
const casParams = Util.getCasParameters();
@ -442,6 +458,8 @@ class LoginPage extends React.Component {
} else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
}
}).finally(() => {
this.setState({loginLoading: false});
});
} else {
// OAuth
@ -497,6 +515,8 @@ class LoginPage extends React.Component {
} else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
}
}).finally(() => {
this.setState({loginLoading: false});
});
}
}
@ -530,9 +550,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 +566,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,22 +584,25 @@ 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} />
<LanguageSelect languages={application.organizationObj.languages} onClick={key => {this.setState({userLang: key});}} />
</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>
)
;
} else if (signinItem.name === "Username") {
if (this.state.loginMethod === "webAuthn") {
return null;
}
return (
<div>
<div key={resultItemKey}>
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
<Form.Item
name="username"
@ -654,14 +679,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,9 +704,10 @@ 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
loading={this.state.loginLoading}
type="primary"
htmlType="submit"
className="login-button"
@ -694,19 +720,25 @@ class LoginPage extends React.Component {
</Button>
{
this.state.loginMethod === "faceId" ?
<Suspense fallback={null}>
<FaceRecognitionModal
visible={this.state.openFaceRecognitionModal}
onOk={(faceId) => {
const values = this.state.values;
values["faceId"] = faceId;
this.state.haveFaceIdProvider ? <Suspense fallback={null}><FaceRecognitionCommonModal visible={this.state.openFaceRecognitionModal} onOk={(FaceIdImage) => {
const values = this.state.values;
values["FaceIdImage"] = FaceIdImage;
this.login(values);
this.setState({openFaceRecognitionModal: false});
}} onCancel={() => this.setState({openFaceRecognitionModal: false, loginLoading: false})} /></Suspense> :
<Suspense fallback={null}>
<FaceRecognitionModal
visible={this.state.openFaceRecognitionModal}
onOk={(faceId) => {
const values = this.state.values;
values["faceId"] = faceId;
this.login(values);
this.setState({openFaceRecognitionModal: false});
}}
onCancel={() => this.setState({openFaceRecognitionModal: false})}
/>
</Suspense>
this.login(values);
this.setState({openFaceRecognitionModal: false});
}}
onCancel={() => this.setState({openFaceRecognitionModal: false, loginLoading: false})}
/>
</Suspense>
:
<>
</>
@ -721,15 +753,21 @@ class LoginPage extends React.Component {
if (signinItem.rule === "None" || signinItem.rule === "") {
signinItem.rule = showForm ? "small" : "big";
}
const searchParams = new URLSearchParams(window.location.search);
const providerHint = searchParams.get("provider_hint");
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) => {
if (providerHint === providerItem.provider.name) {
goToLink(Provider.getAuthUrl(application, providerItem.provider, "signup"));
return;
}
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 +790,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>
@ -803,7 +841,6 @@ class LoginPage extends React.Component {
<Form
name="normal_login"
initialValues={{
organization: application.organization,
application: application.name,
autoSignin: true,
@ -906,7 +943,7 @@ class LoginPage extends React.Component {
this.login(values);
this.setState({openCaptchaModal: false});
}}
onCancel={() => this.setState({openCaptchaModal: false})}
onCancel={() => this.setState({openCaptchaModal: false, loginLoading: false})}
isCurrentProvider={true}
/>;
}
@ -973,7 +1010,7 @@ class LoginPage extends React.Component {
const oAuthParams = Util.getOAuthGetParameters();
this.populateOauthValues(values);
const application = this.getApplicationObj();
return fetch(`${Setting.ServerUrl}/api/webauthn/signin/begin?owner=${application.organization}&name=${username}`, {
return fetch(`${Setting.ServerUrl}/api/webauthn/signin/begin?owner=${application.organization}`, {
method: "GET",
credentials: "include",
})
@ -983,11 +1020,7 @@ class LoginPage extends React.Component {
Setting.showMessage("error", credentialRequestOptions.msg);
throw credentialRequestOptions.status.msg;
}
credentialRequestOptions.publicKey.challenge = UserWebauthnBackend.webAuthnBufferDecode(credentialRequestOptions.publicKey.challenge);
credentialRequestOptions.publicKey.allowCredentials.forEach(function(listItem) {
listItem.id = UserWebauthnBackend.webAuthnBufferDecode(listItem.id);
});
return navigator.credentials.get({
publicKey: credentialRequestOptions.publicKey,
@ -1037,6 +1070,12 @@ class LoginPage extends React.Component {
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}${error}`);
});
}).catch(error => {
Setting.showMessage("error", `${error}`);
}).finally(() => {
this.setState({
loginLoading: false,
});
});
}

View File

@ -387,7 +387,8 @@ export function getAuthUrl(application, provider, method, code) {
}
let endpoint = authInfo[provider.type].endpoint;
let redirectUri = `${window.location.origin}/callback`;
const redirectOrigin = application.forcedRedirectOrigin ? application.forcedRedirectOrigin : window.location.origin;
let redirectUri = `${redirectOrigin}/callback`;
let scope = authInfo[provider.type].scope;
const isShortState = (provider.type === "WeChat" && navigator.userAgent.includes("MicroMessenger")) || (provider.type === "Twitter");
const state = Util.getStateFromQueryParams(application.name, provider.name, method, isShortState);
@ -398,7 +399,7 @@ export function getAuthUrl(application, provider, method, code) {
endpoint = endpoint.replace("common", provider.domain);
}
} else if (provider.type === "Apple") {
redirectUri = `${window.location.origin}/api/callback`;
redirectUri = `${redirectOrigin}/api/callback`;
} else if (provider.type === "Google" && provider.disableSsl) {
scope += "+https://www.googleapis.com/auth/user.phonenumbers.read";
}
@ -420,13 +421,13 @@ export function getAuthUrl(application, provider, method, code) {
} else if (provider.type === "AzureADB2C") {
return `https://${provider.domain}.b2clogin.com/${provider.domain}.onmicrosoft.com/${provider.appId}/oauth2/v2.0/authorize?client_id=${provider.clientId}&nonce=defaultNonce&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${scope}&response_type=code&state=${state}&prompt=login`;
} else if (provider.type === "DingTalk") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&prompt=consent&state=${state}`;
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&prompt=login%20consent&state=${state}`;
} else if (provider.type === "WeChat") {
if (navigator.userAgent.includes("MicroMessenger")) {
return `${authInfo[provider.type].mpEndpoint}?appid=${provider.clientId2}&redirect_uri=${redirectUri}&state=${state}&scope=${authInfo[provider.type].mpScope}&response_type=code#wechat_redirect`;
} else {
if (provider.clientId2 && provider?.disableSsl && provider?.signName === "media") {
return `${window.location.origin}/callback?state=${state}&code=${"wechat_oa:" + code}`;
return `${redirectOrigin}/callback?state=${state}&code=${"wechat_oa:" + code}`;
}
return `${endpoint}?appid=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}#wechat_redirect`;
}
@ -469,7 +470,7 @@ export function getAuthUrl(application, provider, method, code) {
} else if (provider.type === "Apple") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code%20id_token&scope=${scope}&response_mode=form_post`;
} else if (provider.type === "Steam") {
return `${endpoint}?openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns=http://specs.openid.net/auth/2.0&openid.realm=${window.location.origin}&openid.return_to=${redirectUri}?state=${state}`;
return `${endpoint}?openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns=http://specs.openid.net/auth/2.0&openid.realm=${redirectOrigin}&openid.return_to=${redirectUri}?state=${state}`;
} else if (provider.type === "Okta") {
return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} else if (provider.type === "Douyin" || provider.type === "TikTok") {

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,12 +31,12 @@ 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>
) : (
<div style={{margin: "0 15px"}}>
<Row>
<div style={{width: "100%", padding: "0 100px"}}>
<Row style={{justifyContent: "center"}}>
{items.map(item => <SingleCard logo={item.logo} link={item.link} title={item.name} desc={item.description} time={item.createdTime} isSingle={items.length === 1} key={item.name} />)}
</Row>
</div>

View File

@ -18,14 +18,33 @@ import {materialDark} from "@uiw/codemirror-theme-material";
import {langs} from "@uiw/codemirror-extensions-langs";
export const Editor = (props) => {
const fillHeight = props.fillHeight ? {
height: "100%",
style: {height: "100%"},
} : {};
const fillWidth = props.fillWidth ? {
width: "100%",
style: {width: "100%"},
} : {};
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":
@ -50,8 +69,9 @@ export const Editor = (props) => {
<CodeMirror
value={props.value}
{...props}
{...fillHeight}
{...fillWidth}
width={width}
height={height}
style={style}
readOnly={props.readOnly}
theme={props.dark ? materialDark : "light"}
extensions={extensions}

View File

@ -2,7 +2,7 @@ import i18next from "i18next";
import {Tree} from "antd";
import React from "react";
export const NavItemTree = ({disable, checkedKeys, defaultExpandedKeys, onCheck}) => {
export const NavItemTree = ({disabled, checkedKeys, defaultExpandedKeys, onCheck}) => {
const NavItemNodes = [
{
title: i18next.t("organization:All"),
@ -86,7 +86,7 @@ export const NavItemTree = ({disable, checkedKeys, defaultExpandedKeys, onCheck}
return (
<Tree
disabled={disable}
disabled={disabled}
checkable
checkedKeys={checkedKeys}
defaultExpandedKeys={defaultExpandedKeys}

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