mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-16 05:13:49 +08:00
Compare commits
62 Commits
Author | SHA1 | Date | |
---|---|---|---|
36f5de3203 | |||
eae69c41d7 | |||
91057f54f3 | |||
daa7b79915 | |||
d3a5539dae | |||
7d1c614452 | |||
e2eafa909b | |||
56bcef0592 | |||
0860cbf343 | |||
2f4180b1b6 | |||
e3d5619b25 | |||
019fd87b92 | |||
5c41c6c4a5 | |||
b7fafcc62b | |||
493ceddcd9 | |||
fc618b9bd5 | |||
a00900e405 | |||
77ef5828dd | |||
c11f013e04 | |||
b3bafe8402 | |||
f04a431d85 | |||
952538916d | |||
18bb445e71 | |||
cca88e2cb0 | |||
86c10fe0ab | |||
c1b3bf0f45 | |||
62bda61af5 | |||
b6f943e326 | |||
2cc5e82d91 | |||
e55cd94298 | |||
08f7a05e61 | |||
4bee21f4a3 | |||
5417a90223 | |||
131820e34e | |||
2fcbf7cf6c | |||
14ade8b7e4 | |||
a11fe59704 | |||
af55d0547f | |||
81102f8298 | |||
141372cb86 | |||
15a037ca74 | |||
73c680d56f | |||
aafc16e4f4 | |||
7be026dd1f | |||
3e7938e5f6 | |||
30789138e2 | |||
9610ce5b8c | |||
a39a311d2f | |||
08e41ab762 | |||
85ca318e2f | |||
9032865e60 | |||
5692522ee0 | |||
cb1882e589 | |||
41d9422687 | |||
3297db688b | |||
cc82d292f0 | |||
f2e3037bc5 | |||
d986a4a9e0 | |||
2df3878c15 | |||
24ab8880cc | |||
f26b4853c5 | |||
d78e8e9776 |
@ -1,10 +1,10 @@
|
||||
FROM --platform=$BUILDPLATFORM node:18.19.0 AS FRONT
|
||||
WORKDIR /web
|
||||
COPY ./web .
|
||||
RUN yarn install --frozen-lockfile --network-timeout 1000000 && yarn run build
|
||||
RUN yarn install --frozen-lockfile --network-timeout 1000000 && NODE_OPTIONS="--max-old-space-size=4096" yarn run build
|
||||
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.20.12 AS BACK
|
||||
FROM --platform=$BUILDPLATFORM golang:1.21.13 AS BACK
|
||||
WORKDIR /go/src/casdoor
|
||||
COPY . .
|
||||
RUN ./build.sh
|
||||
|
@ -139,6 +139,8 @@ func (c *ApiController) Signup() {
|
||||
invitationName = invitation.Name
|
||||
}
|
||||
|
||||
userEmailVerified := false
|
||||
|
||||
if application.IsSignupItemVisible("Email") && application.GetSignupItemRule("Email") != "No verification" && authForm.Email != "" {
|
||||
var checkResult *object.VerifyResult
|
||||
checkResult, err = object.CheckVerificationCode(authForm.Email, authForm.EmailCode, c.GetAcceptLanguage())
|
||||
@ -150,6 +152,8 @@ func (c *ApiController) Signup() {
|
||||
c.ResponseError(checkResult.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
userEmailVerified = true
|
||||
}
|
||||
|
||||
var checkPhone string
|
||||
@ -228,6 +232,7 @@ func (c *ApiController) Signup() {
|
||||
Karma: 0,
|
||||
Invitation: invitationName,
|
||||
InvitationCode: authForm.InvitationCode,
|
||||
EmailVerified: userEmailVerified,
|
||||
}
|
||||
|
||||
if len(organization.Tags) > 0 {
|
||||
@ -249,6 +254,10 @@ func (c *ApiController) Signup() {
|
||||
user.Groups = []string{invitation.SignupGroup}
|
||||
}
|
||||
|
||||
if application.DefaultGroup != "" && user.Groups == nil {
|
||||
user.Groups = []string{application.DefaultGroup}
|
||||
}
|
||||
|
||||
affected, err := object.AddUser(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
|
@ -29,6 +29,7 @@ import (
|
||||
"github.com/casdoor/casdoor/captcha"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/form"
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/idp"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
@ -402,11 +403,27 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
faceIdProvider, err := object.GetFaceIdProviderByApplication(util.GetId(application.Owner, application.Name), "false", c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
}
|
||||
|
||||
if faceIdProvider == nil {
|
||||
if err := object.CheckFaceId(user, authForm.FaceId, c.GetAcceptLanguage()); err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ok, err := user.CheckUserFace(authForm.FaceIdImage, faceIdProvider)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
c.ResponseError(i18n.Translate(c.GetAcceptLanguage(), "check:Face data does not exist, cannot log in"))
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if authForm.Password == "" {
|
||||
if user, err = object.GetUserByFields(authForm.Organization, authForm.Username); err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
@ -466,6 +483,14 @@ func (c *ApiController) Login() {
|
||||
verificationType = "sms"
|
||||
} else {
|
||||
verificationType = "email"
|
||||
if !user.EmailVerified {
|
||||
user.EmailVerified = true
|
||||
_, err = object.UpdateUser(user.GetId(), user, []string{"email_verified"}, false)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var application *object.Application
|
||||
@ -598,6 +623,9 @@ func (c *ApiController) Login() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if provider == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s does not exist"), authForm.Provider))
|
||||
}
|
||||
|
||||
providerItem := application.GetProviderItem(provider.Name)
|
||||
if !providerItem.IsProviderVisible() {
|
||||
@ -986,6 +1014,18 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
}
|
||||
|
||||
if authForm.Language != "" {
|
||||
user := c.getCurrentUser()
|
||||
if user != nil {
|
||||
user.Language = authForm.Language
|
||||
_, err = object.UpdateUser(user.GetId(), user, []string{"language"}, user.IsAdmin)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = resp
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
@ -269,7 +269,11 @@ func (c *ApiController) SyncLdapUsers() {
|
||||
return
|
||||
}
|
||||
|
||||
exist, failed, _ := object.SyncLdapUsers(owner, users, ldapId)
|
||||
exist, failed, err := object.SyncLdapUsers(owner, users, ldapId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(&LdapSyncResp{
|
||||
Exist: exist,
|
||||
|
@ -321,6 +321,11 @@ func (c *ApiController) IntrospectToken() {
|
||||
return
|
||||
}
|
||||
|
||||
respondWithInactiveToken := func() {
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
tokenTypeHint := c.Input().Get("token_type_hint")
|
||||
var token *object.Token
|
||||
if tokenTypeHint != "" {
|
||||
@ -329,7 +334,12 @@ func (c *ApiController) IntrospectToken() {
|
||||
c.ResponseTokenError(err.Error())
|
||||
return
|
||||
}
|
||||
if token == nil {
|
||||
if token == nil || token.ExpiresIn <= 0 {
|
||||
respondWithInactiveToken()
|
||||
return
|
||||
}
|
||||
|
||||
if token.ExpiresIn <= 0 {
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
return
|
||||
@ -340,12 +350,11 @@ func (c *ApiController) IntrospectToken() {
|
||||
|
||||
if application.TokenFormat == "JWT-Standard" {
|
||||
jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application)
|
||||
if err != nil || jwtToken.Valid() != nil {
|
||||
if err != nil {
|
||||
// and token revoked case. but we not implement
|
||||
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
||||
// refs: https://tools.ietf.org/html/rfc7009
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
respondWithInactiveToken()
|
||||
return
|
||||
}
|
||||
|
||||
@ -365,21 +374,17 @@ func (c *ApiController) IntrospectToken() {
|
||||
}
|
||||
} else {
|
||||
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
|
||||
if err != nil || jwtToken.Valid() != nil {
|
||||
if err != nil {
|
||||
// and token revoked case. but we not implement
|
||||
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
||||
// refs: https://tools.ietf.org/html/rfc7009
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
respondWithInactiveToken()
|
||||
return
|
||||
}
|
||||
|
||||
introspectionResponse = object.IntrospectionResponse{
|
||||
Active: true,
|
||||
Scope: jwtToken.Scope,
|
||||
ClientId: clientId,
|
||||
Username: jwtToken.Name,
|
||||
TokenType: jwtToken.TokenType,
|
||||
Exp: jwtToken.ExpiresAt.Unix(),
|
||||
Iat: jwtToken.IssuedAt.Unix(),
|
||||
Nbf: jwtToken.NotBefore.Unix(),
|
||||
@ -388,6 +393,16 @@ func (c *ApiController) IntrospectToken() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if tokenTypeHint == "" {
|
||||
@ -396,13 +411,15 @@ func (c *ApiController) IntrospectToken() {
|
||||
c.ResponseTokenError(err.Error())
|
||||
return
|
||||
}
|
||||
if token == nil {
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
if token == nil || token.ExpiresIn <= 0 {
|
||||
respondWithInactiveToken()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if token != nil {
|
||||
introspectionResponse.TokenType = token.TokenType
|
||||
}
|
||||
|
||||
c.Data["json"] = introspectionResponse
|
||||
c.ServeJSON()
|
||||
|
@ -602,7 +602,11 @@ func (c *ApiController) CheckUserPassword() {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = object.CheckUserPassword(user.Owner, user.Name, user.Password, c.GetAcceptLanguage())
|
||||
/*
|
||||
* Verified password with user as subject, if field ldap not empty,
|
||||
* then `isPasswordWithLdapEnabled` is true
|
||||
*/
|
||||
_, err = object.CheckUserPassword(user.Owner, user.Name, user.Password, c.GetAcceptLanguage(), false, false, user.Ldap != "")
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
} else {
|
||||
|
@ -242,7 +242,7 @@ func (c *ApiController) SendVerificationCode() {
|
||||
} else if vform.Method == ResetVerification {
|
||||
user = c.getCurrentUser()
|
||||
} else if vform.Method == MfaAuthVerification {
|
||||
mfaProps := user.GetPreferredMfaProps(false)
|
||||
mfaProps := user.GetMfaProps(object.EmailType, false)
|
||||
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
|
||||
vform.Dest = mfaProps.Secret
|
||||
}
|
||||
@ -281,7 +281,7 @@ func (c *ApiController) SendVerificationCode() {
|
||||
}
|
||||
}
|
||||
} else if vform.Method == MfaAuthVerification {
|
||||
mfaProps := user.GetPreferredMfaProps(false)
|
||||
mfaProps := user.GetMfaProps(object.SmsType, false)
|
||||
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
|
||||
vform.Dest = mfaProps.Secret
|
||||
}
|
||||
@ -436,7 +436,8 @@ func (c *ApiController) ResetEmailOrPhone() {
|
||||
switch destType {
|
||||
case object.VerifyTypeEmail:
|
||||
user.Email = dest
|
||||
_, err = object.SetUserField(user, "email", user.Email)
|
||||
user.EmailVerified = true
|
||||
_, err = object.UpdateUser(user.GetId(), user, []string{"email", "email_verified"}, false)
|
||||
case object.VerifyTypePhone:
|
||||
user.Phone = dest
|
||||
_, err = object.SetUserField(user, "phone", user.Phone)
|
||||
|
@ -16,7 +16,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
|
||||
"github.com/casdoor/casdoor/form"
|
||||
@ -118,24 +118,7 @@ func (c *ApiController) WebAuthnSigninBegin() {
|
||||
return
|
||||
}
|
||||
|
||||
userOwner := c.Input().Get("owner")
|
||||
userName := c.Input().Get("name")
|
||||
user, err := object.GetUserByFields(userOwner, userName)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(userOwner, userName)))
|
||||
return
|
||||
}
|
||||
if len(user.WebauthnCredentials) == 0 {
|
||||
c.ResponseError(c.T("webauthn:Found no credentials for this user"))
|
||||
return
|
||||
}
|
||||
|
||||
options, sessionData, err := webauthnObj.BeginLogin(user)
|
||||
options, sessionData, err := webauthnObj.BeginDiscoverableLogin()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@ -168,20 +151,23 @@ func (c *ApiController) WebAuthnSigninFinish() {
|
||||
return
|
||||
}
|
||||
c.Ctx.Request.Body = io.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
|
||||
userId := string(sessionData.UserID)
|
||||
user, err := object.GetUser(userId)
|
||||
|
||||
var user *object.User
|
||||
handler := func(rawID, userHandle []byte) (webauthn.User, error) {
|
||||
user, err = object.GetUserByWebauthID(base64.StdEncoding.EncodeToString(rawID))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
_, err = webauthnObj.FinishLogin(user, sessionData, c.Ctx.Request)
|
||||
_, err = webauthnObj.FinishDiscoverableLogin(handler, sessionData, c.Ctx.Request)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.SetSessionUsername(userId)
|
||||
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
|
||||
c.SetSessionUsername(user.GetId())
|
||||
util.LogInfo(c.Ctx, "API: [%s] signed in", user.GetId())
|
||||
|
||||
var application *object.Application
|
||||
|
||||
|
@ -34,6 +34,8 @@ func GetCredManager(passwordType string) CredManager {
|
||||
return NewPbkdf2SaltCredManager()
|
||||
} else if passwordType == "argon2id" {
|
||||
return NewArgon2idCredManager()
|
||||
} else if passwordType == "pbkdf2-django" {
|
||||
return NewPbkdf2DjangoCredManager()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
71
cred/pbkdf2_django.go
Normal file
71
cred/pbkdf2_django.go
Normal 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
|
||||
}
|
@ -15,6 +15,8 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -26,12 +28,22 @@ import (
|
||||
type HttpEmailProvider struct {
|
||||
endpoint string
|
||||
method string
|
||||
httpHeaders map[string]string
|
||||
bodyMapping map[string]string
|
||||
contentType string
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
func NewHttpEmailProvider(endpoint string, method string) *HttpEmailProvider {
|
||||
client := &HttpEmailProvider{
|
||||
endpoint: endpoint,
|
||||
method: method,
|
||||
httpHeaders: httpHeaders,
|
||||
bodyMapping: bodyMapping,
|
||||
contentType: contentType,
|
||||
}
|
||||
return client
|
||||
}
|
||||
@ -39,18 +51,52 @@ func NewHttpEmailProvider(endpoint string, method string) *HttpEmailProvider {
|
||||
func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
|
||||
var req *http.Request
|
||||
var err error
|
||||
if c.method == "POST" {
|
||||
|
||||
fromNameField := "fromName"
|
||||
toAddressField := "toAddress"
|
||||
subjectField := "subject"
|
||||
contentField := "content"
|
||||
|
||||
for k, v := range c.bodyMapping {
|
||||
switch k {
|
||||
case "fromName":
|
||||
fromNameField = v
|
||||
case "toAddress":
|
||||
toAddressField = v
|
||||
case "subject":
|
||||
subjectField = v
|
||||
case "content":
|
||||
contentField = v
|
||||
}
|
||||
}
|
||||
|
||||
if c.method == "POST" || c.method == "PUT" || c.method == "DELETE" {
|
||||
bodyMap := make(map[string]string)
|
||||
bodyMap[fromNameField] = fromName
|
||||
bodyMap[toAddressField] = toAddress
|
||||
bodyMap[subjectField] = subject
|
||||
bodyMap[contentField] = content
|
||||
|
||||
var fromValueBytes []byte
|
||||
if c.contentType == "application/json" {
|
||||
fromValueBytes, err = json.Marshal(bodyMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err = http.NewRequest(c.method, c.endpoint, bytes.NewBuffer(fromValueBytes))
|
||||
} else {
|
||||
formValues := url.Values{}
|
||||
formValues.Set("fromName", fromName)
|
||||
formValues.Set("toAddress", toAddress)
|
||||
formValues.Set("subject", subject)
|
||||
formValues.Set("content", content)
|
||||
for k, v := range bodyMap {
|
||||
formValues.Add(k, v)
|
||||
}
|
||||
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Content-Type", c.contentType)
|
||||
} else if c.method == "GET" {
|
||||
req, err = http.NewRequest(c.method, c.endpoint, nil)
|
||||
if err != nil {
|
||||
@ -58,15 +104,19 @@ func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("fromName", fromName)
|
||||
q.Add("toAddress", toAddress)
|
||||
q.Add("subject", subject)
|
||||
q.Add("content", content)
|
||||
q.Add(fromNameField, fromName)
|
||||
q.Add(toAddressField, toAddress)
|
||||
q.Add(subjectField, subject)
|
||||
q.Add(contentField, content)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
} else {
|
||||
return fmt.Errorf("HttpEmailProvider's Send() error, unsupported method: %s", c.method)
|
||||
}
|
||||
|
||||
for k, v := range c.httpHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
httpClient := proxy.DefaultHttpClient
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
|
@ -18,11 +18,11 @@ type EmailProvider interface {
|
||||
Send(fromAddress string, fromName, toAddress string, subject string, content string) error
|
||||
}
|
||||
|
||||
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool, endpoint string, method string) EmailProvider {
|
||||
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool, endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string) EmailProvider {
|
||||
if typ == "Azure ACS" {
|
||||
return NewAzureACSEmailProvider(clientSecret, host)
|
||||
} else if typ == "Custom HTTP Email" {
|
||||
return NewHttpEmailProvider(endpoint, method)
|
||||
return NewHttpEmailProvider(endpoint, method, httpHeaders, bodyMapping, contentType)
|
||||
} else if typ == "SendGrid" {
|
||||
return NewSendgridEmailProvider(clientSecret, host, endpoint)
|
||||
} else {
|
||||
|
81
faceId/aliyun.go
Normal file
81
faceId/aliyun.go
Normal 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
23
faceId/provider.go
Normal 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)
|
||||
}
|
@ -34,6 +34,7 @@ type AuthForm struct {
|
||||
Phone string `json:"phone"`
|
||||
Affiliation string `json:"affiliation"`
|
||||
IdCard string `json:"idCard"`
|
||||
Language string `json:"language"`
|
||||
Region string `json:"region"`
|
||||
InvitationCode string `json:"invitationCode"`
|
||||
|
||||
@ -68,6 +69,7 @@ type AuthForm struct {
|
||||
Pricing string `json:"pricing"`
|
||||
|
||||
FaceId []float64 `json:"faceId"`
|
||||
FaceIdImage []string `json:"faceIdImage"`
|
||||
}
|
||||
|
||||
func GetAuthFormFieldValue(form *AuthForm, fieldName string) (bool, string) {
|
||||
|
194
go.mod
194
go.mod
@ -1,10 +1,14 @@
|
||||
module github.com/casdoor/casdoor
|
||||
|
||||
go 1.16
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.3
|
||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.4
|
||||
github.com/alibabacloud-go/facebody-20191230/v5 v5.1.2
|
||||
github.com/alibabacloud-go/tea v1.3.2
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
|
||||
github.com/aws/aws-sdk-go v1.45.5
|
||||
github.com/beego/beego v1.12.12
|
||||
github.com/beevik/etree v1.1.0
|
||||
@ -18,7 +22,6 @@ require (
|
||||
github.com/casvisor/casvisor-go-sdk v1.4.0
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
||||
github.com/denisenkom/go-mssqldb v0.9.0
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
|
||||
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3
|
||||
github.com/fogleman/gg v1.3.0
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5
|
||||
@ -28,8 +31,8 @@ require (
|
||||
github.com/go-pay/gopay v1.5.72
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
||||
github.com/go-webauthn/webauthn v0.6.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/go-webauthn/webauthn v0.10.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/lestrrat-go/jwx v1.2.29
|
||||
@ -46,7 +49,6 @@ require (
|
||||
github.com/russellhaering/gosaml2 v0.9.0
|
||||
github.com/russellhaering/goxmldsig v1.2.0
|
||||
github.com/sendgrid/sendgrid-go v3.14.0+incompatible
|
||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
@ -54,20 +56,194 @@ require (
|
||||
github.com/stripe/stripe-go/v74 v74.29.0
|
||||
github.com/tealeg/xlsx v1.0.5
|
||||
github.com/thanhpk/randstr v1.0.4
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||
github.com/xorm-io/builder v0.3.13
|
||||
github.com/xorm-io/core v0.7.4
|
||||
github.com/xorm-io/xorm v1.1.6
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/oauth2 v0.17.0
|
||||
golang.org/x/text v0.21.0
|
||||
google.golang.org/api v0.150.0
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68
|
||||
maunium.net/go/mautrix v0.16.0
|
||||
modernc.org/sqlite v1.18.2
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.110.8 // indirect
|
||||
cloud.google.com/go/compute v1.23.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v1.1.3 // indirect
|
||||
cloud.google.com/go/storage v1.35.1 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
|
||||
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20221121042443-a3fd332d56d9 // indirect
|
||||
github.com/SherClockHolmes/webpush-go v1.2.0 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/darabonba-number v1.0.4 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
|
||||
github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 // indirect
|
||||
github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect
|
||||
github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect
|
||||
github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/tea-utils v1.3.6 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.545 // indirect
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible // indirect
|
||||
github.com/aliyun/credentials-go v1.3.10 // indirect
|
||||
github.com/apistd/uni-go-sdk v0.0.2 // indirect
|
||||
github.com/atc0005/go-teams-notify/v2 v2.6.1 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.156 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blinkbean/dingtalk v0.0.0-20210905093040-7d935c0f7e19 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/bwmarrin/discordgo v0.27.1 // indirect
|
||||
github.com/casdoor/casdoor-go-sdk v0.50.0 // indirect
|
||||
github.com/casdoor/go-reddit/v2 v2.1.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.3 // indirect
|
||||
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/dghubble/oauth1 v0.7.2 // indirect
|
||||
github.com/dghubble/sling v1.4.0 // indirect
|
||||
github.com/di-wu/parser v0.2.2 // indirect
|
||||
github.com/di-wu/xsd-datetime v1.0.0 // indirect
|
||||
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 // indirect
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||
github.com/go-lark/lark v1.9.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-webauthn/x v0.1.9 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/go-tpm v0.9.0 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/gregdel/pushover v1.2.1 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
|
||||
github.com/markbates/going v1.0.0 // indirect
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-ieproxy v0.0.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mileusna/viber v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect
|
||||
github.com/pingcap/log v0.0.0-20210625125904-98ed8e2eb1c7 // indirect
|
||||
github.com/pingcap/tidb/parser v0.0.0-20221126021158-6b02a5d8ba7d // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/common v0.30.0 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/qiniu/go-sdk/v7 v7.12.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/rs/zerolog v1.30.0 // indirect
|
||||
github.com/scim2/filter-parser/v2 v2.2.0 // indirect
|
||||
github.com/sendgrid/rest v2.6.9+incompatible // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect
|
||||
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/skeema/knownhosts v1.2.1 // indirect
|
||||
github.com/slack-go/slack v0.12.3 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.744 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.744 // indirect
|
||||
github.com/tidwall/gjson v1.16.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||
github.com/tklauser/numcpus v0.4.0 // indirect
|
||||
github.com/twilio/twilio-go v1.13.0 // indirect
|
||||
github.com/ucloud/ucloud-sdk-go v0.22.5 // indirect
|
||||
github.com/utahta/go-linenotify v0.5.0 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.117 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
go.mau.fi/util v0.0.0-20230805171708-199bf3eec776 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.7.0 // indirect
|
||||
go.uber.org/zap v1.19.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20230810033253-352e893a4cad // indirect
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/uint128 v1.1.1 // indirect
|
||||
maunium.net/go/maulogger/v2 v2.4.1 // indirect
|
||||
modernc.org/cc/v3 v3.37.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.9 // indirect
|
||||
modernc.org/libc v1.18.0 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.3.0 // indirect
|
||||
modernc.org/opt v0.1.1 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
)
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
||||
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Uživatelské jméno nemůže být emailová adresa",
|
||||
"Username cannot contain white spaces": "Uživatelské jméno nemůže obsahovat mezery",
|
||||
"Username cannot start with a digit": "Uživatelské jméno nemůže začínat číslicí",
|
||||
"Username is too long (maximum is 39 characters).": "Uživatelské jméno je příliš dlouhé (maximálně 39 znaků).",
|
||||
"Username is too long (maximum is 255 characters).": "Uživatelské jméno je příliš dlouhé (maximálně 255 znaků).",
|
||||
"Username must have at least 2 characters": "Uživatelské jméno musí mít alespoň 2 znaky",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Zadali jste špatné heslo nebo kód příliš mnohokrát, prosím počkejte %d minut a zkuste to znovu",
|
||||
"Your region is not allow to signup by phone": "Vaše oblast neumožňuje registraci pomocí telefonu",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Benutzername kann keine E-Mail-Adresse sein",
|
||||
"Username cannot contain white spaces": "Benutzername darf keine Leerzeichen enthalten",
|
||||
"Username cannot start with a digit": "Benutzername darf nicht mit einer Ziffer beginnen",
|
||||
"Username is too long (maximum is 39 characters).": "Benutzername ist zu lang (das Maximum beträgt 39 Zeichen).",
|
||||
"Username is too long (maximum is 255 characters).": "Benutzername ist zu lang (das Maximum beträgt 255 Zeichen).",
|
||||
"Username must have at least 2 characters": "Benutzername muss mindestens 2 Zeichen lang sein",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Sie haben zu oft das falsche Passwort oder den falschen Code eingegeben. Bitte warten Sie %d Minuten und versuchen Sie es erneut",
|
||||
"Your region is not allow to signup by phone": "Ihre Region ist nicht berechtigt, sich telefonisch anzumelden",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
||||
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Nombre de usuario no puede ser una dirección de correo electrónico",
|
||||
"Username cannot contain white spaces": "Nombre de usuario no puede contener espacios en blanco",
|
||||
"Username cannot start with a digit": "El nombre de usuario no puede empezar con un dígito",
|
||||
"Username is too long (maximum is 39 characters).": "El nombre de usuario es demasiado largo (el máximo es de 39 caracteres).",
|
||||
"Username is too long (maximum is 255 characters).": "El nombre de usuario es demasiado largo (el máximo es de 255 caracteres).",
|
||||
"Username must have at least 2 characters": "Nombre de usuario debe tener al menos 2 caracteres",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Has ingresado la contraseña o código incorrecto demasiadas veces, por favor espera %d minutos e intenta de nuevo",
|
||||
"Your region is not allow to signup by phone": "Tu región no está permitida para registrarse por teléfono",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "نام کاربری نمیتواند یک آدرس ایمیل باشد",
|
||||
"Username cannot contain white spaces": "نام کاربری نمیتواند حاوی فاصله باشد",
|
||||
"Username cannot start with a digit": "نام کاربری نمیتواند با یک رقم شروع شود",
|
||||
"Username is too long (maximum is 39 characters).": "نام کاربری بیش از حد طولانی است (حداکثر ۳۹ کاراکتر).",
|
||||
"Username is too long (maximum is 255 characters).": "نام کاربری بیش از حد طولانی است (حداکثر ۳۹ کاراکتر).",
|
||||
"Username must have at least 2 characters": "نام کاربری باید حداقل ۲ کاراکتر داشته باشد",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "شما رمز عبور یا کد اشتباه را بیش از حد وارد کردهاید، لطفاً %d دقیقه صبر کنید و دوباره تلاش کنید",
|
||||
"Your region is not allow to signup by phone": "منطقه شما اجازه ثبتنام با تلفن را ندارد",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
||||
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Nom d'utilisateur ne peut pas être une adresse e-mail",
|
||||
"Username cannot contain white spaces": "Nom d'utilisateur ne peut pas contenir d'espaces blancs",
|
||||
"Username cannot start with a digit": "Nom d'utilisateur ne peut pas commencer par un chiffre",
|
||||
"Username is too long (maximum is 39 characters).": "Nom d'utilisateur est trop long (maximum de 39 caractères).",
|
||||
"Username is too long (maximum is 255 characters).": "Nom d'utilisateur est trop long (maximum de 255 caractères).",
|
||||
"Username must have at least 2 characters": "Le nom d'utilisateur doit comporter au moins 2 caractères",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Vous avez entré le mauvais mot de passe ou code plusieurs fois, veuillez attendre %d minutes et réessayer",
|
||||
"Your region is not allow to signup by phone": "Votre région n'est pas autorisée à s'inscrire par téléphone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
||||
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username tidak bisa menjadi alamat email",
|
||||
"Username cannot contain white spaces": "Username tidak boleh mengandung spasi",
|
||||
"Username cannot start with a digit": "Username tidak dapat dimulai dengan angka",
|
||||
"Username is too long (maximum is 39 characters).": "Nama pengguna terlalu panjang (maksimum 39 karakter).",
|
||||
"Username is too long (maximum is 255 characters).": "Nama pengguna terlalu panjang (maksimum 255 karakter).",
|
||||
"Username must have at least 2 characters": "Nama pengguna harus memiliki setidaknya 2 karakter",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Anda telah memasukkan kata sandi atau kode yang salah terlalu banyak kali, mohon tunggu selama %d menit dan coba lagi",
|
||||
"Your region is not allow to signup by phone": "Wilayah Anda tidak diizinkan untuk mendaftar melalui telepon",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
||||
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "ユーザー名には電子メールアドレスを使用できません",
|
||||
"Username cannot contain white spaces": "ユーザ名にはスペースを含めることはできません",
|
||||
"Username cannot start with a digit": "ユーザー名は数字で始めることはできません",
|
||||
"Username is too long (maximum is 39 characters).": "ユーザー名が長すぎます(最大39文字)。",
|
||||
"Username is too long (maximum is 255 characters).": "ユーザー名が長すぎます(最大255文字)。",
|
||||
"Username must have at least 2 characters": "ユーザー名は少なくとも2文字必要です",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "あなたは間違ったパスワードまたはコードを何度も入力しました。%d 分間待ってから再度お試しください",
|
||||
"Your region is not allow to signup by phone": "あなたの地域は電話でサインアップすることができません",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
||||
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "사용자 이름은 이메일 주소가 될 수 없습니다",
|
||||
"Username cannot contain white spaces": "사용자 이름에는 공백이 포함될 수 없습니다",
|
||||
"Username cannot start with a digit": "사용자 이름은 숫자로 시작할 수 없습니다",
|
||||
"Username is too long (maximum is 39 characters).": "사용자 이름이 너무 깁니다 (최대 39자).",
|
||||
"Username is too long (maximum is 255 characters).": "사용자 이름이 너무 깁니다 (최대 255자).",
|
||||
"Username must have at least 2 characters": "사용자 이름은 적어도 2개의 문자가 있어야 합니다",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "올바르지 않은 비밀번호나 코드를 여러 번 입력했습니다. %d분 동안 기다리신 후 다시 시도해주세요",
|
||||
"Your region is not allow to signup by phone": "당신의 지역은 전화로 가입할 수 없습니다",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
||||
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
||||
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
||||
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"Username cannot start with a digit": "O nome de usuário não pode começar com um dígito",
|
||||
"Username is too long (maximum is 39 characters).": "Nome de usuário é muito longo (máximo é 39 caracteres).",
|
||||
"Username is too long (maximum is 255 characters).": "Nome de usuário é muito longo (máximo é 255 caracteres).",
|
||||
"Username must have at least 2 characters": "Nome de usuário deve ter pelo menos 2 caracteres",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Имя пользователя не может быть адресом электронной почты",
|
||||
"Username cannot contain white spaces": "Имя пользователя не может содержать пробелы",
|
||||
"Username cannot start with a digit": "Имя пользователя не может начинаться с цифры",
|
||||
"Username is too long (maximum is 39 characters).": "Имя пользователя слишком длинное (максимальная длина - 39 символов).",
|
||||
"Username is too long (maximum is 255 characters).": "Имя пользователя слишком длинное (максимальная длина - 255 символов).",
|
||||
"Username must have at least 2 characters": "Имя пользователя должно содержать не менее 2 символов",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Вы ввели неправильный пароль или код слишком много раз, пожалуйста, подождите %d минут и попробуйте снова",
|
||||
"Your region is not allow to signup by phone": "Ваш регион не разрешает регистрацию по телефону",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Používateľské meno nemôže byť e-mailová adresa",
|
||||
"Username cannot contain white spaces": "Používateľské meno nemôže obsahovať medzery",
|
||||
"Username cannot start with a digit": "Používateľské meno nemôže začínať číslicou",
|
||||
"Username is too long (maximum is 39 characters).": "Používateľské meno je príliš dlhé (maximum je 39 znakov).",
|
||||
"Username is too long (maximum is 255 characters).": "Používateľské meno je príliš dlhé (maximum je 255 znakov).",
|
||||
"Username must have at least 2 characters": "Používateľské meno musí mať aspoň 2 znaky",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Zadali ste nesprávne heslo alebo kód príliš veľa krát, prosím, počkajte %d minút a skúste to znova",
|
||||
"Your region is not allow to signup by phone": "Váš región neumožňuje registráciu cez telefón",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
||||
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Kullanıcı adı bir e-mail adresi olamaz",
|
||||
"Username cannot contain white spaces": "Kullanıcı adı boşluk karakteri içeremez",
|
||||
"Username cannot start with a digit": "Kullanıcı adı rakamla başlayamaz",
|
||||
"Username is too long (maximum is 39 characters).": "Kullanıcı adı çok uzun (en fazla 39 karakter olmalı).",
|
||||
"Username is too long (maximum is 255 characters).": "Kullanıcı adı çok uzun (en fazla 255 karakter olmalı).",
|
||||
"Username must have at least 2 characters": "Kullanıcı adı en az iki karakterden oluşmalı",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Çok fazla hatalı şifre denemesi yaptınız. %d dakika kadar bekleyip yeniden giriş yapmayı deneyebilirsiniz.",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
||||
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Tên người dùng không thể là địa chỉ email",
|
||||
"Username cannot contain white spaces": "Tên người dùng không thể chứa khoảng trắng",
|
||||
"Username cannot start with a digit": "Tên người dùng không thể bắt đầu bằng chữ số",
|
||||
"Username is too long (maximum is 39 characters).": "Tên đăng nhập quá dài (tối đa là 39 ký tự).",
|
||||
"Username is too long (maximum is 255 characters).": "Tên đăng nhập quá dài (tối đa là 255 ký tự).",
|
||||
"Username must have at least 2 characters": "Tên đăng nhập phải có ít nhất 2 ký tự",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Bạn đã nhập sai mật khẩu hoặc mã quá nhiều lần, vui lòng đợi %d phút và thử lại",
|
||||
"Your region is not allow to signup by phone": "Vùng của bạn không được phép đăng ký bằng điện thoại",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "用户名不可以是邮箱地址",
|
||||
"Username cannot contain white spaces": "用户名禁止包含空格",
|
||||
"Username cannot start with a digit": "用户名禁止使用数字开头",
|
||||
"Username is too long (maximum is 39 characters).": "用户名过长(最大允许长度为39个字符)",
|
||||
"Username is too long (maximum is 255 characters).": "用户名过长(最大允许长度为255个字符)",
|
||||
"Username must have at least 2 characters": "用户名至少要有2个字符",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "密码错误次数已达上限,请在 %d 分后重试",
|
||||
"Your region is not allow to signup by phone": "所在地区不支持手机号注册",
|
||||
|
@ -136,12 +136,12 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
|
||||
dtUserInfo := &DingTalkUserResponse{}
|
||||
accessToken := token.AccessToken
|
||||
|
||||
reqest, err := http.NewRequest("GET", idp.Config.Endpoint.AuthURL, nil)
|
||||
request, err := http.NewRequest("GET", idp.Config.Endpoint.AuthURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqest.Header.Add("x-acs-dingtalk-access-token", accessToken)
|
||||
resp, err := idp.Client.Do(reqest)
|
||||
request.Header.Add("x-acs-dingtalk-access-token", accessToken)
|
||||
resp, err := idp.Client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -299,12 +299,12 @@ func GetWechatOfficialAccountQRCode(clientId string, clientSecret string, provid
|
||||
params := fmt.Sprintf(`{"expire_seconds": 3600, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": "%s"}}}`, providerId)
|
||||
|
||||
bodyData := bytes.NewReader([]byte(params))
|
||||
requeset, err := http.NewRequest("POST", qrCodeUrl, bodyData)
|
||||
request, err := http.NewRequest("POST", qrCodeUrl, bodyData)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
resp, err := client.Do(requeset)
|
||||
resp, err := client.Do(request)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
@ -434,7 +434,7 @@
|
||||
"isTopGroup": true,
|
||||
"title": "",
|
||||
"key": "",
|
||||
"children": "",
|
||||
"children": [],
|
||||
"isEnabled": true
|
||||
}
|
||||
],
|
||||
|
@ -185,12 +185,9 @@ func buildUserFilterCondition(filter interface{}) (builder.Cond, error) {
|
||||
attr := string(f.AttributeDesc())
|
||||
|
||||
if attr == ldapMemberOfAttr {
|
||||
groupId := string(f.AssertionValue())
|
||||
users, err := object.GetGroupUsers(groupId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var names []string
|
||||
groupId := string(f.AssertionValue())
|
||||
users := object.GetGroupUsersWithoutError(groupId)
|
||||
for _, user := range users {
|
||||
names = append(names, user.Name)
|
||||
}
|
||||
@ -249,7 +246,7 @@ func buildSafeCondition(filter interface{}) builder.Cond {
|
||||
condition, err := buildUserFilterCondition(filter)
|
||||
if err != nil {
|
||||
log.Printf("err = %v", err.Error())
|
||||
return nil
|
||||
return builder.And(builder.Expr("1 != 1"))
|
||||
}
|
||||
return condition
|
||||
}
|
||||
|
@ -191,12 +191,7 @@ func (adapter *Adapter) InitAdapter() error {
|
||||
}
|
||||
}
|
||||
|
||||
var tableName string
|
||||
if driverName == "mssql" {
|
||||
tableName = fmt.Sprintf("[%s]", adapter.Table)
|
||||
} else {
|
||||
tableName = adapter.Table
|
||||
}
|
||||
tableName := adapter.Table
|
||||
|
||||
adapter.Adapter, err = xormadapter.NewAdapterByEngineWithTableName(engine, tableName, "")
|
||||
if err != nil {
|
||||
|
@ -71,6 +71,7 @@ type Application struct {
|
||||
Description string `xorm:"varchar(100)" json:"description"`
|
||||
Organization string `xorm:"varchar(100)" json:"organization"`
|
||||
Cert string `xorm:"varchar(100)" json:"cert"`
|
||||
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
|
||||
HeaderHtml string `xorm:"mediumtext" json:"headerHtml"`
|
||||
EnablePassword bool `json:"enablePassword"`
|
||||
EnableSignUp bool `json:"enableSignUp"`
|
||||
@ -100,6 +101,7 @@ type Application struct {
|
||||
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
||||
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
|
||||
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
|
||||
ForcedRedirectOrigin string `xorm:"varchar(100)" json:"forcedRedirectOrigin"`
|
||||
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
|
||||
TokenSigningMethod string `xorm:"varchar(100)" json:"tokenSigningMethod"`
|
||||
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
|
||||
@ -120,6 +122,7 @@ type Application struct {
|
||||
FormOffset int `json:"formOffset"`
|
||||
FormSideHtml string `xorm:"mediumtext" json:"formSideHtml"`
|
||||
FormBackgroundUrl string `xorm:"varchar(200)" json:"formBackgroundUrl"`
|
||||
FormBackgroundUrlMobile string `xorm:"varchar(200)" json:"formBackgroundUrlMobile"`
|
||||
|
||||
FailedSigninLimit int `json:"failedSigninLimit"`
|
||||
FailedSigninFrozenTime int `json:"failedSigninFrozenTime"`
|
||||
@ -539,7 +542,7 @@ func GetMaskedApplication(application *Application, userId string) *Application
|
||||
|
||||
providerItems := []*ProviderItem{}
|
||||
for _, providerItem := range application.Providers {
|
||||
if providerItem.Provider != nil && (providerItem.Provider.Category == "OAuth" || providerItem.Provider.Category == "Web3" || providerItem.Provider.Category == "Captcha" || providerItem.Provider.Category == "SAML") {
|
||||
if providerItem.Provider != nil && (providerItem.Provider.Category == "OAuth" || providerItem.Provider.Category == "Web3" || providerItem.Provider.Category == "Captcha" || providerItem.Provider.Category == "SAML" || providerItem.Provider.Category == "Face ID") {
|
||||
providerItems = append(providerItems, providerItem)
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,11 @@ func GetCertCount(owner, field, value string) (int64, error) {
|
||||
|
||||
func GetCerts(owner string) ([]*Cert, error) {
|
||||
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 {
|
||||
return certs, err
|
||||
}
|
||||
@ -146,7 +150,12 @@ func getCertByName(name string) (*Cert, error) {
|
||||
|
||||
func GetCert(id string) (*Cert, error) {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
return getCert(owner, name)
|
||||
cert, err := getCert(owner, name)
|
||||
if cert == nil && owner != "admin" {
|
||||
return getCert("admin", name)
|
||||
} else {
|
||||
return cert, err
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateCert(id string, cert *Cert) (bool, error) {
|
||||
|
@ -517,8 +517,8 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
|
||||
func CheckUsername(username string, lang string) string {
|
||||
if username == "" {
|
||||
return i18n.Translate(lang, "check:Empty username.")
|
||||
} else if len(username) > 39 {
|
||||
return i18n.Translate(lang, "check:Username is too long (maximum is 39 characters).")
|
||||
} else if len(username) > 255 {
|
||||
return i18n.Translate(lang, "check:Username is too long (maximum is 255 characters).")
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex
|
||||
@ -533,8 +533,8 @@ func CheckUsername(username string, lang string) string {
|
||||
func CheckUsernameWithEmail(username string, lang string) string {
|
||||
if username == "" {
|
||||
return i18n.Translate(lang, "check:Empty username.")
|
||||
} else if len(username) > 39 {
|
||||
return i18n.Translate(lang, "check:Username is too long (maximum is 39 characters).")
|
||||
} else if len(username) > 255 {
|
||||
return i18n.Translate(lang, "check:Username is too long (maximum is 255 characters).")
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex
|
||||
|
@ -31,7 +31,7 @@ func TestSmtpServer(provider *Provider) error {
|
||||
}
|
||||
|
||||
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
|
||||
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl, provider.Endpoint, provider.Method)
|
||||
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl, provider.Endpoint, provider.Method, provider.HttpHeaders, provider.UserMapping, provider.IssuerUrl)
|
||||
|
||||
fromAddress := provider.ClientId2
|
||||
if fromAddress == "" {
|
||||
|
@ -83,19 +83,23 @@ func GetPaginationGroups(owner string, offset, limit int, field, value, sortFiel
|
||||
func GetGroupsHaveChildrenMap(groups []*Group) (map[string]*Group, error) {
|
||||
groupsHaveChildren := []*Group{}
|
||||
resultMap := make(map[string]*Group)
|
||||
groupMap := map[string]*Group{}
|
||||
|
||||
groupIds := []string{}
|
||||
for _, group := range groups {
|
||||
groupMap[group.Name] = group
|
||||
groupIds = append(groupIds, group.Name)
|
||||
if !group.IsTopGroup {
|
||||
groupIds = append(groupIds, group.ParentId)
|
||||
}
|
||||
}
|
||||
|
||||
err := ormer.Engine.Cols("owner", "name", "parent_id", "display_name").Distinct("parent_id").In("parent_id", groupIds).Find(&groupsHaveChildren)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, group := range groups {
|
||||
resultMap[group.Name] = group
|
||||
for _, group := range groupsHaveChildren {
|
||||
resultMap[group.ParentId] = groupMap[group.ParentId]
|
||||
}
|
||||
return resultMap, nil
|
||||
}
|
||||
@ -302,7 +306,10 @@ func GetPaginationGroupUsers(groupId string, offset, limit int, field, value, so
|
||||
|
||||
func GetGroupUsers(groupId string) ([]*User, error) {
|
||||
users := []*User{}
|
||||
owner, _ := util.GetOwnerAndNameFromId(groupId)
|
||||
owner, _, err := util.GetOwnerAndNameFromIdWithError(groupId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
names, err := userEnforcer.GetUserNamesByGroupName(groupId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -314,6 +321,11 @@ func GetGroupUsers(groupId string) ([]*User, error) {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func GetGroupUsersWithoutError(groupId string) []*User {
|
||||
users, _ := GetGroupUsers(groupId)
|
||||
return users
|
||||
}
|
||||
|
||||
func ExtendGroupWithUsers(group *Group) error {
|
||||
if group == nil {
|
||||
return nil
|
||||
|
@ -70,12 +70,12 @@ func InitFromFile() {
|
||||
for _, provider := range initData.Providers {
|
||||
initDefinedProvider(provider)
|
||||
}
|
||||
for _, user := range initData.Users {
|
||||
initDefinedUser(user)
|
||||
}
|
||||
for _, application := range initData.Applications {
|
||||
initDefinedApplication(application)
|
||||
}
|
||||
for _, user := range initData.Users {
|
||||
initDefinedUser(user)
|
||||
}
|
||||
for _, cert := range initData.Certs {
|
||||
initDefinedCert(cert)
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ type Ldap struct {
|
||||
Host string `xorm:"varchar(100)" json:"host"`
|
||||
Port int `xorm:"int" json:"port"`
|
||||
EnableSsl bool `xorm:"bool" json:"enableSsl"`
|
||||
AllowSelfSignedCert bool `xorm:"bool" json:"allowSelfSignedCert"`
|
||||
Username string `xorm:"varchar(100)" json:"username"`
|
||||
Password string `xorm:"varchar(100)" json:"password"`
|
||||
BaseDn string `xorm:"varchar(100)" json:"baseDn"`
|
||||
@ -150,7 +151,7 @@ func UpdateLdap(ldap *Ldap) (bool, error) {
|
||||
}
|
||||
|
||||
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
|
||||
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type").Update(ldap)
|
||||
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type", "allow_self_signed_cert").Update(ldap)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
@ -106,6 +106,12 @@ func (l *LdapAutoSynchronizer) syncRoutine(ldap *Ldap, stopChan chan struct{}) e
|
||||
}
|
||||
|
||||
existed, failed, err := SyncLdapUsers(ldap.Owner, AutoAdjustLdapUser(users), ldap.Id)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
logs.Warning(fmt.Sprintf("autoSync failed for %s, error %s", ldap.Id, err))
|
||||
continue
|
||||
}
|
||||
|
||||
if len(failed) != 0 {
|
||||
logs.Warning(fmt.Sprintf("ldap autosync,%d new users,but %d user failed during :", len(users)-len(existed)-len(failed), len(failed)), failed)
|
||||
logs.Warning(err.Error())
|
||||
|
@ -16,6 +16,7 @@ package object
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -64,8 +65,11 @@ type LdapUser struct {
|
||||
|
||||
func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) {
|
||||
var conn *goldap.Conn
|
||||
tlsConfig := tls.Config{
|
||||
InsecureSkipVerify: ldap.AllowSelfSignedCert,
|
||||
}
|
||||
if ldap.EnableSsl {
|
||||
conn, err = goldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port), nil)
|
||||
conn, err = goldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port), &tlsConfig)
|
||||
} else {
|
||||
conn, err = goldap.Dial("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port))
|
||||
}
|
||||
|
@ -60,7 +60,8 @@ func (mfa *SmsMfa) Enable(user *User) error {
|
||||
columns = append(columns, "mfa_phone_enabled", "phone", "country_code")
|
||||
} else if mfa.MfaType == EmailType {
|
||||
user.MfaEmailEnabled = true
|
||||
columns = append(columns, "mfa_email_enabled", "email")
|
||||
user.EmailVerified = true
|
||||
columns = append(columns, "mfa_email_enabled", "email", "email_verified")
|
||||
}
|
||||
|
||||
_, err := UpdateUser(user.GetId(), user, columns, false)
|
||||
|
@ -77,6 +77,7 @@ func getOriginFromHostInternal(host string) (string, string) {
|
||||
return origin, origin
|
||||
}
|
||||
|
||||
isDev := conf.GetConfigString("runmode") == "dev"
|
||||
// "door.casdoor.com"
|
||||
protocol := "https://"
|
||||
if !strings.Contains(host, ".") {
|
||||
@ -87,7 +88,7 @@ func getOriginFromHostInternal(host string) (string, string) {
|
||||
protocol = "http://"
|
||||
}
|
||||
|
||||
if host == "localhost:8000" {
|
||||
if host == "localhost:8000" && isDev {
|
||||
return fmt.Sprintf("%s%s", protocol, "localhost:7001"), fmt.Sprintf("%s%s", protocol, "localhost:8000")
|
||||
} else {
|
||||
return fmt.Sprintf("%s%s", protocol, host), fmt.Sprintf("%s%s", protocol, host)
|
||||
@ -137,7 +138,7 @@ func GetOidcDiscovery(host string) OidcDiscovery {
|
||||
|
||||
func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
|
||||
jwks := jose.JSONWebKeySet{}
|
||||
certs, err := GetCerts("admin")
|
||||
certs, err := GetCerts("")
|
||||
if err != nil {
|
||||
return jwks, err
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ type Organization struct {
|
||||
PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"`
|
||||
PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"`
|
||||
PasswordExpireDays int `json:"passwordExpireDays"`
|
||||
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
|
||||
CountryCodes []string `xorm:"mediumtext" json:"countryCodes"`
|
||||
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
|
||||
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
|
||||
UserTypes []string `xorm:"mediumtext" json:"userTypes"`
|
||||
@ -80,7 +80,8 @@ type Organization struct {
|
||||
UseEmailAsUsername bool `json:"useEmailAsUsername"`
|
||||
EnableTour bool `json:"enableTour"`
|
||||
IpRestriction string `json:"ipRestriction"`
|
||||
NavItems []string `xorm:"varchar(500)" json:"navItems"`
|
||||
NavItems []string `xorm:"varchar(1000)" json:"navItems"`
|
||||
WidgetItems []string `xorm:"varchar(1000)" json:"widgetItems"`
|
||||
|
||||
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
|
||||
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
|
||||
@ -227,6 +228,7 @@ func UpdateOrganization(id string, organization *Organization, isGlobalAdmin boo
|
||||
|
||||
if !isGlobalAdmin {
|
||||
organization.NavItems = org.NavItems
|
||||
organization.WidgetItems = org.WidgetItems
|
||||
}
|
||||
|
||||
session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
|
||||
|
@ -157,7 +157,7 @@ func NewAdapter(driverName string, dataSourceName string, dbName string) (*Ormer
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// NewAdapterFromdb is the constructor for Ormer.
|
||||
// NewAdapterFromDb is the constructor for Ormer.
|
||||
func NewAdapterFromDb(driverName string, dataSourceName string, dbName string, db *sql.DB) (*Ormer, error) {
|
||||
a := &Ormer{}
|
||||
a.driverName = driverName
|
||||
|
@ -148,7 +148,7 @@ func UpdatePermission(id string, permission *Permission) (bool, error) {
|
||||
}
|
||||
|
||||
if permission.ResourceType == "Application" && permission.Model != "" {
|
||||
model, err := GetModelEx(util.GetId(owner, permission.Model))
|
||||
model, err := GetModelEx(util.GetId(permission.Owner, permission.Model))
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if model == nil {
|
||||
|
@ -48,6 +48,7 @@ type Provider struct {
|
||||
CustomLogo string `xorm:"varchar(200)" json:"customLogo"`
|
||||
Scopes string `xorm:"varchar(100)" json:"scopes"`
|
||||
UserMapping map[string]string `xorm:"varchar(500)" json:"userMapping"`
|
||||
HttpHeaders map[string]string `xorm:"varchar(500)" json:"httpHeaders"`
|
||||
|
||||
Host string `xorm:"varchar(100)" json:"host"`
|
||||
Port int `json:"port"`
|
||||
@ -384,6 +385,44 @@ func GetCaptchaProviderByApplication(applicationId, isCurrentProvider, lang stri
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func GetFaceIdProviderByOwnerName(applicationId, lang string) (*Provider, error) {
|
||||
owner, name := util.GetOwnerAndNameFromId(applicationId)
|
||||
provider := Provider{Owner: owner, Name: name, Category: "Face ID"}
|
||||
existed, err := ormer.Engine.Get(&provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !existed {
|
||||
return nil, fmt.Errorf(i18n.Translate(lang, "provider:the provider: %s does not exist"), applicationId)
|
||||
}
|
||||
|
||||
return &provider, nil
|
||||
}
|
||||
|
||||
func GetFaceIdProviderByApplication(applicationId, isCurrentProvider, lang string) (*Provider, error) {
|
||||
if isCurrentProvider == "true" {
|
||||
return GetFaceIdProviderByOwnerName(applicationId, lang)
|
||||
}
|
||||
application, err := GetApplication(applicationId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if application == nil || len(application.Providers) == 0 {
|
||||
return nil, fmt.Errorf(i18n.Translate(lang, "provider:Invalid application id"))
|
||||
}
|
||||
for _, provider := range application.Providers {
|
||||
if provider.Provider == nil {
|
||||
continue
|
||||
}
|
||||
if provider.Provider.Category == "Face ID" {
|
||||
return GetFaceIdProviderByOwnerName(util.GetId(provider.Provider.Owner, provider.Provider.Name), lang)
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func providerChangeTrigger(oldName string, newName string) error {
|
||||
session := ormer.Engine.NewSession()
|
||||
defer session.Close()
|
||||
|
@ -263,6 +263,27 @@ func addWebhookRecord(webhook *Webhook, record *casvisorsdk.Record, statusCode i
|
||||
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 {
|
||||
webhooks, err := getWebhooksByOrganization("")
|
||||
if err != nil {
|
||||
@ -271,7 +292,14 @@ func SendWebhooks(record *casvisorsdk.Record) error {
|
||||
|
||||
errs := []error{}
|
||||
webhooks = getFilteredWebhooks(webhooks, record.Organization, record.Action)
|
||||
|
||||
record2 := *record
|
||||
for _, webhook := range webhooks {
|
||||
|
||||
if len(webhook.ObjectFields) != 0 && webhook.ObjectFields[0] != "All" {
|
||||
record2.Object = filterRecordObject(record.Object, webhook.ObjectFields)
|
||||
}
|
||||
|
||||
var user *User
|
||||
if webhook.IsUserExtended {
|
||||
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 {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
err = addWebhookRecord(webhook, record, statusCode, respBody, err)
|
||||
err = addWebhookRecord(webhook, &record2, statusCode, respBody, err)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
saml "github.com/russellhaering/gosaml2"
|
||||
dsig "github.com/russellhaering/goxmldsig"
|
||||
|
@ -21,7 +21,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
|
@ -19,7 +19,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type ClaimsStandard struct {
|
||||
|
@ -15,13 +15,17 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/faceId"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/xorm-io/builder"
|
||||
@ -48,7 +52,7 @@ func InitUserManager() {
|
||||
|
||||
type User struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
Name string `xorm:"varchar(255) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100) index" json:"createdTime"`
|
||||
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
|
||||
DeletedTime string `xorm:"varchar(100)" json:"deletedTime"`
|
||||
@ -244,6 +248,7 @@ type MfaAccount struct {
|
||||
type FaceId struct {
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
FaceIdData []float64 `json:"faceIdData"`
|
||||
ImageUrl string `json:"ImageUrl"`
|
||||
}
|
||||
|
||||
func GetUserFieldStringValue(user *User, fieldName string) (bool, string, error) {
|
||||
@ -454,6 +459,31 @@ func GetUserByEmail(owner string, email string) (*User, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func GetUserByWebauthID(webauthId string) (*User, error) {
|
||||
user := User{}
|
||||
existed := false
|
||||
var err error
|
||||
|
||||
if ormer.driverName == "postgres" {
|
||||
existed, err = ormer.Engine.Where(builder.Like{"\"webauthnCredentials\"", webauthId}).Get(&user)
|
||||
} else if ormer.driverName == "mssql" {
|
||||
existed, err = ormer.Engine.Where("CAST(webauthnCredentials AS VARCHAR(MAX)) like ?", "%"+webauthId+"%").Get(&user)
|
||||
} else if ormer.driverName == "sqlite" {
|
||||
existed, err = ormer.Engine.Where("CAST(webauthnCredentials AS text) like ?", "%"+webauthId+"%").Get(&user)
|
||||
} else {
|
||||
existed, err = ormer.Engine.Where("webauthnCredentials like ?", "%"+webauthId+"%").Get(&user)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !existed {
|
||||
return nil, fmt.Errorf("user not exist")
|
||||
}
|
||||
|
||||
return &user, err
|
||||
}
|
||||
|
||||
func GetUserByEmailOnly(email string) (*User, error) {
|
||||
if email == "" {
|
||||
return nil, nil
|
||||
@ -807,6 +837,10 @@ func AddUser(user *User) (bool, error) {
|
||||
return false, fmt.Errorf("the user's owner and name should not be empty")
|
||||
}
|
||||
|
||||
if CheckUsername(user.Name, "en") != "" {
|
||||
user.Name = util.GetRandomName()
|
||||
}
|
||||
|
||||
organization, err := GetOrganizationByUser(user)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@ -815,6 +849,16 @@ func AddUser(user *User) (bool, error) {
|
||||
return false, fmt.Errorf("the organization: %s is not found", user.Owner)
|
||||
}
|
||||
|
||||
if user.Owner != "built-in" {
|
||||
applicationCount, err := GetOrganizationApplicationCount(organization.Owner, organization.Name, "", "")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if applicationCount == 0 {
|
||||
return false, fmt.Errorf("The organization: %s should have one application at least", organization.Owner)
|
||||
}
|
||||
}
|
||||
|
||||
if organization.DefaultPassword != "" && user.Password == "123" {
|
||||
user.Password = organization.DefaultPassword
|
||||
}
|
||||
@ -1179,6 +1223,40 @@ func (user *User) IsGlobalAdmin() bool {
|
||||
return user.Owner == "built-in"
|
||||
}
|
||||
|
||||
func (user *User) CheckUserFace(faceIdImage []string, provider *Provider) (bool, error) {
|
||||
faceIdChecker := faceId.GetFaceIdProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Endpoint)
|
||||
httpClient := proxy.DefaultHttpClient
|
||||
errList := []error{}
|
||||
for _, userFaceId := range user.FaceIds {
|
||||
if userFaceId.ImageUrl != "" {
|
||||
imgResp, err := httpClient.Get(userFaceId.ImageUrl)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
imgByte, err := io.ReadAll(imgResp.Body)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
base64Img := base64.StdEncoding.EncodeToString(imgByte)
|
||||
for _, imgBase64 := range faceIdImage {
|
||||
isSuccess, err := faceIdChecker.Check(imgBase64, base64Img)
|
||||
if err != nil {
|
||||
errList = append(errList, err)
|
||||
continue
|
||||
}
|
||||
if isSuccess {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(errList) > 0 {
|
||||
return false, errList[0]
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func GenerateIdForNewUser(application *Application) (string, error) {
|
||||
if application == nil || application.GetSignupItemRule("ID") != "Incremental" {
|
||||
return util.GenerateId(), nil
|
||||
|
@ -38,6 +38,8 @@ type Webhook struct {
|
||||
ContentType string `xorm:"varchar(100)" json:"contentType"`
|
||||
Headers []*Header `xorm:"mediumtext" json:"headers"`
|
||||
Events []string `xorm:"varchar(1000)" json:"events"`
|
||||
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
|
||||
ObjectFields []string `xorm:"varchar(1000)" json:"objectFields"`
|
||||
IsUserExtended bool `json:"isUserExtended"`
|
||||
SingleOrgOnly bool `json:"singleOrgOnly"`
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
|
@ -17,6 +17,7 @@ package object
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@ -25,7 +26,32 @@ import (
|
||||
|
||||
func sendWebhook(webhook *Webhook, record *casvisorsdk.Record, extendedUser *User) (int, string, error) {
|
||||
client := &http.Client{}
|
||||
userMap := make(map[string]interface{})
|
||||
var body io.Reader
|
||||
|
||||
if webhook.TokenFields != nil && len(webhook.TokenFields) > 0 && extendedUser != nil {
|
||||
userValue := reflect.ValueOf(extendedUser).Elem()
|
||||
|
||||
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"`
|
||||
@ -35,7 +61,8 @@ func sendWebhook(webhook *Webhook, record *casvisorsdk.Record, extendedUser *Use
|
||||
ExtendedUser: extendedUser,
|
||||
}
|
||||
|
||||
body := strings.NewReader(util.StructToJson(recordEx))
|
||||
body = strings.NewReader(util.StructToJson(recordEx))
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(webhook.Method, webhook.Url, body)
|
||||
if err != nil {
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@ -180,7 +181,11 @@ func (c *AirwallexClient) authRequest(method, url string, body interface{}) (map
|
||||
return nil, err
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest(method, url, bytes.NewBuffer(b))
|
||||
var reqBody io.Reader
|
||||
if method != "GET" {
|
||||
reqBody = bytes.NewBuffer(b)
|
||||
}
|
||||
req, _ := http.NewRequest(method, url, reqBody)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := c.client.Do(req)
|
||||
|
@ -106,11 +106,14 @@ func getOrganizationThemeCookieFromUrlPath(ctx *context.Context, urlPath string)
|
||||
}
|
||||
|
||||
organizationThemeCookie := &OrganizationThemeCookie{
|
||||
application.ThemeData,
|
||||
application.Logo,
|
||||
application.FooterHtml,
|
||||
organization.Favicon,
|
||||
organization.DisplayName,
|
||||
ThemeData: application.ThemeData,
|
||||
LogoUrl: application.Logo,
|
||||
FooterHtml: application.FooterHtml,
|
||||
}
|
||||
|
||||
if organization != nil {
|
||||
organizationThemeCookie.Favicon = organization.Favicon
|
||||
organizationThemeCookie.DisplayName = organization.DisplayName
|
||||
}
|
||||
|
||||
return organizationThemeCookie, nil
|
||||
|
@ -13,8 +13,8 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, ConfigProvider, Input, InputNumber, Popover, Radio, Result, Row, Select, Switch, Upload} from "antd";
|
||||
import {CopyOutlined, LinkOutlined, UploadOutlined} from "@ant-design/icons";
|
||||
import {Button, Card, Col, ConfigProvider, Input, InputNumber, Popover, Radio, Result, Row, Select, Space, Switch, Upload} from "antd";
|
||||
import {CopyOutlined, HolderOutlined, LinkOutlined, UploadOutlined, UsergroupAddOutlined} from "@ant-design/icons";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
import * as CertBackend from "./backend/CertBackend";
|
||||
import * as Setting from "./Setting";
|
||||
@ -36,6 +36,7 @@ import ThemeEditor from "./common/theme/ThemeEditor";
|
||||
|
||||
import SigninTable from "./table/SigninTable";
|
||||
import Editor from "./common/Editor";
|
||||
import * as GroupBackend from "./backend/GroupBackend";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
@ -52,6 +53,14 @@ const template = `<style>
|
||||
background-color: #333333;
|
||||
box-shadow: 0 0 30px 20px rgba(255, 255, 255, 0.20);
|
||||
}
|
||||
.forget-content {
|
||||
padding: 10px 100px 20px;
|
||||
margin: 30px auto;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 7px;
|
||||
background-color: rgb(255 255 255);
|
||||
box-shadow: 0 0 20px rgb(0 0 0 / 20%);
|
||||
}
|
||||
</style>`;
|
||||
|
||||
const previewGrid = Setting.isMobile() ? 22 : 11;
|
||||
@ -85,11 +94,11 @@ const sideTemplate = `<style>
|
||||
}
|
||||
</style>
|
||||
<div class="left-model">
|
||||
<span class="side-logo"> <img src="https://cdn.casbin.org/img/casdoor-logo_1185x256.png" alt="Casdoor" style="width: 120px">
|
||||
<span class="side-logo"> <img src="${Setting.StaticBaseUrl}/img/casdoor-logo_1185x256.png" alt="Casdoor" style="width: 120px">
|
||||
<span>SSO</span>
|
||||
</span>
|
||||
<div class="img">
|
||||
<img src="https://cdn.casbin.org/img/casbin.svg" alt="Casdoor"/>
|
||||
<img src="${Setting.StaticBaseUrl}/img/casbin.svg" alt="Casdoor"/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -116,6 +125,7 @@ class ApplicationEditPage extends React.Component {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getApplication();
|
||||
this.getOrganizations();
|
||||
this.getGroups();
|
||||
}
|
||||
|
||||
getApplication() {
|
||||
@ -167,6 +177,17 @@ class ApplicationEditPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
getGroups() {
|
||||
GroupBackend.getGroups(this.state.organizationName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
groups: res.data,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCerts(application) {
|
||||
let owner = application.organization;
|
||||
if (application.isShared) {
|
||||
@ -397,6 +418,16 @@ class ApplicationEditPage extends React.Component {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Forced redirect origin"), i18next.t("general:Forced redirect origin - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.application.forcedRedirectOrigin} onChange={e => {
|
||||
this.updateApplicationField("forcedRedirectOrigin", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Token format"), i18next.t("application:Token format - Tooltip"))} :
|
||||
@ -469,6 +500,31 @@ class ApplicationEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.application.defaultGroup ?? []} onChange={(value => {
|
||||
this.updateApplicationField("defaultGroup", value);
|
||||
})}
|
||||
>
|
||||
<Option key={""} value={""}>
|
||||
<Space>
|
||||
{i18next.t("general:Default")}
|
||||
</Space>
|
||||
</Option>
|
||||
{
|
||||
this.state.groups?.map((group) => <Option key={group.name} value={`${group.owner}/${group.name}`}>
|
||||
<Space>
|
||||
{group.type === "Physical" ? <UsergroupAddOutlined /> : <HolderOutlined />}
|
||||
{group.displayName}
|
||||
</Space>
|
||||
</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Enable signup"), i18next.t("application:Enable signup - Tooltip"))} :
|
||||
@ -804,6 +860,33 @@ class ApplicationEditPage extends React.Component {
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Background URL Mobile"), i18next.t("application:Background URL Mobile - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} style={(Setting.isMobile()) ? {maxWidth: "100%"} : {}}>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.application.formBackgroundUrlMobile} onChange={e => {
|
||||
this.updateApplicationField("formBackgroundUrlMobile", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Preview")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<a target="_blank" rel="noreferrer" href={this.state.application.formBackgroundUrlMobile}>
|
||||
<img src={this.state.application.formBackgroundUrlMobile} alt={this.state.application.formBackgroundUrlMobile} height={90} style={{marginBottom: "20px"}} />
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Custom CSS"), i18next.t("application:Custom CSS - Tooltip"))} :
|
||||
|
@ -109,7 +109,7 @@ class EntryPage extends React.Component {
|
||||
<React.Fragment>
|
||||
<CustomHead headerHtml={this.state.application?.headerHtml} />
|
||||
<div className={`${isDarkMode ? "loginBackgroundDark" : "loginBackground"}`}
|
||||
style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}>
|
||||
style={{backgroundImage: Setting.inIframe() ? null : (Setting.isMobile() ? `url(${this.state.application?.formBackgroundUrlMobile})` : `url(${this.state.application?.formBackgroundUrl})`)}}>
|
||||
<Spin size="large" spinning={this.state.application === undefined && this.state.pricing === undefined} tip={i18next.t("login:Loading")}
|
||||
style={{width: "100%", margin: "0 auto", position: "absolute"}} />
|
||||
<Switch>
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Table} from "antd";
|
||||
import {Button, Table, Tooltip} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as GroupBackend from "./backend/GroupBackend";
|
||||
@ -202,12 +202,16 @@ class GroupListPage extends BaseListPage {
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/groups/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
{
|
||||
record.haveChildren ? <Tooltip placement="topLeft" title={i18next.t("group:You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page")}>
|
||||
<Button disabled type="primary" danger>{i18next.t("general:Delete")}</Button>
|
||||
</Tooltip> :
|
||||
<PopconfirmModal
|
||||
disabled={record.haveChildren}
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteGroup(index)}
|
||||
>
|
||||
</PopconfirmModal>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -114,7 +114,7 @@ class InvitationEditPage extends React.Component {
|
||||
const selectedOrganization = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner);
|
||||
defaultApplication = selectedOrganization.defaultApplication;
|
||||
if (!defaultApplication) {
|
||||
Setting.showMessage("error", i18next.t("invitation:You need to specify a default application for ") + selectedOrganization.name);
|
||||
Setting.showMessage("error", i18next.t("invitation:You need to first specify a default application for organization: ") + selectedOrganization.name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -170,6 +170,16 @@ class LdapEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||
{Setting.getLabel(i18next.t("ldap:Allow self-signed certificate"), i18next.t("ldap:Allow self-signed certificate - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Switch checked={this.state.ldap.allowSelfSignedCert} onChange={checked => {
|
||||
this.updateLdapField("allowSelfSignedCert", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||
{Setting.getLabel(i18next.t("ldap:Base DN"), i18next.t("ldap:Base DN - Tooltip"))} :
|
||||
|
@ -95,8 +95,9 @@ import TransactionEditPage from "./TransactionEditPage";
|
||||
import VerificationListPage from "./VerificationListPage";
|
||||
|
||||
function ManagementPage(props) {
|
||||
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const navItems = props.account?.organization?.navItems;
|
||||
const widgetItems = props.account?.organization?.widgetItems;
|
||||
|
||||
function logout() {
|
||||
AuthBackend.logout()
|
||||
@ -175,6 +176,35 @@ function ManagementPage(props) {
|
||||
);
|
||||
}
|
||||
|
||||
function navItemsIsAll() {
|
||||
return !Array.isArray(navItems) || !!navItems?.includes("all");
|
||||
}
|
||||
|
||||
function widgetItemsIsAll() {
|
||||
return !Array.isArray(widgetItems) || !!widgetItems?.includes("all");
|
||||
}
|
||||
|
||||
function renderWidgets() {
|
||||
const widgets = [
|
||||
Setting.getItem(<ThemeSelect themeAlgorithm={props.themeAlgorithm} onChange={props.setLogoAndThemeAlgorithm} />, "theme"),
|
||||
Setting.getItem(<LanguageSelect languages={props.account.organization.languages} />, "language"),
|
||||
Setting.getItem(Conf.AiAssistantUrl?.trim() && (
|
||||
<Tooltip title="Click to open AI assistant">
|
||||
<div className="select-box" onClick={props.openAiAssistant}>
|
||||
<DeploymentUnitOutlined style={{fontSize: "24px"}} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
), "ai-assistant"),
|
||||
Setting.getItem(<OpenTour />, "tour"),
|
||||
];
|
||||
|
||||
if (widgetItemsIsAll()) {
|
||||
return widgets.map(item => item.label);
|
||||
}
|
||||
|
||||
return widgets.filter(item => widgetItems.includes(item.key)).map(item => item.label);
|
||||
}
|
||||
|
||||
function renderAccountMenu() {
|
||||
if (props.account === undefined) {
|
||||
return null;
|
||||
@ -188,20 +218,7 @@ function ManagementPage(props) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{renderRightDropdown()}
|
||||
<ThemeSelect
|
||||
themeAlgorithm={props.themeAlgorithm}
|
||||
onChange={props.setLogoAndThemeAlgorithm} />
|
||||
<LanguageSelect languages={props.account.organization.languages} />
|
||||
{
|
||||
Conf.AiAssistantUrl?.trim() && (
|
||||
<Tooltip title="Click to open AI assistant">
|
||||
<div className="select-box" onClick={props.openAiAssistant}>
|
||||
<DeploymentUnitOutlined style={{fontSize: "24px"}} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
<OpenTour />
|
||||
{renderWidgets()}
|
||||
{Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) &&
|
||||
<OrganizationSelect
|
||||
initValue={Setting.getOrganization()}
|
||||
@ -323,13 +340,7 @@ function ManagementPage(props) {
|
||||
}
|
||||
}
|
||||
|
||||
const navItems = props.account.organization.navItems;
|
||||
|
||||
if (!Array.isArray(navItems)) {
|
||||
return res;
|
||||
}
|
||||
|
||||
if (navItems.includes("all")) {
|
||||
if (navItemsIsAll()) {
|
||||
return res;
|
||||
}
|
||||
|
||||
@ -443,8 +454,6 @@ function ManagementPage(props) {
|
||||
return Setting.isMobile() || window.location.pathname.startsWith("/trees");
|
||||
}
|
||||
|
||||
const menuStyleRight = Setting.isAdminUser(props.account) && !Setting.isMobile() ? "calc(180px + 280px)" : "320px";
|
||||
|
||||
const onClose = () => {
|
||||
setMenuVisible(false);
|
||||
};
|
||||
@ -456,8 +465,9 @@ function ManagementPage(props) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EnableMfaNotification account={props.account} />
|
||||
<Header style={{padding: "0", marginBottom: "3px", backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}} >
|
||||
{props.requiredEnableMfa || (Setting.isMobile() ?
|
||||
<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
|
||||
@ -472,18 +482,23 @@ function ManagementPage(props) {
|
||||
<Button icon={<BarsOutlined />} onClick={showMenu} type="text">
|
||||
{i18next.t("general:Menu")}
|
||||
</Button>
|
||||
</React.Fragment> :
|
||||
</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={{position: "absolute", left: 0, right: menuStyleRight, backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}}
|
||||
style={{backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
renderAccountMenu()
|
||||
</div>
|
||||
))
|
||||
}
|
||||
<div style={{flexShrink: 0}}>
|
||||
{renderAccountMenu()}
|
||||
</div>
|
||||
</Header>
|
||||
<Content style={{display: "flex", flexDirection: "column"}} >
|
||||
{isWithoutCard() ?
|
||||
|
@ -27,6 +27,7 @@ import AccountTable from "./table/AccountTable";
|
||||
import ThemeEditor from "./common/theme/ThemeEditor";
|
||||
import MfaTable from "./table/MfaTable";
|
||||
import {NavItemTree} from "./common/NavItemTree";
|
||||
import {WidgetItemTree} from "./common/WidgetItemTree";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
@ -275,7 +276,7 @@ class OrganizationEditPage extends React.Component {
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<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>
|
||||
</Row>
|
||||
@ -537,7 +538,7 @@ class OrganizationEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Navbar items"), i18next.t("general:Navbar items - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("organization:Navbar items"), i18next.t("organization:Navbar items - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<NavItemTree
|
||||
@ -550,6 +551,21 @@ class OrganizationEditPage extends React.Component {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Widget items"), i18next.t("organization:Widget items - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<WidgetItemTree
|
||||
disabled={!Setting.isAdminUser(this.props.account)}
|
||||
checkedKeys={this.state.organization.widgetItems ?? ["all"]}
|
||||
defaultExpandedKeys={["all"]}
|
||||
onCheck={(checked, _) => {
|
||||
this.updateOrganizationField("widgetItems", checked);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :
|
||||
|
@ -232,6 +232,15 @@ class PlanEditPage extends React.Component {
|
||||
[
|
||||
{id: "USD", name: "USD"},
|
||||
{id: "CNY", name: "CNY"},
|
||||
{id: "EUR", name: "EUR"},
|
||||
{id: "JPY", name: "JPY"},
|
||||
{id: "GBP", name: "GBP"},
|
||||
{id: "AUD", name: "AUD"},
|
||||
{id: "CAD", name: "CAD"},
|
||||
{id: "CHF", name: "CHF"},
|
||||
{id: "HKD", name: "HKD"},
|
||||
{id: "SGD", name: "SGD"},
|
||||
{id: "BRL", name: "BRL"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
|
@ -139,6 +139,8 @@ class ProductBuyPage extends React.Component {
|
||||
return "HK$";
|
||||
} else if (product?.currency === "SGD") {
|
||||
return "S$";
|
||||
} else if (product?.currency === "BRL") {
|
||||
return "R$";
|
||||
} else {
|
||||
return "(Unknown currency)";
|
||||
}
|
||||
|
@ -217,6 +217,7 @@ class ProductEditPage extends React.Component {
|
||||
{id: "CHF", name: "CHF"},
|
||||
{id: "HKD", name: "HKD"},
|
||||
{id: "SGD", name: "SGD"},
|
||||
{id: "BRL", name: "BRL"},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
|
@ -29,6 +29,7 @@ import {CaptchaPreview} from "./common/CaptchaPreview";
|
||||
import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
|
||||
import * as Web3Auth from "./auth/Web3Auth";
|
||||
import Editor from "./common/Editor";
|
||||
import HttpHeaderTable from "./table/HttpHeaderTable";
|
||||
|
||||
const {Option} = Select;
|
||||
const {TextArea} = Input;
|
||||
@ -41,6 +42,13 @@ const defaultUserMapping = {
|
||||
avatarUrl: "avatarUrl",
|
||||
};
|
||||
|
||||
const defaultEmailMapping = {
|
||||
fromName: "fromName",
|
||||
toAddress: "toAddress",
|
||||
subject: "subject",
|
||||
content: "content",
|
||||
};
|
||||
|
||||
class ProviderEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -71,7 +79,16 @@ class ProviderEditPage extends React.Component {
|
||||
|
||||
if (res.status === "ok") {
|
||||
const provider = res.data;
|
||||
if (provider.type === "Custom HTTP Email") {
|
||||
if (!provider.userMapping) {
|
||||
provider.userMapping = provider.userMapping || defaultEmailMapping;
|
||||
}
|
||||
if (!provider.userMapping?.fromName) {
|
||||
provider.userMapping = defaultEmailMapping;
|
||||
}
|
||||
} else {
|
||||
provider.userMapping = provider.userMapping || defaultUserMapping;
|
||||
}
|
||||
this.setState({
|
||||
provider: provider,
|
||||
});
|
||||
@ -145,10 +162,17 @@ class ProviderEditPage extends React.Component {
|
||||
const requiredKeys = ["id", "username", "displayName"];
|
||||
const provider = this.state.provider;
|
||||
|
||||
if (provider.type === "Custom HTTP Email") {
|
||||
if (value === "") {
|
||||
Setting.showMessage("error", i18next.t("provider:This field is required"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (value === "" && requiredKeys.includes(key)) {
|
||||
Setting.showMessage("error", i18next.t("provider:This field is required"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
provider.userMapping[key] = value;
|
||||
|
||||
@ -183,6 +207,30 @@ class ProviderEditPage extends React.Component {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderEmailMappingInput() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{Setting.getLabel(i18next.t("provider:From name"), i18next.t("provider:From name - Tooltip"))} :
|
||||
<Input value={this.state.provider.userMapping.fromName} onChange={e => {
|
||||
this.updateUserMappingField("fromName", e.target.value);
|
||||
}} />
|
||||
{Setting.getLabel(i18next.t("provider:From address"), i18next.t("provider:From address - Tooltip"))} :
|
||||
<Input value={this.state.provider.userMapping.toAddress} onChange={e => {
|
||||
this.updateUserMappingField("toAddress", e.target.value);
|
||||
}} />
|
||||
{Setting.getLabel(i18next.t("provider:Subject"), i18next.t("provider:Subject - Tooltip"))} :
|
||||
<Input value={this.state.provider.userMapping.subject} onChange={e => {
|
||||
this.updateUserMappingField("subject", e.target.value);
|
||||
}} />
|
||||
{Setting.getLabel(i18next.t("provider:Email content"), i18next.t("provider:Email content - Tooltip"))} :
|
||||
<Input value={this.state.provider.userMapping.content} onChange={e => {
|
||||
this.updateUserMappingField("content", e.target.value);
|
||||
}} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
getClientIdLabel(provider) {
|
||||
switch (provider.category) {
|
||||
case "OAuth":
|
||||
@ -288,10 +336,8 @@ class ProviderEditPage extends React.Component {
|
||||
default:
|
||||
if (provider.type === "Aliyun Captcha") {
|
||||
return Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip"));
|
||||
} else if (provider.type === "WeChat Pay") {
|
||||
} else if (provider.type === "WeChat Pay" || provider.type === "CUCloud") {
|
||||
return Setting.getLabel(i18next.t("provider:App ID"), i18next.t("provider:App ID - Tooltip"));
|
||||
} else if (provider.type === "CUCloud") {
|
||||
return Setting.getLabel(i18next.t("provider:Account ID"), i18next.t("provider:Account ID - Tooltip"));
|
||||
} else {
|
||||
return Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"));
|
||||
}
|
||||
@ -389,8 +435,8 @@ class ProviderEditPage extends React.Component {
|
||||
text = i18next.t("provider:App Key");
|
||||
tooltip = i18next.t("provider:App Key - Tooltip");
|
||||
} else if (provider.type === "CUCloud") {
|
||||
text = i18next.t("provider:Topic name");
|
||||
tooltip = i18next.t("provider:Topic name - Tooltip");
|
||||
text = "Topic name";
|
||||
tooltip = "Topic name - Tooltip";
|
||||
}
|
||||
}
|
||||
|
||||
@ -567,6 +613,8 @@ class ProviderEditPage extends React.Component {
|
||||
this.updateProviderField("type", "MetaMask");
|
||||
} else if (value === "Notification") {
|
||||
this.updateProviderField("type", "Telegram");
|
||||
} else if (value === "Face ID") {
|
||||
this.updateProviderField("type", "Alibaba Cloud Facebody");
|
||||
}
|
||||
})}>
|
||||
{
|
||||
@ -580,6 +628,7 @@ class ProviderEditPage extends React.Component {
|
||||
{id: "SMS", name: "SMS"},
|
||||
{id: "Storage", name: "Storage"},
|
||||
{id: "Web3", name: "Web3"},
|
||||
{id: "Face ID", name: "Face ID"},
|
||||
]
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
|
||||
@ -770,6 +819,7 @@ class ProviderEditPage extends React.Component {
|
||||
(this.state.provider.category === "Web3") ||
|
||||
(this.state.provider.category === "Storage" && this.state.provider.type === "Local File System") ||
|
||||
(this.state.provider.category === "SMS" && this.state.provider.type === "Custom HTTP SMS") ||
|
||||
(this.state.provider.category === "Email" && this.state.provider.type === "Custom HTTP Email") ||
|
||||
(this.state.provider.category === "Notification" && (this.state.provider.type === "Google Chat" || this.state.provider.type === "Custom HTTP") || this.state.provider.type === "Balance") ? null : (
|
||||
<React.Fragment>
|
||||
{
|
||||
@ -901,7 +951,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
{this.state.provider.category === "Storage" || ["Custom HTTP SMS", "Custom HTTP Email", "SendGrid", "CUCloud"].includes(this.state.provider.type) ? (
|
||||
{["Face ID", "Storage"].includes(this.state.provider.category) || ["Custom HTTP SMS", "Custom HTTP Email", "SendGrid", "CUCloud"].includes(this.state.provider.type) ? (
|
||||
<div>
|
||||
{["Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
@ -915,7 +965,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
{["Custom HTTP SMS", "SendGrid", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||
{["Custom HTTP SMS", "Custom HTTP Email", "SendGrid", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor", "CUCloud", "Alibaba Cloud Facebody"].includes(this.state.provider.type) ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
|
||||
@ -927,7 +977,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
{["Custom HTTP SMS", "SendGrid", "Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||
{["Custom HTTP SMS", "Custom HTTP Email", "SendGrid", "Local File System", "CUCloud", "Alibaba Cloud Facebody"].includes(this.state.provider.type) ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{["Casdoor"].includes(this.state.provider.type) ?
|
||||
@ -941,7 +991,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
{["Custom HTTP SMS", "SendGrid", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||
{["Custom HTTP SMS", "Custom HTTP Email", "SendGrid", "CUCloud", "Alibaba Cloud Facebody"].includes(this.state.provider.type) ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} :
|
||||
@ -953,7 +1003,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
{["Custom HTTP SMS", "SendGrid", "Synology", "Casdoor", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||
{["Custom HTTP SMS", "Custom HTTP Email", "SendGrid", "Synology", "Casdoor", "CUCloud", "Alibaba Cloud Facebody"].includes(this.state.provider.type) ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
|
||||
@ -1094,6 +1144,66 @@ class ProviderEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
{
|
||||
!["Custom HTTP Email"].includes(this.state.provider.type) ? null : (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.method} onChange={value => {
|
||||
this.updateProviderField("method", value);
|
||||
}}>
|
||||
{
|
||||
[
|
||||
{id: "GET", name: "GET"},
|
||||
{id: "POST", name: "POST"},
|
||||
{id: "PUT", name: "PUT"},
|
||||
{id: "DELETE", name: "DELETE"},
|
||||
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.provider.method !== "GET" ? (<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("webhook:Content type"), i18next.t("webhook:Content type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.issuerUrl === "" ? "application/x-www-form-urlencoded" : this.state.provider.issuerUrl} onChange={value => {
|
||||
this.updateProviderField("issuerUrl", value);
|
||||
}}>
|
||||
{
|
||||
[
|
||||
{id: "application/json", name: "application/json"},
|
||||
{id: "application/x-www-form-urlencoded", name: "application/x-www-form-urlencoded"},
|
||||
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>) : null
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:HTTP header"), i18next.t("provider:HTTP header - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<HttpHeaderTable httpHeaders={this.state.provider.httpHeaders} onUpdateTable={(value) => {this.updateProviderField("httpHeaders", value);}} />
|
||||
</Col>
|
||||
</Row>
|
||||
{this.state.provider.method !== "GET" ? <Row style={{marginTop: "20px"}}>
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:HTTP body mapping"), i18next.t("provider:HTTP body mapping - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
{this.renderEmailMappingInput()}
|
||||
</Col>
|
||||
</Row> : null}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Email title"), i18next.t("provider:Email title - Tooltip"))} :
|
||||
@ -1162,7 +1272,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Button>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
) : this.state.provider.category === "SMS" ? (
|
||||
) : ["SMS"].includes(this.state.provider.category) ? (
|
||||
<React.Fragment>
|
||||
{["Custom HTTP SMS", "Twilio SMS", "Amazon SNS", "Azure ACS", "Msg91 SMS", "Infobip SMS"].includes(this.state.provider.type) ?
|
||||
null :
|
||||
@ -1280,7 +1390,7 @@ class ProviderEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
<Col span={16} >
|
||||
<Button type="primary" loading={this.state.metadataLoading} onClick={() => {this.fetchSamlMetadata();}}>{i18next.t("general:Request")}</Button>
|
||||
<Button style={{marginLeft: "10px"}} type="primary" loading={this.state.metadataLoading} onClick={() => {this.fetchSamlMetadata();}}>{i18next.t("general:Request")}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
|
@ -416,6 +416,12 @@ export const OtherProviderInfo = {
|
||||
url: "https://www.cucloud.cn/",
|
||||
},
|
||||
},
|
||||
"Face ID": {
|
||||
"Alibaba Cloud Facebody": {
|
||||
logo: `${StaticBaseUrl}/img/social_aliyun.png`,
|
||||
url: "https://vision.aliyun.com/facebody",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function initCountries() {
|
||||
@ -1150,6 +1156,10 @@ export function getProviderTypeOptions(category) {
|
||||
{id: "Viber", name: "Viber"},
|
||||
{id: "CUCloud", name: "CUCloud"},
|
||||
]);
|
||||
} else if (category === "Face ID") {
|
||||
return ([
|
||||
{id: "Alibaba Cloud Facebody", name: "Alibaba Cloud Facebody"},
|
||||
]);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
@ -1522,7 +1532,7 @@ export function getUserCommonFields() {
|
||||
}
|
||||
|
||||
export function getDefaultFooterContent() {
|
||||
return "Powered by <a target=\"_blank\" href=\"https://casdoor.org\" rel=\"noreferrer\"><img style=\"padding-bottom: 3px\" height=\"20\" alt=\"Casdoor\" src=\"https://cdn.casbin.org/img/casdoor-logo_1185x256.png\"/></a>";
|
||||
return `Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style="padding-bottom: 3px" height="20" alt="Casdoor" src="${StaticBaseUrl}/img/casdoor-logo_1185x256.png"/></a>`;
|
||||
}
|
||||
|
||||
export function getEmptyFooterContent() {
|
||||
@ -1554,7 +1564,7 @@ export function getDefaultHtmlEmailContent() {
|
||||
<div class="email-container">
|
||||
<div class="header">
|
||||
<h3>Casbin Organization</h3>
|
||||
<img src="https://cdn.casbin.org/img/casdoor-logo_1185x256.png" alt="Casdoor Logo" width="300">
|
||||
<img src="${StaticBaseUrl}/img/casdoor-logo_1185x256.png" alt="Casdoor Logo" width="300">
|
||||
</div>
|
||||
<p><strong>%{user.friendlyName}</strong>, here is your verification code</p>
|
||||
<p>Use this code for your transaction. It's valid for 5 minutes</p>
|
||||
@ -1593,6 +1603,8 @@ export function getCurrencyText(product) {
|
||||
return i18next.t("currency:HKD");
|
||||
} else if (product?.currency === "SGD") {
|
||||
return i18next.t("currency:SGD");
|
||||
} else if (product?.currency === "BRL") {
|
||||
return i18next.t("currency:BRL");
|
||||
} else {
|
||||
return "(Unknown currency)";
|
||||
}
|
||||
|
@ -37,17 +37,35 @@ class SystemInfo extends React.Component {
|
||||
UNSAFE_componentWillMount() {
|
||||
SystemBackend.getSystemInfo("").then(res => {
|
||||
this.setState({
|
||||
systemInfo: res.data,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
systemInfo: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
this.stopTimer();
|
||||
}
|
||||
|
||||
const id = setInterval(() => {
|
||||
SystemBackend.getSystemInfo("").then(res => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
systemInfo: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
this.stopTimer();
|
||||
}
|
||||
}).catch(error => {
|
||||
Setting.showMessage("error", `System info failed to get: ${error}`);
|
||||
this.stopTimer();
|
||||
});
|
||||
SystemBackend.getPrometheusInfo().then(res => {
|
||||
this.setState({
|
||||
@ -55,17 +73,25 @@ class SystemInfo extends React.Component {
|
||||
});
|
||||
});
|
||||
}, 1000 * 2);
|
||||
|
||||
this.setState({intervalId: id});
|
||||
}).catch(error => {
|
||||
Setting.showMessage("error", `System info failed to get: ${error}`);
|
||||
this.stopTimer();
|
||||
});
|
||||
|
||||
SystemBackend.getVersionInfo().then(res => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
versionInfo: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
this.stopTimer();
|
||||
}
|
||||
}).catch(err => {
|
||||
Setting.showMessage("error", `Version info failed to get: ${err}`);
|
||||
this.stopTimer();
|
||||
});
|
||||
}
|
||||
|
||||
@ -77,10 +103,14 @@ class SystemInfo extends React.Component {
|
||||
this.setState({isTourVisible: TourConfig.getTourVisible()});
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
stopTimer() {
|
||||
if (this.state.intervalId !== null) {
|
||||
clearInterval(this.state.intervalId);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.stopTimer();
|
||||
window.removeEventListener("storageTourChanged", this.handleTourChange);
|
||||
}
|
||||
|
||||
@ -125,9 +155,9 @@ class SystemInfo extends React.Component {
|
||||
<br /> <br />
|
||||
<Progress type="circle" percent={Number((Number(this.state.systemInfo.memoryUsed) / Number(this.state.systemInfo.memoryTotal) * 100).toFixed(2))} />
|
||||
</div>;
|
||||
const latencyUi = this.state.prometheusInfo.apiLatency === null || this.state.prometheusInfo.apiLatency?.length <= 0 ? <Spin size="large" /> :
|
||||
const latencyUi = this.state.prometheusInfo?.apiLatency === null || this.state.prometheusInfo?.apiLatency?.length <= 0 ? <Spin size="large" /> :
|
||||
<PrometheusInfoTable prometheusInfo={this.state.prometheusInfo} table={"latency"} />;
|
||||
const throughputUi = this.state.prometheusInfo.apiThroughput === null || this.state.prometheusInfo.apiThroughput?.length <= 0 ? <Spin size="large" /> :
|
||||
const throughputUi = this.state.prometheusInfo?.apiThroughput === null || this.state.prometheusInfo?.apiThroughput?.length <= 0 ? <Spin size="large" /> :
|
||||
<PrometheusInfoTable prometheusInfo={this.state.prometheusInfo} table={"throughput"} />;
|
||||
const link = this.state.versionInfo?.version !== "" ? `https://github.com/casdoor/casdoor/releases/tag/${this.state.versionInfo?.version}` : "";
|
||||
let versionText = this.state.versionInfo?.version !== "" ? this.state.versionInfo?.version : i18next.t("system:Unknown version");
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import * as Setting from "./Setting";
|
||||
|
||||
export const TourObj = {
|
||||
home: [
|
||||
@ -8,7 +9,7 @@ export const TourObj = {
|
||||
cover: (
|
||||
<img
|
||||
alt="casdoor.png"
|
||||
src="https://cdn.casbin.org/img/casdoor-logo_1185x256.png"
|
||||
src={`${Setting.StaticBaseUrl}/img/casdoor-logo_1185x256.png`}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
@ -1054,6 +1054,7 @@ class UserEditPage extends React.Component {
|
||||
<FaceIdTable
|
||||
title={i18next.t("user:Face IDs")}
|
||||
table={this.state.user.faceIds}
|
||||
{...this.props}
|
||||
onUpdateTable={(table) => {this.updateUserField("faceIds", table);}}
|
||||
/>
|
||||
</Col>
|
||||
|
@ -144,6 +144,9 @@ class WebhookEditPage extends React.Component {
|
||||
if (["port"].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
}
|
||||
if (key === "objectFields") {
|
||||
value = value.includes("All") ? ["All"] : value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@ -174,8 +177,17 @@ class WebhookEditPage extends React.Component {
|
||||
renderWebhook() {
|
||||
const preview = Setting.deepCopy(previewTemplate);
|
||||
if (this.state.webhook.isUserExtended) {
|
||||
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);
|
||||
|
||||
return (
|
||||
@ -285,6 +297,19 @@ class WebhookEditPage extends React.Component {
|
||||
</Select>
|
||||
</Col>
|
||||
</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"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{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>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("webhook:Extended user fields"), i18next.t("webhook:Extended user fields - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="tags" showSearch style={{width: "100%"}} value={this.state.webhook.tokenFields} onChange={(value => {this.updateWebhookField("tokenFields", value);})}>
|
||||
{
|
||||
Setting.getUserCommonFields().map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
|
||||
|
@ -471,6 +471,8 @@ class ForgetPage extends React.Component {
|
||||
<React.Fragment>
|
||||
<CustomGithubCorner />
|
||||
<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"
|
||||
style={{position: "relative", left: Setting.isMobile() ? "10px" : "-90px", top: 0}}
|
||||
icon={<ArrowLeftOutlined style={{fontSize: "24px"}} />}
|
||||
|
@ -37,6 +37,8 @@ import RedirectForm from "../common/RedirectForm";
|
||||
import {RequiredMfa} from "./mfa/MfaAuthVerifyForm";
|
||||
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
|
||||
import * as ProviderButton from "./ProviderButton";
|
||||
import {goToLink} from "../Setting";
|
||||
const FaceRecognitionCommonModal = lazy(() => import("../common/modal/FaceRecognitionCommonModal"));
|
||||
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
|
||||
|
||||
class LoginPage extends React.Component {
|
||||
@ -61,6 +63,8 @@ class LoginPage extends React.Component {
|
||||
isTermsOfUseVisible: false,
|
||||
termsOfUseContent: "",
|
||||
orgChoiceMode: new URLSearchParams(props.location?.search).get("orgChoiceMode") ?? null,
|
||||
userLang: null,
|
||||
loginLoading: false,
|
||||
};
|
||||
|
||||
if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) {
|
||||
@ -262,6 +266,13 @@ class LoginPage extends React.Component {
|
||||
|
||||
onUpdateApplication(application) {
|
||||
this.props.onUpdateApplication(application);
|
||||
for (const idx in application.providers) {
|
||||
const provider = application.providers[idx];
|
||||
if (provider.provider?.category === "Face ID") {
|
||||
this.setState({haveFaceIdProvider: true});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseOffset(offset) {
|
||||
@ -355,6 +366,7 @@ class LoginPage extends React.Component {
|
||||
}
|
||||
|
||||
onFinish(values) {
|
||||
this.setState({loginLoading: true});
|
||||
if (this.state.loginMethod === "webAuthn") {
|
||||
let username = this.state.username;
|
||||
if (username === null || username === "") {
|
||||
@ -379,6 +391,9 @@ class LoginPage extends React.Component {
|
||||
}).then(res => res.json())
|
||||
.then((res) => {
|
||||
if (res.status === "error") {
|
||||
this.setState({
|
||||
loginLoading: false,
|
||||
});
|
||||
Setting.showMessage("error", res.msg);
|
||||
return;
|
||||
}
|
||||
@ -415,6 +430,7 @@ class LoginPage extends React.Component {
|
||||
|
||||
login(values) {
|
||||
// here we are supposed to determine whether Casdoor is working as an OAuth server or CAS server
|
||||
values["language"] = this.state.userLang ?? "";
|
||||
if (this.state.type === "cas") {
|
||||
// CAS
|
||||
const casParams = Util.getCasParameters();
|
||||
@ -442,6 +458,8 @@ class LoginPage extends React.Component {
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.setState({loginLoading: false});
|
||||
});
|
||||
} else {
|
||||
// OAuth
|
||||
@ -497,6 +515,8 @@ class LoginPage extends React.Component {
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.setState({loginLoading: false});
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -566,7 +586,7 @@ class LoginPage extends React.Component {
|
||||
return (
|
||||
<div key={resultItemKey} className="login-languages">
|
||||
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
|
||||
<LanguageSelect languages={application.organizationObj.languages} />
|
||||
<LanguageSelect languages={application.organizationObj.languages} onClick={key => {this.setState({userLang: key});}} />
|
||||
</div>
|
||||
);
|
||||
} else if (signinItem.name === "Signin methods") {
|
||||
@ -578,6 +598,9 @@ class LoginPage extends React.Component {
|
||||
)
|
||||
;
|
||||
} else if (signinItem.name === "Username") {
|
||||
if (this.state.loginMethod === "webAuthn") {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div key={resultItemKey}>
|
||||
<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">
|
||||
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
|
||||
<Button
|
||||
loading={this.state.loginLoading}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
className="login-button"
|
||||
@ -696,6 +720,12 @@ class LoginPage extends React.Component {
|
||||
</Button>
|
||||
{
|
||||
this.state.loginMethod === "faceId" ?
|
||||
this.state.haveFaceIdProvider ? <Suspense fallback={null}><FaceRecognitionCommonModal visible={this.state.openFaceRecognitionModal} onOk={(FaceIdImage) => {
|
||||
const values = this.state.values;
|
||||
values["FaceIdImage"] = FaceIdImage;
|
||||
this.login(values);
|
||||
this.setState({openFaceRecognitionModal: false});
|
||||
}} onCancel={() => this.setState({openFaceRecognitionModal: false, loginLoading: false})} /></Suspense> :
|
||||
<Suspense fallback={null}>
|
||||
<FaceRecognitionModal
|
||||
visible={this.state.openFaceRecognitionModal}
|
||||
@ -706,7 +736,7 @@ class LoginPage extends React.Component {
|
||||
this.login(values);
|
||||
this.setState({openFaceRecognitionModal: false});
|
||||
}}
|
||||
onCancel={() => this.setState({openFaceRecognitionModal: false})}
|
||||
onCancel={() => this.setState({openFaceRecognitionModal: false, loginLoading: false})}
|
||||
/>
|
||||
</Suspense>
|
||||
:
|
||||
@ -723,6 +753,8 @@ class LoginPage extends React.Component {
|
||||
if (signinItem.rule === "None" || signinItem.rule === "") {
|
||||
signinItem.rule = showForm ? "small" : "big";
|
||||
}
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const providerHint = searchParams.get("provider_hint");
|
||||
|
||||
return (
|
||||
<div key={resultItemKey}>
|
||||
@ -730,6 +762,10 @@ class LoginPage extends React.Component {
|
||||
<Form.Item>
|
||||
{
|
||||
application.providers.filter(providerItem => this.isProviderVisible(providerItem)).map((providerItem, id) => {
|
||||
if (providerHint === providerItem.provider.name) {
|
||||
goToLink(Provider.getAuthUrl(application, providerItem.provider, "signup"));
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<span key={id} onClick={(e) => {
|
||||
const agreementChecked = this.form.current.getFieldValue("agreement");
|
||||
@ -805,7 +841,6 @@ class LoginPage extends React.Component {
|
||||
<Form
|
||||
name="normal_login"
|
||||
initialValues={{
|
||||
|
||||
organization: application.organization,
|
||||
application: application.name,
|
||||
autoSignin: true,
|
||||
@ -908,7 +943,7 @@ class LoginPage extends React.Component {
|
||||
this.login(values);
|
||||
this.setState({openCaptchaModal: false});
|
||||
}}
|
||||
onCancel={() => this.setState({openCaptchaModal: false})}
|
||||
onCancel={() => this.setState({openCaptchaModal: false, loginLoading: false})}
|
||||
isCurrentProvider={true}
|
||||
/>;
|
||||
}
|
||||
@ -975,7 +1010,7 @@ class LoginPage extends React.Component {
|
||||
const oAuthParams = Util.getOAuthGetParameters();
|
||||
this.populateOauthValues(values);
|
||||
const application = this.getApplicationObj();
|
||||
return fetch(`${Setting.ServerUrl}/api/webauthn/signin/begin?owner=${application.organization}&name=${username}`, {
|
||||
return fetch(`${Setting.ServerUrl}/api/webauthn/signin/begin?owner=${application.organization}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
})
|
||||
@ -985,11 +1020,7 @@ class LoginPage extends React.Component {
|
||||
Setting.showMessage("error", credentialRequestOptions.msg);
|
||||
throw credentialRequestOptions.status.msg;
|
||||
}
|
||||
|
||||
credentialRequestOptions.publicKey.challenge = UserWebauthnBackend.webAuthnBufferDecode(credentialRequestOptions.publicKey.challenge);
|
||||
credentialRequestOptions.publicKey.allowCredentials.forEach(function(listItem) {
|
||||
listItem.id = UserWebauthnBackend.webAuthnBufferDecode(listItem.id);
|
||||
});
|
||||
|
||||
return navigator.credentials.get({
|
||||
publicKey: credentialRequestOptions.publicKey,
|
||||
@ -1039,6 +1070,12 @@ class LoginPage extends React.Component {
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}${error}`);
|
||||
});
|
||||
}).catch(error => {
|
||||
Setting.showMessage("error", `${error}`);
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
loginLoading: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -387,7 +387,8 @@ export function getAuthUrl(application, provider, method, code) {
|
||||
}
|
||||
|
||||
let endpoint = authInfo[provider.type].endpoint;
|
||||
let redirectUri = `${window.location.origin}/callback`;
|
||||
const redirectOrigin = application.forcedRedirectOrigin ? application.forcedRedirectOrigin : window.location.origin;
|
||||
let redirectUri = `${redirectOrigin}/callback`;
|
||||
let scope = authInfo[provider.type].scope;
|
||||
const isShortState = (provider.type === "WeChat" && navigator.userAgent.includes("MicroMessenger")) || (provider.type === "Twitter");
|
||||
const state = Util.getStateFromQueryParams(application.name, provider.name, method, isShortState);
|
||||
@ -398,7 +399,7 @@ export function getAuthUrl(application, provider, method, code) {
|
||||
endpoint = endpoint.replace("common", provider.domain);
|
||||
}
|
||||
} else if (provider.type === "Apple") {
|
||||
redirectUri = `${window.location.origin}/api/callback`;
|
||||
redirectUri = `${redirectOrigin}/api/callback`;
|
||||
} else if (provider.type === "Google" && provider.disableSsl) {
|
||||
scope += "+https://www.googleapis.com/auth/user.phonenumbers.read";
|
||||
}
|
||||
@ -420,13 +421,13 @@ export function getAuthUrl(application, provider, method, code) {
|
||||
} else if (provider.type === "AzureADB2C") {
|
||||
return `https://${provider.domain}.b2clogin.com/${provider.domain}.onmicrosoft.com/${provider.appId}/oauth2/v2.0/authorize?client_id=${provider.clientId}&nonce=defaultNonce&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${scope}&response_type=code&state=${state}&prompt=login`;
|
||||
} else if (provider.type === "DingTalk") {
|
||||
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&prompt=consent&state=${state}`;
|
||||
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&prompt=login%20consent&state=${state}`;
|
||||
} else if (provider.type === "WeChat") {
|
||||
if (navigator.userAgent.includes("MicroMessenger")) {
|
||||
return `${authInfo[provider.type].mpEndpoint}?appid=${provider.clientId2}&redirect_uri=${redirectUri}&state=${state}&scope=${authInfo[provider.type].mpScope}&response_type=code#wechat_redirect`;
|
||||
} else {
|
||||
if (provider.clientId2 && provider?.disableSsl && provider?.signName === "media") {
|
||||
return `${window.location.origin}/callback?state=${state}&code=${"wechat_oa:" + code}`;
|
||||
return `${redirectOrigin}/callback?state=${state}&code=${"wechat_oa:" + code}`;
|
||||
}
|
||||
return `${endpoint}?appid=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}#wechat_redirect`;
|
||||
}
|
||||
@ -469,7 +470,7 @@ export function getAuthUrl(application, provider, method, code) {
|
||||
} else if (provider.type === "Apple") {
|
||||
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code%20id_token&scope=${scope}&response_mode=form_post`;
|
||||
} else if (provider.type === "Steam") {
|
||||
return `${endpoint}?openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns=http://specs.openid.net/auth/2.0&openid.realm=${window.location.origin}&openid.return_to=${redirectUri}?state=${state}`;
|
||||
return `${endpoint}?openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns=http://specs.openid.net/auth/2.0&openid.realm=${redirectOrigin}&openid.return_to=${redirectUri}?state=${state}`;
|
||||
} else if (provider.type === "Okta") {
|
||||
return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
||||
} else if (provider.type === "Douyin" || provider.type === "TikTok") {
|
||||
|
@ -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} />)}
|
||||
</Card>
|
||||
) : (
|
||||
<div style={{margin: "0 15px"}}>
|
||||
<Row>
|
||||
<div style={{width: "100%", padding: "0 100px"}}>
|
||||
<Row style={{justifyContent: "center"}}>
|
||||
{items.map(item => <SingleCard logo={item.logo} link={item.link} title={item.name} desc={item.description} time={item.createdTime} isSingle={items.length === 1} key={item.name} />)}
|
||||
</Row>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@ import i18next from "i18next";
|
||||
import {Tree} from "antd";
|
||||
import React from "react";
|
||||
|
||||
export const NavItemTree = ({disable, checkedKeys, defaultExpandedKeys, onCheck}) => {
|
||||
export const NavItemTree = ({disabled, checkedKeys, defaultExpandedKeys, onCheck}) => {
|
||||
const NavItemNodes = [
|
||||
{
|
||||
title: i18next.t("organization:All"),
|
||||
@ -86,7 +86,7 @@ export const NavItemTree = ({disable, checkedKeys, defaultExpandedKeys, onCheck}
|
||||
|
||||
return (
|
||||
<Tree
|
||||
disabled={disable}
|
||||
disabled={disabled}
|
||||
checkable
|
||||
checkedKeys={checkedKeys}
|
||||
defaultExpandedKeys={defaultExpandedKeys}
|
||||
|
@ -51,6 +51,8 @@ function testEmailProvider(provider, email = "") {
|
||||
receivers: email === "" ? ["TestSmtpServer"] : [email],
|
||||
provider: provider.name,
|
||||
providerObject: provider,
|
||||
owner: provider.owner,
|
||||
name: provider.name,
|
||||
};
|
||||
|
||||
return fetch(`${Setting.ServerUrl}/api/send-email`, {
|
||||
|
@ -16,7 +16,7 @@ import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
export function sendTestNotification(provider) {
|
||||
testNotificationProvider(provider.content, provider.name)
|
||||
testNotificationProvider(provider)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
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 = {
|
||||
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",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(notificationForm),
|
||||
|
@ -33,6 +33,8 @@ function testSmsProvider(provider, phone = "") {
|
||||
const SmsForm = {
|
||||
content: "123456",
|
||||
receivers: [phone],
|
||||
owner: provider.owner,
|
||||
name: provider.name,
|
||||
};
|
||||
|
||||
return fetch(`${Setting.ServerUrl}/api/send-sms?provider=` + provider.name, {
|
||||
|
29
web/src/common/WidgetItemTree.js
Normal file
29
web/src/common/WidgetItemTree.js
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
177
web/src/common/modal/FaceRecognitionCommonModal.js
Normal file
177
web/src/common/modal/FaceRecognitionCommonModal.js
Normal 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;
|
@ -14,11 +14,13 @@
|
||||
|
||||
import * as faceapi from "face-api.js";
|
||||
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 Dragger from "antd/es/upload/Dragger";
|
||||
import * as Setting from "../../Setting";
|
||||
|
||||
const FaceRecognitionModal = (props) => {
|
||||
const {visible, onOk, onCancel} = props;
|
||||
const {visible, onOk, onCancel, withImage} = props;
|
||||
const [modelsLoaded, setModelsLoaded] = React.useState(false);
|
||||
const [isCameraCaptured, setIsCameraCaptured] = useState(false);
|
||||
|
||||
@ -28,12 +30,14 @@ const FaceRecognitionModal = (props) => {
|
||||
const mediaStreamRef = React.useRef(null);
|
||||
const [percent, setPercent] = useState(0);
|
||||
|
||||
const [files, setFiles] = useState([]);
|
||||
const [currentFaceId, setCurrentFaceId] = React.useState();
|
||||
const [currentFaceIndex, setCurrentFaceIndex] = React.useState();
|
||||
|
||||
React.useEffect(() => {
|
||||
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://cdn.casbin.org/site/casdoor/models";
|
||||
const MODEL_URL = "https://cdn.casdoor.com/casdoor/models";
|
||||
const MODEL_URL = `${Setting.StaticBaseUrl}/casdoor/models`;
|
||||
|
||||
Promise.all([
|
||||
faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),
|
||||
@ -50,6 +54,9 @@ const FaceRecognitionModal = (props) => {
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (withImage) {
|
||||
return;
|
||||
}
|
||||
if (visible) {
|
||||
setPercent(0);
|
||||
if (modelsLoaded) {
|
||||
@ -75,6 +82,9 @@ const FaceRecognitionModal = (props) => {
|
||||
}, [visible, modelsLoaded]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (withImage) {
|
||||
return;
|
||||
}
|
||||
if (isCameraCaptured) {
|
||||
let count = 0;
|
||||
const interval = setInterval(() => {
|
||||
@ -98,6 +108,9 @@ const FaceRecognitionModal = (props) => {
|
||||
}, [isCameraCaptured]);
|
||||
|
||||
const handleStreamVideo = () => {
|
||||
if (withImage) {
|
||||
return;
|
||||
}
|
||||
let count = 0;
|
||||
let goodCount = 0;
|
||||
if (!detection.current) {
|
||||
@ -148,6 +161,16 @@ const FaceRecognitionModal = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getBase64 = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
};
|
||||
|
||||
if (!withImage) {
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
@ -164,7 +187,14 @@ const FaceRecognitionModal = (props) => {
|
||||
]}
|
||||
>
|
||||
<Progress percent={percent} />
|
||||
<div style={{marginTop: "20px", marginBottom: "50px", justifyContent: "center", alignContent: "center", position: "relative", flexDirection: "column"}}>
|
||||
<div style={{
|
||||
marginTop: "20px",
|
||||
marginBottom: "50px",
|
||||
justifyContent: "center",
|
||||
alignContent: "center",
|
||||
position: "relative",
|
||||
flexDirection: "column",
|
||||
}}>
|
||||
{
|
||||
modelsLoaded ?
|
||||
<div style={{display: "flex", justifyContent: "center", alignContent: "center"}}>
|
||||
@ -206,7 +236,8 @@ const FaceRecognitionModal = (props) => {
|
||||
</div>
|
||||
:
|
||||
<div>
|
||||
<Spin tip={i18next.t("login:Loading")} size="large" style={{display: "flex", justifyContent: "center", alignContent: "center"}}>
|
||||
<Spin tip={i18next.t("login:Loading")} size="large"
|
||||
style={{display: "flex", justifyContent: "center", alignContent: "center"}}>
|
||||
<div className="content" />
|
||||
</Spin>
|
||||
</div>
|
||||
@ -215,6 +246,78 @@ const FaceRecognitionModal = (props) => {
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <div>
|
||||
<Modal closable={false}
|
||||
maskClosable={false}
|
||||
destroyOnClose={true}
|
||||
open={visible}
|
||||
title={i18next.t("login:Face Recognition")}
|
||||
width={350}
|
||||
footer={[
|
||||
<Button key="ok" type={"primary"} disabled={!currentFaceId || currentFaceId?.length === 0} onClick={() => {
|
||||
onOk(Array.from(currentFaceId.descriptor));
|
||||
}}>
|
||||
Ok
|
||||
</Button>,
|
||||
<Button key="back" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>,
|
||||
]}>
|
||||
<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 ? <Button style={{width: "100%"}} onClick={async() => {
|
||||
let maxScore = 0;
|
||||
for (const file of files) {
|
||||
const fileIndex = files.indexOf(file);
|
||||
const img = new Image();
|
||||
img.src = file.base64;
|
||||
const faceIds = await faceapi.detectAllFaces(img, new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks().withFaceDescriptors();
|
||||
if (faceIds[0]?.detection.score > 0.9 && faceIds[0]?.detection.score > maxScore) {
|
||||
maxScore = faceIds[0]?.detection.score;
|
||||
setCurrentFaceId(faceIds[0]);
|
||||
setCurrentFaceIndex(fileIndex);
|
||||
}
|
||||
}
|
||||
if (maxScore < 0.9) {
|
||||
message.error(i18next.t("login:Face recognition failed"));
|
||||
}
|
||||
}}> {i18next.t("application:Generate Face ID")}</Button> : null
|
||||
}
|
||||
</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>
|
||||
</div>;
|
||||
}
|
||||
};
|
||||
|
||||
export default FaceRecognitionModal;
|
||||
|
@ -30,6 +30,7 @@ class LanguageSelect extends React.Component {
|
||||
this.state = {
|
||||
classes: props,
|
||||
languages: props.languages ?? Setting.Countries.map(item => item.key),
|
||||
onClick: props.onClick,
|
||||
};
|
||||
|
||||
Setting.Countries.forEach((country) => {
|
||||
@ -50,6 +51,9 @@ class LanguageSelect extends React.Component {
|
||||
render() {
|
||||
const languageItems = this.getOrganizationLanguages(this.state.languages);
|
||||
const onClick = (e) => {
|
||||
if (typeof this.state.onClick === "function") {
|
||||
this.state.onClick(e.key);
|
||||
}
|
||||
Setting.setLanguage(e.key);
|
||||
};
|
||||
|
||||
|
@ -17,11 +17,15 @@
|
||||
"Use same DB - Tooltip": "Use same DB - Tooltip"
|
||||
},
|
||||
"application": {
|
||||
"Add Face ID": "Add Face ID",
|
||||
"Add Face ID with Image": "Add Face ID with Image",
|
||||
"Always": "Always",
|
||||
"Auto signin": "Auto signin",
|
||||
"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 - 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",
|
||||
"Binding providers": "Binding providers",
|
||||
"CSS style": "CSS style",
|
||||
@ -61,8 +65,10 @@
|
||||
"Footer HTML": "Footer HTML",
|
||||
"Footer HTML - Edit": "Footer HTML - Edit",
|
||||
"Footer HTML - Tooltip": "Custom the footer of your application",
|
||||
"Forced redirect origin": "Forced redirect origin",
|
||||
"Form position": "Form position",
|
||||
"Form position - Tooltip": "Location of the signup, signin and forget password forms",
|
||||
"Generate Face ID": "Generate Face ID",
|
||||
"Grant types": "Grant types",
|
||||
"Grant types - Tooltip": "Select which grant types are allowed in the OAuth protocol",
|
||||
"Header HTML": "Header HTML",
|
||||
@ -163,6 +169,7 @@
|
||||
},
|
||||
"currency": {
|
||||
"AUD": "AUD",
|
||||
"BRL": "BRL",
|
||||
"CAD": "CAD",
|
||||
"CHF": "CHF",
|
||||
"CNY": "CNY",
|
||||
@ -189,6 +196,7 @@
|
||||
"Verify": "Verify"
|
||||
},
|
||||
"general": {
|
||||
"AI Assistant": "AI Assistant",
|
||||
"API key": "API key",
|
||||
"API key - Tooltip": "API key - Tooltip",
|
||||
"Access key": "Access key",
|
||||
@ -201,7 +209,6 @@
|
||||
"Adapter - Tooltip": "Table name of the policy store",
|
||||
"Adapters": "Adapters",
|
||||
"Add": "Add",
|
||||
"Add Face Id": "Add Face Id",
|
||||
"Add custom item": "Add custom item",
|
||||
"Admin": "Admin",
|
||||
"Affiliation URL": "Affiliation URL",
|
||||
@ -231,6 +238,7 @@
|
||||
"Created time": "Created time",
|
||||
"Custom": "Custom",
|
||||
"Dashboard": "Dashboard",
|
||||
"Data": "Data",
|
||||
"Default": "Default",
|
||||
"Default application": "Default application",
|
||||
"Default application - Tooltip": "Default application for users registered directly from the organization page",
|
||||
@ -241,6 +249,7 @@
|
||||
"Delete": "Delete",
|
||||
"Description": "Description",
|
||||
"Description - Tooltip": "Detailed description information for reference, Casdoor itself will not use it",
|
||||
"Detail": "详情",
|
||||
"Disable": "Disable",
|
||||
"Display name": "Display name",
|
||||
"Display name - Tooltip": "A user-friendly, easily readable name displayed publicly in the UI",
|
||||
@ -258,7 +267,6 @@
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
"FaceIdData": "FaceIdData",
|
||||
"Failed to add": "Failed to add",
|
||||
"Failed to connect to server": "Failed to connect to server",
|
||||
"Failed to delete": "Failed to delete",
|
||||
@ -271,6 +279,7 @@
|
||||
"Favicon": "Favicon",
|
||||
"Favicon - Tooltip": "Favicon icon URL used in all Casdoor pages of the organization",
|
||||
"First name": "First name",
|
||||
"Forced redirect origin - Tooltip": "Forced redirect origin - Tooltip",
|
||||
"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",
|
||||
"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 only": "Phone only",
|
||||
"Phone or Email": "Phone or Email",
|
||||
"Plain": "Plain",
|
||||
"Plan": "Plan",
|
||||
"Plan - Tooltip": "Plan - Tooltip",
|
||||
"Plans": "Plans",
|
||||
@ -362,6 +372,7 @@
|
||||
"QR code is too large": "QR code is too large",
|
||||
"Real name": "Real name",
|
||||
"Records": "Records",
|
||||
"Request": "Request",
|
||||
"Request URI": "Request URI",
|
||||
"Resources": "Resources",
|
||||
"Role": "Role",
|
||||
@ -412,6 +423,7 @@
|
||||
"This is a read-only demo site!": "This is a read-only demo site!",
|
||||
"Timestamp": "Timestamp",
|
||||
"Tokens": "Tokens",
|
||||
"Tour": "Tour",
|
||||
"Transactions": "Transactions",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
@ -442,7 +454,8 @@
|
||||
"Parent group - Tooltip": "Parent group - Tooltip",
|
||||
"Physical": "Physical",
|
||||
"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": {
|
||||
"New users past 30 days": "New users past 30 days",
|
||||
@ -461,13 +474,16 @@
|
||||
"Quota": "Quota",
|
||||
"Quota - Tooltip": "Quota - Tooltip",
|
||||
"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": {
|
||||
"Admin": "Admin",
|
||||
"Admin - Tooltip": "CN or ID of the LDAP server administrator",
|
||||
"Admin Password": "Admin 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 - Tooltip": "Auto-sync configuration, disabled at 0",
|
||||
"Base DN": "Base DN",
|
||||
@ -586,11 +602,6 @@
|
||||
"Your phone is": "Your phone is",
|
||||
"preferred": "preferred"
|
||||
},
|
||||
"mfaAccount": {
|
||||
"Account Name": "Account Name",
|
||||
"Issuer": "Issuer",
|
||||
"Secret Key": "Secret Key"
|
||||
},
|
||||
"model": {
|
||||
"Advanced Editor": "Advanced Editor",
|
||||
"Basic Editor": "Basic Editor",
|
||||
@ -610,6 +621,8 @@
|
||||
"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",
|
||||
"Modify rule": "Modify rule",
|
||||
"Navbar items": "Navbar items",
|
||||
"Navbar items - Tooltip": "Navbar items - Tooltip",
|
||||
"New Organization": "New Organization",
|
||||
"Optional": "Optional",
|
||||
"Password expire days": "Password expire days",
|
||||
@ -622,10 +635,14 @@
|
||||
"Tags - Tooltip": "Collection of tags available for users to choose from",
|
||||
"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",
|
||||
"User types": "User types",
|
||||
"User types - Tooltip": "User types - Tooltip",
|
||||
"View rule": "View rule",
|
||||
"Visible": "Visible",
|
||||
"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": {
|
||||
"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"
|
||||
},
|
||||
"product": {
|
||||
"AirWallex": "AirWallex",
|
||||
"Alipay": "Alipay",
|
||||
"Buy": "Buy",
|
||||
"Buy Product": "Buy Product",
|
||||
@ -757,7 +775,6 @@
|
||||
"Sold": "Sold",
|
||||
"Sold - Tooltip": "Quantity sold",
|
||||
"Stripe": "Stripe",
|
||||
"AirWallex": "AirWallex",
|
||||
"Tag - Tooltip": "Tag of product",
|
||||
"Test buy page..": "Test buy page..",
|
||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||
@ -817,6 +834,8 @@
|
||||
"Edit Provider": "Edit Provider",
|
||||
"Email content": "Email content",
|
||||
"Email content - Tooltip": "Content of the Email",
|
||||
"Email regex": "Email regex",
|
||||
"Email regex - Tooltip": "Email regex - Tooltip",
|
||||
"Email title": "Email title",
|
||||
"Email title - Tooltip": "Title of the email",
|
||||
"Endpoint": "Endpoint",
|
||||
@ -830,6 +849,8 @@
|
||||
"From name - Tooltip": "Name of \"From\"",
|
||||
"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",
|
||||
"HTTP header": "HTTP header",
|
||||
"HTTP header - Tooltip": "HTTP header - Tooltip",
|
||||
"Host": "Host",
|
||||
"Host - Tooltip": "Name of host",
|
||||
"IdP": "IdP",
|
||||
@ -844,6 +865,8 @@
|
||||
"Key text - Tooltip": "Key text - Tooltip",
|
||||
"Metadata": "Metadata",
|
||||
"Metadata - Tooltip": "SAML metadata",
|
||||
"Metadata url": "Metadata url",
|
||||
"Metadata url - Tooltip": "Metadata url - Tooltip",
|
||||
"Method - Tooltip": "Login method, QR code or silent login",
|
||||
"New Provider": "New Provider",
|
||||
"Normal": "Normal",
|
||||
@ -1244,6 +1267,8 @@
|
||||
"Edit Webhook": "Edit Webhook",
|
||||
"Events": "Events",
|
||||
"Events - Tooltip": "Events",
|
||||
"Extended user fields": "Extended user fields",
|
||||
"Extended user fields - Tooltip": "Extended user fields - Tooltip",
|
||||
"Headers": "Headers",
|
||||
"Headers - Tooltip": "HTTP headers (key-value pairs)",
|
||||
"Is user extended": "Is user extended",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user