feat: add local uniface face id support (#5588)
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package faceId
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LocalUniFaceProvider struct {
|
||||
Endpoint string
|
||||
ApiKey string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
type localUniFaceCompareRequest struct {
|
||||
ImageA string `json:"imageA"`
|
||||
ImageB string `json:"imageB"`
|
||||
}
|
||||
|
||||
type localUniFaceCompareResponse struct {
|
||||
Matched bool `json:"matched"`
|
||||
Score float64 `json:"score"`
|
||||
Reason string `json:"reason"`
|
||||
Detail string `json:"detail"`
|
||||
}
|
||||
|
||||
func NewLocalUniFaceProvider(endpoint string, apiKey string) *LocalUniFaceProvider {
|
||||
return &LocalUniFaceProvider{
|
||||
Endpoint: strings.TrimRight(endpoint, "/"),
|
||||
ApiKey: apiKey,
|
||||
Client: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *LocalUniFaceProvider) Check(base64ImageA string, base64ImageB string) (bool, error) {
|
||||
if provider.Endpoint == "" {
|
||||
return false, fmt.Errorf("Local UniFace endpoint is empty")
|
||||
}
|
||||
|
||||
body, err := json.Marshal(localUniFaceCompareRequest{
|
||||
ImageA: base64ImageA,
|
||||
ImageB: base64ImageB,
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/v1/compare", provider.Endpoint), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
if provider.ApiKey != "" {
|
||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", provider.ApiKey))
|
||||
}
|
||||
|
||||
client := provider.Client
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
responseBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
|
||||
return false, fmt.Errorf("Local UniFace compare failed with status %d: %s", response.StatusCode, string(responseBody))
|
||||
}
|
||||
|
||||
var compareResponse localUniFaceCompareResponse
|
||||
if err = json.Unmarshal(responseBody, &compareResponse); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return compareResponse.Matched, nil
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package faceId
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLocalUniFaceProviderCheckCallsCompareEndpoint(t *testing.T) {
|
||||
var requestBody map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/compare" {
|
||||
t.Fatalf("expected path /v1/compare, got %s", r.URL.Path)
|
||||
}
|
||||
if r.Method != http.MethodPost {
|
||||
t.Fatalf("expected POST, got %s", r.Method)
|
||||
}
|
||||
if r.Header.Get("Content-Type") != "application/json" {
|
||||
t.Fatalf("expected application/json content type, got %s", r.Header.Get("Content-Type"))
|
||||
}
|
||||
if r.Header.Get("Authorization") != "Bearer secret" {
|
||||
t.Fatalf("expected bearer token authorization, got %s", r.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&requestBody)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, _ = w.Write([]byte(`{"matched":true,"score":0.82,"threshold":0.6}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
provider := NewLocalUniFaceProvider(server.URL+"/", "secret")
|
||||
matched, err := provider.Check("login-image", "registered-image")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !matched {
|
||||
t.Fatal("expected match")
|
||||
}
|
||||
if requestBody["imageA"] != "login-image" {
|
||||
t.Fatalf("expected imageA login-image, got %#v", requestBody["imageA"])
|
||||
}
|
||||
if requestBody["imageB"] != "registered-image" {
|
||||
t.Fatalf("expected imageB registered-image, got %#v", requestBody["imageB"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalUniFaceProviderCheckReturnsFalseForNonMatch(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"matched":false,"score":0.3,"threshold":0.6}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
provider := NewLocalUniFaceProvider(server.URL, "")
|
||||
matched, err := provider.Check("login-image", "registered-image")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if matched {
|
||||
t.Fatal("expected non-match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalUniFaceProviderCheckReturnsErrorForServiceError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, `{"detail":"no face detected"}`, http.StatusBadRequest)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
provider := NewLocalUniFaceProvider(server.URL, "")
|
||||
_, err := provider.Check("login-image", "registered-image")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
@@ -19,5 +19,9 @@ type FaceIdProvider interface {
|
||||
}
|
||||
|
||||
func GetFaceIdProvider(typ string, clientId string, clientSecret string, endPoint string) FaceIdProvider {
|
||||
if typ == "Local UniFace" {
|
||||
return NewLocalUniFaceProvider(endPoint, clientSecret)
|
||||
}
|
||||
|
||||
return NewAliyunFaceIdProvider(clientId, clientSecret, endPoint)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package faceId
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGetFaceIdProviderLocalUniFace(t *testing.T) {
|
||||
provider := GetFaceIdProvider("Local UniFace", "", "secret", "http://127.0.0.1:8100")
|
||||
|
||||
localProvider, ok := provider.(*LocalUniFaceProvider)
|
||||
if !ok {
|
||||
t.Fatalf("expected *LocalUniFaceProvider, got %T", provider)
|
||||
}
|
||||
if localProvider.ApiKey != "secret" {
|
||||
t.Fatalf("expected api key secret, got %s", localProvider.ApiKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFaceIdProviderAlibabaCloudFacebody(t *testing.T) {
|
||||
provider := GetFaceIdProvider("Alibaba Cloud Facebody", "accessKey", "accessSecret", "endpoint")
|
||||
|
||||
if _, ok := provider.(*AliyunFaceIdProvider); !ok {
|
||||
t.Fatalf("expected *AliyunFaceIdProvider, got %T", provider)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -292,7 +292,7 @@ type Address struct {
|
||||
type FaceId struct {
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
FaceIdData []float64 `json:"faceIdData"`
|
||||
ImageUrl string `json:"ImageUrl"`
|
||||
ImageUrl string `json:"imageUrl"`
|
||||
}
|
||||
|
||||
func GetUserFieldStringValue(user *User, fieldName string) (bool, string, error) {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -33,6 +34,35 @@ func updateUserColumn(column string, user *User) bool {
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
func TestFaceIdUsesLowerCamelImageUrlJsonField(t *testing.T) {
|
||||
var faceId FaceId
|
||||
err := json.Unmarshal([]byte(`{"name":"face","imageUrl":"http://example.com/face.jpg","faceIdData":[]}`), &faceId)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if faceId.ImageUrl != "http://example.com/face.jpg" {
|
||||
t.Fatalf("ImageUrl = %q, want %q", faceId.ImageUrl, "http://example.com/face.jpg")
|
||||
}
|
||||
|
||||
data, err := json.Marshal(faceId)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var fields map[string]interface{}
|
||||
if err := json.Unmarshal(data, &fields); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, ok := fields["imageUrl"]; !ok {
|
||||
t.Fatalf("marshaled FaceId does not contain imageUrl: %s", string(data))
|
||||
}
|
||||
if _, ok := fields["ImageUrl"]; ok {
|
||||
t.Fatalf("marshaled FaceId unexpectedly contains ImageUrl: %s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncAvatarsFromGitHub(t *testing.T) {
|
||||
InitConfig()
|
||||
|
||||
|
||||
@@ -1050,6 +1050,7 @@ class ProviderEditPage extends React.Component {
|
||||
{
|
||||
(this.state.provider.category === "Storage" && this.state.provider.type === "Google Cloud Storage") ||
|
||||
(this.state.provider.category === "Email" && (this.state.provider.type === "Azure ACS" || this.state.provider.type === "SendGrid" || this.state.provider.type === "Resend")) ||
|
||||
(this.state.provider.category === "Face ID" && this.state.provider.type === "Local UniFace") ||
|
||||
(this.state.provider.category === "Notification" && (this.state.provider.type === "Line" || this.state.provider.type === "Telegram" || this.state.provider.type === "Bark" || this.state.provider.type === "Discord" || this.state.provider.type === "Slack" || this.state.provider.type === "Pushbullet" || this.state.provider.type === "Pushover" || this.state.provider.type === "Lark" || this.state.provider.type === "Microsoft Teams" || this.state.provider.type === "WeCom")) ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
|
||||
@@ -442,6 +442,10 @@ export const OtherProviderInfo = {
|
||||
logo: `${StaticBaseUrl}/img/social_aliyun.png`,
|
||||
url: "https://vision.aliyun.com/facebody",
|
||||
},
|
||||
"Local UniFace": {
|
||||
logo: `${StaticBaseUrl}/img/social_default.png`,
|
||||
url: "https://github.com/yakhyo/uniface",
|
||||
},
|
||||
},
|
||||
"MFA": {
|
||||
"RADIUS": {
|
||||
@@ -1475,6 +1479,7 @@ export function getProviderTypeOptions(category) {
|
||||
} else if (category === "Face ID") {
|
||||
return ([
|
||||
{id: "Alibaba Cloud Facebody", name: "Alibaba Cloud Facebody"},
|
||||
{id: "Local UniFace", name: "Local UniFace"},
|
||||
]);
|
||||
} else if (category === "MFA") {
|
||||
return ([
|
||||
|
||||
@@ -951,9 +951,9 @@ class LoginPage extends React.Component {
|
||||
}
|
||||
{
|
||||
this.state.loginMethod === "faceId" ?
|
||||
this.state.haveFaceIdProvider ? <Suspense fallback={null}><FaceRecognitionCommonModal visible={this.state.openFaceRecognitionModal} onOk={(FaceIdImage) => {
|
||||
this.state.haveFaceIdProvider ? <Suspense fallback={null}><FaceRecognitionCommonModal visible={this.state.openFaceRecognitionModal} onOk={(faceIdImage) => {
|
||||
const values = this.state.values;
|
||||
values["FaceIdImage"] = FaceIdImage;
|
||||
values["faceIdImage"] = faceIdImage;
|
||||
this.login(values);
|
||||
this.setState({openFaceRecognitionModal: false});
|
||||
}} onCancel={() => this.setState({openFaceRecognitionModal: false, loginLoading: false})} /></Suspense> :
|
||||
|
||||
@@ -20,8 +20,12 @@ import i18next from "i18next";
|
||||
import Dragger from "antd/es/upload/Dragger";
|
||||
import * as Setting from "../../Setting";
|
||||
|
||||
// This modal has three modes controlled by props:
|
||||
// withImage=false → camera mode: captures face descriptor array (faceIdData) for recognition login
|
||||
// withImage=true → image-upload mode: user drags/drops a photo; camera is skipped
|
||||
// withImage=true captureImage=true → camera-capture mode: captures a JPEG image URL for image-based enrollment
|
||||
const FaceRecognitionModal = (props) => {
|
||||
const {visible, onOk, onCancel, withImage} = props;
|
||||
const {visible, onOk, onCancel, withImage, captureImage} = props;
|
||||
const [modelsLoaded, setModelsLoaded] = React.useState(false);
|
||||
const [isCameraCaptured, setIsCameraCaptured] = useState(false);
|
||||
|
||||
@@ -59,7 +63,7 @@ const FaceRecognitionModal = (props) => {
|
||||
}, [visible, modelsLoaded]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (withImage) {
|
||||
if (withImage && !captureImage) {
|
||||
return;
|
||||
}
|
||||
if (visible) {
|
||||
@@ -87,7 +91,7 @@ const FaceRecognitionModal = (props) => {
|
||||
}, [visible, modelsLoaded]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (withImage) {
|
||||
if (withImage && !captureImage) {
|
||||
return;
|
||||
}
|
||||
if (isCameraCaptured) {
|
||||
@@ -113,7 +117,7 @@ const FaceRecognitionModal = (props) => {
|
||||
}, [isCameraCaptured]);
|
||||
|
||||
const handleStreamVideo = () => {
|
||||
if (withImage) {
|
||||
if (withImage && !captureImage) {
|
||||
return;
|
||||
}
|
||||
let count = 0;
|
||||
@@ -138,7 +142,16 @@ const FaceRecognitionModal = (props) => {
|
||||
goodCount++;
|
||||
if (face.detection.score > 0.99 || goodCount > 10) {
|
||||
clearInterval(detection.current);
|
||||
onOk(array);
|
||||
if (captureImage) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = videoRef.current.videoWidth;
|
||||
canvas.height = videoRef.current.videoHeight;
|
||||
const context = canvas.getContext("2d");
|
||||
context.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
|
||||
onOk(canvas.toDataURL("image/jpeg", 0.92));
|
||||
} else {
|
||||
onOk(array);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -175,7 +188,7 @@ const FaceRecognitionModal = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
if (!withImage) {
|
||||
if (!withImage || captureImage) {
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
},
|
||||
"application": {
|
||||
"Add Face ID": "Add Face ID",
|
||||
"Add Face ID with Camera": "Add Face ID with Camera",
|
||||
"Add Face ID with Image": "Add Face ID with Image",
|
||||
"Always": "Always",
|
||||
"Array": "Array",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
},
|
||||
"application": {
|
||||
"Add Face ID": "添加人脸ID",
|
||||
"Add Face ID with Camera": "拍照添加人脸ID",
|
||||
"Add Face ID with Image": "添加图片人脸ID",
|
||||
"Always": "始终开启",
|
||||
"Array": "数组",
|
||||
|
||||
@@ -21,7 +21,9 @@ import i18next from "i18next";
|
||||
export function renderFaceIdProviderFields(provider, updateProviderField) {
|
||||
return (
|
||||
<>
|
||||
{["Alibaba Cloud Facebody"].includes(provider.type) ? null : (
|
||||
{/* Only show the intranet endpoint field for provider types that use it.
|
||||
Add new Face ID provider types here when they support intranet/extranet endpoint pairs. */}
|
||||
{[].includes(provider.type) ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
|
||||
@@ -32,7 +34,7 @@ export function renderFaceIdProviderFields(provider, updateProviderField) {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
) : null}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:Region endpoint for Internet"))} :
|
||||
|
||||
@@ -16,7 +16,7 @@ import React, {Suspense, lazy} from "react";
|
||||
import {Button, Col, Input, Row, Table, Upload} from "antd";
|
||||
import i18next from "i18next";
|
||||
import * as Setting from "../Setting";
|
||||
import {UploadOutlined} from "@ant-design/icons";
|
||||
import {CameraOutlined, UploadOutlined} from "@ant-design/icons";
|
||||
import * as ResourceBackend from "../backend/ResourceBackend";
|
||||
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
|
||||
|
||||
@@ -26,6 +26,7 @@ class FaceIdTable extends React.Component {
|
||||
this.state = {
|
||||
classes: props,
|
||||
openFaceRecognitionModal: false,
|
||||
openFaceImageCameraModal: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,6 +69,29 @@ class FaceIdTable extends React.Component {
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
async dataUrlToFile(dataUrl, filename) {
|
||||
const res = await fetch(dataUrl);
|
||||
const blob = await res.blob();
|
||||
return new File([blob], filename, {type: blob.type || "image/jpeg"});
|
||||
}
|
||||
|
||||
uploadFaceImage(table, file, loadingField) {
|
||||
this.setState({[loadingField]: true});
|
||||
const filename = file.name;
|
||||
const fullFilePath = `resource/${this.props.account.owner}/${this.props.account.name}/${filename}`;
|
||||
ResourceBackend.uploadResource(this.props.account.owner, this.props.account.name, "custom", "ResourceListPage", fullFilePath, file)
|
||||
.then(res => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("application:File uploaded successfully"));
|
||||
this.addFaceImage(table, res.data);
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.setState({[loadingField]: false, openFaceImageCameraModal: false});
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(table) {
|
||||
const columns = [
|
||||
{
|
||||
@@ -116,21 +140,7 @@ class FaceIdTable extends React.Component {
|
||||
];
|
||||
|
||||
const handleUpload = (info) => {
|
||||
this.setState({uploading: true});
|
||||
const filename = info.fileList[0].name;
|
||||
const fullFilePath = `resource/${this.props.account.owner}/${this.props.account.name}/${filename}`;
|
||||
ResourceBackend.uploadResource(this.props.account.owner, this.props.account.name, "custom", "ResourceListPage", fullFilePath, info.file)
|
||||
.then(res => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("application:File uploaded successfully"));
|
||||
|
||||
this.addFaceImage(table, res.data);
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.setState({uploading: false});
|
||||
});
|
||||
this.uploadFaceImage(table, info.file, "uploading");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -144,6 +154,9 @@ class FaceIdTable extends React.Component {
|
||||
<Button disabled={this.props.table?.length >= 5} style={{marginRight: "10px"}} size="small" onClick={() => this.setState({openFaceRecognitionModal: true, withImage: true})}>
|
||||
{i18next.t("application:Add Face ID with Image")}
|
||||
</Button>
|
||||
<Button disabled={this.props.table?.length >= 5} style={{marginRight: "10px"}} icon={<CameraOutlined />} loading={this.state.uploadingCamera} size="small" onClick={() => this.setState({openFaceImageCameraModal: true})}>
|
||||
{i18next.t("application:Add Face ID with Camera")}
|
||||
</Button>
|
||||
<Upload maxCount={1} accept="image/*" showUploadList={false}
|
||||
beforeUpload={file => {return false;}} onChange={info => {handleUpload(info);}}>
|
||||
<Button id="upload-button" icon={<UploadOutlined />} loading={this.state.uploading} size="small">
|
||||
@@ -160,6 +173,16 @@ class FaceIdTable extends React.Component {
|
||||
}}
|
||||
onCancel={() => this.setState({openFaceRecognitionModal: false})}
|
||||
/>
|
||||
<FaceRecognitionModal
|
||||
visible={this.state.openFaceImageCameraModal}
|
||||
withImage={true}
|
||||
captureImage={true}
|
||||
onOk={async(imageDataUrl) => {
|
||||
const file = await this.dataUrlToFile(imageDataUrl, `face-id-${Date.now()}.jpg`);
|
||||
this.uploadFaceImage(table, file, "uploadingCamera");
|
||||
}}
|
||||
onCancel={() => this.setState({openFaceImageCameraModal: false})}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user