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