mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-21 16:33:49 +08:00
Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
be88b00278 | |||
1bd0245e7a | |||
cc84bd37cf | |||
8302fcf805 | |||
391a533ce1 | |||
57431a59ad | |||
88a4736520 | |||
2cb6ff69ae | |||
e1e5943a3e | |||
3875896c1e | |||
7e2f265420 | |||
53ef179e9b | |||
376ef0ed14 | |||
ca183be336 | |||
e5da57a005 | |||
e4e225db32 | |||
a1add992ee | |||
2aac265ed4 |
@ -327,7 +327,38 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
|
||||
var user *object.User
|
||||
if authForm.Password == "" {
|
||||
if authForm.SigninMethod == "Face ID" {
|
||||
if user, err = object.GetUserByFields(authForm.Organization, authForm.Username); err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
} else if user == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(authForm.Organization, authForm.Username)))
|
||||
return
|
||||
}
|
||||
|
||||
var application *object.Application
|
||||
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
|
||||
return
|
||||
}
|
||||
|
||||
if !application.IsFaceIdEnabled() {
|
||||
c.ResponseError(c.T("auth:The login method: login with face is not enabled for the application"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := object.CheckFaceId(user, authForm.FaceId, c.GetAcceptLanguage()); err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
} else if authForm.Password == "" {
|
||||
if user, err = object.GetUserByFields(authForm.Organization, authForm.Username); err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
|
@ -85,6 +85,11 @@ func (c *ApiController) GetRecords() {
|
||||
// @Success 200 {object} object.Record The Response object
|
||||
// @router /get-records-filter [post]
|
||||
func (c *ApiController) GetRecordsByFilter() {
|
||||
_, ok := c.RequireAdmin()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
body := string(c.Ctx.Input.RequestBody)
|
||||
|
||||
record := &casvisorsdk.Record{}
|
||||
|
@ -60,7 +60,6 @@ func (c *ApiController) SendEmail() {
|
||||
}
|
||||
|
||||
var emailForm EmailForm
|
||||
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &emailForm)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@ -87,7 +86,7 @@ func (c *ApiController) SendEmail() {
|
||||
|
||||
// when receiver is the reserved keyword: "TestSmtpServer", it means to test the SMTP server instead of sending a real Email
|
||||
if len(emailForm.Receivers) == 1 && emailForm.Receivers[0] == "TestSmtpServer" {
|
||||
err := object.DailSmtpServer(provider)
|
||||
err = object.DailSmtpServer(provider)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@ -112,20 +111,23 @@ func (c *ApiController) SendEmail() {
|
||||
return
|
||||
}
|
||||
|
||||
code := "123456"
|
||||
content := emailForm.Content
|
||||
if content == "" {
|
||||
code := "123456"
|
||||
|
||||
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
|
||||
content := strings.Replace(provider.Content, "%s", code, 1)
|
||||
if !strings.HasPrefix(userId, "app/") {
|
||||
var user *object.User
|
||||
user, err = object.GetUser(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
|
||||
content = strings.Replace(provider.Content, "%s", code, 1)
|
||||
if !strings.HasPrefix(userId, "app/") {
|
||||
var user *object.User
|
||||
user, err = object.GetUser(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
content = strings.Replace(content, "%{user.friendlyName}", user.GetFriendlyName(), 1)
|
||||
if user != nil {
|
||||
content = strings.Replace(content, "%{user.friendlyName}", user.GetFriendlyName(), 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,6 +127,11 @@ func (c *ApiController) RequireAdmin() (string, bool) {
|
||||
if user.Owner == "built-in" {
|
||||
return "", true
|
||||
}
|
||||
|
||||
if !user.IsAdmin {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return user.Owner, true
|
||||
}
|
||||
|
||||
|
@ -61,6 +61,8 @@ type AuthForm struct {
|
||||
|
||||
Plan string `json:"plan"`
|
||||
Pricing string `json:"pricing"`
|
||||
|
||||
FaceId []float64 `json:"faceId"`
|
||||
}
|
||||
|
||||
func GetAuthFormFieldValue(form *AuthForm, fieldName string) (bool, string) {
|
||||
|
2
go.mod
2
go.mod
@ -14,7 +14,7 @@ require (
|
||||
github.com/casdoor/notify v0.45.0
|
||||
github.com/casdoor/oss v1.6.0
|
||||
github.com/casdoor/xorm-adapter/v3 v3.1.0
|
||||
github.com/casvisor/casvisor-go-sdk v1.1.0
|
||||
github.com/casvisor/casvisor-go-sdk v1.3.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
|
||||
|
4
go.sum
4
go.sum
@ -1093,8 +1093,8 @@ github.com/casdoor/oss v1.6.0 h1:IOWrGLJ+VO82qS796eaRnzFPPA1Sn3cotYTi7O/VIlQ=
|
||||
github.com/casdoor/oss v1.6.0/go.mod h1:rJAWA0hLhtu94t6IRpotLUkXO1NWMASirywQYaGizJE=
|
||||
github.com/casdoor/xorm-adapter/v3 v3.1.0 h1:NodWayRtSLVSeCvL9H3Hc61k0G17KhV9IymTCNfh3kk=
|
||||
github.com/casdoor/xorm-adapter/v3 v3.1.0/go.mod h1:4WTcUw+bTgBylGHeGHzTtBvuTXRS23dtwzFLl9tsgFM=
|
||||
github.com/casvisor/casvisor-go-sdk v1.1.0 h1:XRDrlDuMjXfPsNa1INLHASPHdV/vgwh1gPZ7+v9fNHk=
|
||||
github.com/casvisor/casvisor-go-sdk v1.1.0/go.mod h1:frnNtH5GA0wxzAQLyZxxfL0RSsSub9GQPi2Ybe86ocE=
|
||||
github.com/casvisor/casvisor-go-sdk v1.3.0 h1:HVgm2g3lWpNX2wBNidzR743QY4O5kAjLUJ9tS2juO8g=
|
||||
github.com/casvisor/casvisor-go-sdk v1.3.0/go.mod h1:frnNtH5GA0wxzAQLyZxxfL0RSsSub9GQPi2Ybe86ocE=
|
||||
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
|
||||
|
@ -505,7 +505,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") {
|
||||
if providerItem.Provider != nil && (providerItem.Provider.Category == "OAuth" || providerItem.Provider.Category == "Web3" || providerItem.Provider.Category == "Captcha") {
|
||||
providerItems = append(providerItems, providerItem)
|
||||
}
|
||||
}
|
||||
@ -757,6 +757,17 @@ func (application *Application) IsLdapEnabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (application *Application) IsFaceIdEnabled() bool {
|
||||
if len(application.SigninMethods) > 0 {
|
||||
for _, signinMethod := range application.SigninMethods {
|
||||
if signinMethod.Name == "Face ID" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsOriginAllowed(origin string) (bool, error) {
|
||||
applications, err := GetApplications("")
|
||||
if err != nil {
|
||||
|
@ -16,7 +16,6 @@ package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
@ -206,26 +205,41 @@ func (p *Cert) GetId() string {
|
||||
}
|
||||
|
||||
func (p *Cert) populateContent() error {
|
||||
if p.Certificate == "" || p.PrivateKey == "" {
|
||||
var err error
|
||||
var certificate, privateKey string
|
||||
if strings.HasPrefix(p.CryptoAlgorithm, "RS") {
|
||||
certificate, privateKey, err = generateRsaKeys(p.BitSize, util.ParseInt(p.CryptoAlgorithm[2:]), p.ExpireInYears, p.Name, p.Owner)
|
||||
} else if strings.HasPrefix(p.CryptoAlgorithm, "ES") {
|
||||
certificate, privateKey, err = generateEsKeys(p.BitSize, util.ParseInt(p.CryptoAlgorithm[2:]), p.ExpireInYears, p.Name, p.Owner)
|
||||
} else if strings.HasPrefix(p.CryptoAlgorithm, "PS") {
|
||||
certificate, privateKey, err = generateRsaPssKeys(p.BitSize, util.ParseInt(p.CryptoAlgorithm[2:]), p.ExpireInYears, p.Name, p.Owner)
|
||||
} else {
|
||||
err = fmt.Errorf("Crypto algorithm %s is not found", p.CryptoAlgorithm)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Certificate = certificate
|
||||
p.PrivateKey = privateKey
|
||||
if p.Certificate != "" && p.PrivateKey != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(p.CryptoAlgorithm) < 3 {
|
||||
err := fmt.Errorf("populateContent() error, unsupported crypto algorithm: %s", p.CryptoAlgorithm)
|
||||
return err
|
||||
}
|
||||
|
||||
if p.CryptoAlgorithm == "RSA" {
|
||||
p.CryptoAlgorithm = "RS256"
|
||||
}
|
||||
|
||||
sigAlgorithm := p.CryptoAlgorithm[:2]
|
||||
shaSize, err := util.ParseIntWithError(p.CryptoAlgorithm[2:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var certificate, privateKey string
|
||||
if sigAlgorithm == "RS" {
|
||||
certificate, privateKey, err = generateRsaKeys(p.BitSize, shaSize, p.ExpireInYears, p.Name, p.Owner)
|
||||
} else if sigAlgorithm == "ES" {
|
||||
certificate, privateKey, err = generateEsKeys(shaSize, p.ExpireInYears, p.Name, p.Owner)
|
||||
} else if sigAlgorithm == "PS" {
|
||||
certificate, privateKey, err = generateRsaPssKeys(p.BitSize, shaSize, p.ExpireInYears, p.Name, p.Owner)
|
||||
} else {
|
||||
err = fmt.Errorf("populateContent() error, unsupported signature algorithm: %s", sigAlgorithm)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Certificate = certificate
|
||||
p.PrivateKey = privateKey
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,10 @@ type Dashboard struct {
|
||||
}
|
||||
|
||||
func GetDashboard(owner string) (*Dashboard, error) {
|
||||
if owner == "All" {
|
||||
owner = ""
|
||||
}
|
||||
|
||||
dashboard := &Dashboard{
|
||||
OrganizationCounts: make([]int, 31),
|
||||
UserCounts: make([]int, 31),
|
||||
@ -36,14 +40,13 @@ func GetDashboard(owner string) (*Dashboard, error) {
|
||||
SubscriptionCounts: make([]int, 31),
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
organizations := []Organization{}
|
||||
users := []User{}
|
||||
providers := []Provider{}
|
||||
applications := []Application{}
|
||||
subscriptions := []Subscription{}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(5)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
@ -54,6 +54,8 @@ type Organization struct {
|
||||
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
WebsiteUrl string `xorm:"varchar(100)" json:"websiteUrl"`
|
||||
Logo string `xorm:"varchar(200)" json:"logo"`
|
||||
LogoDark string `xorm:"varchar(200)" json:"logoDark"`
|
||||
Favicon string `xorm:"varchar(100)" json:"favicon"`
|
||||
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
|
||||
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
|
||||
|
@ -63,6 +63,7 @@ func NewRecord(ctx *context.Context) *casvisorsdk.Record {
|
||||
Action: action,
|
||||
Language: languageCode,
|
||||
Object: object,
|
||||
Response: "",
|
||||
IsTriggered: false,
|
||||
}
|
||||
return &record
|
||||
|
@ -179,8 +179,8 @@ type IdpSSODescriptor struct {
|
||||
}
|
||||
|
||||
type NameIDFormat struct {
|
||||
XMLName xml.Name
|
||||
Value string `xml:",innerxml"`
|
||||
// XMLName xml.Name
|
||||
Value string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type SingleSignOnService struct {
|
||||
@ -190,7 +190,7 @@ type SingleSignOnService struct {
|
||||
}
|
||||
|
||||
type Attribute struct {
|
||||
XMLName xml.Name
|
||||
// XMLName xml.Name
|
||||
Name string `xml:"Name,attr"`
|
||||
NameFormat string `xml:"NameFormat,attr"`
|
||||
FriendlyName string `xml:"FriendlyName,attr"`
|
||||
|
@ -27,7 +27,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func generateRsaKeys(bitSize int, algorithmType int, expireInYears int, commonName string, organization string) (string, string, error) {
|
||||
func generateRsaKeys(bitSize int, shaSize int, expireInYears int, commonName string, organization string) (string, string, error) {
|
||||
// https://stackoverflow.com/questions/64104586/use-golang-to-get-rsa-key-the-same-way-openssl-genrsa
|
||||
// https://stackoverflow.com/questions/43822945/golang-can-i-create-x509keypair-using-rsa-key
|
||||
|
||||
@ -58,7 +58,7 @@ func generateRsaKeys(bitSize int, algorithmType int, expireInYears int, commonNa
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
switch algorithmType {
|
||||
switch shaSize {
|
||||
case 256:
|
||||
tml.SignatureAlgorithm = x509.SHA256WithRSA
|
||||
case 384:
|
||||
@ -66,7 +66,7 @@ func generateRsaKeys(bitSize int, algorithmType int, expireInYears int, commonNa
|
||||
case 512:
|
||||
tml.SignatureAlgorithm = x509.SHA512WithRSA
|
||||
default:
|
||||
return "", "", fmt.Errorf("unsupported algorithm type")
|
||||
return "", "", fmt.Errorf("generateRsaKeys() error, unsupported SHA size: %d", shaSize)
|
||||
}
|
||||
|
||||
cert, err := x509.CreateCertificate(rand.Reader, &tml, &tml, &key.PublicKey, key)
|
||||
@ -83,9 +83,9 @@ func generateRsaKeys(bitSize int, algorithmType int, expireInYears int, commonNa
|
||||
return string(certPem), string(privateKeyPem), nil
|
||||
}
|
||||
|
||||
func generateEsKeys(bitSize int, algorithmType int, expireInYears int, commonName string, organization string) (string, string, error) {
|
||||
func generateEsKeys(shaSize int, expireInYears int, commonName string, organization string) (string, string, error) {
|
||||
var curve elliptic.Curve
|
||||
switch algorithmType {
|
||||
switch shaSize {
|
||||
case 256:
|
||||
curve = elliptic.P256()
|
||||
case 384:
|
||||
@ -93,7 +93,7 @@ func generateEsKeys(bitSize int, algorithmType int, expireInYears int, commonNam
|
||||
case 512:
|
||||
curve = elliptic.P521() // ES512(P521,SHA512)
|
||||
default:
|
||||
return "", "", fmt.Errorf("unsupported algorithm type")
|
||||
return "", "", fmt.Errorf("generateEsKeys() error, unsupported SHA size: %d", shaSize)
|
||||
}
|
||||
|
||||
// Generate ECDSA key pair.
|
||||
@ -139,7 +139,7 @@ func generateEsKeys(bitSize int, algorithmType int, expireInYears int, commonNam
|
||||
return string(certPem), string(privateKeyPem), nil
|
||||
}
|
||||
|
||||
func generateRsaPssKeys(bitSize int, algorithmType int, expireInYears int, commonName string, organization string) (string, string, error) {
|
||||
func generateRsaPssKeys(bitSize int, shaSize int, expireInYears int, commonName string, organization string) (string, string, error) {
|
||||
// Generate RSA key.
|
||||
key, err := rsa.GenerateKey(rand.Reader, bitSize)
|
||||
if err != nil {
|
||||
@ -173,7 +173,7 @@ func generateRsaPssKeys(bitSize int, algorithmType int, expireInYears int, commo
|
||||
}
|
||||
|
||||
// Set the signature algorithm based on the hash function
|
||||
switch algorithmType {
|
||||
switch shaSize {
|
||||
case 256:
|
||||
tml.SignatureAlgorithm = x509.SHA256WithRSAPSS
|
||||
case 384:
|
||||
@ -181,7 +181,7 @@ func generateRsaPssKeys(bitSize int, algorithmType int, expireInYears int, commo
|
||||
case 512:
|
||||
tml.SignatureAlgorithm = x509.SHA512WithRSAPSS
|
||||
default:
|
||||
return "", "", fmt.Errorf("unsupported algorithm type")
|
||||
return "", "", fmt.Errorf("generateRsaPssKeys() error, unsupported SHA size: %d", shaSize)
|
||||
}
|
||||
|
||||
cert, err := x509.CreateCertificate(rand.Reader, &tml, &tml, &key.PublicKey, key)
|
||||
|
@ -37,7 +37,7 @@ func TestGenerateRsaKeys(t *testing.T) {
|
||||
|
||||
func TestGenerateEsKeys(t *testing.T) {
|
||||
fileId := "token_jwt_key"
|
||||
certificate, privateKey, err := generateEsKeys(4096, 256, 20, "Casdoor Cert", "Casdoor Organization")
|
||||
certificate, privateKey, err := generateEsKeys(256, 20, "Casdoor Cert", "Casdoor Organization")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -190,6 +190,7 @@ type User struct {
|
||||
MultiFactorAuths []*MfaProps `xorm:"-" json:"multiFactorAuths,omitempty"`
|
||||
Invitation string `xorm:"varchar(100) index" json:"invitation"`
|
||||
InvitationCode string `xorm:"varchar(100) index" json:"invitationCode"`
|
||||
FaceIds []*FaceId `json:"faceIds"`
|
||||
|
||||
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
|
||||
Properties map[string]string `json:"properties"`
|
||||
@ -227,6 +228,11 @@ type ManagedAccount struct {
|
||||
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
|
||||
}
|
||||
|
||||
type FaceId struct {
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
FaceIdData []float64 `json:"faceIdData"`
|
||||
}
|
||||
|
||||
func GetUserFieldStringValue(user *User, fieldName string) (bool, string, error) {
|
||||
val := reflect.ValueOf(*user)
|
||||
fieldValue := val.FieldByName(fieldName)
|
||||
@ -667,7 +673,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
||||
columns = []string{
|
||||
"owner", "display_name", "avatar", "first_name", "last_name",
|
||||
"location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
|
||||
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts",
|
||||
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids",
|
||||
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret",
|
||||
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
|
||||
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
|
||||
|
@ -387,6 +387,11 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
}
|
||||
|
||||
if newUser.FaceIds != nil {
|
||||
item := GetAccountItemByName("Face ID", organization)
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
}
|
||||
|
||||
if oldUser.IsAdmin != newUser.IsAdmin {
|
||||
item := GetAccountItemByName("Is admin", organization)
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
|
@ -17,6 +17,7 @@ package object
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
@ -53,7 +54,7 @@ type VerificationRecord struct {
|
||||
Type string `xorm:"varchar(10)"`
|
||||
User string `xorm:"varchar(100) notnull"`
|
||||
Provider string `xorm:"varchar(100) notnull"`
|
||||
Receiver string `xorm:"varchar(100) notnull"`
|
||||
Receiver string `xorm:"varchar(100) index notnull"`
|
||||
Code string `xorm:"varchar(10) notnull"`
|
||||
Time int64 `xorm:"notnull"`
|
||||
IsUsed bool
|
||||
@ -183,7 +184,7 @@ func CheckVerificationCode(dest string, code string, lang string) (*VerifyResult
|
||||
return nil, err
|
||||
}
|
||||
if record == nil {
|
||||
return &VerifyResult{noRecordError, i18n.Translate(lang, "verification:Code has not been sent yet!")}, nil
|
||||
return &VerifyResult{noRecordError, i18n.Translate(lang, "verification:The verification code has not been sent yet, or has already been used!")}, nil
|
||||
}
|
||||
|
||||
timeout, err := conf.GetConfigInt64("verificationCodeTimeout")
|
||||
@ -236,6 +237,28 @@ func CheckSigninCode(user *User, dest, code, lang string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func CheckFaceId(user *User, faceId []float64, lang string) error {
|
||||
if len(user.FaceIds) == 0 {
|
||||
return fmt.Errorf(i18n.Translate(lang, "check:Face data does not exist, cannot log in"))
|
||||
}
|
||||
|
||||
for _, userFaceId := range user.FaceIds {
|
||||
if faceId == nil || len(userFaceId.FaceIdData) != len(faceId) {
|
||||
continue
|
||||
}
|
||||
var sumOfSquares float64
|
||||
for i := 0; i < len(userFaceId.FaceIdData); i++ {
|
||||
diff := userFaceId.FaceIdData[i] - faceId[i]
|
||||
sumOfSquares += diff * diff
|
||||
}
|
||||
if math.Sqrt(sumOfSquares) < 0.25 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf(i18n.Translate(lang, "check:Face data mismatch"))
|
||||
}
|
||||
|
||||
func GetVerifyType(username string) (verificationCodeType string) {
|
||||
if strings.Contains(username, "@") {
|
||||
return VerifyTypeEmail
|
||||
|
@ -33,7 +33,7 @@ type Webhook struct {
|
||||
|
||||
Organization string `xorm:"varchar(100) index" json:"organization"`
|
||||
|
||||
Url string `xorm:"varchar(100)" json:"url"`
|
||||
Url string `xorm:"varchar(200)" json:"url"`
|
||||
Method string `xorm:"varchar(100)" json:"method"`
|
||||
ContentType string `xorm:"varchar(100)" json:"contentType"`
|
||||
Headers []*Header `xorm:"mediumtext" json:"headers"`
|
||||
|
@ -29,6 +29,7 @@
|
||||
"craco-less": "^2.0.0",
|
||||
"echarts": "^5.4.3",
|
||||
"ethers": "5.6.9",
|
||||
"face-api.js": "^0.22.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18n-iso-countries": "^7.0.0",
|
||||
"i18next": "^19.8.9",
|
||||
@ -50,7 +51,8 @@
|
||||
"react-metamask-avatar": "^1.2.1",
|
||||
"react-router-dom": "^5.3.3",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-social-login-buttons": "^3.4.0"
|
||||
"react-social-login-buttons": "^3.4.0",
|
||||
"react-webcam": "^7.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env PORT=7001 craco start",
|
||||
|
@ -153,11 +153,7 @@ class App extends Component {
|
||||
}
|
||||
|
||||
getLogo(themes) {
|
||||
if (themes.includes("dark")) {
|
||||
return `${Setting.StaticBaseUrl}/img/casdoor-logo_1185x256_dark.png`;
|
||||
} else {
|
||||
return `${Setting.StaticBaseUrl}/img/casdoor-logo_1185x256.png`;
|
||||
}
|
||||
return Setting.getLogo(themes);
|
||||
}
|
||||
|
||||
setLanguage(account) {
|
||||
|
@ -1081,7 +1081,7 @@ class ApplicationEditPage extends React.Component {
|
||||
submitApplicationEdit(exitAfterSave) {
|
||||
const application = Setting.deepCopy(this.state.application);
|
||||
application.providers = application.providers?.filter(provider => this.state.providers.map(provider => provider.name).includes(provider.name));
|
||||
application.signinMethods = application.signinMethods?.filter(signinMethod => ["Password", "Verification code", "WebAuthn", "LDAP"].includes(signinMethod.name));
|
||||
application.signinMethods = application.signinMethods?.filter(signinMethod => ["Password", "Verification code", "WebAuthn", "LDAP", "Face ID"].includes(signinMethod.name));
|
||||
|
||||
ApplicationBackend.updateApplication("admin", this.state.applicationName, application)
|
||||
.then((res) => {
|
||||
|
@ -171,21 +171,15 @@ class CertEditPage extends React.Component {
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.cert.cryptoAlgorithm} onChange={(value => {
|
||||
this.updateCertField("cryptoAlgorithm", value);
|
||||
if (value === "RS256" || value === "PS256") {
|
||||
this.updateCertField("bitSize", 2048);
|
||||
} else if (value === "RS384" || value === "PS384") {
|
||||
this.updateCertField("bitSize", 2048);
|
||||
} else if (value === "RS512" || value === "PS512") {
|
||||
this.updateCertField("bitSize", 2048);
|
||||
} else if (value === "ES256") {
|
||||
this.updateCertField("bitSize", 256);
|
||||
} else if (value === "ES384") {
|
||||
this.updateCertField("bitSize", 384);
|
||||
} else if (value === "ES521") {
|
||||
this.updateCertField("bitSize", 521);
|
||||
} else {
|
||||
|
||||
if (value.startsWith("ES")) {
|
||||
this.updateCertField("bitSize", 0);
|
||||
} else {
|
||||
if (this.state.cert.bitSize !== 1024 && this.state.cert.bitSize !== 2048 && this.state.cert.bitSize !== 4096) {
|
||||
this.updateCertField("bitSize", 2048);
|
||||
}
|
||||
}
|
||||
|
||||
this.updateCertField("certificate", "");
|
||||
this.updateCertField("privateKey", "");
|
||||
})}>
|
||||
@ -205,22 +199,26 @@ class CertEditPage extends React.Component {
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("cert:Bit size"), i18next.t("cert:Bit size - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.cert.bitSize} onChange={(value => {
|
||||
this.updateCertField("bitSize", value);
|
||||
this.updateCertField("certificate", "");
|
||||
this.updateCertField("privateKey", "");
|
||||
})}>
|
||||
{
|
||||
Setting.getCryptoAlgorithmOptions(this.state.cert.cryptoAlgorithm).map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.cert.cryptoAlgorithm.startsWith("ES") ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("cert:Bit size"), i18next.t("cert:Bit size - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.cert.bitSize} onChange={(value => {
|
||||
this.updateCertField("bitSize", value);
|
||||
this.updateCertField("certificate", "");
|
||||
this.updateCertField("privateKey", "");
|
||||
})}>
|
||||
{
|
||||
Setting.getCryptoAlgorithmOptions(this.state.cert.cryptoAlgorithm).map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("cert:Expire in years"), i18next.t("cert:Expire in years - Tooltip"))} :
|
||||
|
@ -220,9 +220,29 @@ function ManagementPage(props) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const textColor = props.themeAlgorithm.includes("dark") ? "white" : "black";
|
||||
let textColor = "black";
|
||||
const twoToneColor = props.themeData.colorPrimary;
|
||||
|
||||
let logo = props.account.organization.logo ? props.account.organization.logo : Setting.getLogo(props.themeAlgorithm);
|
||||
if (props.themeAlgorithm.includes("dark")) {
|
||||
if (props.account.organization.logoDark) {
|
||||
logo = props.account.organization.logoDark;
|
||||
}
|
||||
textColor = "white";
|
||||
}
|
||||
|
||||
!Setting.isMobile() ? res.push({
|
||||
label:
|
||||
<Link to="/">
|
||||
<img className="logo" src={logo ?? props.logo} alt="logo" />
|
||||
</Link>,
|
||||
disabled: true,
|
||||
style: {
|
||||
padding: 0,
|
||||
height: "auto",
|
||||
},
|
||||
}) : null;
|
||||
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/">{i18next.t("general:Home")}</Link>, "/home", <HomeTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/">{i18next.t("general:Dashboard")}</Link>, "/"),
|
||||
Setting.getItem(<Link to="/shortcuts">{i18next.t("general:Shortcuts")}</Link>, "/shortcuts"),
|
||||
@ -400,11 +420,6 @@ function ManagementPage(props) {
|
||||
<React.Fragment>
|
||||
<EnableMfaNotification account={props.account} />
|
||||
<Header style={{padding: "0", marginBottom: "3px", backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}} >
|
||||
{Setting.isMobile() ? null : (
|
||||
<Link to={"/"}>
|
||||
<div className="logo" style={{background: `url(${props.logo})`}} />
|
||||
</Link>
|
||||
)}
|
||||
{props.requiredEnableMfa || (Setting.isMobile() ?
|
||||
<React.Fragment>
|
||||
<Drawer title={i18next.t("general:Close")} placement="left" visible={menuVisible} onClose={onClose}>
|
||||
@ -426,7 +441,7 @@ function ManagementPage(props) {
|
||||
items={getMenuItems()}
|
||||
mode={"horizontal"}
|
||||
selectedKeys={[props.selectedMenuKey]}
|
||||
style={{position: "absolute", left: "145px", right: menuStyleRight, backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}}
|
||||
style={{position: "absolute", left: 0, right: menuStyleRight, backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
|
@ -56,6 +56,7 @@ class OrganizationEditPage extends React.Component {
|
||||
this.props.history.push("/404");
|
||||
return;
|
||||
}
|
||||
organization["enableDarkLogo"] = !!organization["logoDark"];
|
||||
|
||||
this.setState({
|
||||
organization: organization,
|
||||
@ -141,6 +142,78 @@ class OrganizationEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Enable dark logo"), i18next.t("general:Enable dark logo - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Switch checked={this.state.organization.enableDarkLogo} onChange={e => {
|
||||
this.updateOrganizationField("enableDarkLogo", e);
|
||||
if (!e) {
|
||||
this.updateOrganizationField("logoDark", "");
|
||||
}
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Logo"), i18next.t("general:Logo - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
|
||||
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={23} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.organization.logo} onChange={e => {
|
||||
this.updateOrganizationField("logo", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
|
||||
{i18next.t("general:Preview")}:
|
||||
</Col>
|
||||
<Col span={23}>
|
||||
<a target="_blank" rel="noreferrer" href={this.state.organization.logo}>
|
||||
<img src={this.state.organization.logo ? this.state.organization.logo : Setting.getLogo([""])} alt={this.state.organization.logo} height={90} style={{background: "white", marginBottom: "20px"}} />
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
!this.state.organization.enableDarkLogo ? null : (<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Logo dark"), i18next.t("general:Logo dark - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
|
||||
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={23}>
|
||||
<Input prefix={<LinkOutlined />} value={this.state.organization.logoDark} onChange={e => {
|
||||
this.updateOrganizationField("logoDark", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 1}>
|
||||
{i18next.t("general:Preview")}:
|
||||
</Col>
|
||||
<Col span={23}>
|
||||
<a target="_blank" rel="noreferrer" href={this.state.organization.logoDark}>
|
||||
<img
|
||||
src={this.state.organization.logoDark ? this.state.organization.logoDark : Setting.getLogo(["dark"])}
|
||||
alt={this.state.organization.logoDark} height={90}
|
||||
style={{background: "#141414", marginBottom: "20px"}} />
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>)
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Favicon"), i18next.t("general:Favicon - Tooltip"))} :
|
||||
|
@ -65,7 +65,7 @@ class RecordListPage extends BaseListPage {
|
||||
title: i18next.t("general:Client IP"),
|
||||
dataIndex: "clientIp",
|
||||
key: "clientIp",
|
||||
width: "150px",
|
||||
width: "100px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("clientIp"),
|
||||
render: (text, record, index) => {
|
||||
@ -80,7 +80,7 @@ class RecordListPage extends BaseListPage {
|
||||
title: i18next.t("general:Timestamp"),
|
||||
dataIndex: "createdTime",
|
||||
key: "createdTime",
|
||||
width: "180px",
|
||||
width: "150px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
@ -105,7 +105,7 @@ class RecordListPage extends BaseListPage {
|
||||
title: i18next.t("general:User"),
|
||||
dataIndex: "user",
|
||||
key: "user",
|
||||
width: "120px",
|
||||
width: "100px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("user"),
|
||||
render: (text, record, index) => {
|
||||
@ -151,6 +151,14 @@ class RecordListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("language"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("record:Object"),
|
||||
dataIndex: "object",
|
||||
key: "object",
|
||||
width: "90px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("object"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "action",
|
||||
|
@ -89,6 +89,14 @@ export function getAlgorithmNames(themeData) {
|
||||
return algorithms;
|
||||
}
|
||||
|
||||
export function getLogo(themes) {
|
||||
if (themes.includes("dark")) {
|
||||
return `${StaticBaseUrl}/img/casdoor-logo_1185x256_dark.png`;
|
||||
} else {
|
||||
return `${StaticBaseUrl}/img/casdoor-logo_1185x256.png`;
|
||||
}
|
||||
}
|
||||
|
||||
export const OtherProviderInfo = {
|
||||
SMS: {
|
||||
"Aliyun SMS": {
|
||||
@ -1101,7 +1109,9 @@ export function getProviderTypeOptions(category) {
|
||||
}
|
||||
|
||||
export function getCryptoAlgorithmOptions(cryptoAlgorithm) {
|
||||
if (cryptoAlgorithm === "RS256") {
|
||||
if (cryptoAlgorithm.startsWith("ES")) {
|
||||
return [];
|
||||
} else {
|
||||
return (
|
||||
[
|
||||
{id: 1024, name: "1024"},
|
||||
@ -1109,26 +1119,6 @@ export function getCryptoAlgorithmOptions(cryptoAlgorithm) {
|
||||
{id: 4096, name: "4096"},
|
||||
]
|
||||
);
|
||||
} else if (cryptoAlgorithm === "HS256" || cryptoAlgorithm === "ES256") {
|
||||
return (
|
||||
[
|
||||
{id: 256, name: "256"},
|
||||
]
|
||||
);
|
||||
} else if (cryptoAlgorithm === "ES384") {
|
||||
return (
|
||||
[
|
||||
{id: 384, name: "384"},
|
||||
]
|
||||
);
|
||||
} else if (cryptoAlgorithm === "ES521") {
|
||||
return (
|
||||
[
|
||||
{id: 521, name: "521"},
|
||||
]
|
||||
);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1174,6 +1164,10 @@ export function isLdapEnabled(application) {
|
||||
return isSigninMethodEnabled(application, "LDAP");
|
||||
}
|
||||
|
||||
export function isFaceIdEnabled(application) {
|
||||
return isSigninMethodEnabled(application, "Face ID");
|
||||
}
|
||||
|
||||
export function getLoginLink(application) {
|
||||
let url;
|
||||
if (application === null) {
|
||||
|
@ -40,6 +40,7 @@ import {DeleteMfa} from "./backend/MfaBackend";
|
||||
import {CheckCircleOutlined, HolderOutlined, UsergroupAddOutlined} from "@ant-design/icons";
|
||||
import * as MfaBackend from "./backend/MfaBackend";
|
||||
import AccountAvatar from "./account/AccountAvatar";
|
||||
import FaceIdTable from "./table/FaceIdTable";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
@ -59,6 +60,7 @@ class UserEditPage extends React.Component {
|
||||
loading: true,
|
||||
returnUrl: null,
|
||||
idCardInfo: ["ID card front", "ID card back", "ID card with person"],
|
||||
openFaceRecognitionModal: false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -974,6 +976,21 @@ class UserEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "Face ID") {
|
||||
return (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("user:Face ids"), i18next.t("user:Face ids"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<FaceIdTable
|
||||
title={i18next.t("user:Face ids")}
|
||||
table={this.state.user.faceIds}
|
||||
onUpdateTable={(table) => {this.updateUserField("faceIds", table);}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import React, {Suspense, lazy} from "react";
|
||||
import {Button, Checkbox, Col, Form, Input, Result, Spin, Tabs} from "antd";
|
||||
import {ArrowLeftOutlined, LockOutlined, UserOutlined} from "@ant-design/icons";
|
||||
import {withRouter} from "react-router-dom";
|
||||
@ -36,6 +36,7 @@ import {CaptchaModal, CaptchaRule} from "../common/modal/CaptchaModal";
|
||||
import RedirectForm from "../common/RedirectForm";
|
||||
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./mfa/MfaAuthVerifyForm";
|
||||
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
|
||||
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
|
||||
|
||||
class LoginPage extends React.Component {
|
||||
constructor(props) {
|
||||
@ -52,6 +53,7 @@ class LoginPage extends React.Component {
|
||||
validEmail: false,
|
||||
enableCaptchaModal: CaptchaRule.Never,
|
||||
openCaptchaModal: false,
|
||||
openFaceRecognitionModal: false,
|
||||
verifyCaptcha: undefined,
|
||||
samlResponse: "",
|
||||
relayState: "",
|
||||
@ -214,6 +216,7 @@ class LoginPage extends React.Component {
|
||||
}
|
||||
case "WebAuthn": return "webAuthn";
|
||||
case "LDAP": return "ldap";
|
||||
case "Face ID": return "faceId";
|
||||
}
|
||||
}
|
||||
|
||||
@ -263,6 +266,8 @@ class LoginPage extends React.Component {
|
||||
values["signinMethod"] = "WebAuthn";
|
||||
} else if (this.state.loginMethod === "ldap") {
|
||||
values["signinMethod"] = "LDAP";
|
||||
} else if (this.state.loginMethod === "faceId") {
|
||||
values["signinMethod"] = "Face ID";
|
||||
}
|
||||
const oAuthParams = Util.getOAuthGetParameters();
|
||||
|
||||
@ -340,6 +345,13 @@ class LoginPage extends React.Component {
|
||||
this.signInWithWebAuthn(username, values);
|
||||
return;
|
||||
}
|
||||
if (this.state.loginMethod === "faceId") {
|
||||
this.setState({
|
||||
openFaceRecognitionModal: true,
|
||||
values: values,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.state.loginMethod === "password" || this.state.loginMethod === "ldap") {
|
||||
if (this.state.enableCaptchaModal === CaptchaRule.Always) {
|
||||
this.setState({
|
||||
@ -657,6 +669,25 @@ class LoginPage extends React.Component {
|
||||
i18next.t("login:Sign In")
|
||||
}
|
||||
</Button>
|
||||
{
|
||||
this.state.loginMethod === "faceId" ?
|
||||
<Suspense fallback={null}>
|
||||
<FaceRecognitionModal
|
||||
visible={this.state.openFaceRecognitionModal}
|
||||
onOk={(faceId) => {
|
||||
const values = this.state.values;
|
||||
values["faceId"] = faceId;
|
||||
|
||||
this.login(values);
|
||||
this.setState({openFaceRecognitionModal: false});
|
||||
}}
|
||||
onCancel={() => this.setState({openFaceRecognitionModal: false})}
|
||||
/>
|
||||
</Suspense>
|
||||
:
|
||||
<>
|
||||
</>
|
||||
}
|
||||
{
|
||||
this.renderCaptchaModal(application)
|
||||
}
|
||||
@ -721,7 +752,7 @@ class LoginPage extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
const showForm = Setting.isPasswordEnabled(application) || Setting.isCodeSigninEnabled(application) || Setting.isWebAuthnEnabled(application) || Setting.isLdapEnabled(application);
|
||||
const showForm = Setting.isPasswordEnabled(application) || Setting.isCodeSigninEnabled(application) || Setting.isWebAuthnEnabled(application) || Setting.isLdapEnabled(application) || Setting.isFaceIdEnabled(application);
|
||||
if (showForm) {
|
||||
let loginWidth = 320;
|
||||
if (Setting.getLanguage() === "fr") {
|
||||
@ -1029,6 +1060,7 @@ class LoginPage extends React.Component {
|
||||
[generateItemKey("Verification code", "Phone only"), {label: i18next.t("login:Verification code"), key: "verificationCodePhone"}],
|
||||
[generateItemKey("WebAuthn", "None"), {label: i18next.t("login:WebAuthn"), key: "webAuthn"}],
|
||||
[generateItemKey("LDAP", "None"), {label: i18next.t("login:LDAP"), key: "ldap"}],
|
||||
[generateItemKey("Face ID", "None"), {label: i18next.t("login:Face ID"), key: "faceId"}],
|
||||
]);
|
||||
|
||||
application?.signinMethods?.forEach((signinMethod) => {
|
||||
@ -1117,7 +1149,7 @@ class LoginPage extends React.Component {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{height: 300, width: 300}}>
|
||||
<div style={{height: 300}}>
|
||||
{renderChoiceBox()}
|
||||
</div>
|
||||
);
|
||||
@ -1188,11 +1220,9 @@ class LoginPage extends React.Component {
|
||||
</div>
|
||||
<div className="login-form">
|
||||
<div>
|
||||
<div>
|
||||
{
|
||||
this.renderLoginPanel(application)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
this.renderLoginPanel(application)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -31,17 +31,32 @@ const Dashboard = (props) => {
|
||||
return () => window.removeEventListener("storageTourChanged", handleTourChange);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener("storageOrganizationChanged", handleOrganizationChange);
|
||||
return () => window.removeEventListener("storageOrganizationChanged", handleOrganizationChange);
|
||||
}, [props.owner]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!Setting.isLocalAdminUser(props.account)) {
|
||||
props.history.push("/apps");
|
||||
}
|
||||
}, [props.account]);
|
||||
|
||||
const getOrganizationName = () => {
|
||||
let organization = localStorage.getItem("organization") === "All" ? "" : localStorage.getItem("organization");
|
||||
if (!Setting.isAdminUser(props.account) && Setting.isLocalAdminUser(props.account)) {
|
||||
organization = props.account.owner;
|
||||
}
|
||||
return organization;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!Setting.isLocalAdminUser(props.account)) {
|
||||
return;
|
||||
}
|
||||
DashboardBackend.getDashboard(props.account.owner).then((res) => {
|
||||
|
||||
const organization = getOrganizationName();
|
||||
DashboardBackend.getDashboard(organization).then((res) => {
|
||||
if (res.status === "ok") {
|
||||
setDashboardData(res.data);
|
||||
} else {
|
||||
@ -54,6 +69,21 @@ const Dashboard = (props) => {
|
||||
setIsTourVisible(TourConfig.getTourVisible());
|
||||
};
|
||||
|
||||
const handleOrganizationChange = () => {
|
||||
if (!Setting.isLocalAdminUser(props.account)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const organization = getOrganizationName();
|
||||
DashboardBackend.getDashboard(organization).then((res) => {
|
||||
if (res.status === "ok") {
|
||||
setDashboardData(res.data);
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setIsTourToLocal = () => {
|
||||
TourConfig.setIsTourVisible(false);
|
||||
setIsTourVisible(false);
|
||||
|
180
web/src/common/modal/FaceRecognitionModal.js
Normal file
180
web/src/common/modal/FaceRecognitionModal.js
Normal file
@ -0,0 +1,180 @@
|
||||
// Copyright 2024 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 * as faceapi from "face-api.js";
|
||||
import React, {useState} from "react";
|
||||
import Webcam from "react-webcam";
|
||||
import {Button, Modal, Progress, Spin, message} from "antd";
|
||||
import i18next from "i18next";
|
||||
|
||||
const FaceRecognitionModal = (props) => {
|
||||
const {visible, onOk, onCancel} = props;
|
||||
const [modelsLoaded, setModelsLoaded] = React.useState(false);
|
||||
|
||||
const webcamRef = React.useRef();
|
||||
const canvasRef = React.useRef();
|
||||
const detection = React.useRef(null);
|
||||
const [percent, setPercent] = useState(0);
|
||||
|
||||
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";
|
||||
|
||||
Promise.all([
|
||||
faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),
|
||||
faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL),
|
||||
faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URL),
|
||||
]).then((val) => {
|
||||
setModelsLoaded(true);
|
||||
}).catch((err) => {
|
||||
message.error(i18next.t("login:Model loading failure"));
|
||||
onCancel();
|
||||
});
|
||||
};
|
||||
loadModels();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (visible) {
|
||||
setPercent(0);
|
||||
if (modelsLoaded && webcamRef.current?.video) {
|
||||
handleStreamVideo(null);
|
||||
}
|
||||
} else {
|
||||
clearInterval(detection.current);
|
||||
detection.current = null;
|
||||
}
|
||||
return () => {
|
||||
clearInterval(detection.current);
|
||||
detection.current = null;
|
||||
};
|
||||
}, [visible]);
|
||||
|
||||
const handleStreamVideo = (e) => {
|
||||
let count = 0;
|
||||
let goodCount = 0;
|
||||
if (!detection.current) {
|
||||
detection.current = setInterval(async() => {
|
||||
if (modelsLoaded && webcamRef.current?.video && visible) {
|
||||
const faces = await faceapi.detectAllFaces(webcamRef.current.video, new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks().withFaceDescriptors();
|
||||
|
||||
count++;
|
||||
if (count % 50 === 0) {
|
||||
message.warning(i18next.t("login:Please ensure sufficient lighting and align your face in the center of the recognition box"));
|
||||
} else if (count > 300) {
|
||||
message.error(i18next.t("login:Face recognition failed"));
|
||||
onCancel();
|
||||
}
|
||||
if (faces.length === 1) {
|
||||
const face = faces[0];
|
||||
setPercent(Math.round(face.detection.score * 100));
|
||||
const array = Array.from(face.descriptor);
|
||||
if (face.detection.score > 0.9) {
|
||||
goodCount++;
|
||||
if (face.detection.score > 0.99 || goodCount > 10) {
|
||||
clearInterval(detection.current);
|
||||
onOk(array);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setPercent(Math.round(percent / 2));
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCameraError = (error) => {
|
||||
// https://github.com/mozmorris/react-webcam/issues/272
|
||||
if (error.message.includes("device not found")) {
|
||||
message.error(i18next.t("login:You need to have a camera device to login with Face ID"));
|
||||
} else {
|
||||
message.error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
closable={false}
|
||||
maskClosable={false}
|
||||
open={visible}
|
||||
title={i18next.t("login:Face Recognition")}
|
||||
width={350}
|
||||
footer={[
|
||||
<Button key="back" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Progress percent={percent} />
|
||||
<div style={{marginTop: "20px", marginBottom: "50px", justifyContent: "center", alignContent: "center", position: "relative", flexDirection: "column"}}>
|
||||
{
|
||||
modelsLoaded ?
|
||||
<div style={{display: "flex", justifyContent: "center", alignContent: "center"}}>
|
||||
<Webcam
|
||||
ref={webcamRef}
|
||||
videoConstraints={{facingMode: "user"}}
|
||||
onUserMedia={handleStreamVideo}
|
||||
onUserMediaError={handleCameraError}
|
||||
style={{
|
||||
borderRadius: "50%",
|
||||
height: "220px",
|
||||
verticalAlign: "middle",
|
||||
width: "220px",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
></Webcam>
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
width: "240px",
|
||||
height: "240px",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}>
|
||||
<svg width="240" height="240" fill="none">
|
||||
<circle
|
||||
strokeDasharray="700"
|
||||
strokeDashoffset={700 - 6.9115 * percent}
|
||||
strokeWidth="4"
|
||||
cx="120"
|
||||
cy="120"
|
||||
r="110"
|
||||
stroke="#5734d3"
|
||||
transform="rotate(-90, 120, 120)"
|
||||
strokeLinecap="round"
|
||||
style={{transition: "all .2s linear"}}
|
||||
></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<canvas ref={canvasRef} style={{position: "absolute"}} />
|
||||
</div>
|
||||
:
|
||||
<div>
|
||||
<Spin tip={i18next.t("login:Loading")} size="large" style={{display: "flex", justifyContent: "center", alignContent: "center"}}>
|
||||
<div className="content" />
|
||||
</Spin>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FaceRecognitionModal;
|
@ -27,9 +27,6 @@ code {
|
||||
}
|
||||
|
||||
.logo {
|
||||
background: url("https://cdn.casbin.org/img/casdoor-logo_1185x256.png");
|
||||
background-size: 130px, 27px !important;
|
||||
width: 130px;
|
||||
height: 27px;
|
||||
margin: 17px 0 16px 15px;
|
||||
float: left;
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Valid email address",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Icons that the application presents to the outside world",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Master password",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Gültige E-Mail-Adresse",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Symbole, die die Anwendung der Außenwelt präsentiert",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Hauptpasswort",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Valid email address",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Icons that the application presents to the outside world",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Master password",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Dirección de correo electrónico válida",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Logotipo",
|
||||
"Logo - Tooltip": "Iconos que la aplicación presenta al mundo exterior",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Contraseña maestra",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Valid email address",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Icons that the application presents to the outside world",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Master password",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Valid email address",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Icons that the application presents to the outside world",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Master password",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Adresse e-mail valide",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Activer",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Activé",
|
||||
"Enabled successfully": "Activé avec succès",
|
||||
"Enforcers": "Exécuteurs",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Journalisation et Audit",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Icônes que l'application présente au monde extérieur",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "Type d'authentification multifacteur",
|
||||
"MFA items - Tooltip": "Types d'authentification multifacteur - Infobulle",
|
||||
"Master password": "Mot de passe passe-partout",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Valid email address",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Icons that the application presents to the outside world",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Master password",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Alamat email yang valid",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Ikon-ikon yang disajikan aplikasi ke dunia luar",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Kata sandi utama",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Valid email address",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Icons that the application presents to the outside world",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Master password",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "有効な電子メールアドレス",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "ロゴ",
|
||||
"Logo - Tooltip": "アプリケーションが外部世界に示すアイコン",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "マスターパスワード",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Valid email address",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Icons that the application presents to the outside world",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Master password",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "유효한 이메일 주소",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "로고",
|
||||
"Logo - Tooltip": "애플리케이션이 외부 세계에 제시하는 아이콘들",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "마스터 비밀번호",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Valid email address",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Icons that the application presents to the outside world",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Master password",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Valid email address",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Icons that the application presents to the outside world",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Master password",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Valid email address",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Icons that the application presents to the outside world",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Master password",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Endereço de e-mail válido",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Habilitar",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Habilitado",
|
||||
"Enabled successfully": "Habilitado com sucesso",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Ícones que o aplicativo apresenta para o mundo externo",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Senha mestra",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Действительный адрес электронной почты",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Включить",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Включено",
|
||||
"Enabled successfully": "Успешно включено",
|
||||
"Enforcers": "Контролёры доступа",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Логотип",
|
||||
"Logo - Tooltip": "Иконки, которые приложение представляет во внешний мир",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Главный пароль",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Valid email address",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Icons that the application presents to the outside world",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Master password",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Geçerli e-posta adresi",
|
||||
"Email only": "Sadece eposta",
|
||||
"Enable": "Etkinleştir",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Etkin",
|
||||
"Enabled successfully": "Başarıyla etkinleştirildi",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Icons that the application presents to the outside world",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Master password",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Valid email address",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "Icons that the application presents to the outside world",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Master password",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "Địa chỉ email hợp lệ",
|
||||
"Email only": "Email only",
|
||||
"Enable": "Enable",
|
||||
"Enable dark logo": "Enable dark logo",
|
||||
"Enable dark logo - Tooltip": "Enable dark logo",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled successfully": "Enabled successfully",
|
||||
"Enforcers": "Enforcers",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "Logging & Auditing",
|
||||
"Logo": "Biểu tượng",
|
||||
"Logo - Tooltip": "Biểu tượng mà ứng dụng hiển thị ra ngoài thế giới",
|
||||
"Logo dark": "Logo dark",
|
||||
"Logo dark - Tooltip": "The logo used in dark theme",
|
||||
"MFA items": "MFA items",
|
||||
"MFA items - Tooltip": "MFA items - Tooltip",
|
||||
"Master password": "Mật khẩu chính",
|
||||
|
@ -229,6 +229,8 @@
|
||||
"Email - Tooltip": "合法的电子邮件地址",
|
||||
"Email only": "仅支持邮件",
|
||||
"Enable": "启用",
|
||||
"Enable dark logo": "开启暗黑Logo",
|
||||
"Enable dark logo - Tooltip": "开启暗黑Logo",
|
||||
"Enabled": "已开启",
|
||||
"Enabled successfully": "启用成功",
|
||||
"Enforcers": "Casbin执行器",
|
||||
@ -268,6 +270,8 @@
|
||||
"Logging & Auditing": "日志 & 审计",
|
||||
"Logo": "Logo",
|
||||
"Logo - Tooltip": "应用程序向外展示的图标",
|
||||
"Logo dark": "暗黑logo",
|
||||
"Logo dark - Tooltip": "暗黑主题下使用的logo",
|
||||
"MFA items": "MFA 项",
|
||||
"MFA items - Tooltip": "MFA 项 - Tooltip",
|
||||
"Master password": "万能密码",
|
||||
|
@ -105,6 +105,7 @@ class AccountTable extends React.Component {
|
||||
{name: "Multi-factor authentication", label: i18next.t("user:Multi-factor authentication")},
|
||||
{name: "WebAuthn credentials", label: i18next.t("user:WebAuthn credentials")},
|
||||
{name: "Managed accounts", label: i18next.t("user:Managed accounts")},
|
||||
{name: "Face ID", label: i18next.t("user:Face ID")},
|
||||
];
|
||||
};
|
||||
|
||||
|
134
web/src/table/FaceIdTable.js
Normal file
134
web/src/table/FaceIdTable.js
Normal file
@ -0,0 +1,134 @@
|
||||
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React, {Suspense, lazy} from "react";
|
||||
import {Button, Col, Input, Row, Table} from "antd";
|
||||
import i18next from "i18next";
|
||||
import * as Setting from "../Setting";
|
||||
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
|
||||
|
||||
class FaceIdTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
openFaceRecognitionModal: false,
|
||||
};
|
||||
}
|
||||
|
||||
updateTable(table) {
|
||||
this.props.onUpdateTable(table);
|
||||
}
|
||||
|
||||
updateField(table, index, key, value) {
|
||||
table[index][key] = value;
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
deleteRow(table, i) {
|
||||
table = Setting.deleteRow(table, i);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
addFaceId(table, faceIdData) {
|
||||
const faceId = {
|
||||
name: Setting.getRandomName(),
|
||||
faceIdData: faceIdData,
|
||||
};
|
||||
if (table === undefined || table === null) {
|
||||
table = [];
|
||||
}
|
||||
table = Setting.addRow(table, faceId);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
renderTable(table) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "200px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input defaultValue={text} onChange={e => {
|
||||
this.updateField(table, index, "name", e.target.value);
|
||||
}} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:FaceIdData"),
|
||||
dataIndex: "faceIdData",
|
||||
key: "faceIdData",
|
||||
render: (text, record, index) => {
|
||||
const front = text.slice(0, 3).join(", ");
|
||||
const back = text.slice(-3).join(", ");
|
||||
return "[" + front + " ... " + back + "]";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
key: "action",
|
||||
width: "100px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Button style={{marginTop: "5px", marginBottom: "5px", marginRight: "5px"}} type="primary" danger onClick={() => {this.deleteRow(table, index);}}>
|
||||
{i18next.t("general:Delete")}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={this.props.table} size="middle" bordered pagination={false}
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("user:Face ids")}
|
||||
<Button disabled={this.props.table?.length >= 5} style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.setState({openFaceRecognitionModal: true})}>
|
||||
{i18next.t("general:Add Face Id")}
|
||||
</Button>
|
||||
<Suspense fallback={null}>
|
||||
<FaceRecognitionModal
|
||||
visible={this.state.openFaceRecognitionModal}
|
||||
onOk={(faceIdData) => {
|
||||
this.addFaceId(table, faceIdData);
|
||||
this.setState({openFaceRecognitionModal: false});
|
||||
}}
|
||||
onCancel={() => this.setState({openFaceRecognitionModal: false})}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={24}>
|
||||
{
|
||||
this.renderTable(this.props.table)
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FaceIdTable;
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {DeleteOutlined, DownOutlined, LinkOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {Button, Col, Input, Row, Select, Table, Tooltip} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
@ -98,14 +98,27 @@ class ManagedAccountTable extends React.Component {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Signin URL"),
|
||||
dataIndex: "signinUrl",
|
||||
key: "signinUrl",
|
||||
// width: "420px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input prefix={<LinkOutlined />} value={text} onChange={e => {
|
||||
this.updateField(table, index, "signinUrl", e.target.value);
|
||||
}} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("signup:Username"),
|
||||
dataIndex: "username",
|
||||
key: "username",
|
||||
width: "420px",
|
||||
width: "200px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input defaultValue={text} onChange={e => {
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "username", e.target.value);
|
||||
}} />
|
||||
);
|
||||
@ -115,10 +128,10 @@ class ManagedAccountTable extends React.Component {
|
||||
title: i18next.t("general:Password"),
|
||||
dataIndex: "password",
|
||||
key: "password",
|
||||
width: "420px",
|
||||
width: "200px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input defaultValue={text} onChange={e => {
|
||||
<Input.Password value={text} onChange={e => {
|
||||
this.updateField(table, index, "password", e.target.value);
|
||||
}} />
|
||||
);
|
||||
|
@ -71,6 +71,7 @@ class SigninMethodTable extends React.Component {
|
||||
{name: "Verification code", displayName: i18next.t("login:Verification code")},
|
||||
{name: "WebAuthn", displayName: i18next.t("login:WebAuthn")},
|
||||
{name: "LDAP", displayName: i18next.t("login:LDAP")},
|
||||
{name: "Face ID", displayName: i18next.t("login:Face ID")},
|
||||
];
|
||||
const columns = [
|
||||
{
|
||||
|
@ -3718,6 +3718,18 @@
|
||||
"@svgr/plugin-svgo" "^5.5.0"
|
||||
loader-utils "^2.0.0"
|
||||
|
||||
"@tensorflow/tfjs-core@1.7.0":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@tensorflow/tfjs-core/-/tfjs-core-1.7.0.tgz#9207c8f2481c52a6a40135a6aaf21a9bb0339bdf"
|
||||
integrity sha512-uwQdiklNjqBnHPeseOdG0sGxrI3+d6lybaKu2+ou3ajVeKdPEwpWbgqA6iHjq1iylnOGkgkbbnQ6r2lwkiIIHw==
|
||||
dependencies:
|
||||
"@types/offscreencanvas" "~2019.3.0"
|
||||
"@types/seedrandom" "2.4.27"
|
||||
"@types/webgl-ext" "0.0.30"
|
||||
"@types/webgl2" "0.0.4"
|
||||
node-fetch "~2.1.2"
|
||||
seedrandom "2.4.3"
|
||||
|
||||
"@testing-library/dom@*":
|
||||
version "9.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.1.tgz#8094f560e9389fb973fe957af41bf766937a9ee9"
|
||||
@ -4026,6 +4038,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
|
||||
integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
|
||||
|
||||
"@types/offscreencanvas@~2019.3.0":
|
||||
version "2019.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz#3336428ec7e9180cf4566dfea5da04eb586a6553"
|
||||
integrity sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==
|
||||
|
||||
"@types/parse-json@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||
@ -4089,6 +4106,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
|
||||
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
|
||||
|
||||
"@types/seedrandom@2.4.27":
|
||||
version "2.4.27"
|
||||
resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.27.tgz#9db563937dd86915f69092bc43259d2f48578e41"
|
||||
integrity sha512-YvMLqFak/7rt//lPBtEHv3M4sRNA+HGxrhFZ+DQs9K2IkYJbNwVIb8avtJfhDiuaUBX/AW0jnjv48FV8h3u9bQ==
|
||||
|
||||
"@types/semver@^7.3.12":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a"
|
||||
@ -4168,6 +4190,16 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311"
|
||||
integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==
|
||||
|
||||
"@types/webgl-ext@0.0.30":
|
||||
version "0.0.30"
|
||||
resolved "https://registry.yarnpkg.com/@types/webgl-ext/-/webgl-ext-0.0.30.tgz#0ce498c16a41a23d15289e0b844d945b25f0fb9d"
|
||||
integrity sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg==
|
||||
|
||||
"@types/webgl2@0.0.4":
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/webgl2/-/webgl2-0.0.4.tgz#c3b0f9d6b465c66138e84e64cb3bdf8373c2c279"
|
||||
integrity sha512-PACt1xdErJbMUOUweSrbVM7gSIYm1vTncW2hF6Os/EeWi6TXYAYMPp+8v6rzHmypE5gHrxaxZNXgMkJVIdZpHw==
|
||||
|
||||
"@types/ws@^7.4.4":
|
||||
version "7.4.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702"
|
||||
@ -7720,6 +7752,14 @@ eyes@^0.1.8:
|
||||
resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
|
||||
integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==
|
||||
|
||||
face-api.js@^0.22.2:
|
||||
version "0.22.2"
|
||||
resolved "https://registry.yarnpkg.com/face-api.js/-/face-api.js-0.22.2.tgz#5accbf7e53b1569685d116a7e18dbc4800770d39"
|
||||
integrity sha512-9Bbv/yaBRTKCXjiDqzryeKhYxmgSjJ7ukvOvEBy6krA0Ah/vNBlsf7iBNfJljWiPA8Tys1/MnB3lyP2Hfmsuyw==
|
||||
dependencies:
|
||||
"@tensorflow/tfjs-core" "1.7.0"
|
||||
tslib "^1.11.1"
|
||||
|
||||
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
@ -10493,6 +10533,11 @@ node-fetch@^2.6.12:
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-fetch@~2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5"
|
||||
integrity sha512-IHLHYskTc2arMYsHZH82PVX8CSKT5lzb7AXeyO06QnjGDKtkv+pv3mEki6S7reB/x1QPo+YPxQRNEVgR5V/w3Q==
|
||||
|
||||
node-forge@^1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
|
||||
@ -12620,6 +12665,11 @@ react-social-login-buttons@^3.4.0:
|
||||
resolved "https://registry.yarnpkg.com/react-social-login-buttons/-/react-social-login-buttons-3.9.1.tgz#c0595ac314a09e4d6024134ff0cc9901879179ff"
|
||||
integrity sha512-KtucVWvdnIZ0icG99WJ3usQUJYmlKsOIBYGyngcuNSVyyYdZtif4KHY80qnCg+teDlgYr54ToQtg3x26ZqaS2w==
|
||||
|
||||
react-webcam@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-webcam/-/react-webcam-7.2.0.tgz#64141c4c7bdd3e956620500187fa3fcc77e1fd49"
|
||||
integrity sha512-xkrzYPqa1ag2DP+2Q/kLKBmCIfEx49bVdgCCCcZf88oF+0NPEbkwYk3/s/C7Zy0mhM8k+hpdNkBLzxg8H0aWcg==
|
||||
|
||||
react@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
@ -13073,6 +13123,11 @@ scrypt-js@3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312"
|
||||
integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==
|
||||
|
||||
seedrandom@2.4.3:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-2.4.3.tgz#2438504dad33917314bff18ac4d794f16d6aaecc"
|
||||
integrity sha512-2CkZ9Wn2dS4mMUWQaXLsOAfGD+irMlLEeSP3cMxpGbgyOOzJGFa+MWCOMTOCMyZinHRPxyOj/S/C57li/1to6Q==
|
||||
|
||||
select-hose@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
|
||||
@ -14211,7 +14266,7 @@ tslib@2.3.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||
|
||||
tslib@^1.8.1, tslib@^1.9.0:
|
||||
tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
Reference in New Issue
Block a user