Compare commits

...

62 Commits

Author SHA1 Message Date
36f5de3203 feat: allow jwks to include the certs from non-admin owner (#3749) 2025-04-28 09:31:56 +08:00
eae69c41d7 feat: add object field filter for webhook (#3746) 2025-04-26 22:05:36 +08:00
91057f54f3 feat: add Pbkdf2DjangoCredManager (#3745) 2025-04-25 16:16:50 +08:00
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
128 changed files with 3019 additions and 2380 deletions

View File

@ -1,10 +1,10 @@
FROM --platform=$BUILDPLATFORM node:18.19.0 AS FRONT FROM --platform=$BUILDPLATFORM node:18.19.0 AS FRONT
WORKDIR /web WORKDIR /web
COPY ./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 WORKDIR /go/src/casdoor
COPY . . COPY . .
RUN ./build.sh RUN ./build.sh

View File

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

View File

@ -29,6 +29,7 @@ import (
"github.com/casdoor/casdoor/captcha" "github.com/casdoor/casdoor/captcha"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/form" "github.com/casdoor/casdoor/form"
"github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/idp" "github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/proxy" "github.com/casdoor/casdoor/proxy"
@ -402,11 +403,27 @@ func (c *ApiController) Login() {
return return
} }
if err := object.CheckFaceId(user, authForm.FaceId, c.GetAcceptLanguage()); err != nil { faceIdProvider, err := object.GetFaceIdProviderByApplication(util.GetId(application.Owner, application.Name), "false", c.GetAcceptLanguage())
c.ResponseError(err.Error(), nil) if err != nil {
return 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 == "" { } else if authForm.Password == "" {
if user, err = object.GetUserByFields(authForm.Organization, authForm.Username); err != nil { if user, err = object.GetUserByFields(authForm.Organization, authForm.Username); err != nil {
c.ResponseError(err.Error(), nil) c.ResponseError(err.Error(), nil)
@ -466,6 +483,14 @@ func (c *ApiController) Login() {
verificationType = "sms" verificationType = "sms"
} else { } else {
verificationType = "email" 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 { } else {
var application *object.Application var application *object.Application
@ -598,6 +623,9 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if provider == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s does not exist"), authForm.Provider))
}
providerItem := application.GetProviderItem(provider.Name) providerItem := application.GetProviderItem(provider.Name)
if !providerItem.IsProviderVisible() { 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.Data["json"] = resp
c.ServeJSON() c.ServeJSON()
} }

View File

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

View File

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

View File

@ -457,10 +457,10 @@ func (c *ApiController) SetPassword() {
newPassword := c.Ctx.Request.Form.Get("newPassword") newPassword := c.Ctx.Request.Form.Get("newPassword")
code := c.Ctx.Request.Form.Get("code") 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")) // c.ResponseError(c.T("auth:Unauthorized operation"))
// return // return
//} // }
if strings.Contains(newPassword, " ") { if strings.Contains(newPassword, " ") {
c.ResponseError(c.T("user:New password cannot contain blank space.")) c.ResponseError(c.T("user:New password cannot contain blank space."))
@ -602,7 +602,11 @@ func (c *ApiController) CheckUserPassword() {
return 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 { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
} else { } else {

View File

@ -242,7 +242,7 @@ func (c *ApiController) SendVerificationCode() {
} else if vform.Method == ResetVerification { } else if vform.Method == ResetVerification {
user = c.getCurrentUser() user = c.getCurrentUser()
} else if vform.Method == MfaAuthVerification { } else if vform.Method == MfaAuthVerification {
mfaProps := user.GetPreferredMfaProps(false) mfaProps := user.GetMfaProps(object.EmailType, false)
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest { if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
vform.Dest = mfaProps.Secret vform.Dest = mfaProps.Secret
} }
@ -281,7 +281,7 @@ func (c *ApiController) SendVerificationCode() {
} }
} }
} else if vform.Method == MfaAuthVerification { } else if vform.Method == MfaAuthVerification {
mfaProps := user.GetPreferredMfaProps(false) mfaProps := user.GetMfaProps(object.SmsType, false)
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest { if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
vform.Dest = mfaProps.Secret vform.Dest = mfaProps.Secret
} }
@ -436,7 +436,8 @@ func (c *ApiController) ResetEmailOrPhone() {
switch destType { switch destType {
case object.VerifyTypeEmail: case object.VerifyTypeEmail:
user.Email = dest 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: case object.VerifyTypePhone:
user.Phone = dest user.Phone = dest
_, err = object.SetUserField(user, "phone", user.Phone) _, err = object.SetUserField(user, "phone", user.Phone)

View File

@ -16,7 +16,7 @@ package controllers
import ( import (
"bytes" "bytes"
"fmt" "encoding/base64"
"io" "io"
"github.com/casdoor/casdoor/form" "github.com/casdoor/casdoor/form"
@ -118,24 +118,7 @@ func (c *ApiController) WebAuthnSigninBegin() {
return return
} }
userOwner := c.Input().Get("owner") options, sessionData, err := webauthnObj.BeginDiscoverableLogin()
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)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -168,20 +151,23 @@ func (c *ApiController) WebAuthnSigninFinish() {
return return
} }
c.Ctx.Request.Body = io.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody)) c.Ctx.Request.Body = io.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
userId := string(sessionData.UserID)
user, err := object.GetUser(userId) var user *object.User
if err != nil { handler := func(rawID, userHandle []byte) (webauthn.User, error) {
c.ResponseError(err.Error()) user, err = object.GetUserByWebauthID(base64.StdEncoding.EncodeToString(rawID))
return 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 { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
c.SetSessionUsername(userId) c.SetSessionUsername(user.GetId())
util.LogInfo(c.Ctx, "API: [%s] signed in", userId) util.LogInfo(c.Ctx, "API: [%s] signed in", user.GetId())
var application *object.Application var application *object.Application

View File

@ -34,6 +34,8 @@ func GetCredManager(passwordType string) CredManager {
return NewPbkdf2SaltCredManager() return NewPbkdf2SaltCredManager()
} else if passwordType == "argon2id" { } else if passwordType == "argon2id" {
return NewArgon2idCredManager() return NewArgon2idCredManager()
} else if passwordType == "pbkdf2-django" {
return NewPbkdf2DjangoCredManager()
} }
return nil return nil
} }

71
cred/pbkdf2_django.go Normal file
View File

@ -0,0 +1,71 @@
// 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 cred
import (
"crypto/sha256"
"encoding/base64"
"strconv"
"strings"
"golang.org/x/crypto/pbkdf2"
)
// password type: pbkdf2-django
type Pbkdf2DjangoCredManager struct{}
func NewPbkdf2DjangoCredManager() *Pbkdf2DjangoCredManager {
cm := &Pbkdf2DjangoCredManager{}
return cm
}
func (m *Pbkdf2DjangoCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
iterations := 260000
salt := userSalt
if salt == "" {
salt = organizationSalt
}
saltBytes := []byte(salt)
passwordBytes := []byte(password)
computedHash := pbkdf2.Key(passwordBytes, saltBytes, iterations, sha256.Size, sha256.New)
hashBase64 := base64.StdEncoding.EncodeToString(computedHash)
return "pbkdf2_sha256$" + strconv.Itoa(iterations) + "$" + salt + "$" + hashBase64
}
func (m *Pbkdf2DjangoCredManager) IsPasswordCorrect(password string, passwordHash string, userSalt string, organizationSalt string) bool {
parts := strings.Split(passwordHash, "$")
if len(parts) != 4 {
return false
}
algorithm, iterations, salt, hash := parts[0], parts[1], parts[2], parts[3]
if algorithm != "pbkdf2_sha256" {
return false
}
iter, err := strconv.Atoi(iterations)
if err != nil {
return false
}
saltBytes := []byte(salt)
passwordBytes := []byte(password)
computedHash := pbkdf2.Key(passwordBytes, saltBytes, iter, sha256.Size, sha256.New)
computedHashBase64 := base64.StdEncoding.EncodeToString(computedHash)
return computedHashBase64 == hash
}

View File

@ -15,6 +15,8 @@
package email package email
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
@ -24,14 +26,24 @@ import (
) )
type HttpEmailProvider struct { type HttpEmailProvider struct {
endpoint string endpoint string
method 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{ client := &HttpEmailProvider{
endpoint: endpoint, endpoint: endpoint,
method: method, method: method,
httpHeaders: httpHeaders,
bodyMapping: bodyMapping,
contentType: contentType,
} }
return client 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 { func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
var req *http.Request var req *http.Request
var err error var err error
if c.method == "POST" {
formValues := url.Values{} fromNameField := "fromName"
formValues.Set("fromName", fromName) toAddressField := "toAddress"
formValues.Set("toAddress", toAddress) subjectField := "subject"
formValues.Set("subject", subject) contentField := "content"
formValues.Set("content", content)
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode())) 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 { if err != nil {
return err return err
} }
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", c.contentType)
} else if c.method == "GET" { } else if c.method == "GET" {
req, err = http.NewRequest(c.method, c.endpoint, nil) req, err = http.NewRequest(c.method, c.endpoint, nil)
if err != nil { if err != nil {
@ -58,15 +104,19 @@ func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress
} }
q := req.URL.Query() q := req.URL.Query()
q.Add("fromName", fromName) q.Add(fromNameField, fromName)
q.Add("toAddress", toAddress) q.Add(toAddressField, toAddress)
q.Add("subject", subject) q.Add(subjectField, subject)
q.Add("content", content) q.Add(contentField, content)
req.URL.RawQuery = q.Encode() req.URL.RawQuery = q.Encode()
} else { } else {
return fmt.Errorf("HttpEmailProvider's Send() error, unsupported method: %s", c.method) 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 httpClient := proxy.DefaultHttpClient
resp, err := httpClient.Do(req) resp, err := httpClient.Do(req)
if err != nil { if err != nil {

View File

@ -18,11 +18,11 @@ type EmailProvider interface {
Send(fromAddress string, fromName, toAddress string, subject string, content string) error 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" { if typ == "Azure ACS" {
return NewAzureACSEmailProvider(clientSecret, host) return NewAzureACSEmailProvider(clientSecret, host)
} else if typ == "Custom HTTP Email" { } else if typ == "Custom HTTP Email" {
return NewHttpEmailProvider(endpoint, method) return NewHttpEmailProvider(endpoint, method, httpHeaders, bodyMapping, contentType)
} else if typ == "SendGrid" { } else if typ == "SendGrid" {
return NewSendgridEmailProvider(clientSecret, host, endpoint) return NewSendgridEmailProvider(clientSecret, host, endpoint)
} else { } 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"` Phone string `json:"phone"`
Affiliation string `json:"affiliation"` Affiliation string `json:"affiliation"`
IdCard string `json:"idCard"` IdCard string `json:"idCard"`
Language string `json:"language"`
Region string `json:"region"` Region string `json:"region"`
InvitationCode string `json:"invitationCode"` InvitationCode string `json:"invitationCode"`
@ -67,7 +68,8 @@ type AuthForm struct {
Plan string `json:"plan"` Plan string `json:"plan"`
Pricing string `json:"pricing"` Pricing string `json:"pricing"`
FaceId []float64 `json:"faceId"` FaceId []float64 `json:"faceId"`
FaceIdImage []string `json:"faceIdImage"`
} }
func GetAuthFormFieldValue(form *AuthForm, fieldName string) (bool, string) { func GetAuthFormFieldValue(form *AuthForm, fieldName string) (bool, string) {

194
go.mod
View File

@ -1,10 +1,14 @@
module github.com/casdoor/casdoor module github.com/casdoor/casdoor
go 1.16 go 1.21
require ( require (
github.com/Masterminds/squirrel v1.5.3 github.com/Masterminds/squirrel v1.5.3
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387 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/aws/aws-sdk-go v1.45.5
github.com/beego/beego v1.12.12 github.com/beego/beego v1.12.12
github.com/beevik/etree v1.1.0 github.com/beevik/etree v1.1.0
@ -18,7 +22,6 @@ require (
github.com/casvisor/casvisor-go-sdk v1.4.0 github.com/casvisor/casvisor-go-sdk v1.4.0
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/denisenkom/go-mssqldb v0.9.0 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/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3
github.com/fogleman/gg v1.3.0 github.com/fogleman/gg v1.3.0
github.com/go-asn1-ber/asn1-ber v1.5.5 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-pay/gopay v1.5.72
github.com/go-sql-driver/mysql v1.6.0 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-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/go-webauthn/webauthn v0.6.0 github.com/go-webauthn/webauthn v0.10.2
github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/lestrrat-go/jwx v1.2.29 github.com/lestrrat-go/jwx v1.2.29
@ -46,7 +49,6 @@ require (
github.com/russellhaering/gosaml2 v0.9.0 github.com/russellhaering/gosaml2 v0.9.0
github.com/russellhaering/goxmldsig v1.2.0 github.com/russellhaering/goxmldsig v1.2.0
github.com/sendgrid/sendgrid-go v3.14.0+incompatible 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/shirou/gopsutil v3.21.11+incompatible
github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 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/stripe/stripe-go/v74 v74.29.0
github.com/tealeg/xlsx v1.0.5 github.com/tealeg/xlsx v1.0.5
github.com/thanhpk/randstr v1.0.4 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/builder v0.3.13
github.com/xorm-io/core v0.7.4 github.com/xorm-io/core v0.7.4
github.com/xorm-io/xorm v1.1.6 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/crypto v0.32.0
golang.org/x/net v0.34.0 golang.org/x/net v0.34.0
golang.org/x/oauth2 v0.17.0 golang.org/x/oauth2 v0.17.0
golang.org/x/text v0.21.0 golang.org/x/text v0.21.0
google.golang.org/api v0.150.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 gopkg.in/square/go-jose.v2 v2.6.0
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68 layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68
maunium.net/go/mautrix v0.16.0 maunium.net/go/mautrix v0.16.0
modernc.org/sqlite v1.18.2 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 be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces", "Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit", "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", "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", "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", "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 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 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 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", "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", "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", "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 be an email address": "Benutzername kann keine E-Mail-Adresse sein",
"Username cannot contain white spaces": "Benutzername darf keine Leerzeichen enthalten", "Username cannot contain white spaces": "Benutzername darf keine Leerzeichen enthalten",
"Username cannot start with a digit": "Benutzername darf nicht mit einer Ziffer beginnen", "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", "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", "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", "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 be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces", "Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit", "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", "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", "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", "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 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 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 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", "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", "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", "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 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 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": "شما رمز عبور یا کد اشتباه را بیش از حد وارد کرده‌اید، لطفاً %d دقیقه صبر کنید و دوباره تلاش کنید", "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": "منطقه شما اجازه ثبت‌نام با تلفن را ندارد", "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 be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces", "Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit", "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", "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", "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", "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 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 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 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", "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", "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", "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 be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces", "Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit", "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", "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", "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", "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 be an email address": "Username tidak bisa menjadi alamat email",
"Username cannot contain white spaces": "Username tidak boleh mengandung spasi", "Username cannot contain white spaces": "Username tidak boleh mengandung spasi",
"Username cannot start with a digit": "Username tidak dapat dimulai dengan angka", "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", "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", "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", "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 be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces", "Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit", "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", "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", "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", "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).": "ユーザー名が長すぎます(最大39文字)。", "Username is too long (maximum is 255 characters).": "ユーザー名が長すぎます(最大255文字)。",
"Username must have at least 2 characters": "ユーザー名は少なくとも2文字必要です", "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 分間待ってから再度お試しください", "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": "あなたの地域は電話でサインアップすることができません", "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 be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces", "Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit", "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", "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", "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", "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).": "사용자 이름이 너무 깁니다 (최대 39자).", "Username is too long (maximum is 255 characters).": "사용자 이름이 너무 깁니다 (최대 255자).",
"Username must have at least 2 characters": "사용자 이름은 적어도 2개의 문자가 있어야 합니다", "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분 동안 기다리신 후 다시 시도해주세요", "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": "당신의 지역은 전화로 가입할 수 없습니다", "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 be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces", "Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit", "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", "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", "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", "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 be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces", "Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit", "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", "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", "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", "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 be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces", "Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit", "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", "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", "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", "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 be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces", "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 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", "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", "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", "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).": "Имя пользователя слишком длинное (максимальная длина - 39 символов).", "Username is too long (maximum is 255 characters).": "Имя пользователя слишком длинное (максимальная длина - 255 символов).",
"Username must have at least 2 characters": "Имя пользователя должно содержать не менее 2 символов", "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 минут и попробуйте снова", "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": "Ваш регион не разрешает регистрацию по телефону", "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 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 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 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", "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", "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", "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 be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces", "Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit", "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", "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", "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", "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 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 contain white spaces": "Kullanıcı adı boşluk karakteri içeremez",
"Username cannot start with a digit": "Kullanıcı adı rakamla başlayamaz", "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ı", "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.", "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", "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 be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces", "Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit", "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", "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", "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", "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 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 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 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ự", "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", "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", "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 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).": "用户名过长(最大允许长度为39个字符)", "Username is too long (maximum is 255 characters).": "用户名过长(最大允许长度为255个字符)",
"Username must have at least 2 characters": "用户名至少要有2个字符", "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 分后重试", "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": "所在地区不支持手机号注册", "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{} dtUserInfo := &DingTalkUserResponse{}
accessToken := token.AccessToken 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 { if err != nil {
return nil, err return nil, err
} }
reqest.Header.Add("x-acs-dingtalk-access-token", accessToken) request.Header.Add("x-acs-dingtalk-access-token", accessToken)
resp, err := idp.Client.Do(reqest) resp, err := idp.Client.Do(request)
if err != nil { if err != nil {
return nil, err 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) params := fmt.Sprintf(`{"expire_seconds": 3600, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": "%s"}}}`, providerId)
bodyData := bytes.NewReader([]byte(params)) bodyData := bytes.NewReader([]byte(params))
requeset, err := http.NewRequest("POST", qrCodeUrl, bodyData) request, err := http.NewRequest("POST", qrCodeUrl, bodyData)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
resp, err := client.Do(requeset) resp, err := client.Do(request)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -63,7 +63,11 @@ func GetCertCount(owner, field, value string) (int64, error) {
func GetCerts(owner string) ([]*Cert, error) { func GetCerts(owner string) ([]*Cert, error) {
certs := []*Cert{} certs := []*Cert{}
err := ormer.Engine.Where("owner = ? or owner = ? ", "admin", owner).Desc("created_time").Find(&certs, &Cert{}) db := ormer.Engine.NewSession()
if owner != "" {
db = db.Where("owner = ? or owner = ? ", "admin", owner)
}
err := db.Desc("created_time").Find(&certs, &Cert{})
if err != nil { if err != nil {
return certs, err return certs, err
} }
@ -146,7 +150,12 @@ func getCertByName(name string) (*Cert, error) {
func GetCert(id string) (*Cert, error) { func GetCert(id string) (*Cert, error) {
owner, name := util.GetOwnerAndNameFromId(id) 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) { 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 { func CheckUsername(username string, lang string) string {
if username == "" { if username == "" {
return i18n.Translate(lang, "check:Empty username.") return i18n.Translate(lang, "check:Empty username.")
} else if len(username) > 39 { } else if len(username) > 255 {
return i18n.Translate(lang, "check:Username is too long (maximum is 39 characters).") return i18n.Translate(lang, "check:Username is too long (maximum is 255 characters).")
} }
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex // 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 { func CheckUsernameWithEmail(username string, lang string) string {
if username == "" { if username == "" {
return i18n.Translate(lang, "check:Empty username.") return i18n.Translate(lang, "check:Empty username.")
} else if len(username) > 39 { } else if len(username) > 255 {
return i18n.Translate(lang, "check:Username is too long (maximum is 39 characters).") return i18n.Translate(lang, "check:Username is too long (maximum is 255 characters).")
} }
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex // 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 { 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 fromAddress := provider.ClientId2
if fromAddress == "" { 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) { func GetGroupsHaveChildrenMap(groups []*Group) (map[string]*Group, error) {
groupsHaveChildren := []*Group{} groupsHaveChildren := []*Group{}
resultMap := make(map[string]*Group) resultMap := make(map[string]*Group)
groupMap := map[string]*Group{}
groupIds := []string{} groupIds := []string{}
for _, group := range groups { for _, group := range groups {
groupMap[group.Name] = group
groupIds = append(groupIds, group.Name) 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) err := ormer.Engine.Cols("owner", "name", "parent_id", "display_name").Distinct("parent_id").In("parent_id", groupIds).Find(&groupsHaveChildren)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, group := range groups { for _, group := range groupsHaveChildren {
resultMap[group.Name] = group resultMap[group.ParentId] = groupMap[group.ParentId]
} }
return resultMap, nil return resultMap, nil
} }
@ -302,7 +306,10 @@ func GetPaginationGroupUsers(groupId string, offset, limit int, field, value, so
func GetGroupUsers(groupId string) ([]*User, error) { func GetGroupUsers(groupId string) ([]*User, error) {
users := []*User{} users := []*User{}
owner, _ := util.GetOwnerAndNameFromId(groupId) owner, _, err := util.GetOwnerAndNameFromIdWithError(groupId)
if err != nil {
return nil, err
}
names, err := userEnforcer.GetUserNamesByGroupName(groupId) names, err := userEnforcer.GetUserNamesByGroupName(groupId)
if err != nil { if err != nil {
return nil, err return nil, err
@ -314,6 +321,11 @@ func GetGroupUsers(groupId string) ([]*User, error) {
return users, nil return users, nil
} }
func GetGroupUsersWithoutError(groupId string) []*User {
users, _ := GetGroupUsers(groupId)
return users
}
func ExtendGroupWithUsers(group *Group) error { func ExtendGroupWithUsers(group *Group) error {
if group == nil { if group == nil {
return nil return nil

View File

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

View File

@ -23,17 +23,18 @@ type Ldap struct {
Owner string `xorm:"varchar(100)" json:"owner"` Owner string `xorm:"varchar(100)" json:"owner"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"` CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
ServerName string `xorm:"varchar(100)" json:"serverName"` ServerName string `xorm:"varchar(100)" json:"serverName"`
Host string `xorm:"varchar(100)" json:"host"` Host string `xorm:"varchar(100)" json:"host"`
Port int `xorm:"int" json:"port"` Port int `xorm:"int" json:"port"`
EnableSsl bool `xorm:"bool" json:"enableSsl"` EnableSsl bool `xorm:"bool" json:"enableSsl"`
Username string `xorm:"varchar(100)" json:"username"` AllowSelfSignedCert bool `xorm:"bool" json:"allowSelfSignedCert"`
Password string `xorm:"varchar(100)" json:"password"` Username string `xorm:"varchar(100)" json:"username"`
BaseDn string `xorm:"varchar(100)" json:"baseDn"` Password string `xorm:"varchar(100)" json:"password"`
Filter string `xorm:"varchar(200)" json:"filter"` BaseDn string `xorm:"varchar(100)" json:"baseDn"`
FilterFields []string `xorm:"varchar(100)" json:"filterFields"` Filter string `xorm:"varchar(200)" json:"filter"`
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"` FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"` DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
AutoSync int `json:"autoSync"` AutoSync int `json:"autoSync"`
LastSync string `xorm:"varchar(100)" json:"lastSync"` 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", 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 { if err != nil {
return false, 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) 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 { 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(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()) logs.Warning(err.Error())

View File

@ -16,6 +16,7 @@ package object
import ( import (
"crypto/md5" "crypto/md5"
"crypto/tls"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
@ -64,8 +65,11 @@ type LdapUser struct {
func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) { func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) {
var conn *goldap.Conn var conn *goldap.Conn
tlsConfig := tls.Config{
InsecureSkipVerify: ldap.AllowSelfSignedCert,
}
if ldap.EnableSsl { 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 { } else {
conn, err = goldap.Dial("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port)) 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") columns = append(columns, "mfa_phone_enabled", "phone", "country_code")
} else if mfa.MfaType == EmailType { } else if mfa.MfaType == EmailType {
user.MfaEmailEnabled = true 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) _, err := UpdateUser(user.GetId(), user, columns, false)

View File

@ -77,6 +77,7 @@ func getOriginFromHostInternal(host string) (string, string) {
return origin, origin return origin, origin
} }
isDev := conf.GetConfigString("runmode") == "dev"
// "door.casdoor.com" // "door.casdoor.com"
protocol := "https://" protocol := "https://"
if !strings.Contains(host, ".") { if !strings.Contains(host, ".") {
@ -87,7 +88,7 @@ func getOriginFromHostInternal(host string) (string, string) {
protocol = "http://" 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") return fmt.Sprintf("%s%s", protocol, "localhost:7001"), fmt.Sprintf("%s%s", protocol, "localhost:8000")
} else { } else {
return fmt.Sprintf("%s%s", protocol, host), fmt.Sprintf("%s%s", protocol, host) return fmt.Sprintf("%s%s", protocol, host), fmt.Sprintf("%s%s", protocol, host)
@ -137,7 +138,7 @@ func GetOidcDiscovery(host string) OidcDiscovery {
func GetJsonWebKeySet() (jose.JSONWebKeySet, error) { func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
jwks := jose.JSONWebKeySet{} jwks := jose.JSONWebKeySet{}
certs, err := GetCerts("admin") certs, err := GetCerts("")
if err != nil { if err != nil {
return jwks, err return jwks, err
} }

View File

@ -63,7 +63,7 @@ type Organization struct {
PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"` PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"`
PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"` PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"`
PasswordExpireDays int `json:"passwordExpireDays"` PasswordExpireDays int `json:"passwordExpireDays"`
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"` CountryCodes []string `xorm:"mediumtext" json:"countryCodes"`
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"` DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"` DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
UserTypes []string `xorm:"mediumtext" json:"userTypes"` UserTypes []string `xorm:"mediumtext" json:"userTypes"`
@ -80,7 +80,8 @@ type Organization struct {
UseEmailAsUsername bool `json:"useEmailAsUsername"` UseEmailAsUsername bool `json:"useEmailAsUsername"`
EnableTour bool `json:"enableTour"` EnableTour bool `json:"enableTour"`
IpRestriction string `json:"ipRestriction"` 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"` MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"` AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
@ -227,6 +228,7 @@ func UpdateOrganization(id string, organization *Organization, isGlobalAdmin boo
if !isGlobalAdmin { if !isGlobalAdmin {
organization.NavItems = org.NavItems organization.NavItems = org.NavItems
organization.WidgetItems = org.WidgetItems
} }
session := ormer.Engine.ID(core.PK{owner, name}).AllCols() 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 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) { func NewAdapterFromDb(driverName string, dataSourceName string, dbName string, db *sql.DB) (*Ormer, error) {
a := &Ormer{} a := &Ormer{}
a.driverName = driverName a.driverName = driverName

View File

@ -148,7 +148,7 @@ func UpdatePermission(id string, permission *Permission) (bool, error) {
} }
if permission.ResourceType == "Application" && permission.Model != "" { 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 { if err != nil {
return false, err return false, err
} else if model == nil { } else if model == nil {

View File

@ -48,6 +48,7 @@ type Provider struct {
CustomLogo string `xorm:"varchar(200)" json:"customLogo"` CustomLogo string `xorm:"varchar(200)" json:"customLogo"`
Scopes string `xorm:"varchar(100)" json:"scopes"` Scopes string `xorm:"varchar(100)" json:"scopes"`
UserMapping map[string]string `xorm:"varchar(500)" json:"userMapping"` 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"` Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"` Port int `json:"port"`
@ -384,6 +385,44 @@ func GetCaptchaProviderByApplication(applicationId, isCurrentProvider, lang stri
return nil, nil 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 { func providerChangeTrigger(oldName string, newName string) error {
session := ormer.Engine.NewSession() session := ormer.Engine.NewSession()
defer session.Close() defer session.Close()

View File

@ -263,6 +263,27 @@ func addWebhookRecord(webhook *Webhook, record *casvisorsdk.Record, statusCode i
return err return err
} }
func filterRecordObject(object string, objectFields []string) string {
var rawObject map[string]interface{}
_ = json.Unmarshal([]byte(object), &rawObject)
if rawObject == nil {
return object
}
filteredObject := make(map[string]interface{})
for _, field := range objectFields {
fieldValue, ok := rawObject[field]
if !ok {
continue
}
filteredObject[field] = fieldValue
}
return util.StructToJson(filteredObject)
}
func SendWebhooks(record *casvisorsdk.Record) error { func SendWebhooks(record *casvisorsdk.Record) error {
webhooks, err := getWebhooksByOrganization("") webhooks, err := getWebhooksByOrganization("")
if err != nil { if err != nil {
@ -271,7 +292,14 @@ func SendWebhooks(record *casvisorsdk.Record) error {
errs := []error{} errs := []error{}
webhooks = getFilteredWebhooks(webhooks, record.Organization, record.Action) webhooks = getFilteredWebhooks(webhooks, record.Organization, record.Action)
record2 := *record
for _, webhook := range webhooks { for _, webhook := range webhooks {
if len(webhook.ObjectFields) != 0 && webhook.ObjectFields[0] != "All" {
record2.Object = filterRecordObject(record.Object, webhook.ObjectFields)
}
var user *User var user *User
if webhook.IsUserExtended { if webhook.IsUserExtended {
user, err = getUser(record.Organization, record.User) user, err = getUser(record.Organization, record.User)
@ -287,12 +315,12 @@ func SendWebhooks(record *casvisorsdk.Record) error {
} }
} }
statusCode, respBody, err := sendWebhook(webhook, record, user) statusCode, respBody, err := sendWebhook(webhook, &record2, user)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
err = addWebhookRecord(webhook, record, statusCode, respBody, err) err = addWebhookRecord(webhook, &record2, statusCode, respBody, err)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
} }

View File

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

View File

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

View File

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

View File

@ -15,13 +15,17 @@
package object package object
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/faceId"
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/go-webauthn/webauthn/webauthn" "github.com/go-webauthn/webauthn/webauthn"
"github.com/xorm-io/builder" "github.com/xorm-io/builder"
@ -48,7 +52,7 @@ func InitUserManager() {
type User struct { type User struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"` 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"` CreatedTime string `xorm:"varchar(100) index" json:"createdTime"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"` UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
DeletedTime string `xorm:"varchar(100)" json:"deletedTime"` DeletedTime string `xorm:"varchar(100)" json:"deletedTime"`
@ -244,6 +248,7 @@ type MfaAccount struct {
type FaceId struct { type FaceId struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`
FaceIdData []float64 `json:"faceIdData"` FaceIdData []float64 `json:"faceIdData"`
ImageUrl string `json:"ImageUrl"`
} }
func GetUserFieldStringValue(user *User, fieldName string) (bool, string, error) { 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) { func GetUserByEmailOnly(email string) (*User, error) {
if email == "" { if email == "" {
return nil, nil 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") 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) organization, err := GetOrganizationByUser(user)
if err != nil { if err != nil {
return false, err 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) 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" { if organization.DefaultPassword != "" && user.Password == "123" {
user.Password = organization.DefaultPassword user.Password = organization.DefaultPassword
} }
@ -1179,6 +1223,40 @@ func (user *User) IsGlobalAdmin() bool {
return user.Owner == "built-in" 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) { func GenerateIdForNewUser(application *Application) (string, error) {
if application == nil || application.GetSignupItemRule("ID") != "Incremental" { if application == nil || application.GetSignupItemRule("ID") != "Incremental" {
return util.GenerateId(), nil return util.GenerateId(), nil

View File

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

View File

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

View File

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

View File

@ -106,11 +106,14 @@ func getOrganizationThemeCookieFromUrlPath(ctx *context.Context, urlPath string)
} }
organizationThemeCookie := &OrganizationThemeCookie{ organizationThemeCookie := &OrganizationThemeCookie{
application.ThemeData, ThemeData: application.ThemeData,
application.Logo, LogoUrl: application.Logo,
application.FooterHtml, FooterHtml: application.FooterHtml,
organization.Favicon, }
organization.DisplayName,
if organization != nil {
organizationThemeCookie.Favicon = organization.Favicon
organizationThemeCookie.DisplayName = organization.DisplayName
} }
return organizationThemeCookie, nil return organizationThemeCookie, nil

View File

@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Card, Col, ConfigProvider, Input, InputNumber, Popover, Radio, Result, Row, Select, Switch, Upload} from "antd"; import {Button, Card, Col, ConfigProvider, Input, InputNumber, Popover, Radio, Result, Row, Select, Space, Switch, Upload} from "antd";
import {CopyOutlined, LinkOutlined, UploadOutlined} from "@ant-design/icons"; import {CopyOutlined, HolderOutlined, LinkOutlined, UploadOutlined, UsergroupAddOutlined} from "@ant-design/icons";
import * as ApplicationBackend from "./backend/ApplicationBackend"; import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as CertBackend from "./backend/CertBackend"; import * as CertBackend from "./backend/CertBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
@ -36,6 +36,7 @@ import ThemeEditor from "./common/theme/ThemeEditor";
import SigninTable from "./table/SigninTable"; import SigninTable from "./table/SigninTable";
import Editor from "./common/Editor"; import Editor from "./common/Editor";
import * as GroupBackend from "./backend/GroupBackend";
const {Option} = Select; const {Option} = Select;
@ -52,6 +53,14 @@ const template = `<style>
background-color: #333333; background-color: #333333;
box-shadow: 0 0 30px 20px rgba(255, 255, 255, 0.20); 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>`; </style>`;
const previewGrid = Setting.isMobile() ? 22 : 11; const previewGrid = Setting.isMobile() ? 22 : 11;
@ -85,11 +94,11 @@ const sideTemplate = `<style>
} }
</style> </style>
<div class="left-model"> <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>SSO</span>
</span> </span>
<div class="img"> <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>
</div> </div>
`; `;
@ -116,6 +125,7 @@ class ApplicationEditPage extends React.Component {
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this.getApplication(); this.getApplication();
this.getOrganizations(); this.getOrganizations();
this.getGroups();
} }
getApplication() { 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) { getCerts(application) {
let owner = application.organization; let owner = application.organization;
if (application.isShared) { if (application.isShared) {
@ -397,6 +418,16 @@ class ApplicationEditPage extends React.Component {
/> />
</Col> </Col>
</Row> </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"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Token format"), i18next.t("application:Token format - Tooltip"))} : {Setting.getLabel(i18next.t("application:Token format"), i18next.t("application:Token format - Tooltip"))} :
@ -469,6 +500,31 @@ class ApplicationEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </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"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Enable signup"), i18next.t("application:Enable signup - Tooltip"))} : {Setting.getLabel(i18next.t("application:Enable signup"), i18next.t("application:Enable signup - Tooltip"))} :
@ -804,6 +860,33 @@ class ApplicationEditPage extends React.Component {
</Row> </Row>
</Col> </Col>
</Row> </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> <Row>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Custom CSS"), i18next.t("application:Custom CSS - Tooltip"))} : {Setting.getLabel(i18next.t("application:Custom CSS"), i18next.t("application:Custom CSS - Tooltip"))} :

View File

@ -109,7 +109,7 @@ class EntryPage extends React.Component {
<React.Fragment> <React.Fragment>
<CustomHead headerHtml={this.state.application?.headerHtml} /> <CustomHead headerHtml={this.state.application?.headerHtml} />
<div className={`${isDarkMode ? "loginBackgroundDark" : "loginBackground"}`} <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")} <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"}} /> style={{width: "100%", margin: "0 auto", position: "absolute"}} />
<Switch> <Switch>

View File

@ -14,7 +14,7 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Button, Table} from "antd"; import {Button, Table, Tooltip} from "antd";
import moment from "moment"; import moment from "moment";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as GroupBackend from "./backend/GroupBackend"; import * as GroupBackend from "./backend/GroupBackend";
@ -202,12 +202,16 @@ class GroupListPage extends BaseListPage {
return ( return (
<div> <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> <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} 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")}>
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`} <Button disabled type="primary" danger>{i18next.t("general:Delete")}</Button>
onConfirm={() => this.deleteGroup(index)} </Tooltip> :
> <PopconfirmModal
</PopconfirmModal> title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteGroup(index)}
>
</PopconfirmModal>
}
</div> </div>
); );
}, },

View File

@ -114,7 +114,7 @@ class InvitationEditPage extends React.Component {
const selectedOrganization = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner); const selectedOrganization = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner);
defaultApplication = selectedOrganization.defaultApplication; defaultApplication = selectedOrganization.defaultApplication;
if (!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; return;
} }
} }

View File

@ -170,6 +170,16 @@ class LdapEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </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"}}> <Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}> <Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Base DN"), i18next.t("ldap:Base DN - Tooltip"))} : {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"; import VerificationListPage from "./VerificationListPage";
function ManagementPage(props) { function ManagementPage(props) {
const [menuVisible, setMenuVisible] = useState(false); const [menuVisible, setMenuVisible] = useState(false);
const navItems = props.account?.organization?.navItems;
const widgetItems = props.account?.organization?.widgetItems;
function logout() { function logout() {
AuthBackend.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() { function renderAccountMenu() {
if (props.account === undefined) { if (props.account === undefined) {
return null; return null;
@ -188,20 +218,7 @@ function ManagementPage(props) {
return ( return (
<React.Fragment> <React.Fragment>
{renderRightDropdown()} {renderRightDropdown()}
<ThemeSelect {renderWidgets()}
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 />
{Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) && {Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) &&
<OrganizationSelect <OrganizationSelect
initValue={Setting.getOrganization()} initValue={Setting.getOrganization()}
@ -323,13 +340,7 @@ function ManagementPage(props) {
} }
} }
const navItems = props.account.organization.navItems; if (navItemsIsAll()) {
if (!Array.isArray(navItems)) {
return res;
}
if (navItems.includes("all")) {
return res; return res;
} }
@ -443,8 +454,6 @@ function ManagementPage(props) {
return Setting.isMobile() || window.location.pathname.startsWith("/trees"); return Setting.isMobile() || window.location.pathname.startsWith("/trees");
} }
const menuStyleRight = Setting.isAdminUser(props.account) && !Setting.isMobile() ? "calc(180px + 280px)" : "320px";
const onClose = () => { const onClose = () => {
setMenuVisible(false); setMenuVisible(false);
}; };
@ -456,34 +465,40 @@ function ManagementPage(props) {
return ( return (
<React.Fragment> <React.Fragment>
<EnableMfaNotification account={props.account} /> <EnableMfaNotification account={props.account} />
<Header style={{padding: "0", marginBottom: "3px", 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"}} >
{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> :
<Menu
onClick={onClose}
items={getMenuItems()}
mode={"horizontal"}
selectedKeys={[props.selectedMenuKey]}
style={{position: "absolute", left: 0, right: menuStyleRight, 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> </Header>
<Content style={{display: "flex", flexDirection: "column"}} > <Content style={{display: "flex", flexDirection: "column"}} >
{isWithoutCard() ? {isWithoutCard() ?

View File

@ -27,6 +27,7 @@ import AccountTable from "./table/AccountTable";
import ThemeEditor from "./common/theme/ThemeEditor"; import ThemeEditor from "./common/theme/ThemeEditor";
import MfaTable from "./table/MfaTable"; import MfaTable from "./table/MfaTable";
import {NavItemTree} from "./common/NavItemTree"; import {NavItemTree} from "./common/NavItemTree";
import {WidgetItemTree} from "./common/WidgetItemTree";
const {Option} = Select; const {Option} = Select;
@ -275,7 +276,7 @@ class OrganizationEditPage extends React.Component {
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.passwordType} onChange={(value => {this.updateOrganizationField("passwordType", value);})} <Select virtual={false} style={{width: "100%"}} value={this.state.organization.passwordType} onChange={(value => {this.updateOrganizationField("passwordType", value);})}
options={["plain", "salt", "sha512-salt", "md5-salt", "bcrypt", "pbkdf2-salt", "argon2id"].map(item => Setting.getOption(item, item))} options={["plain", "salt", "sha512-salt", "md5-salt", "bcrypt", "pbkdf2-salt", "argon2id", "pbkdf2-django"].map(item => Setting.getOption(item, item))}
/> />
</Col> </Col>
</Row> </Row>
@ -537,7 +538,7 @@ class OrganizationEditPage extends React.Component {
</Row> </Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <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>
<Col span={22} > <Col span={22} >
<NavItemTree <NavItemTree
@ -550,6 +551,21 @@ class OrganizationEditPage extends React.Component {
/> />
</Col> </Col>
</Row> </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"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} : {Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :

View File

@ -232,6 +232,15 @@ class PlanEditPage extends React.Component {
[ [
{id: "USD", name: "USD"}, {id: "USD", name: "USD"},
{id: "CNY", name: "CNY"}, {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>) ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>

View File

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

View File

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

View File

@ -29,6 +29,7 @@ import {CaptchaPreview} from "./common/CaptchaPreview";
import {CountryCodeSelect} from "./common/select/CountryCodeSelect"; import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
import * as Web3Auth from "./auth/Web3Auth"; import * as Web3Auth from "./auth/Web3Auth";
import Editor from "./common/Editor"; import Editor from "./common/Editor";
import HttpHeaderTable from "./table/HttpHeaderTable";
const {Option} = Select; const {Option} = Select;
const {TextArea} = Input; const {TextArea} = Input;
@ -41,6 +42,13 @@ const defaultUserMapping = {
avatarUrl: "avatarUrl", avatarUrl: "avatarUrl",
}; };
const defaultEmailMapping = {
fromName: "fromName",
toAddress: "toAddress",
subject: "subject",
content: "content",
};
class ProviderEditPage extends React.Component { class ProviderEditPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -71,7 +79,16 @@ class ProviderEditPage extends React.Component {
if (res.status === "ok") { if (res.status === "ok") {
const provider = res.data; 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({ this.setState({
provider: provider, provider: provider,
}); });
@ -145,9 +162,16 @@ class ProviderEditPage extends React.Component {
const requiredKeys = ["id", "username", "displayName"]; const requiredKeys = ["id", "username", "displayName"];
const provider = this.state.provider; const provider = this.state.provider;
if (value === "" && requiredKeys.includes(key)) { if (provider.type === "Custom HTTP Email") {
Setting.showMessage("error", i18next.t("provider:This field is required")); if (value === "") {
return; 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; provider.userMapping[key] = value;
@ -183,6 +207,30 @@ class ProviderEditPage extends React.Component {
</React.Fragment> </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) { getClientIdLabel(provider) {
switch (provider.category) { switch (provider.category) {
case "OAuth": case "OAuth":
@ -288,10 +336,8 @@ class ProviderEditPage extends React.Component {
default: default:
if (provider.type === "Aliyun Captcha") { if (provider.type === "Aliyun Captcha") {
return Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip")); 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")); 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 { } else {
return Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip")); 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"); text = i18next.t("provider:App Key");
tooltip = i18next.t("provider:App Key - Tooltip"); tooltip = i18next.t("provider:App Key - Tooltip");
} else if (provider.type === "CUCloud") { } else if (provider.type === "CUCloud") {
text = i18next.t("provider:Topic name"); text = "Topic name";
tooltip = i18next.t("provider:Topic name - Tooltip"); tooltip = "Topic name - Tooltip";
} }
} }
@ -567,6 +613,8 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("type", "MetaMask"); this.updateProviderField("type", "MetaMask");
} else if (value === "Notification") { } else if (value === "Notification") {
this.updateProviderField("type", "Telegram"); 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: "SMS", name: "SMS"},
{id: "Storage", name: "Storage"}, {id: "Storage", name: "Storage"},
{id: "Web3", name: "Web3"}, {id: "Web3", name: "Web3"},
{id: "Face ID", name: "Face ID"},
] ]
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
.map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>) .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 === "Web3") ||
(this.state.provider.category === "Storage" && this.state.provider.type === "Local File System") || (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 === "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 : ( (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> <React.Fragment>
{ {
@ -901,7 +951,7 @@ class ProviderEditPage extends React.Component {
</Row> </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> <div>
{["Local File System", "CUCloud"].includes(this.state.provider.type) ? null : ( {["Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
@ -915,7 +965,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </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"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} : {Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
@ -927,7 +977,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </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"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{["Casdoor"].includes(this.state.provider.type) ? {["Casdoor"].includes(this.state.provider.type) ?
@ -941,7 +991,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </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"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} :
@ -953,7 +1003,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </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"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
@ -1094,6 +1144,66 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </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"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Email title"), i18next.t("provider:Email title - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Email title"), i18next.t("provider:Email title - Tooltip"))} :
@ -1162,7 +1272,7 @@ class ProviderEditPage extends React.Component {
</Button> </Button>
</Row> </Row>
</React.Fragment> </React.Fragment>
) : this.state.provider.category === "SMS" ? ( ) : ["SMS"].includes(this.state.provider.category) ? (
<React.Fragment> <React.Fragment>
{["Custom HTTP SMS", "Twilio SMS", "Amazon SNS", "Azure ACS", "Msg91 SMS", "Infobip SMS"].includes(this.state.provider.type) ? {["Custom HTTP SMS", "Twilio SMS", "Amazon SNS", "Azure ACS", "Msg91 SMS", "Infobip SMS"].includes(this.state.provider.type) ?
null : null :
@ -1280,7 +1390,7 @@ class ProviderEditPage extends React.Component {
}} /> }} />
</Col> </Col>
<Col span={16} > <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> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >

View File

@ -416,6 +416,12 @@ export const OtherProviderInfo = {
url: "https://www.cucloud.cn/", 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() { export function initCountries() {
@ -1150,6 +1156,10 @@ export function getProviderTypeOptions(category) {
{id: "Viber", name: "Viber"}, {id: "Viber", name: "Viber"},
{id: "CUCloud", name: "CUCloud"}, {id: "CUCloud", name: "CUCloud"},
]); ]);
} else if (category === "Face ID") {
return ([
{id: "Alibaba Cloud Facebody", name: "Alibaba Cloud Facebody"},
]);
} else { } else {
return []; return [];
} }
@ -1522,7 +1532,7 @@ export function getUserCommonFields() {
} }
export function getDefaultFooterContent() { 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() { export function getEmptyFooterContent() {
@ -1554,7 +1564,7 @@ export function getDefaultHtmlEmailContent() {
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<h3>Casbin Organization</h3> <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> </div>
<p><strong>%{user.friendlyName}</strong>, here is your verification code</p> <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> <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"); return i18next.t("currency:HKD");
} else if (product?.currency === "SGD") { } else if (product?.currency === "SGD") {
return i18next.t("currency:SGD"); return i18next.t("currency:SGD");
} else if (product?.currency === "BRL") {
return i18next.t("currency:BRL");
} else { } else {
return "(Unknown currency)"; return "(Unknown currency)";
} }

View File

@ -37,17 +37,35 @@ class SystemInfo extends React.Component {
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
SystemBackend.getSystemInfo("").then(res => { SystemBackend.getSystemInfo("").then(res => {
this.setState({ this.setState({
systemInfo: res.data,
loading: false, loading: false,
}); });
if (res.status === "ok") {
this.setState({
systemInfo: res.data,
});
} else {
Setting.showMessage("error", res.msg);
this.stopTimer();
}
const id = setInterval(() => { const id = setInterval(() => {
SystemBackend.getSystemInfo("").then(res => { SystemBackend.getSystemInfo("").then(res => {
this.setState({ 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 => { }).catch(error => {
Setting.showMessage("error", `System info failed to get: ${error}`); Setting.showMessage("error", `System info failed to get: ${error}`);
this.stopTimer();
}); });
SystemBackend.getPrometheusInfo().then(res => { SystemBackend.getPrometheusInfo().then(res => {
this.setState({ this.setState({
@ -55,17 +73,25 @@ class SystemInfo extends React.Component {
}); });
}); });
}, 1000 * 2); }, 1000 * 2);
this.setState({intervalId: id}); this.setState({intervalId: id});
}).catch(error => { }).catch(error => {
Setting.showMessage("error", `System info failed to get: ${error}`); Setting.showMessage("error", `System info failed to get: ${error}`);
this.stopTimer();
}); });
SystemBackend.getVersionInfo().then(res => { SystemBackend.getVersionInfo().then(res => {
this.setState({ if (res.status === "ok") {
versionInfo: res.data, this.setState({
}); versionInfo: res.data,
});
} else {
Setting.showMessage("error", res.msg);
this.stopTimer();
}
}).catch(err => { }).catch(err => {
Setting.showMessage("error", `Version info failed to get: ${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()}); this.setState({isTourVisible: TourConfig.getTourVisible()});
}; };
componentWillUnmount() { stopTimer() {
if (this.state.intervalId !== null) { if (this.state.intervalId !== null) {
clearInterval(this.state.intervalId); clearInterval(this.state.intervalId);
} }
}
componentWillUnmount() {
this.stopTimer();
window.removeEventListener("storageTourChanged", this.handleTourChange); window.removeEventListener("storageTourChanged", this.handleTourChange);
} }
@ -125,9 +155,9 @@ class SystemInfo extends React.Component {
<br /> <br /> <br /> <br />
<Progress type="circle" percent={Number((Number(this.state.systemInfo.memoryUsed) / Number(this.state.systemInfo.memoryTotal) * 100).toFixed(2))} /> <Progress type="circle" percent={Number((Number(this.state.systemInfo.memoryUsed) / Number(this.state.systemInfo.memoryTotal) * 100).toFixed(2))} />
</div>; </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"} />; <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"} />; <PrometheusInfoTable prometheusInfo={this.state.prometheusInfo} table={"throughput"} />;
const link = this.state.versionInfo?.version !== "" ? `https://github.com/casdoor/casdoor/releases/tag/${this.state.versionInfo?.version}` : ""; 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"); 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 React from "react";
import * as Setting from "./Setting";
export const TourObj = { export const TourObj = {
home: [ home: [
@ -8,7 +9,7 @@ export const TourObj = {
cover: ( cover: (
<img <img
alt="casdoor.png" 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 <FaceIdTable
title={i18next.t("user:Face IDs")} title={i18next.t("user:Face IDs")}
table={this.state.user.faceIds} table={this.state.user.faceIds}
{...this.props}
onUpdateTable={(table) => {this.updateUserField("faceIds", table);}} onUpdateTable={(table) => {this.updateUserField("faceIds", table);}}
/> />
</Col> </Col>

View File

@ -144,6 +144,9 @@ class WebhookEditPage extends React.Component {
if (["port"].includes(key)) { if (["port"].includes(key)) {
value = Setting.myParseInt(value); value = Setting.myParseInt(value);
} }
if (key === "objectFields") {
value = value.includes("All") ? ["All"] : value;
}
return value; return value;
} }
@ -174,7 +177,16 @@ class WebhookEditPage extends React.Component {
renderWebhook() { renderWebhook() {
const preview = Setting.deepCopy(previewTemplate); const preview = Setting.deepCopy(previewTemplate);
if (this.state.webhook.isUserExtended) { 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); const previewText = JSON.stringify(preview, null, 2);
@ -285,6 +297,19 @@ class WebhookEditPage extends React.Component {
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("webhook:Object fields"), i18next.t("webhook:Object fields - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" showSearch style={{width: "100%"}} value={this.state.webhook.objectFields} onChange={(value => {this.updateWebhookField("objectFields", value);})}>
<Option key="All" value="All">{i18next.t("general:All")}</Option>
{
["owner", "name", "createdTime", "updatedTime", "deletedTime", "id", "displayName"].map((item, index) => <Option key={index} value={item}>{item}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("webhook:Is user extended"), i18next.t("webhook:Is user extended - Tooltip"))} : {Setting.getLabel(i18next.t("webhook:Is user extended"), i18next.t("webhook:Is user extended - Tooltip"))} :
@ -295,6 +320,18 @@ class WebhookEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </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"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} : {Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :

View File

@ -471,6 +471,8 @@ class ForgetPage extends React.Component {
<React.Fragment> <React.Fragment>
<CustomGithubCorner /> <CustomGithubCorner />
<div className="forget-content" style={{padding: Setting.isMobile() ? "0" : null, boxShadow: Setting.isMobile() ? "none" : null}}> <div className="forget-content" style={{padding: Setting.isMobile() ? "0" : null, boxShadow: Setting.isMobile() ? "none" : null}}>
{Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
{Setting.inIframe() || !Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCssMobile}} />}
<Button type="text" <Button type="text"
style={{position: "relative", left: Setting.isMobile() ? "10px" : "-90px", top: 0}} style={{position: "relative", left: Setting.isMobile() ? "10px" : "-90px", top: 0}}
icon={<ArrowLeftOutlined style={{fontSize: "24px"}} />} icon={<ArrowLeftOutlined style={{fontSize: "24px"}} />}

View File

@ -37,6 +37,8 @@ import RedirectForm from "../common/RedirectForm";
import {RequiredMfa} from "./mfa/MfaAuthVerifyForm"; import {RequiredMfa} from "./mfa/MfaAuthVerifyForm";
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton"; import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
import * as ProviderButton from "./ProviderButton"; import * as ProviderButton from "./ProviderButton";
import {goToLink} from "../Setting";
const FaceRecognitionCommonModal = lazy(() => import("../common/modal/FaceRecognitionCommonModal"));
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal")); const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
class LoginPage extends React.Component { class LoginPage extends React.Component {
@ -61,6 +63,8 @@ class LoginPage extends React.Component {
isTermsOfUseVisible: false, isTermsOfUseVisible: false,
termsOfUseContent: "", termsOfUseContent: "",
orgChoiceMode: new URLSearchParams(props.location?.search).get("orgChoiceMode") ?? null, orgChoiceMode: new URLSearchParams(props.location?.search).get("orgChoiceMode") ?? null,
userLang: null,
loginLoading: false,
}; };
if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) { if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) {
@ -262,6 +266,13 @@ class LoginPage extends React.Component {
onUpdateApplication(application) { onUpdateApplication(application) {
this.props.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) { parseOffset(offset) {
@ -355,6 +366,7 @@ class LoginPage extends React.Component {
} }
onFinish(values) { onFinish(values) {
this.setState({loginLoading: true});
if (this.state.loginMethod === "webAuthn") { if (this.state.loginMethod === "webAuthn") {
let username = this.state.username; let username = this.state.username;
if (username === null || username === "") { if (username === null || username === "") {
@ -379,6 +391,9 @@ class LoginPage extends React.Component {
}).then(res => res.json()) }).then(res => res.json())
.then((res) => { .then((res) => {
if (res.status === "error") { if (res.status === "error") {
this.setState({
loginLoading: false,
});
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
return; return;
} }
@ -415,6 +430,7 @@ class LoginPage extends React.Component {
login(values) { login(values) {
// here we are supposed to determine whether Casdoor is working as an OAuth server or CAS server // 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") { if (this.state.type === "cas") {
// CAS // CAS
const casParams = Util.getCasParameters(); const casParams = Util.getCasParameters();
@ -442,6 +458,8 @@ class LoginPage extends React.Component {
} else { } else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
} }
}).finally(() => {
this.setState({loginLoading: false});
}); });
} else { } else {
// OAuth // OAuth
@ -497,6 +515,8 @@ class LoginPage extends React.Component {
} else { } else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
} }
}).finally(() => {
this.setState({loginLoading: false});
}); });
} }
} }
@ -566,7 +586,7 @@ class LoginPage extends React.Component {
return ( return (
<div key={resultItemKey} className="login-languages"> <div key={resultItemKey} className="login-languages">
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} /> <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> </div>
); );
} else if (signinItem.name === "Signin methods") { } else if (signinItem.name === "Signin methods") {
@ -578,6 +598,9 @@ class LoginPage extends React.Component {
) )
; ;
} else if (signinItem.name === "Username") { } else if (signinItem.name === "Username") {
if (this.state.loginMethod === "webAuthn") {
return null;
}
return ( return (
<div key={resultItemKey}> <div key={resultItemKey}>
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} /> <div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
@ -684,6 +707,7 @@ class LoginPage extends React.Component {
<Form.Item key={resultItemKey} className="login-button-box"> <Form.Item key={resultItemKey} className="login-button-box">
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} /> <div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
<Button <Button
loading={this.state.loginLoading}
type="primary" type="primary"
htmlType="submit" htmlType="submit"
className="login-button" className="login-button"
@ -696,19 +720,25 @@ class LoginPage extends React.Component {
</Button> </Button>
{ {
this.state.loginMethod === "faceId" ? this.state.loginMethod === "faceId" ?
<Suspense fallback={null}> this.state.haveFaceIdProvider ? <Suspense fallback={null}><FaceRecognitionCommonModal visible={this.state.openFaceRecognitionModal} onOk={(FaceIdImage) => {
<FaceRecognitionModal const values = this.state.values;
visible={this.state.openFaceRecognitionModal} values["FaceIdImage"] = FaceIdImage;
onOk={(faceId) => { this.login(values);
const values = this.state.values; this.setState({openFaceRecognitionModal: false});
values["faceId"] = faceId; }} 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.login(values);
this.setState({openFaceRecognitionModal: false}); this.setState({openFaceRecognitionModal: false});
}} }}
onCancel={() => this.setState({openFaceRecognitionModal: false})} onCancel={() => this.setState({openFaceRecognitionModal: false, loginLoading: false})}
/> />
</Suspense> </Suspense>
: :
<> <>
</> </>
@ -723,6 +753,8 @@ class LoginPage extends React.Component {
if (signinItem.rule === "None" || signinItem.rule === "") { if (signinItem.rule === "None" || signinItem.rule === "") {
signinItem.rule = showForm ? "small" : "big"; signinItem.rule = showForm ? "small" : "big";
} }
const searchParams = new URLSearchParams(window.location.search);
const providerHint = searchParams.get("provider_hint");
return ( return (
<div key={resultItemKey}> <div key={resultItemKey}>
@ -730,6 +762,10 @@ class LoginPage extends React.Component {
<Form.Item> <Form.Item>
{ {
application.providers.filter(providerItem => this.isProviderVisible(providerItem)).map((providerItem, id) => { application.providers.filter(providerItem => this.isProviderVisible(providerItem)).map((providerItem, id) => {
if (providerHint === providerItem.provider.name) {
goToLink(Provider.getAuthUrl(application, providerItem.provider, "signup"));
return;
}
return ( return (
<span key={id} onClick={(e) => { <span key={id} onClick={(e) => {
const agreementChecked = this.form.current.getFieldValue("agreement"); const agreementChecked = this.form.current.getFieldValue("agreement");
@ -805,7 +841,6 @@ class LoginPage extends React.Component {
<Form <Form
name="normal_login" name="normal_login"
initialValues={{ initialValues={{
organization: application.organization, organization: application.organization,
application: application.name, application: application.name,
autoSignin: true, autoSignin: true,
@ -908,7 +943,7 @@ class LoginPage extends React.Component {
this.login(values); this.login(values);
this.setState({openCaptchaModal: false}); this.setState({openCaptchaModal: false});
}} }}
onCancel={() => this.setState({openCaptchaModal: false})} onCancel={() => this.setState({openCaptchaModal: false, loginLoading: false})}
isCurrentProvider={true} isCurrentProvider={true}
/>; />;
} }
@ -975,7 +1010,7 @@ class LoginPage extends React.Component {
const oAuthParams = Util.getOAuthGetParameters(); const oAuthParams = Util.getOAuthGetParameters();
this.populateOauthValues(values); this.populateOauthValues(values);
const application = this.getApplicationObj(); 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", method: "GET",
credentials: "include", credentials: "include",
}) })
@ -985,11 +1020,7 @@ class LoginPage extends React.Component {
Setting.showMessage("error", credentialRequestOptions.msg); Setting.showMessage("error", credentialRequestOptions.msg);
throw credentialRequestOptions.status.msg; throw credentialRequestOptions.status.msg;
} }
credentialRequestOptions.publicKey.challenge = UserWebauthnBackend.webAuthnBufferDecode(credentialRequestOptions.publicKey.challenge); credentialRequestOptions.publicKey.challenge = UserWebauthnBackend.webAuthnBufferDecode(credentialRequestOptions.publicKey.challenge);
credentialRequestOptions.publicKey.allowCredentials.forEach(function(listItem) {
listItem.id = UserWebauthnBackend.webAuthnBufferDecode(listItem.id);
});
return navigator.credentials.get({ return navigator.credentials.get({
publicKey: credentialRequestOptions.publicKey, publicKey: credentialRequestOptions.publicKey,
@ -1039,6 +1070,12 @@ class LoginPage extends React.Component {
.catch(error => { .catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}${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 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; let scope = authInfo[provider.type].scope;
const isShortState = (provider.type === "WeChat" && navigator.userAgent.includes("MicroMessenger")) || (provider.type === "Twitter"); const isShortState = (provider.type === "WeChat" && navigator.userAgent.includes("MicroMessenger")) || (provider.type === "Twitter");
const state = Util.getStateFromQueryParams(application.name, provider.name, method, isShortState); 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); endpoint = endpoint.replace("common", provider.domain);
} }
} else if (provider.type === "Apple") { } else if (provider.type === "Apple") {
redirectUri = `${window.location.origin}/api/callback`; redirectUri = `${redirectOrigin}/api/callback`;
} else if (provider.type === "Google" && provider.disableSsl) { } else if (provider.type === "Google" && provider.disableSsl) {
scope += "+https://www.googleapis.com/auth/user.phonenumbers.read"; 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") { } 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`; 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") { } 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") { } else if (provider.type === "WeChat") {
if (navigator.userAgent.includes("MicroMessenger")) { 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`; return `${authInfo[provider.type].mpEndpoint}?appid=${provider.clientId2}&redirect_uri=${redirectUri}&state=${state}&scope=${authInfo[provider.type].mpScope}&response_type=code#wechat_redirect`;
} else { } else {
if (provider.clientId2 && provider?.disableSsl && provider?.signName === "media") { 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`; 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") { } 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`; 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") { } 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") { } else if (provider.type === "Okta") {
return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`; 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") { } else if (provider.type === "Douyin" || provider.type === "TikTok") {

View File

@ -35,8 +35,8 @@ const GridCards = (props) => {
{items.map(item => <SingleCard key={item.link} logo={item.logo} link={item.link} title={item.name} desc={item.description} isSingle={items.length === 1} />)} {items.map(item => <SingleCard key={item.link} logo={item.logo} link={item.link} title={item.name} desc={item.description} isSingle={items.length === 1} />)}
</Card> </Card>
) : ( ) : (
<div style={{margin: "0 15px"}}> <div style={{width: "100%", padding: "0 100px"}}>
<Row> <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} />)} {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> </Row>
</div> </div>

View File

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

View File

@ -51,6 +51,8 @@ function testEmailProvider(provider, email = "") {
receivers: email === "" ? ["TestSmtpServer"] : [email], receivers: email === "" ? ["TestSmtpServer"] : [email],
provider: provider.name, provider: provider.name,
providerObject: provider, providerObject: provider,
owner: provider.owner,
name: provider.name,
}; };
return fetch(`${Setting.ServerUrl}/api/send-email`, { return fetch(`${Setting.ServerUrl}/api/send-email`, {

View File

@ -16,7 +16,7 @@ import * as Setting from "../Setting";
import i18next from "i18next"; import i18next from "i18next";
export function sendTestNotification(provider) { export function sendTestNotification(provider) {
testNotificationProvider(provider.content, provider.name) testNotificationProvider(provider)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully sent")); Setting.showMessage("success", i18next.t("general:Successfully sent"));
@ -29,12 +29,14 @@ export function sendTestNotification(provider) {
}); });
} }
function testNotificationProvider(content, name) { function testNotificationProvider(provider) {
const notificationForm = { const notificationForm = {
content: content, content: provider.content,
owner: provider.owner,
name: provider.name,
}; };
return fetch(`${Setting.ServerUrl}/api/send-notification?provider=${name}`, { return fetch(`${Setting.ServerUrl}/api/send-notification?provider=${provider.name}`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
body: JSON.stringify(notificationForm), body: JSON.stringify(notificationForm),

View File

@ -33,6 +33,8 @@ function testSmsProvider(provider, phone = "") {
const SmsForm = { const SmsForm = {
content: "123456", content: "123456",
receivers: [phone], receivers: [phone],
owner: provider.owner,
name: provider.name,
}; };
return fetch(`${Setting.ServerUrl}/api/send-sms?provider=` + provider.name, { return fetch(`${Setting.ServerUrl}/api/send-sms?provider=` + provider.name, {

View File

@ -0,0 +1,29 @@
import i18next from "i18next";
import {Tree} from "antd";
import React from "react";
export const WidgetItemTree = ({disabled, checkedKeys, defaultExpandedKeys, onCheck}) => {
const WidgetItemNodes = [
{
title: i18next.t("organization:All"),
key: "all",
children: [
{title: i18next.t("general:Tour"), key: "tour"},
{title: i18next.t("general:AI Assistant"), key: "ai-assistant"},
{title: i18next.t("user:Language"), key: "language"},
{title: i18next.t("theme:Theme"), key: "theme"},
],
},
];
return (
<Tree
disabled={disabled}
checkable
checkedKeys={checkedKeys}
defaultExpandedKeys={defaultExpandedKeys}
onCheck={onCheck}
treeData={WidgetItemNodes}
/>
);
};

View File

@ -0,0 +1,177 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Button, Modal, Progress, message} from "antd";
import React, {useState} from "react";
import i18next from "i18next";
const FaceRecognitionCommonModal = (props) => {
const {visible, onOk, onCancel} = props;
const videoRef = React.useRef();
const canvasRef = React.useRef();
const [percent, setPercent] = useState(0);
const mediaStreamRef = React.useRef(null);
const [isCameraCaptured, setIsCameraCaptured] = useState(false);
const [capturedImageArray, setCapturedImageArray] = useState([]);
React.useEffect(() => {
if (isCameraCaptured) {
let count = 0;
let count2 = 0;
const interval = setInterval(() => {
count++;
if (videoRef.current) {
videoRef.current.srcObject = mediaStreamRef.current;
videoRef.current.play();
const interval2 = setInterval(() => {
if (!visible) {
clearInterval(interval);
setPercent(0);
}
count2++;
if (count2 >= 8) {
clearInterval(interval2);
setPercent(0);
onOk(capturedImageArray);
} else if (count2 > 3) {
setPercent((count2 - 4) * 20);
const canvas = document.createElement("canvas");
canvas.width = videoRef.current.videoWidth;
canvas.height = videoRef.current.videoHeight;
const context = canvas.getContext("2d");
context.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
const b64 = canvas.toDataURL("image/png");
capturedImageArray.push(b64);
setCapturedImageArray(capturedImageArray);
}
}, 1000);
clearInterval(interval);
}
if (count >= 30) {
clearInterval(interval);
}
}, 100);
} else {
mediaStreamRef.current?.getTracks().forEach(track => track.stop());
if (videoRef.current) {
videoRef.current.srcObject = null;
}
}
}, [isCameraCaptured]);
React.useEffect(() => {
if (visible) {
navigator.mediaDevices
.getUserMedia({video: {facingMode: "user"}})
.then((stream) => {
mediaStreamRef.current = stream;
setIsCameraCaptured(true);
}).catch((error) => {
handleCameraError(error);
});
} else {
setIsCameraCaptured(false);
setCapturedImageArray([]);
}
}, [visible]);
const handleCameraError = (error) => {
if (error instanceof DOMException) {
if (error.name === "NotFoundError" || error.name === "DevicesNotFoundError") {
message.error(i18next.t("login:Please ensure that you have a camera device for facial recognition"));
} else if (error.name === "NotAllowedError" || error.name === "PermissionDeniedError") {
message.error(i18next.t("login:Please provide permission to access the camera"));
} else if (error.name === "NotReadableError" || error.name === "TrackStartError") {
message.error(i18next.t("login:The camera is currently in use by another webpage"));
} else if (error.name === "TypeError") {
message.error(i18next.t("login:Please load the webpage using HTTPS, otherwise the camera cannot be accessed"));
} else {
message.error(error.message);
}
}
};
return <div>
<Modal
closable={false}
maskClosable={false}
title={i18next.t("login:Face Recognition")}
width={350}
footer={[
<Button key="ok" type={"primary"} disabled={capturedImageArray.length === 0} onClick={() => {
onOk(capturedImageArray);
}}>
Ok
</Button>,
<Button key="back" onClick={onCancel}>
Cancel
</Button>,
]}
destroyOnClose={true}
open={visible}>
<Progress percent={percent} />
<div style={{
marginTop: "20px",
marginBottom: "50px",
justifyContent: "center",
alignContent: "center",
position: "relative",
flexDirection: "column",
}}>
{
<div style={{display: "flex", justifyContent: "center", alignContent: "center"}}>
<video
ref={videoRef}
style={{
borderRadius: "50%",
height: "220px",
verticalAlign: "middle",
width: "220px",
objectFit: "cover",
}}
></video>
<div style={{
position: "absolute",
width: "240px",
height: "240px",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
}}>
<svg width="240" height="240" fill="none">
<circle
strokeDasharray="700"
strokeDashoffset={700 - 6.9115 * percent}
strokeWidth="4"
cx="120"
cy="120"
r="110"
stroke="#5734d3"
transform="rotate(-90, 120, 120)"
strokeLinecap="round"
style={{transition: "all .2s linear"}}
></circle>
</svg>
</div>
<canvas ref={canvasRef} style={{position: "absolute"}} />
</div>
}
</div>
</Modal>
</div>;
};
export default FaceRecognitionCommonModal;

View File

@ -14,11 +14,13 @@
import * as faceapi from "face-api.js"; import * as faceapi from "face-api.js";
import React, {useState} from "react"; import React, {useState} from "react";
import {Button, Modal, Progress, Spin, message} from "antd"; import {Button, Modal, Progress, Space, Spin, message} from "antd";
import i18next from "i18next"; import i18next from "i18next";
import Dragger from "antd/es/upload/Dragger";
import * as Setting from "../../Setting";
const FaceRecognitionModal = (props) => { const FaceRecognitionModal = (props) => {
const {visible, onOk, onCancel} = props; const {visible, onOk, onCancel, withImage} = props;
const [modelsLoaded, setModelsLoaded] = React.useState(false); const [modelsLoaded, setModelsLoaded] = React.useState(false);
const [isCameraCaptured, setIsCameraCaptured] = useState(false); const [isCameraCaptured, setIsCameraCaptured] = useState(false);
@ -28,12 +30,14 @@ const FaceRecognitionModal = (props) => {
const mediaStreamRef = React.useRef(null); const mediaStreamRef = React.useRef(null);
const [percent, setPercent] = useState(0); const [percent, setPercent] = useState(0);
const [files, setFiles] = useState([]);
const [currentFaceId, setCurrentFaceId] = React.useState();
const [currentFaceIndex, setCurrentFaceIndex] = React.useState();
React.useEffect(() => { React.useEffect(() => {
const loadModels = async() => { const loadModels = async() => {
// const MODEL_URL = process.env.PUBLIC_URL + "/models";
// const MODEL_URL = "https://justadudewhohacks.github.io/face-api.js/models"; // const MODEL_URL = "https://justadudewhohacks.github.io/face-api.js/models";
// const MODEL_URL = "https://cdn.casbin.org/site/casdoor/models"; const MODEL_URL = `${Setting.StaticBaseUrl}/casdoor/models`;
const MODEL_URL = "https://cdn.casdoor.com/casdoor/models";
Promise.all([ Promise.all([
faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL), faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),
@ -50,6 +54,9 @@ const FaceRecognitionModal = (props) => {
}, []); }, []);
React.useEffect(() => { React.useEffect(() => {
if (withImage) {
return;
}
if (visible) { if (visible) {
setPercent(0); setPercent(0);
if (modelsLoaded) { if (modelsLoaded) {
@ -75,6 +82,9 @@ const FaceRecognitionModal = (props) => {
}, [visible, modelsLoaded]); }, [visible, modelsLoaded]);
React.useEffect(() => { React.useEffect(() => {
if (withImage) {
return;
}
if (isCameraCaptured) { if (isCameraCaptured) {
let count = 0; let count = 0;
const interval = setInterval(() => { const interval = setInterval(() => {
@ -98,6 +108,9 @@ const FaceRecognitionModal = (props) => {
}, [isCameraCaptured]); }, [isCameraCaptured]);
const handleStreamVideo = () => { const handleStreamVideo = () => {
if (withImage) {
return;
}
let count = 0; let count = 0;
let goodCount = 0; let goodCount = 0;
if (!detection.current) { if (!detection.current) {
@ -148,73 +161,163 @@ const FaceRecognitionModal = (props) => {
} }
}; };
return ( const getBase64 = (file) => {
<div> return new Promise((resolve, reject) => {
<Modal const reader = new FileReader();
closable={false} reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
};
if (!withImage) {
return (
<div>
<Modal
closable={false}
maskClosable={false}
destroyOnClose={true}
open={visible && isCameraCaptured}
title={i18next.t("login:Face Recognition")}
width={350}
footer={[
<Button key="back" onClick={onCancel}>
Cancel
</Button>,
]}
>
<Progress percent={percent} />
<div style={{
marginTop: "20px",
marginBottom: "50px",
justifyContent: "center",
alignContent: "center",
position: "relative",
flexDirection: "column",
}}>
{
modelsLoaded ?
<div style={{display: "flex", justifyContent: "center", alignContent: "center"}}>
<video
ref={videoRef}
onPlay={handleStreamVideo}
style={{
borderRadius: "50%",
height: "220px",
verticalAlign: "middle",
width: "220px",
objectFit: "cover",
}}
></video>
<div style={{
position: "absolute",
width: "240px",
height: "240px",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
}}>
<svg width="240" height="240" fill="none">
<circle
strokeDasharray="700"
strokeDashoffset={700 - 6.9115 * percent}
strokeWidth="4"
cx="120"
cy="120"
r="110"
stroke="#5734d3"
transform="rotate(-90, 120, 120)"
strokeLinecap="round"
style={{transition: "all .2s linear"}}
></circle>
</svg>
</div>
<canvas ref={canvasRef} style={{position: "absolute"}} />
</div>
:
<div>
<Spin tip={i18next.t("login:Loading")} size="large"
style={{display: "flex", justifyContent: "center", alignContent: "center"}}>
<div className="content" />
</Spin>
</div>
}
</div>
</Modal>
</div>
);
} else {
return <div>
<Modal closable={false}
maskClosable={false} maskClosable={false}
destroyOnClose={true} destroyOnClose={true}
open={visible && isCameraCaptured} open={visible}
title={i18next.t("login:Face Recognition")} title={i18next.t("login:Face Recognition")}
width={350} width={350}
footer={[ footer={[
<Button key="back" onClick={onCancel}> <Button key="ok" type={"primary"} disabled={!currentFaceId || currentFaceId?.length === 0} onClick={() => {
Cancel onOk(Array.from(currentFaceId.descriptor));
}}>
Ok
</Button>, </Button>,
]} <Button key="back" onClick={onCancel}>
> Cancel
<Progress percent={percent} /> </Button>,
<div style={{marginTop: "20px", marginBottom: "50px", justifyContent: "center", alignContent: "center", position: "relative", flexDirection: "column"}}> ]}>
<Space direction={"vertical"} style={{width: "100%"}}>
<Dragger
multiple={true}
defaultFileList={files}
style={{width: "100%"}}
beforeUpload={(file) => {
getBase64(file).then(res => {
file.base64 = res;
files.push(file);
});
setCurrentFaceId([]);
return false;
}}
onRemove={(file) => {
const index = files.indexOf(file);
const newFileList = files.slice();
newFileList.splice(index, 1);
setFiles(newFileList);
setCurrentFaceId([]);
}}
>
<p>{i18next.t("general:Click to Upload")}</p>
</Dragger >
{ {
modelsLoaded ? modelsLoaded ? <Button style={{width: "100%"}} onClick={async() => {
<div style={{display: "flex", justifyContent: "center", alignContent: "center"}}> let maxScore = 0;
<video for (const file of files) {
ref={videoRef} const fileIndex = files.indexOf(file);
onPlay={handleStreamVideo} const img = new Image();
style={{ img.src = file.base64;
borderRadius: "50%", const faceIds = await faceapi.detectAllFaces(img, new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks().withFaceDescriptors();
height: "220px", if (faceIds[0]?.detection.score > 0.9 && faceIds[0]?.detection.score > maxScore) {
verticalAlign: "middle", maxScore = faceIds[0]?.detection.score;
width: "220px", setCurrentFaceId(faceIds[0]);
objectFit: "cover", setCurrentFaceIndex(fileIndex);
}} }
></video> }
<div style={{ if (maxScore < 0.9) {
position: "absolute", message.error(i18next.t("login:Face recognition failed"));
width: "240px", }
height: "240px", }}> {i18next.t("application:Generate Face ID")}</Button> : null
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
}}>
<svg width="240" height="240" fill="none">
<circle
strokeDasharray="700"
strokeDashoffset={700 - 6.9115 * percent}
strokeWidth="4"
cx="120"
cy="120"
r="110"
stroke="#5734d3"
transform="rotate(-90, 120, 120)"
strokeLinecap="round"
style={{transition: "all .2s linear"}}
></circle>
</svg>
</div>
<canvas ref={canvasRef} style={{position: "absolute"}} />
</div>
:
<div>
<Spin tip={i18next.t("login:Loading")} size="large" style={{display: "flex", justifyContent: "center", alignContent: "center"}}>
<div className="content" />
</Spin>
</div>
} }
</div> </Space>
{
currentFaceId && currentFaceId.length !== 0 ? (
<React.Fragment>
<div>{i18next.t("application:Select")}:{files[currentFaceIndex]?.name}</div>
<div><img src={files[currentFaceIndex]?.base64} alt="selected" style={{width: "100%"}} /></div>
</React.Fragment>
) : null
}
</Modal> </Modal>
</div> </div>;
); }
}; };
export default FaceRecognitionModal; export default FaceRecognitionModal;

View File

@ -30,6 +30,7 @@ class LanguageSelect extends React.Component {
this.state = { this.state = {
classes: props, classes: props,
languages: props.languages ?? Setting.Countries.map(item => item.key), languages: props.languages ?? Setting.Countries.map(item => item.key),
onClick: props.onClick,
}; };
Setting.Countries.forEach((country) => { Setting.Countries.forEach((country) => {
@ -50,6 +51,9 @@ class LanguageSelect extends React.Component {
render() { render() {
const languageItems = this.getOrganizationLanguages(this.state.languages); const languageItems = this.getOrganizationLanguages(this.state.languages);
const onClick = (e) => { const onClick = (e) => {
if (typeof this.state.onClick === "function") {
this.state.onClick(e.key);
}
Setting.setLanguage(e.key); Setting.setLanguage(e.key);
}; };

View File

@ -17,11 +17,15 @@
"Use same DB - Tooltip": "Use same DB - Tooltip" "Use same DB - Tooltip": "Use same DB - Tooltip"
}, },
"application": { "application": {
"Add Face ID": "Add Face ID",
"Add Face ID with Image": "Add Face ID with Image",
"Always": "Always", "Always": "Always",
"Auto signin": "Auto signin", "Auto signin": "Auto signin",
"Auto signin - Tooltip": "When a logged-in session exists in Casdoor, it is automatically used for application-side login", "Auto signin - Tooltip": "When a logged-in session exists in Casdoor, it is automatically used for application-side login",
"Background URL": "Background URL", "Background URL": "Background URL",
"Background URL - Tooltip": "URL of the background image used in the login page", "Background URL - Tooltip": "URL of the background image used in the login page",
"Background URL Mobile": "Background URL Mobile",
"Background URL Mobile - Tooltip": "Background URL Mobile - Tooltip",
"Big icon": "Big icon", "Big icon": "Big icon",
"Binding providers": "Binding providers", "Binding providers": "Binding providers",
"CSS style": "CSS style", "CSS style": "CSS style",
@ -61,8 +65,10 @@
"Footer HTML": "Footer HTML", "Footer HTML": "Footer HTML",
"Footer HTML - Edit": "Footer HTML - Edit", "Footer HTML - Edit": "Footer HTML - Edit",
"Footer HTML - Tooltip": "Custom the footer of your application", "Footer HTML - Tooltip": "Custom the footer of your application",
"Forced redirect origin": "Forced redirect origin",
"Form position": "Form position", "Form position": "Form position",
"Form position - Tooltip": "Location of the signup, signin and forget password forms", "Form position - Tooltip": "Location of the signup, signin and forget password forms",
"Generate Face ID": "Generate Face ID",
"Grant types": "Grant types", "Grant types": "Grant types",
"Grant types - Tooltip": "Select which grant types are allowed in the OAuth protocol", "Grant types - Tooltip": "Select which grant types are allowed in the OAuth protocol",
"Header HTML": "Header HTML", "Header HTML": "Header HTML",
@ -163,6 +169,7 @@
}, },
"currency": { "currency": {
"AUD": "AUD", "AUD": "AUD",
"BRL": "BRL",
"CAD": "CAD", "CAD": "CAD",
"CHF": "CHF", "CHF": "CHF",
"CNY": "CNY", "CNY": "CNY",
@ -189,6 +196,7 @@
"Verify": "Verify" "Verify": "Verify"
}, },
"general": { "general": {
"AI Assistant": "AI Assistant",
"API key": "API key", "API key": "API key",
"API key - Tooltip": "API key - Tooltip", "API key - Tooltip": "API key - Tooltip",
"Access key": "Access key", "Access key": "Access key",
@ -201,7 +209,6 @@
"Adapter - Tooltip": "Table name of the policy store", "Adapter - Tooltip": "Table name of the policy store",
"Adapters": "Adapters", "Adapters": "Adapters",
"Add": "Add", "Add": "Add",
"Add Face Id": "Add Face Id",
"Add custom item": "Add custom item", "Add custom item": "Add custom item",
"Admin": "Admin", "Admin": "Admin",
"Affiliation URL": "Affiliation URL", "Affiliation URL": "Affiliation URL",
@ -231,6 +238,7 @@
"Created time": "Created time", "Created time": "Created time",
"Custom": "Custom", "Custom": "Custom",
"Dashboard": "Dashboard", "Dashboard": "Dashboard",
"Data": "Data",
"Default": "Default", "Default": "Default",
"Default application": "Default application", "Default application": "Default application",
"Default application - Tooltip": "Default application for users registered directly from the organization page", "Default application - Tooltip": "Default application for users registered directly from the organization page",
@ -241,6 +249,7 @@
"Delete": "Delete", "Delete": "Delete",
"Description": "Description", "Description": "Description",
"Description - Tooltip": "Detailed description information for reference, Casdoor itself will not use it", "Description - Tooltip": "Detailed description information for reference, Casdoor itself will not use it",
"Detail": "详情",
"Disable": "Disable", "Disable": "Disable",
"Display name": "Display name", "Display name": "Display name",
"Display name - Tooltip": "A user-friendly, easily readable name displayed publicly in the UI", "Display name - Tooltip": "A user-friendly, easily readable name displayed publicly in the UI",
@ -258,7 +267,6 @@
"Enabled": "Enabled", "Enabled": "Enabled",
"Enabled successfully": "Enabled successfully", "Enabled successfully": "Enabled successfully",
"Enforcers": "Enforcers", "Enforcers": "Enforcers",
"FaceIdData": "FaceIdData",
"Failed to add": "Failed to add", "Failed to add": "Failed to add",
"Failed to connect to server": "Failed to connect to server", "Failed to connect to server": "Failed to connect to server",
"Failed to delete": "Failed to delete", "Failed to delete": "Failed to delete",
@ -271,6 +279,7 @@
"Favicon": "Favicon", "Favicon": "Favicon",
"Favicon - Tooltip": "Favicon icon URL used in all Casdoor pages of the organization", "Favicon - Tooltip": "Favicon icon URL used in all Casdoor pages of the organization",
"First name": "First name", "First name": "First name",
"Forced redirect origin - Tooltip": "Forced redirect origin - Tooltip",
"Forget URL": "Forget URL", "Forget URL": "Forget URL",
"Forget URL - Tooltip": "Custom URL for the \"Forget password\" page. If not set, the default Casdoor \"Forget password\" page will be used. When set, the \"Forget password\" link on the login page will redirect to this URL", "Forget URL - Tooltip": "Custom URL for the \"Forget password\" page. If not set, the default Casdoor \"Forget password\" page will be used. When set, the \"Forget password\" link on the login page will redirect to this URL",
"Found some texts still not translated? Please help us translate at": "Found some texts still not translated? Please help us translate at", "Found some texts still not translated? Please help us translate at": "Found some texts still not translated? Please help us translate at",
@ -344,6 +353,7 @@
"Phone - Tooltip": "Phone number", "Phone - Tooltip": "Phone number",
"Phone only": "Phone only", "Phone only": "Phone only",
"Phone or Email": "Phone or Email", "Phone or Email": "Phone or Email",
"Plain": "Plain",
"Plan": "Plan", "Plan": "Plan",
"Plan - Tooltip": "Plan - Tooltip", "Plan - Tooltip": "Plan - Tooltip",
"Plans": "Plans", "Plans": "Plans",
@ -362,6 +372,7 @@
"QR code is too large": "QR code is too large", "QR code is too large": "QR code is too large",
"Real name": "Real name", "Real name": "Real name",
"Records": "Records", "Records": "Records",
"Request": "Request",
"Request URI": "Request URI", "Request URI": "Request URI",
"Resources": "Resources", "Resources": "Resources",
"Role": "Role", "Role": "Role",
@ -412,6 +423,7 @@
"This is a read-only demo site!": "This is a read-only demo site!", "This is a read-only demo site!": "This is a read-only demo site!",
"Timestamp": "Timestamp", "Timestamp": "Timestamp",
"Tokens": "Tokens", "Tokens": "Tokens",
"Tour": "Tour",
"Transactions": "Transactions", "Transactions": "Transactions",
"Type": "Type", "Type": "Type",
"Type - Tooltip": "Type - Tooltip", "Type - Tooltip": "Type - Tooltip",
@ -442,7 +454,8 @@
"Parent group - Tooltip": "Parent group - Tooltip", "Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical", "Physical": "Physical",
"Show all": "Show all", "Show all": "Show all",
"Virtual": "Virtual" "Virtual": "Virtual",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page"
}, },
"home": { "home": {
"New users past 30 days": "New users past 30 days", "New users past 30 days": "New users past 30 days",
@ -461,13 +474,16 @@
"Quota": "Quota", "Quota": "Quota",
"Quota - Tooltip": "Quota - Tooltip", "Quota - Tooltip": "Quota - Tooltip",
"Used count": "Used count", "Used count": "Used count",
"Used count - Tooltip": "Used count - Tooltip" "Used count - Tooltip": "Used count - Tooltip",
"You need to first specify a default application for organization: ": "You need to first specify a default application for organization: "
}, },
"ldap": { "ldap": {
"Admin": "Admin", "Admin": "Admin",
"Admin - Tooltip": "CN or ID of the LDAP server administrator", "Admin - Tooltip": "CN or ID of the LDAP server administrator",
"Admin Password": "Admin Password", "Admin Password": "Admin Password",
"Admin Password - Tooltip": "LDAP server administrator password", "Admin Password - Tooltip": "LDAP server administrator password",
"Allow self-signed certificate": "Allow self-signed certificate",
"Allow self-signed certificate - Tooltip": "Allow self-signed certificate - Tooltip",
"Auto Sync": "Auto Sync", "Auto Sync": "Auto Sync",
"Auto Sync - Tooltip": "Auto-sync configuration, disabled at 0", "Auto Sync - Tooltip": "Auto-sync configuration, disabled at 0",
"Base DN": "Base DN", "Base DN": "Base DN",
@ -586,11 +602,6 @@
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"
}, },
"mfaAccount": {
"Account Name": "Account Name",
"Issuer": "Issuer",
"Secret Key": "Secret Key"
},
"model": { "model": {
"Advanced Editor": "Advanced Editor", "Advanced Editor": "Advanced Editor",
"Basic Editor": "Basic Editor", "Basic Editor": "Basic Editor",
@ -610,6 +621,8 @@
"Is profile public": "Is profile public", "Is profile public": "Is profile public",
"Is profile public - Tooltip": "After being closed, only global administrators or users in the same organization can access the user's profile page", "Is profile public - Tooltip": "After being closed, only global administrators or users in the same organization can access the user's profile page",
"Modify rule": "Modify rule", "Modify rule": "Modify rule",
"Navbar items": "Navbar items",
"Navbar items - Tooltip": "Navbar items - Tooltip",
"New Organization": "New Organization", "New Organization": "New Organization",
"Optional": "Optional", "Optional": "Optional",
"Password expire days": "Password expire days", "Password expire days": "Password expire days",
@ -622,10 +635,14 @@
"Tags - Tooltip": "Collection of tags available for users to choose from", "Tags - Tooltip": "Collection of tags available for users to choose from",
"Use Email as username": "Use Email as username", "Use Email as username": "Use Email as username",
"Use Email as username - Tooltip": "Use Email as username if the username field is not visible at signup", "Use Email as username - Tooltip": "Use Email as username if the username field is not visible at signup",
"User types": "User types",
"User types - Tooltip": "User types - Tooltip",
"View rule": "View rule", "View rule": "View rule",
"Visible": "Visible", "Visible": "Visible",
"Website URL": "Website URL", "Website URL": "Website URL",
"Website URL - Tooltip": "The homepage URL of the organization. This field is not used in Casdoor" "Website URL - Tooltip": "The homepage URL of the organization. This field is not used in Casdoor",
"Widget items": "Widget items",
"Widget items - Tooltip": "Widget items - Tooltip"
}, },
"payment": { "payment": {
"Confirm your invoice information": "Confirm your invoice information", "Confirm your invoice information": "Confirm your invoice information",
@ -728,6 +745,7 @@
"paid-user do not have active subscription or pending subscription, please select a plan to buy": "paid-user do not have active subscription or pending subscription, please select a plan to buy" "paid-user do not have active subscription or pending subscription, please select a plan to buy": "paid-user do not have active subscription or pending subscription, please select a plan to buy"
}, },
"product": { "product": {
"AirWallex": "AirWallex",
"Alipay": "Alipay", "Alipay": "Alipay",
"Buy": "Buy", "Buy": "Buy",
"Buy Product": "Buy Product", "Buy Product": "Buy Product",
@ -757,7 +775,6 @@
"Sold": "Sold", "Sold": "Sold",
"Sold - Tooltip": "Quantity sold", "Sold - Tooltip": "Quantity sold",
"Stripe": "Stripe", "Stripe": "Stripe",
"AirWallex": "AirWallex",
"Tag - Tooltip": "Tag of product", "Tag - Tooltip": "Tag of product",
"Test buy page..": "Test buy page..", "Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.", "There is no payment channel for this product.": "There is no payment channel for this product.",
@ -817,6 +834,8 @@
"Edit Provider": "Edit Provider", "Edit Provider": "Edit Provider",
"Email content": "Email content", "Email content": "Email content",
"Email content - Tooltip": "Content of the Email", "Email content - Tooltip": "Content of the Email",
"Email regex": "Email regex",
"Email regex - Tooltip": "Email regex - Tooltip",
"Email title": "Email title", "Email title": "Email title",
"Email title - Tooltip": "Title of the email", "Email title - Tooltip": "Title of the email",
"Endpoint": "Endpoint", "Endpoint": "Endpoint",
@ -830,6 +849,8 @@
"From name - Tooltip": "Name of \"From\"", "From name - Tooltip": "Name of \"From\"",
"Get phone number": "Get phone number", "Get phone number": "Get phone number",
"Get phone number - Tooltip": "If sync phone number is enabled, you should enable google people api first and add scope https://www.googleapis.com/auth/user.phonenumbers.read", "Get phone number - Tooltip": "If sync phone number is enabled, you should enable google people api first and add scope https://www.googleapis.com/auth/user.phonenumbers.read",
"HTTP header": "HTTP header",
"HTTP header - Tooltip": "HTTP header - Tooltip",
"Host": "Host", "Host": "Host",
"Host - Tooltip": "Name of host", "Host - Tooltip": "Name of host",
"IdP": "IdP", "IdP": "IdP",
@ -844,6 +865,8 @@
"Key text - Tooltip": "Key text - Tooltip", "Key text - Tooltip": "Key text - Tooltip",
"Metadata": "Metadata", "Metadata": "Metadata",
"Metadata - Tooltip": "SAML metadata", "Metadata - Tooltip": "SAML metadata",
"Metadata url": "Metadata url",
"Metadata url - Tooltip": "Metadata url - Tooltip",
"Method - Tooltip": "Login method, QR code or silent login", "Method - Tooltip": "Login method, QR code or silent login",
"New Provider": "New Provider", "New Provider": "New Provider",
"Normal": "Normal", "Normal": "Normal",
@ -1244,6 +1267,8 @@
"Edit Webhook": "Edit Webhook", "Edit Webhook": "Edit Webhook",
"Events": "Events", "Events": "Events",
"Events - Tooltip": "Events", "Events - Tooltip": "Events",
"Extended user fields": "Extended user fields",
"Extended user fields - Tooltip": "Extended user fields - Tooltip",
"Headers": "Headers", "Headers": "Headers",
"Headers - Tooltip": "HTTP headers (key-value pairs)", "Headers - Tooltip": "HTTP headers (key-value pairs)",
"Is user extended": "Is user extended", "Is user extended": "Is user extended",

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