Compare commits

...

12 Commits

Author SHA1 Message Date
HGZ-20
bfa2ab63ad feat: Support metamask mobile login (#2844) 2024-03-30 00:08:52 +08:00
DacongDA
505054b0eb feat: use minWidth for a better display effect in org select (#2843) 2024-03-29 15:47:27 +08:00
Yang Luo
f95ce13b82 fix: support "Email or Phone" in signup table 2024-03-29 09:07:37 +08:00
xyt
5315f16a48 feat: can specify UI theme via /?theme=default and /?theme=dark (#2842)
* feat: set themeType through URL parameter

* Update App.js

---------

Co-authored-by: Eric Luo <hsluoyz@qq.com>
2024-03-29 00:52:18 +08:00
Will.Feng
d054f3e001 feat: The /login/oauth/access_token api supports the token and id_token grant types. (#2836)
* In the response of the /api/get-captcha endpoint, add the parameters "owner" and "name" because these two parameters will be used when calling the /api/verify-captcha endpoint.

* The /login/oauth/access_token api supports the token and id_token grant types.
2024-03-28 00:41:54 +08:00
Yang Luo
b158b840bd Add "new-user" to webhook event list 2024-03-27 15:23:06 +08:00
Yang Luo
b16f1807b3 fix: fix bug in "new-user" record 2024-03-27 15:15:40 +08:00
Yang Luo
d0cce1bf7a Order by "id" in GetPaginationRecords() 2024-03-27 15:14:41 +08:00
Yang Luo
9892cd20ab Improve erorr message in CheckVerificationCode() 2024-03-27 15:14:20 +08:00
Yang Luo
d1f31dd327 feat: fix linter 2024-03-26 23:24:53 +08:00
Yang Luo
94743246a1 Improve "%{user.friendlyName}" handling 2024-03-25 21:26:36 +08:00
Yang Luo
39ad1bc593 Add signup's object in AfterRecordMessage() 2024-03-25 21:20:33 +08:00
23 changed files with 2003 additions and 182 deletions

View File

@@ -273,7 +273,8 @@ func (c *ApiController) Signup() {
return
}
c.Ctx.Input.SetParam("recordUserId", fmt.Sprintf("%s/%s", application.Organization, user.Name))
c.Ctx.Input.SetParam("recordUserId", user.GetId())
c.Ctx.Input.SetParam("recordSignup", "true")
userId := user.GetId()
util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId)

View File

@@ -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() {

View File

@@ -125,9 +125,11 @@ func (c *ApiController) SendEmail() {
return
}
userString := "Hi"
if user != nil {
content = strings.Replace(content, "%{user.friendlyName}", user.GetFriendlyName(), 1)
userString = user.GetFriendlyName()
}
content = strings.Replace(content, "%{user.friendlyName}", userString, 1)
}
}

View File

@@ -164,6 +164,7 @@ func (c *ApiController) GetOAuthToken() {
code := c.Input().Get("code")
verifier := c.Input().Get("code_verifier")
scope := c.Input().Get("scope")
nonce := c.Input().Get("nonce")
username := c.Input().Get("username")
password := c.Input().Get("password")
tag := c.Input().Get("tag")
@@ -197,6 +198,9 @@ func (c *ApiController) GetOAuthToken() {
if scope == "" {
scope = tokenRequest.Scope
}
if nonce == "" {
nonce = tokenRequest.Nonce
}
if username == "" {
username = tokenRequest.Username
}
@@ -216,7 +220,7 @@ func (c *ApiController) GetOAuthToken() {
}
host := c.Ctx.Request.Host
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage())
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return

View File

@@ -21,6 +21,7 @@ type TokenRequest struct {
Code string `json:"code"`
Verifier string `json:"code_verifier"`
Scope string `json:"scope"`
Nonce string `json:"nonce"`
Username string `json:"username"`
Password string `json:"password"`
Tag string `json:"tag"`

4
go.mod
View File

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

453
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -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
}

View File

@@ -140,6 +140,12 @@ func GetRecords() ([]*casvisorsdk.Record, error) {
func GetPaginationRecords(offset, limit int, field, value, sortField, sortOrder string, filterRecord *casvisorsdk.Record) ([]*casvisorsdk.Record, error) {
records := []*casvisorsdk.Record{}
if sortField == "" || sortOrder == "" {
sortField = "id"
sortOrder = "descend"
}
session := GetSession("", offset, limit, field, value, sortField, sortOrder)
err := session.Find(&records, filterRecord)
if err != nil {
@@ -159,6 +165,25 @@ func GetRecordsByField(record *casvisorsdk.Record) ([]*casvisorsdk.Record, error
return records, nil
}
func CopyRecord(record *casvisorsdk.Record) *casvisorsdk.Record {
res := &casvisorsdk.Record{
Owner: record.Owner,
Name: record.Name,
CreatedTime: record.CreatedTime,
Organization: record.Organization,
ClientIp: record.ClientIp,
User: record.User,
Method: record.Method,
RequestUri: record.RequestUri,
Action: record.Action,
Language: record.Language,
Object: record.Object,
Response: record.Response,
IsTriggered: record.IsTriggered,
}
return res
}
func getFilteredWebhooks(webhooks []*Webhook, organization string, action string) []*Webhook {
res := []*Webhook{}
for _, webhook := range webhooks {

View File

@@ -189,7 +189,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
}, nil
}
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, username string, password string, host string, refreshToken string, tag string, avatar string, lang string) (interface{}, error) {
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, nonce string, username string, password string, host string, refreshToken string, tag string, avatar string, lang string) (interface{}, error) {
application, err := GetApplicationByClientId(clientId)
if err != nil {
return nil, err
@@ -220,6 +220,8 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
token, tokenError, err = GetPasswordToken(application, username, password, scope, host)
case "client_credentials": // Client Credentials Grant
token, tokenError, err = GetClientCredentialsToken(application, clientSecret, scope, host)
case "token", "id_token": // Implicit Grant
token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host)
case "refresh_token":
refreshToken2, err := RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
if err != nil {
@@ -582,6 +584,33 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
return token, nil, nil
}
// GetImplicitToken
// Implicit flow
func GetImplicitToken(application *Application, username string, scope string, nonce string, host string) (*Token, *TokenError, error) {
user, err := GetUserByFields(application.Organization, username)
if err != nil {
return nil, nil, err
}
if user == nil {
return nil, &TokenError{
Error: InvalidGrant,
ErrorDescription: "the user does not exist",
}, nil
}
if user.IsForbidden {
return nil, &TokenError{
Error: InvalidGrant,
ErrorDescription: "the user is forbidden to sign in, please contact the administrator",
}, nil
}
token, err := GetTokenByUser(application, user, scope, nonce, host)
if err != nil {
return nil, nil, err
}
return token, nil, nil
}
// GetTokenByUser
// Implicit flow
func GetTokenByUser(application *Application, user *User, scope string, nonce string, host string) (*Token, error) {

View File

@@ -92,9 +92,12 @@ func SendVerificationCodeToEmail(organization *Organization, user *User, provide
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
content := strings.Replace(provider.Content, "%s", code, 1)
userString := "Hi"
if user != nil {
content = strings.Replace(content, "%{user.friendlyName}", user.GetFriendlyName(), 1)
userString = user.GetFriendlyName()
}
content = strings.Replace(content, "%{user.friendlyName}", userString, 1)
err := IsAllowSend(user, remoteAddr, provider.Category)
if err != nil {
@@ -187,14 +190,17 @@ func CheckVerificationCode(dest string, code string, lang string) (*VerifyResult
return &VerifyResult{noRecordError, i18n.Translate(lang, "verification:The verification code has not been sent yet, or has already been used!")}, nil
}
timeout, err := conf.GetConfigInt64("verificationCodeTimeout")
timeoutInMinutes, err := conf.GetConfigInt64("verificationCodeTimeout")
if err != nil {
return nil, err
}
now := time.Now().Unix()
if now-record.Time > timeout*60 {
return &VerifyResult{timeoutError, fmt.Sprintf(i18n.Translate(lang, "verification:You should verify your code in %d min!"), timeout)}, nil
if now-record.Time > timeoutInMinutes*60*10 {
return &VerifyResult{noRecordError, i18n.Translate(lang, "verification:The verification code has not been sent yet!")}, nil
}
if now-record.Time > timeoutInMinutes*60 {
return &VerifyResult{timeoutError, fmt.Sprintf(i18n.Translate(lang, "verification:You should verify your code in %d min!"), timeoutInMinutes)}, nil
}
if record.Code != code {

View File

@@ -15,9 +15,12 @@
package routers
import (
"fmt"
"github.com/beego/beego/context"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
"github.com/casvisor/casvisor-go-sdk/casvisorsdk"
)
func getUser(ctx *context.Context) (username string) {
@@ -68,6 +71,7 @@ func RecordMessage(ctx *context.Context) {
func AfterRecordMessage(ctx *context.Context) {
record, err := object.NewRecord(ctx)
if err != nil {
fmt.Printf("AfterRecordMessage() error: %s\n", err.Error())
return
}
@@ -76,14 +80,32 @@ func AfterRecordMessage(ctx *context.Context) {
record.Organization, record.User = util.GetOwnerAndNameFromId(userId)
}
var record2 *casvisorsdk.Record
recordSignup := ctx.Input.Params()["recordSignup"]
if recordSignup == "true" {
record2 := *record
record2.Action = "signup"
util.SafeGoroutine(func() { object.AddRecord(&record2) })
record2 = object.CopyRecord(record)
record2.Action = "new-user"
var user *object.User
user, err = object.GetUser(userId)
if err != nil {
fmt.Printf("AfterRecordMessage() error: %s\n", err.Error())
return
}
if user == nil {
err = fmt.Errorf("the user: %s is not found", userId)
fmt.Printf("AfterRecordMessage() error: %s\n", err.Error())
return
}
record2.Object = util.StructToJson(user)
}
util.SafeGoroutine(func() {
object.AddRecord(record)
if record2 != nil {
object.AddRecord(record2)
}
})
}

View File

@@ -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",

View File

@@ -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) {

View File

@@ -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"))} :

View File

@@ -187,7 +187,7 @@ class RecordListPage extends BaseListPage {
sorter: true,
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
if (!["signup", "login", "logout", "update-user"].includes(record.action)) {
if (!["signup", "login", "logout", "update-user", "new-user"].includes(record.action)) {
return null;
}

View File

@@ -275,7 +275,7 @@ class WebhookEditPage extends React.Component {
}} >
{
(
["signup", "login", "logout"].concat(this.getApiPaths()).map((option, index) => {
["signup", "login", "logout", "new-user"].concat(this.getApiPaths()).map((option, index) => {
return (
<Option key={option} value={option}>{option}</Option>
);

View File

@@ -1173,7 +1173,7 @@ class LoginPage extends React.Component {
};
return (
<div style={{height: 300}}>
<div style={{height: 300, minWidth: 320}}>
{renderChoiceBox()}
</div>
);

View 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;

View File

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

View File

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

View File

@@ -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")},

File diff suppressed because it is too large Load Diff