From 391a533ce193fa8337e1c8b3541e87d51a665107 Mon Sep 17 00:00:00 2001 From: HGZ-20 Date: Sat, 16 Mar 2024 09:04:00 +0800 Subject: [PATCH] feat: add "Face ID" login method (#2782) Face Login via face-api.js --- controllers/auth.go | 33 +++- form/auth.go | 2 + object/application.go | 11 ++ object/user.go | 8 +- object/user_util.go | 5 + object/verification.go | 23 +++ web/package.json | 4 +- web/src/ApplicationEditPage.js | 2 +- web/src/Setting.js | 4 + web/src/UserEditPage.js | 17 ++ web/src/auth/LoginPage.js | 36 +++- web/src/common/modal/FaceRecognitionModal.js | 175 +++++++++++++++++++ web/src/table/AccountTable.js | 1 + web/src/table/FaceIdTable.js | 134 ++++++++++++++ web/src/table/SigninMethodTable.js | 1 + web/yarn.lock | 57 +++++- 16 files changed, 506 insertions(+), 7 deletions(-) create mode 100644 web/src/common/modal/FaceRecognitionModal.js create mode 100644 web/src/table/FaceIdTable.js diff --git a/controllers/auth.go b/controllers/auth.go index ffa75447..8fe6437c 100644 --- a/controllers/auth.go +++ b/controllers/auth.go @@ -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 diff --git a/form/auth.go b/form/auth.go index 4a7239f6..86df5980 100644 --- a/form/auth.go +++ b/form/auth.go @@ -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) { diff --git a/object/application.go b/object/application.go index 5cc980bb..953bb691 100644 --- a/object/application.go +++ b/object/application.go @@ -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 { diff --git a/object/user.go b/object/user.go index 2ddcf12a..418e7e90 100644 --- a/object/user.go +++ b/object/user.go @@ -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", diff --git a/object/user_util.go b/object/user_util.go index 9f587003..e615fb0c 100644 --- a/object/user_util.go +++ b/object/user_util.go @@ -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) diff --git a/object/verification.go b/object/verification.go index da668ee6..ca3124fd 100644 --- a/object/verification.go +++ b/object/verification.go @@ -17,6 +17,7 @@ package object import ( "errors" "fmt" + "math" "math/rand" "strings" "time" @@ -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 diff --git a/web/package.json b/web/package.json index b246eaff..a9f168d7 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/ApplicationEditPage.js b/web/src/ApplicationEditPage.js index 45a373e5..0912ff08 100644 --- a/web/src/ApplicationEditPage.js +++ b/web/src/ApplicationEditPage.js @@ -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) => { diff --git a/web/src/Setting.js b/web/src/Setting.js index 0fddb389..f4f42dd9 100644 --- a/web/src/Setting.js +++ b/web/src/Setting.js @@ -1164,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) { diff --git a/web/src/UserEditPage.js b/web/src/UserEditPage.js index e27ba9a1..4c13948f 100644 --- a/web/src/UserEditPage.js +++ b/web/src/UserEditPage.js @@ -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 { ); + } else if (accountItem.name === "Face ID") { + return ( + + + {Setting.getLabel(i18next.t("user:Face ids"), i18next.t("user:Face ids"))} : + + + {this.updateUserField("faceIds", table);}} + /> + + + ); } } diff --git a/web/src/auth/LoginPage.js b/web/src/auth/LoginPage.js index bed06014..79de21b6 100644 --- a/web/src/auth/LoginPage.js +++ b/web/src/auth/LoginPage.js @@ -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") } + { + this.state.loginMethod === "faceId" ? + + { + const values = this.state.values; + values["faceId"] = faceId; + + this.login(values); + this.setState({openFaceRecognitionModal: false}); + }} + onCancel={() => this.setState({openFaceRecognitionModal: false})} + /> + + : + <> + + } { 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) => { diff --git a/web/src/common/modal/FaceRecognitionModal.js b/web/src/common/modal/FaceRecognitionModal.js new file mode 100644 index 00000000..3075a39f --- /dev/null +++ b/web/src/common/modal/FaceRecognitionModal.js @@ -0,0 +1,175 @@ +// 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 = () => { + message.error(i18next.t("login:There was a problem accessing the WEBCAM. Grant permission and reload the page.")); + }; + + return ( +
+ + Cancel + , + ]} + > + +
+ { + modelsLoaded ? +
+ +
+ + + +
+ +
+ : +
+ +
+ +
+ } +
+ +
+ ); +}; + +export default FaceRecognitionModal; diff --git a/web/src/table/AccountTable.js b/web/src/table/AccountTable.js index d730b8fb..22cbba24 100644 --- a/web/src/table/AccountTable.js +++ b/web/src/table/AccountTable.js @@ -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")}, ]; }; diff --git a/web/src/table/FaceIdTable.js b/web/src/table/FaceIdTable.js new file mode 100644 index 00000000..f3d71694 --- /dev/null +++ b/web/src/table/FaceIdTable.js @@ -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 ( + { + 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 ( + + ); + }, + }, + ]; + + return ( + ( +
+ {i18next.t("user:Face ids")}     + + + { + this.addFaceId(table, faceIdData); + this.setState({openFaceRecognitionModal: false}); + }} + onCancel={() => this.setState({openFaceRecognitionModal: false})} + /> + +
+ )} + /> + ); + } + + render() { + return ( +
+ +
+ { + this.renderTable(this.props.table) + } + + + + ); + } +} + +export default FaceIdTable; diff --git a/web/src/table/SigninMethodTable.js b/web/src/table/SigninMethodTable.js index aae17ba7..e9b581d4 100644 --- a/web/src/table/SigninMethodTable.js +++ b/web/src/table/SigninMethodTable.js @@ -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 = [ { diff --git a/web/yarn.lock b/web/yarn.lock index 20644366..abe67cc0 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -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==