mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-23 14:33:28 +08:00
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
91057f54f3 | |||
daa7b79915 | |||
d3a5539dae | |||
7d1c614452 | |||
e2eafa909b | |||
56bcef0592 | |||
0860cbf343 |
@ -34,6 +34,8 @@ func GetCredManager(passwordType string) CredManager {
|
||||
return NewPbkdf2SaltCredManager()
|
||||
} else if passwordType == "argon2id" {
|
||||
return NewArgon2idCredManager()
|
||||
} else if passwordType == "pbkdf2-django" {
|
||||
return NewPbkdf2DjangoCredManager()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
71
cred/pbkdf2_django.go
Normal file
71
cred/pbkdf2_django.go
Normal file
@ -0,0 +1,71 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cred
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
// password type: pbkdf2-django
|
||||
|
||||
type Pbkdf2DjangoCredManager struct{}
|
||||
|
||||
func NewPbkdf2DjangoCredManager() *Pbkdf2DjangoCredManager {
|
||||
cm := &Pbkdf2DjangoCredManager{}
|
||||
return cm
|
||||
}
|
||||
|
||||
func (m *Pbkdf2DjangoCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
|
||||
iterations := 260000
|
||||
salt := userSalt
|
||||
if salt == "" {
|
||||
salt = organizationSalt
|
||||
}
|
||||
|
||||
saltBytes := []byte(salt)
|
||||
passwordBytes := []byte(password)
|
||||
computedHash := pbkdf2.Key(passwordBytes, saltBytes, iterations, sha256.Size, sha256.New)
|
||||
hashBase64 := base64.StdEncoding.EncodeToString(computedHash)
|
||||
return "pbkdf2_sha256$" + strconv.Itoa(iterations) + "$" + salt + "$" + hashBase64
|
||||
}
|
||||
|
||||
func (m *Pbkdf2DjangoCredManager) IsPasswordCorrect(password string, passwordHash string, userSalt string, organizationSalt string) bool {
|
||||
parts := strings.Split(passwordHash, "$")
|
||||
if len(parts) != 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
algorithm, iterations, salt, hash := parts[0], parts[1], parts[2], parts[3]
|
||||
if algorithm != "pbkdf2_sha256" {
|
||||
return false
|
||||
}
|
||||
|
||||
iter, err := strconv.Atoi(iterations)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
saltBytes := []byte(salt)
|
||||
passwordBytes := []byte(password)
|
||||
computedHash := pbkdf2.Key(passwordBytes, saltBytes, iter, sha256.Size, sha256.New)
|
||||
computedHashBase64 := base64.StdEncoding.EncodeToString(computedHash)
|
||||
|
||||
return computedHashBase64 == hash
|
||||
}
|
@ -15,6 +15,8 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -27,13 +29,21 @@ type HttpEmailProvider struct {
|
||||
endpoint string
|
||||
method string
|
||||
httpHeaders map[string]string
|
||||
bodyMapping map[string]string
|
||||
contentType string
|
||||
}
|
||||
|
||||
func NewHttpEmailProvider(endpoint string, method string, httpHeaders map[string]string) *HttpEmailProvider {
|
||||
func NewHttpEmailProvider(endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string) *HttpEmailProvider {
|
||||
if contentType == "" {
|
||||
contentType = "application/x-www-form-urlencoded"
|
||||
}
|
||||
|
||||
client := &HttpEmailProvider{
|
||||
endpoint: endpoint,
|
||||
method: method,
|
||||
httpHeaders: httpHeaders,
|
||||
bodyMapping: bodyMapping,
|
||||
contentType: contentType,
|
||||
}
|
||||
return client
|
||||
}
|
||||
@ -41,18 +51,52 @@ func NewHttpEmailProvider(endpoint string, method string, httpHeaders map[string
|
||||
func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
|
||||
var req *http.Request
|
||||
var err error
|
||||
|
||||
fromNameField := "fromName"
|
||||
toAddressField := "toAddress"
|
||||
subjectField := "subject"
|
||||
contentField := "content"
|
||||
|
||||
for k, v := range c.bodyMapping {
|
||||
switch k {
|
||||
case "fromName":
|
||||
fromNameField = v
|
||||
case "toAddress":
|
||||
toAddressField = v
|
||||
case "subject":
|
||||
subjectField = v
|
||||
case "content":
|
||||
contentField = v
|
||||
}
|
||||
}
|
||||
|
||||
if c.method == "POST" || c.method == "PUT" || c.method == "DELETE" {
|
||||
formValues := url.Values{}
|
||||
formValues.Set("fromName", fromName)
|
||||
formValues.Set("toAddress", toAddress)
|
||||
formValues.Set("subject", subject)
|
||||
formValues.Set("content", content)
|
||||
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
|
||||
bodyMap := make(map[string]string)
|
||||
bodyMap[fromNameField] = fromName
|
||||
bodyMap[toAddressField] = toAddress
|
||||
bodyMap[subjectField] = subject
|
||||
bodyMap[contentField] = content
|
||||
|
||||
var fromValueBytes []byte
|
||||
if c.contentType == "application/json" {
|
||||
fromValueBytes, err = json.Marshal(bodyMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err = http.NewRequest(c.method, c.endpoint, bytes.NewBuffer(fromValueBytes))
|
||||
} else {
|
||||
formValues := url.Values{}
|
||||
for k, v := range bodyMap {
|
||||
formValues.Add(k, v)
|
||||
}
|
||||
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Content-Type", c.contentType)
|
||||
} else if c.method == "GET" {
|
||||
req, err = http.NewRequest(c.method, c.endpoint, nil)
|
||||
if err != nil {
|
||||
@ -60,10 +104,10 @@ func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("fromName", fromName)
|
||||
q.Add("toAddress", toAddress)
|
||||
q.Add("subject", subject)
|
||||
q.Add("content", content)
|
||||
q.Add(fromNameField, fromName)
|
||||
q.Add(toAddressField, toAddress)
|
||||
q.Add(subjectField, subject)
|
||||
q.Add(contentField, content)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
} else {
|
||||
return fmt.Errorf("HttpEmailProvider's Send() error, unsupported method: %s", c.method)
|
||||
|
@ -18,11 +18,11 @@ type EmailProvider interface {
|
||||
Send(fromAddress string, fromName, toAddress string, subject string, content string) error
|
||||
}
|
||||
|
||||
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool, endpoint string, method string, httpHeaders map[string]string) EmailProvider {
|
||||
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool, endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string) EmailProvider {
|
||||
if typ == "Azure ACS" {
|
||||
return NewAzureACSEmailProvider(clientSecret, host)
|
||||
} else if typ == "Custom HTTP Email" {
|
||||
return NewHttpEmailProvider(endpoint, method, httpHeaders)
|
||||
return NewHttpEmailProvider(endpoint, method, httpHeaders, bodyMapping, contentType)
|
||||
} else if typ == "SendGrid" {
|
||||
return NewSendgridEmailProvider(clientSecret, host, endpoint)
|
||||
} else {
|
||||
|
@ -31,7 +31,7 @@ func TestSmtpServer(provider *Provider) error {
|
||||
}
|
||||
|
||||
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
|
||||
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl, provider.Endpoint, provider.Method, provider.HttpHeaders)
|
||||
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl, provider.Endpoint, provider.Method, provider.HttpHeaders, provider.UserMapping, provider.IssuerUrl)
|
||||
|
||||
fromAddress := provider.ClientId2
|
||||
if fromAddress == "" {
|
||||
|
@ -837,6 +837,10 @@ func AddUser(user *User) (bool, error) {
|
||||
return false, fmt.Errorf("the user's owner and name should not be empty")
|
||||
}
|
||||
|
||||
if CheckUsername(user.Name, "en") != "" {
|
||||
user.Name = util.GetRandomName()
|
||||
}
|
||||
|
||||
organization, err := GetOrganizationByUser(user)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
@ -53,6 +53,14 @@ const template = `<style>
|
||||
background-color: #333333;
|
||||
box-shadow: 0 0 30px 20px rgba(255, 255, 255, 0.20);
|
||||
}
|
||||
.forget-content {
|
||||
padding: 10px 100px 20px;
|
||||
margin: 30px auto;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 7px;
|
||||
background-color: rgb(255 255 255);
|
||||
box-shadow: 0 0 20px rgb(0 0 0 / 20%);
|
||||
}
|
||||
</style>`;
|
||||
|
||||
const previewGrid = Setting.isMobile() ? 22 : 11;
|
||||
|
@ -276,7 +276,7 @@ class OrganizationEditPage extends React.Component {
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.passwordType} onChange={(value => {this.updateOrganizationField("passwordType", value);})}
|
||||
options={["plain", "salt", "sha512-salt", "md5-salt", "bcrypt", "pbkdf2-salt", "argon2id"].map(item => Setting.getOption(item, item))}
|
||||
options={["plain", "salt", "sha512-salt", "md5-salt", "bcrypt", "pbkdf2-salt", "argon2id", "pbkdf2-django"].map(item => Setting.getOption(item, item))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -42,6 +42,13 @@ const defaultUserMapping = {
|
||||
avatarUrl: "avatarUrl",
|
||||
};
|
||||
|
||||
const defaultEmailMapping = {
|
||||
fromName: "fromName",
|
||||
toAddress: "toAddress",
|
||||
subject: "subject",
|
||||
content: "content",
|
||||
};
|
||||
|
||||
class ProviderEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -72,7 +79,16 @@ class ProviderEditPage extends React.Component {
|
||||
|
||||
if (res.status === "ok") {
|
||||
const provider = res.data;
|
||||
provider.userMapping = provider.userMapping || defaultUserMapping;
|
||||
if (provider.type === "Custom HTTP Email") {
|
||||
if (!provider.userMapping) {
|
||||
provider.userMapping = provider.userMapping || defaultEmailMapping;
|
||||
}
|
||||
if (!provider.userMapping?.fromName) {
|
||||
provider.userMapping = defaultEmailMapping;
|
||||
}
|
||||
} else {
|
||||
provider.userMapping = provider.userMapping || defaultUserMapping;
|
||||
}
|
||||
this.setState({
|
||||
provider: provider,
|
||||
});
|
||||
@ -146,9 +162,16 @@ class ProviderEditPage extends React.Component {
|
||||
const requiredKeys = ["id", "username", "displayName"];
|
||||
const provider = this.state.provider;
|
||||
|
||||
if (value === "" && requiredKeys.includes(key)) {
|
||||
Setting.showMessage("error", i18next.t("provider:This field is required"));
|
||||
return;
|
||||
if (provider.type === "Custom HTTP Email") {
|
||||
if (value === "") {
|
||||
Setting.showMessage("error", i18next.t("provider:This field is required"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (value === "" && requiredKeys.includes(key)) {
|
||||
Setting.showMessage("error", i18next.t("provider:This field is required"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
provider.userMapping[key] = value;
|
||||
@ -184,6 +207,30 @@ class ProviderEditPage extends React.Component {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderEmailMappingInput() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{Setting.getLabel(i18next.t("provider:From name"), i18next.t("provider:From name - Tooltip"))} :
|
||||
<Input value={this.state.provider.userMapping.fromName} onChange={e => {
|
||||
this.updateUserMappingField("fromName", e.target.value);
|
||||
}} />
|
||||
{Setting.getLabel(i18next.t("provider:From address"), i18next.t("provider:From address - Tooltip"))} :
|
||||
<Input value={this.state.provider.userMapping.toAddress} onChange={e => {
|
||||
this.updateUserMappingField("toAddress", e.target.value);
|
||||
}} />
|
||||
{Setting.getLabel(i18next.t("provider:Subject"), i18next.t("provider:Subject - Tooltip"))} :
|
||||
<Input value={this.state.provider.userMapping.subject} onChange={e => {
|
||||
this.updateUserMappingField("subject", e.target.value);
|
||||
}} />
|
||||
{Setting.getLabel(i18next.t("provider:Email content"), i18next.t("provider:Email content - Tooltip"))} :
|
||||
<Input value={this.state.provider.userMapping.content} onChange={e => {
|
||||
this.updateUserMappingField("content", e.target.value);
|
||||
}} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
getClientIdLabel(provider) {
|
||||
switch (provider.category) {
|
||||
case "OAuth":
|
||||
@ -1097,33 +1144,66 @@ class ProviderEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.method} onChange={value => {
|
||||
this.updateProviderField("method", value);
|
||||
}}>
|
||||
{
|
||||
!["Custom HTTP Email"].includes(this.state.provider.type) ? null : (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.method} onChange={value => {
|
||||
this.updateProviderField("method", value);
|
||||
}}>
|
||||
{
|
||||
[
|
||||
{id: "GET", name: "GET"},
|
||||
{id: "POST", name: "POST"},
|
||||
{id: "PUT", name: "PUT"},
|
||||
{id: "DELETE", name: "DELETE"},
|
||||
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
[
|
||||
{id: "GET", name: "GET"},
|
||||
{id: "POST", name: "POST"},
|
||||
{id: "PUT", name: "PUT"},
|
||||
{id: "DELETE", name: "DELETE"},
|
||||
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
|
||||
this.state.provider.method !== "GET" ? (<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("webhook:Content type"), i18next.t("webhook:Content type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.issuerUrl === "" ? "application/x-www-form-urlencoded" : this.state.provider.issuerUrl} onChange={value => {
|
||||
this.updateProviderField("issuerUrl", value);
|
||||
}}>
|
||||
{
|
||||
[
|
||||
{id: "application/json", name: "application/json"},
|
||||
{id: "application/x-www-form-urlencoded", name: "application/x-www-form-urlencoded"},
|
||||
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>) : null
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:HTTP header"), i18next.t("provider:HTTP header - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<HttpHeaderTable httpHeaders={this.state.provider.httpHeaders} onUpdateTable={(value) => {this.updateProviderField("httpHeaders", value);}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:HTTP header"), i18next.t("provider:HTTP header - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<HttpHeaderTable httpHeaders={this.state.provider.httpHeaders} onUpdateTable={(value) => {this.updateProviderField("httpHeaders", value);}} />
|
||||
</Col>
|
||||
</Row>
|
||||
{this.state.provider.method !== "GET" ? <Row style={{marginTop: "20px"}}>
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:HTTP body mapping"), i18next.t("provider:HTTP body mapping - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
{this.renderEmailMappingInput()}
|
||||
</Col>
|
||||
</Row> : null}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Email title"), i18next.t("provider:Email title - Tooltip"))} :
|
||||
|
@ -471,6 +471,8 @@ class ForgetPage extends React.Component {
|
||||
<React.Fragment>
|
||||
<CustomGithubCorner />
|
||||
<div className="forget-content" style={{padding: Setting.isMobile() ? "0" : null, boxShadow: Setting.isMobile() ? "none" : null}}>
|
||||
{Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
|
||||
{Setting.inIframe() || !Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCssMobile}} />}
|
||||
<Button type="text"
|
||||
style={{position: "relative", left: Setting.isMobile() ? "10px" : "-90px", top: 0}}
|
||||
icon={<ArrowLeftOutlined style={{fontSize: "24px"}} />}
|
||||
|
@ -391,6 +391,9 @@ class LoginPage extends React.Component {
|
||||
}).then(res => res.json())
|
||||
.then((res) => {
|
||||
if (res.status === "error") {
|
||||
this.setState({
|
||||
loginLoading: false,
|
||||
});
|
||||
Setting.showMessage("error", res.msg);
|
||||
return;
|
||||
}
|
||||
@ -455,6 +458,7 @@ class LoginPage extends React.Component {
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.setState({loginLoading: false});
|
||||
});
|
||||
} else {
|
||||
@ -511,6 +515,7 @@ class LoginPage extends React.Component {
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.setState({loginLoading: false});
|
||||
});
|
||||
}
|
||||
@ -720,7 +725,7 @@ class LoginPage extends React.Component {
|
||||
values["FaceIdImage"] = FaceIdImage;
|
||||
this.login(values);
|
||||
this.setState({openFaceRecognitionModal: false});
|
||||
}} onCancel={() => this.setState({openFaceRecognitionModal: false})} /></Suspense> :
|
||||
}} onCancel={() => this.setState({openFaceRecognitionModal: false, loginLoading: false})} /></Suspense> :
|
||||
<Suspense fallback={null}>
|
||||
<FaceRecognitionModal
|
||||
visible={this.state.openFaceRecognitionModal}
|
||||
@ -731,7 +736,7 @@ class LoginPage extends React.Component {
|
||||
this.login(values);
|
||||
this.setState({openFaceRecognitionModal: false});
|
||||
}}
|
||||
onCancel={() => this.setState({openFaceRecognitionModal: false})}
|
||||
onCancel={() => this.setState({openFaceRecognitionModal: false, loginLoading: false})}
|
||||
/>
|
||||
</Suspense>
|
||||
:
|
||||
@ -1065,6 +1070,12 @@ class LoginPage extends React.Component {
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}${error}`);
|
||||
});
|
||||
}).catch(error => {
|
||||
Setting.showMessage("error", `${error}`);
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
loginLoading: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ import React, {useState} from "react";
|
||||
import {Button, Modal, Progress, Space, Spin, message} from "antd";
|
||||
import i18next from "i18next";
|
||||
import Dragger from "antd/es/upload/Dragger";
|
||||
import * as Setting from "../../Setting";
|
||||
|
||||
const FaceRecognitionModal = (props) => {
|
||||
const {visible, onOk, onCancel, withImage} = props;
|
||||
@ -35,9 +36,8 @@ const FaceRecognitionModal = (props) => {
|
||||
|
||||
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.casdoor.com/casdoor/models";
|
||||
const MODEL_URL = `${Setting.StaticBaseUrl}/casdoor/models`;
|
||||
|
||||
Promise.all([
|
||||
faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),
|
||||
|
Reference in New Issue
Block a user