mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-23 22:53:31 +08:00
Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
bfa2ab63ad | |||
505054b0eb | |||
f95ce13b82 | |||
5315f16a48 |
@ -68,7 +68,7 @@ func (c *ApiController) GetCerts() {
|
||||
// GetGlobalCerts
|
||||
// @Title GetGlobalCerts
|
||||
// @Tag Cert API
|
||||
// @Description get globle certs
|
||||
// @Description get global certs
|
||||
// @Success 200 {array} object.Cert The Response object
|
||||
// @router /get-global-certs [get]
|
||||
func (c *ApiController) GetGlobalCerts() {
|
||||
|
4
go.mod
4
go.mod
@ -19,6 +19,7 @@ require (
|
||||
github.com/denisenkom/go-mssqldb v0.9.0
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
|
||||
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3
|
||||
github.com/ethereum/go-ethereum v1.13.14
|
||||
github.com/fogleman/gg v1.3.0
|
||||
github.com/forestmgy/ldapserver v1.1.0
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5
|
||||
@ -39,7 +40,7 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nyaruka/phonenumbers v1.1.5
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/prometheus/client_golang v1.11.1
|
||||
github.com/prometheus/client_golang v1.12.0
|
||||
github.com/prometheus/client_model v0.4.0
|
||||
github.com/qiangmzsx/string-adapter/v2 v2.1.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
@ -54,7 +55,6 @@ require (
|
||||
github.com/tealeg/xlsx v1.0.5
|
||||
github.com/thanhpk/randstr v1.0.4
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||
github.com/xorm-io/builder v0.3.13
|
||||
github.com/xorm-io/core v0.7.4
|
||||
github.com/xorm-io/xorm v1.1.6
|
||||
|
@ -15,15 +15,43 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type EIP712Message struct {
|
||||
Domain struct {
|
||||
ChainId string `json:"chainId"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
} `json:"domain"`
|
||||
Message struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Nonce string `json:"nonce"`
|
||||
CreateAt string `json:"createAt"`
|
||||
} `json:"message"`
|
||||
PrimaryType string `json:"primaryType"`
|
||||
Types struct {
|
||||
EIP712Domain []struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
} `json:"EIP712Domain"`
|
||||
AuthRequest []struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
} `json:"AuthRequest"`
|
||||
} `json:"types"`
|
||||
}
|
||||
|
||||
type MetaMaskIdProvider struct {
|
||||
Client *http.Client
|
||||
}
|
||||
@ -42,6 +70,15 @@ func (idp *MetaMaskIdProvider) GetToken(code string) (*oauth2.Token, error) {
|
||||
if err := json.Unmarshal([]byte(code), &web3AuthToken); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
valid, err := VerifySignature(web3AuthToken.Address, web3AuthToken.TypedData, web3AuthToken.Signature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return nil, fmt.Errorf("invalid signature")
|
||||
}
|
||||
|
||||
token := &oauth2.Token{
|
||||
AccessToken: web3AuthToken.Signature,
|
||||
TokenType: "Bearer",
|
||||
@ -68,3 +105,43 @@ func (idp *MetaMaskIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
|
||||
}
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func VerifySignature(userAddress string, originalMessage string, signatureHex string) (bool, error) {
|
||||
var eip712Mes EIP712Message
|
||||
err := json.Unmarshal([]byte(originalMessage), &eip712Mes)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid signature (Error parsing JSON)")
|
||||
}
|
||||
|
||||
createAtTime, err := time.Parse("2006/1/2 15:04:05", eip712Mes.Message.CreateAt)
|
||||
currentTime := time.Now()
|
||||
if createAtTime.Before(currentTime.Add(-1*time.Minute)) && createAtTime.After(currentTime) {
|
||||
return false, fmt.Errorf("invalid signature (signature does not meet time requirements)")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(signatureHex, "0x") {
|
||||
signatureHex = "0x" + signatureHex
|
||||
}
|
||||
|
||||
signatureBytes, err := hex.DecodeString(signatureHex[2:])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if signatureBytes[64] != 27 && signatureBytes[64] != 28 {
|
||||
return false, fmt.Errorf("invalid signature (incorrect recovery id)")
|
||||
}
|
||||
signatureBytes[64] -= 27
|
||||
|
||||
msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len([]byte(originalMessage)), []byte(originalMessage))
|
||||
hash := crypto.Keccak256Hash([]byte(msg))
|
||||
|
||||
pubKey, err := crypto.SigToPub(hash.Bytes(), signatureBytes)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
recoveredAddr := crypto.PubkeyToAddress(*pubKey)
|
||||
|
||||
return strings.EqualFold(recoveredAddr.Hex(), userAddress), nil
|
||||
}
|
||||
|
@ -10,6 +10,7 @@
|
||||
"@ctrl/tinycolor": "^3.5.0",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@metamask/eth-sig-util": "^6.0.0",
|
||||
"@metamask/sdk-react": "^0.18.0",
|
||||
"@web3-onboard/coinbase": "^2.2.5",
|
||||
"@web3-onboard/core": "^2.20.5",
|
||||
"@web3-onboard/frontier": "^2.0.4",
|
||||
|
@ -41,6 +41,7 @@ setTwoToneColor("rgb(87,52,211)");
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.setThemeAlgorithm();
|
||||
let storageThemeAlgorithm = [];
|
||||
try {
|
||||
storageThemeAlgorithm = localStorage.getItem("themeAlgorithm") ? JSON.parse(localStorage.getItem("themeAlgorithm")) : ["default"];
|
||||
@ -157,6 +158,15 @@ class App extends Component {
|
||||
return Setting.getLogo(themes);
|
||||
}
|
||||
|
||||
setThemeAlgorithm() {
|
||||
const currentUrl = window.location.href;
|
||||
const url = new URL(currentUrl);
|
||||
const themeType = url.searchParams.get("theme");
|
||||
if (themeType === "dark" || themeType === "default") {
|
||||
localStorage.setItem("themeAlgorithm", JSON.stringify([themeType]));
|
||||
}
|
||||
}
|
||||
|
||||
setLanguage(account) {
|
||||
const language = account?.language;
|
||||
if (language !== null && language !== "" && language !== i18next.language) {
|
||||
|
@ -1337,6 +1337,20 @@ class ProviderEditPage extends React.Component {
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.provider.type === "MetaMask" ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Signature messages"), i18next.t("provider:Signature messages - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<Input value={this.state.provider.metadata} onChange={e => {
|
||||
this.updateProviderField("metadata", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Provider URL"), i18next.t("provider:Provider URL - Tooltip"))} :
|
||||
|
@ -1173,7 +1173,7 @@ class LoginPage extends React.Component {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{height: 300}}>
|
||||
<div style={{height: 300, minWidth: 320}}>
|
||||
{renderChoiceBox()}
|
||||
</div>
|
||||
);
|
||||
|
107
web/src/auth/MetaMaskLoginButton.js
Normal file
107
web/src/auth/MetaMaskLoginButton.js
Normal file
@ -0,0 +1,107 @@
|
||||
// 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 {getAuthUrl} from "./Provider";
|
||||
import {getProviderLogoURL, goToLink, showMessage} from "../Setting";
|
||||
import i18next from "i18next";
|
||||
import {
|
||||
generateNonce,
|
||||
getWeb3AuthTokenKey,
|
||||
setWeb3AuthToken
|
||||
} from "./Web3Auth";
|
||||
import {useSDK} from "@metamask/sdk-react";
|
||||
import React, {useEffect} from "react";
|
||||
|
||||
export function MetaMaskLoginButton(props) {
|
||||
const {application, web3Provider, method, width, margin} = props;
|
||||
const {sdk, chainId, account} = useSDK();
|
||||
const [typedData, setTypedData] = React.useState("");
|
||||
const [nonce, setNonce] = React.useState("");
|
||||
const [signature, setSignature] = React.useState();
|
||||
|
||||
useEffect(() => {
|
||||
if (account && signature) {
|
||||
const date = new Date();
|
||||
|
||||
const token = {
|
||||
address: account,
|
||||
nonce: nonce,
|
||||
createAt: Math.floor(date.getTime() / 1000),
|
||||
typedData: typedData,
|
||||
signature: signature,
|
||||
};
|
||||
setWeb3AuthToken(token);
|
||||
|
||||
const redirectUri = `${getAuthUrl(application, web3Provider, method)}&web3AuthTokenKey=${getWeb3AuthTokenKey(account)}`;
|
||||
goToLink(redirectUri);
|
||||
}
|
||||
}, [account, signature]);
|
||||
|
||||
const handleConnectAndSign = async() => {
|
||||
try {
|
||||
terminate();
|
||||
|
||||
const date = new Date();
|
||||
|
||||
const nonce = generateNonce();
|
||||
setNonce(nonce);
|
||||
|
||||
const prompt = web3Provider?.metadata === "" ? "Casdoor: In order to authenticate to this website, sign this request and your public address will be sent to the server in a verifiable way." : web3Provider.metadata;
|
||||
const typedData = JSON.stringify({
|
||||
domain: {
|
||||
chainId: chainId,
|
||||
name: "Casdoor",
|
||||
version: "1",
|
||||
},
|
||||
message: {
|
||||
prompt: `${prompt}`,
|
||||
nonce: nonce,
|
||||
createAt: `${date.toLocaleString()}`,
|
||||
},
|
||||
primaryType: "AuthRequest",
|
||||
types: {
|
||||
EIP712Domain: [
|
||||
{name: "name", type: "string"},
|
||||
{name: "version", type: "string"},
|
||||
{name: "chainId", type: "uint256"},
|
||||
],
|
||||
AuthRequest: [
|
||||
{name: "prompt", type: "string"},
|
||||
{name: "nonce", type: "string"},
|
||||
{name: "createAt", type: "string"},
|
||||
],
|
||||
},
|
||||
});
|
||||
setTypedData(typedData);
|
||||
|
||||
const sig = await sdk.connectAndSign({msg: typedData});
|
||||
setSignature(sig);
|
||||
} catch (err) {
|
||||
showMessage("error", `${i18next.t("login:Failed to obtain MetaMask authorization")}: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const terminate = () => {
|
||||
sdk?.terminate();
|
||||
};
|
||||
|
||||
return (
|
||||
<a key={web3Provider.displayName} onClick={handleConnectAndSign}>
|
||||
<img width={width} height={width} src={getProviderLogoURL(web3Provider)} alt={web3Provider.displayName}
|
||||
className="provider-img" style={{margin: margin}} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetaMaskLoginButton;
|
@ -43,6 +43,8 @@ import DouyinLoginButton from "./DouyinLoginButton";
|
||||
import LoginButton from "./LoginButton";
|
||||
import * as AuthBackend from "./AuthBackend";
|
||||
import {WechatOfficialAccountModal} from "./Util";
|
||||
import {MetaMaskProvider} from "@metamask/sdk-react";
|
||||
import MetaMaskLoginButton from "./MetaMaskLoginButton";
|
||||
|
||||
function getSigninButton(provider) {
|
||||
const text = i18next.t("login:Sign in with {type}").replace("{type}", provider.displayName !== "" ? provider.displayName : provider.type);
|
||||
@ -160,11 +162,36 @@ export function renderProviderLogo(provider, application, width, margin, size, l
|
||||
</a>
|
||||
);
|
||||
} else if (provider.category === "Web3") {
|
||||
return (
|
||||
<a key={provider.displayName} onClick={() => goToWeb3Url(application, provider, "signup")}>
|
||||
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} className="provider-img" style={{margin: margin}} />
|
||||
</a>
|
||||
);
|
||||
if (provider.type === "MetaMask") {
|
||||
return (
|
||||
<MetaMaskProvider
|
||||
debug={false}
|
||||
sdkOptions={{
|
||||
communicationServerUrl: process.env.REACT_APP_COMM_SERVER_URL,
|
||||
checkInstallationImmediately: false, // This will automatically connect to MetaMask on page load
|
||||
dappMetadata: {
|
||||
name: "Casdoor",
|
||||
url: window.location.protocol + "//" + window.location.host,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MetaMaskLoginButton
|
||||
application={application}
|
||||
web3Provider={provider}
|
||||
method={"signup"}
|
||||
width={width}
|
||||
margin={margin}
|
||||
/>
|
||||
</MetaMaskProvider>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a key={provider.displayName} onClick={() => goToWeb3Url(application, provider, "signup")}>
|
||||
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName}
|
||||
className="provider-img" style={{margin: margin}} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (provider.type === "Custom") {
|
||||
// style definition
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Form, Input, Result} from "antd";
|
||||
import {Button, Form, Input, Radio, Result, Row} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import * as AuthBackend from "./AuthBackend";
|
||||
import * as ProviderButton from "./ProviderButton";
|
||||
@ -71,6 +71,7 @@ class SignupPage extends React.Component {
|
||||
applicationName: (props.applicationName ?? props.match?.params?.applicationName) ?? null,
|
||||
email: "",
|
||||
phone: "",
|
||||
emailOrPhoneMode: "",
|
||||
countryCode: "",
|
||||
emailCode: "",
|
||||
phoneCode: "",
|
||||
@ -360,130 +361,176 @@ class SignupPage extends React.Component {
|
||||
<RegionSelect onChange={(value) => {this.setState({region: value});}} />
|
||||
</Form.Item>
|
||||
);
|
||||
} else if (signupItem.name === "Email") {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label={signupItem.label ? signupItem.label : i18next.t("general:Email")}
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
message: i18next.t("signup:Please input your Email!"),
|
||||
},
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (this.state.email !== "" && !Setting.isValidEmail(this.state.email)) {
|
||||
this.setState({validEmail: false});
|
||||
return Promise.reject(i18next.t("signup:The input is not valid Email!"));
|
||||
}
|
||||
|
||||
this.setState({validEmail: true});
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation.email !== ""} onChange={e => this.setState({email: e.target.value})} />
|
||||
</Form.Item>
|
||||
{
|
||||
signupItem.rule !== "No verification" &&
|
||||
} else if (signupItem.name === "Email" || signupItem.name === "Phone" || signupItem.name === "Email or Phone" || signupItem.name === "Phone or Email") {
|
||||
const renderEmailItem = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Form.Item
|
||||
name="emailCode"
|
||||
label={signupItem.label ? signupItem.label : i18next.t("code:Email code")}
|
||||
rules={[{
|
||||
required: required,
|
||||
message: i18next.t("code:Please input your verification code!"),
|
||||
}]}
|
||||
>
|
||||
<SendCodeInput
|
||||
disabled={!this.state.validEmail}
|
||||
method={"signup"}
|
||||
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(application)]}
|
||||
application={application}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
} else if (signupItem.name === "Phone") {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Form.Item label={signupItem.label ? signupItem.label : i18next.t("general:Phone")} required={required}>
|
||||
<Input.Group compact>
|
||||
<Form.Item
|
||||
name="countryCode"
|
||||
noStyle
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
message: i18next.t("signup:Please select your country code!"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CountryCodeSelect
|
||||
style={{width: "35%"}}
|
||||
countryCodes={this.getApplicationObj().organizationObj.countryCodes}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="phone"
|
||||
dependencies={["countryCode"]}
|
||||
noStyle
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
message: i18next.t("signup:Please input your phone number!"),
|
||||
},
|
||||
({getFieldValue}) => ({
|
||||
validator: (_, value) => {
|
||||
if (!required && !value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (value && !Setting.isValidPhone(value, getFieldValue("countryCode"))) {
|
||||
this.setState({validPhone: false});
|
||||
return Promise.reject(i18next.t("signup:The input is not valid Phone!"));
|
||||
}
|
||||
|
||||
this.setState({validPhone: true});
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder={signupItem.placeholder}
|
||||
style={{width: "65%"}}
|
||||
disabled={this.state.invitation !== undefined && this.state.invitation.phone !== ""}
|
||||
onChange={e => this.setState({phone: e.target.value})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
{
|
||||
signupItem.rule !== "No verification" &&
|
||||
<Form.Item
|
||||
name="phoneCode"
|
||||
label={signupItem.label ? signupItem.label : i18next.t("code:Phone code")}
|
||||
name="email"
|
||||
label={signupItem.label ? signupItem.label : i18next.t("general:Email")}
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
message: i18next.t("code:Please input your phone verification code!"),
|
||||
message: i18next.t("signup:Please input your Email!"),
|
||||
},
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (this.state.email !== "" && !Setting.isValidEmail(this.state.email)) {
|
||||
this.setState({validEmail: false});
|
||||
return Promise.reject(i18next.t("signup:The input is not valid Email!"));
|
||||
}
|
||||
|
||||
this.setState({validEmail: true});
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<SendCodeInput
|
||||
disabled={!this.state.validPhone}
|
||||
method={"signup"}
|
||||
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(application)]}
|
||||
application={application}
|
||||
countryCode={this.form.current?.getFieldValue("countryCode")}
|
||||
/>
|
||||
<Input placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation.email !== ""} onChange={e => this.setState({email: e.target.value})} />
|
||||
</Form.Item>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
{
|
||||
signupItem.rule !== "No verification" &&
|
||||
<Form.Item
|
||||
name="emailCode"
|
||||
label={signupItem.label ? signupItem.label : i18next.t("code:Email code")}
|
||||
rules={[{
|
||||
required: required,
|
||||
message: i18next.t("code:Please input your verification code!"),
|
||||
}]}
|
||||
>
|
||||
<SendCodeInput
|
||||
disabled={!this.state.validEmail}
|
||||
method={"signup"}
|
||||
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(application)]}
|
||||
application={application}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPhoneItem = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Form.Item label={signupItem.label ? signupItem.label : i18next.t("general:Phone")} required={required}>
|
||||
<Input.Group compact>
|
||||
<Form.Item
|
||||
name="countryCode"
|
||||
noStyle
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
message: i18next.t("signup:Please select your country code!"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CountryCodeSelect
|
||||
style={{width: "35%"}}
|
||||
countryCodes={this.getApplicationObj().organizationObj.countryCodes}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="phone"
|
||||
dependencies={["countryCode"]}
|
||||
noStyle
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
message: i18next.t("signup:Please input your phone number!"),
|
||||
},
|
||||
({getFieldValue}) => ({
|
||||
validator: (_, value) => {
|
||||
if (!required && !value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (value && !Setting.isValidPhone(value, getFieldValue("countryCode"))) {
|
||||
this.setState({validPhone: false});
|
||||
return Promise.reject(i18next.t("signup:The input is not valid Phone!"));
|
||||
}
|
||||
|
||||
this.setState({validPhone: true});
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder={signupItem.placeholder}
|
||||
style={{width: "65%"}}
|
||||
disabled={this.state.invitation !== undefined && this.state.invitation.phone !== ""}
|
||||
onChange={e => this.setState({phone: e.target.value})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
{
|
||||
signupItem.rule !== "No verification" &&
|
||||
<Form.Item
|
||||
name="phoneCode"
|
||||
label={signupItem.label ? signupItem.label : i18next.t("code:Phone code")}
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
message: i18next.t("code:Please input your phone verification code!"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<SendCodeInput
|
||||
disabled={!this.state.validPhone}
|
||||
method={"signup"}
|
||||
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(application)]}
|
||||
application={application}
|
||||
countryCode={this.form.current?.getFieldValue("countryCode")}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
if (signupItem.name === "Email") {
|
||||
return renderEmailItem();
|
||||
} else if (signupItem.name === "Phone") {
|
||||
return renderPhoneItem();
|
||||
} else if (signupItem.name === "Email or Phone" || signupItem.name === "Phone or Email") {
|
||||
let emailOrPhoneMode = this.state.emailOrPhoneMode;
|
||||
if (emailOrPhoneMode === "") {
|
||||
emailOrPhoneMode = signupItem.name === "Email or Phone" ? "Email" : "Phone";
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "30px", marginBottom: "20px"}} >
|
||||
<Radio.Group style={{width: "400px"}} buttonStyle="solid" onChange={e => {
|
||||
this.setState({
|
||||
emailOrPhoneMode: e.target.value,
|
||||
});
|
||||
}} value={emailOrPhoneMode}>
|
||||
{
|
||||
signupItem.name === "Email or Phone" ? (
|
||||
<React.Fragment>
|
||||
<Radio.Button value={"Email"}>{i18next.t("general:Email")}</Radio.Button>
|
||||
<Radio.Button value={"Phone"}>{i18next.t("general:Phone")}</Radio.Button>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<Radio.Button value={"Phone"}>{i18next.t("general:Phone")}</Radio.Button>
|
||||
<Radio.Button value={"Email"}>{i18next.t("general:Email")}</Radio.Button>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
</Radio.Group>
|
||||
</Row>
|
||||
{
|
||||
emailOrPhoneMode === "Email" ? renderEmailItem() : renderPhoneItem()
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (signupItem.name === "Password") {
|
||||
return (
|
||||
<Form.Item
|
||||
|
@ -81,10 +81,12 @@ class SignupTable extends React.Component {
|
||||
{name: "Affiliation", displayName: i18next.t("user:Affiliation")},
|
||||
{name: "Country/Region", displayName: i18next.t("user:Country/Region")},
|
||||
{name: "ID card", displayName: i18next.t("user:ID card")},
|
||||
{name: "Email", displayName: i18next.t("general:Email")},
|
||||
{name: "Password", displayName: i18next.t("general:Password")},
|
||||
{name: "Confirm password", displayName: i18next.t("signup:Confirm")},
|
||||
{name: "Email", displayName: i18next.t("general:Email")},
|
||||
{name: "Phone", displayName: i18next.t("general:Phone")},
|
||||
{name: "Email or Phone", displayName: i18next.t("general:Email or Phone")},
|
||||
{name: "Phone or Email", displayName: i18next.t("general:Phone or Email")},
|
||||
{name: "Invitation code", displayName: i18next.t("application:Invitation code")},
|
||||
{name: "Agreement", displayName: i18next.t("signup:Agreement")},
|
||||
{name: "Text 1", displayName: i18next.t("signup:Text 1")},
|
||||
|
1077
web/yarn.lock
1077
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user