mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-04 13:20:19 +08:00
feat: support MetaMask provider (#2084)
* feat: add metamask provider * feat: add eth login * feat: check eth sign * feat: finish metamask signin/signup * feat: support MetaMask provider link/unlink * feat: update web/craco.config.js to handle polyfill * feat: gofumpt idp/metamask.go * feat: update MetaMask logo path * feat: support MetaMask avatar
This commit is contained in:
@ -89,6 +89,8 @@ import {withTranslation} from "react-i18next";
|
||||
import LanguageSelect from "./common/select/LanguageSelect";
|
||||
import ThemeSelect from "./common/select/ThemeSelect";
|
||||
import OrganizationSelect from "./common/select/OrganizationSelect";
|
||||
import {clearWeb3AuthToken} from "./auth/Web3Auth";
|
||||
import AccountAvatar from "./account/AccountAvatar";
|
||||
|
||||
const {Header, Footer, Content} = Layout;
|
||||
|
||||
@ -312,12 +314,11 @@ class App extends Component {
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const owner = this.state.account.owner;
|
||||
|
||||
this.setState({
|
||||
account: null,
|
||||
themeAlgorithm: ["default"],
|
||||
});
|
||||
|
||||
clearWeb3AuthToken();
|
||||
Setting.showMessage("success", i18next.t("application:Logged out successfully"));
|
||||
const redirectUri = res.data2;
|
||||
if (redirectUri !== null && redirectUri !== undefined && redirectUri !== "") {
|
||||
@ -348,7 +349,9 @@ class App extends Component {
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Avatar src={this.state.account.avatar} style={{verticalAlign: "middle"}} size="large">
|
||||
<Avatar src={this.state.account.avatar} style={{verticalAlign: "middle"}} size="large"
|
||||
icon={<AccountAvatar src={this.state.account.avatar} style={{verticalAlign: "middle"}} size={40} />}
|
||||
>
|
||||
{Setting.getShortName(this.state.account.name)}
|
||||
</Avatar>
|
||||
);
|
||||
|
@ -358,6 +358,8 @@ class ProviderEditPage extends React.Component {
|
||||
this.updateProviderField("type", "Default");
|
||||
} else if (value === "AI") {
|
||||
this.updateProviderField("type", "OpenAI API - GPT");
|
||||
} else if (value === "Web3") {
|
||||
this.updateProviderField("type", "MetaMask");
|
||||
}
|
||||
})}>
|
||||
{
|
||||
@ -370,6 +372,7 @@ class ProviderEditPage extends React.Component {
|
||||
{id: "SAML", name: "SAML"},
|
||||
{id: "SMS", name: "SMS"},
|
||||
{id: "Storage", name: "Storage"},
|
||||
{id: "Web3", name: "Web3"},
|
||||
]
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
|
||||
@ -524,7 +527,7 @@ class ProviderEditPage extends React.Component {
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.provider.category === "Captcha" && this.state.provider.type === "Default" ? null : (
|
||||
(this.state.provider.category === "Captcha" && this.state.provider.type === "Default") || this.state.provider.category === "Web3" ? null : (
|
||||
<React.Fragment>
|
||||
{
|
||||
this.state.provider.category === "AI" ? null : (
|
||||
|
@ -216,6 +216,12 @@ export const OtherProviderInfo = {
|
||||
url: "https://platform.openai.com",
|
||||
},
|
||||
},
|
||||
Web3: {
|
||||
"MetaMask": {
|
||||
logo: `${StaticBaseUrl}/img/social_metamask.svg`,
|
||||
url: "https://metamask.io/",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function initCountries() {
|
||||
@ -288,7 +294,7 @@ export function isProviderVisible(providerItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (providerItem.provider.category !== "OAuth" && providerItem.provider.category !== "SAML") {
|
||||
if (!["OAuth", "SAML", "Web3"].includes(providerItem.provider.category)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -891,6 +897,10 @@ export function getProviderTypeOptions(category) {
|
||||
return ([
|
||||
{id: "OpenAI API - GPT", name: "OpenAI API - GPT"},
|
||||
]);
|
||||
} else if (category === "Web3") {
|
||||
return ([
|
||||
{id: "MetaMask", name: "MetaMask"},
|
||||
]);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
import {DeleteMfa} from "./backend/MfaBackend";
|
||||
import {CheckCircleOutlined, HolderOutlined, UsergroupAddOutlined} from "@ant-design/icons";
|
||||
import * as MfaBackend from "./backend/MfaBackend";
|
||||
import AccountAvatar from "./account/AccountAvatar";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
@ -791,10 +792,23 @@ class UserEditPage extends React.Component {
|
||||
{
|
||||
(this.state.application === null || this.state.user === null) ? null : (
|
||||
this.state.application?.providers.filter(providerItem => Setting.isProviderVisible(providerItem)).map((providerItem) =>
|
||||
(providerItem.provider.category === "OAuth") ? (
|
||||
<OAuthWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} account={this.props.account} onUnlinked={() => {return this.unlinked();}} />
|
||||
(providerItem.provider.category === "OAuth" || providerItem.provider.category === "Web3") ? (
|
||||
<OAuthWidget
|
||||
key={providerItem.name}
|
||||
labelSpan={(Setting.isMobile()) ? 10 : 3}
|
||||
user={this.state.user}
|
||||
application={this.state.application}
|
||||
providerItem={providerItem}
|
||||
account={this.props.account}
|
||||
onUnlinked={() => {return this.unlinked();}} />
|
||||
) : (
|
||||
<SamlWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => {return this.unlinked();}} />
|
||||
<SamlWidget
|
||||
key={providerItem.name}
|
||||
labelSpan={(Setting.isMobile()) ? 10 : 3}
|
||||
user={this.state.user}
|
||||
application={this.state.application}
|
||||
providerItem={providerItem}
|
||||
onUnlinked={() => {return this.unlinked();}} />
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -971,7 +985,7 @@ class UserEditPage extends React.Component {
|
||||
{
|
||||
imgUrl ?
|
||||
<a target="_blank" rel="noreferrer" href={imgUrl} style={{marginBottom: "10px"}}>
|
||||
<img src={imgUrl} alt={imgUrl} height={90} style={{marginBottom: "20px"}} />
|
||||
<AccountAvatar src={imgUrl} alt={imgUrl} size={90} style={{marginBottom: "20px"}} />
|
||||
</a>
|
||||
:
|
||||
<Col style={{height: "78%", border: "1px dotted grey", borderRadius: 3, marginBottom: 5}}>
|
||||
|
@ -23,6 +23,7 @@ import * as UserBackend from "./backend/UserBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
import AccountAvatar from "./account/AccountAvatar";
|
||||
|
||||
class UserListPage extends BaseListPage {
|
||||
constructor(props) {
|
||||
@ -270,7 +271,7 @@ class UserListPage extends BaseListPage {
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<a target="_blank" rel="noreferrer" href={text}>
|
||||
<img referrerPolicy="no-referrer" src={text} alt={text} width={50} />
|
||||
<AccountAvatar referrerPolicy="no-referrer" src={text} alt={text} size={50} />
|
||||
</a>
|
||||
);
|
||||
},
|
||||
|
36
web/src/account/AccountAvatar.js
Normal file
36
web/src/account/AccountAvatar.js
Normal file
@ -0,0 +1,36 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {MetaMaskAvatar} from "react-metamask-avatar";
|
||||
|
||||
class AccountAvatar extends React.Component {
|
||||
render() {
|
||||
const {src, size} = this.props;
|
||||
// The avatar for Metamask account is directly generated by an algorithm based on the address
|
||||
// src = "metamask:0xC304b2cC0Be8E9ce10fF3Afd34820Ed306A23600";
|
||||
const matchMetaMask = src.match(/^metamask:(\w+)$/);
|
||||
if (matchMetaMask) {
|
||||
const address = matchMetaMask[1];
|
||||
return (
|
||||
<MetaMaskAvatar address={address} size={size} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<img width={size} height={size} src={src} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AccountAvatar;
|
@ -95,6 +95,12 @@ class AuthCallback extends React.Component {
|
||||
if (code === null) {
|
||||
code = params.get("authCode");
|
||||
}
|
||||
// The code for Metamask is the JSON-serialized string of Web3AuthToken
|
||||
// Due to the limited length of URLs, we only pass the web3AuthTokenKey
|
||||
if (code === null) {
|
||||
code = params.get("web3AuthTokenKey");
|
||||
code = localStorage.getItem(code);
|
||||
}
|
||||
// Steam don't use code, so we should use all params as code.
|
||||
if (isSteam !== null && code === null) {
|
||||
code = this.props.location.search;
|
||||
|
@ -317,6 +317,10 @@ const authInfo = {
|
||||
scope: "user:read",
|
||||
endpoint: "https://zoom.us/oauth/authorize",
|
||||
},
|
||||
MetaMask: {
|
||||
scope: "",
|
||||
endpoint: "",
|
||||
},
|
||||
};
|
||||
|
||||
export function getProviderUrl(provider) {
|
||||
@ -459,5 +463,7 @@ export function getAuthUrl(application, provider, method) {
|
||||
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&state=${state}&grant_options[]=per-user`;
|
||||
} else if (provider.type === "Twitter" || provider.type === "Fitbit") {
|
||||
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}&code_challenge=${codeChallenge}&code_challenge_method=S256`;
|
||||
} else if (provider.type === "MetaMask") {
|
||||
return `${redirectUri}?state=${state}`;
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import i18next from "i18next";
|
||||
import * as Provider from "./Provider";
|
||||
import {getProviderLogoURL} from "../Setting";
|
||||
import {GithubLoginButton, GoogleLoginButton} from "react-social-login-buttons";
|
||||
import {authViaMetaMask} from "./Web3Auth";
|
||||
import QqLoginButton from "./QqLoginButton";
|
||||
import FacebookLoginButton from "./FacebookLoginButton";
|
||||
import WeiboLoginButton from "./WeiboLoginButton";
|
||||
@ -117,6 +118,12 @@ function goToSamlUrl(provider, location) {
|
||||
});
|
||||
}
|
||||
|
||||
export function goToWeb3Url(application, provider, method) {
|
||||
if (provider.type === "MetaMask") {
|
||||
authViaMetaMask(application, provider, method);
|
||||
}
|
||||
}
|
||||
|
||||
export function renderProviderLogo(provider, application, width, margin, size, location) {
|
||||
if (size === "small") {
|
||||
if (provider.category === "OAuth") {
|
||||
@ -153,6 +160,12 @@ export function renderProviderLogo(provider, application, width, margin, size, l
|
||||
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} style={{margin: margin}} />
|
||||
</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} style={{margin: margin}} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
} else if (provider.type === "Custom") {
|
||||
// style definition
|
||||
@ -192,6 +205,16 @@ export function renderProviderLogo(provider, application, width, margin, size, l
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
} else if (provider.category === "Web3") {
|
||||
return (
|
||||
<div key={provider.displayName} style={{marginBottom: "10px"}}>
|
||||
<a onClick={() => goToWeb3Url(application, provider, "signup")}>
|
||||
{
|
||||
getSigninButton(provider)
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={provider.displayName} style={{marginBottom: "10px"}}>
|
||||
|
149
web/src/auth/Web3Auth.js
Normal file
149
web/src/auth/Web3Auth.js
Normal file
@ -0,0 +1,149 @@
|
||||
// // Copyright 2023 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 {goToLink, showMessage} from "../Setting";
|
||||
import i18next from "i18next";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import {SignTypedDataVersion, recoverTypedSignature} from "@metamask/eth-sig-util";
|
||||
import {getAuthUrl} from "./Provider";
|
||||
import {Buffer} from "buffer";
|
||||
// import {toChecksumAddress} from "ethereumjs-util";
|
||||
global.Buffer = Buffer;
|
||||
|
||||
export function generateNonce() {
|
||||
const nonce = uuidv4();
|
||||
return nonce;
|
||||
}
|
||||
|
||||
export function getWeb3AuthTokenKey(address) {
|
||||
return `Web3AuthToken_${address}`;
|
||||
}
|
||||
|
||||
export function setWeb3AuthToken(token) {
|
||||
const key = getWeb3AuthTokenKey(token.address);
|
||||
localStorage.setItem(key, JSON.stringify(token));
|
||||
}
|
||||
|
||||
export function getWeb3AuthToken(address) {
|
||||
const key = getWeb3AuthTokenKey(address);
|
||||
return JSON.parse(localStorage.getItem(key));
|
||||
}
|
||||
|
||||
export function delWeb3AuthToken(address) {
|
||||
const key = getWeb3AuthTokenKey(address);
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
export function clearWeb3AuthToken() {
|
||||
const keys = Object.keys(localStorage);
|
||||
keys.forEach(key => {
|
||||
if (key.startsWith("Web3AuthToken_")) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function detectMetaMaskPlugin() {
|
||||
// check if ethereum extension MetaMask is installed
|
||||
return window.ethereum && window.ethereum.isMetaMask;
|
||||
}
|
||||
|
||||
export function requestEthereumAccount() {
|
||||
const method = "eth_requestAccounts";
|
||||
const selectedAccount = window.ethereum.request({method})
|
||||
.then((accounts) => {
|
||||
return accounts[0];
|
||||
});
|
||||
return selectedAccount;
|
||||
}
|
||||
|
||||
export function signEthereumTypedData(from, nonce) {
|
||||
// https://docs.metamask.io/wallet/how-to/sign-data/
|
||||
const date = new Date();
|
||||
const typedData = JSON.stringify({
|
||||
domain: {
|
||||
chainId: window.ethereum.chainId,
|
||||
name: "Casdoor",
|
||||
version: "1",
|
||||
},
|
||||
message: {
|
||||
prompt: "In order to authenticate to this website, sign this request and your public address will be sent to the server in a verifiable way.",
|
||||
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"},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const method = "eth_signTypedData_v4";
|
||||
const params = [from, typedData];
|
||||
|
||||
return window.ethereum.request({method, params})
|
||||
.then((sign) => {
|
||||
return {
|
||||
address: from,
|
||||
createAt: Math.floor(date.getTime() / 1000),
|
||||
typedData: typedData,
|
||||
signature: sign,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function checkEthereumSignedTypedData(token) {
|
||||
if (token === undefined || token === null) {
|
||||
return false;
|
||||
}
|
||||
if (token.address && token.typedData && token.signature) {
|
||||
const recoveredAddr = recoverTypedSignature({
|
||||
data: JSON.parse(token.typedData),
|
||||
signature: token.signature,
|
||||
version: SignTypedDataVersion.V4,
|
||||
});
|
||||
// const recoveredAddr = token.address;
|
||||
return recoveredAddr === token.address;
|
||||
// return toChecksumAddress(recoveredAddr) === toChecksumAddress(token.address);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function authViaMetaMask(application, provider, method) {
|
||||
if (!detectMetaMaskPlugin()) {
|
||||
showMessage("error", `${i18next.t("login:MetaMask plugin not detected")}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const account = await requestEthereumAccount();
|
||||
let token = getWeb3AuthToken(account);
|
||||
if (!checkEthereumSignedTypedData(token)) {
|
||||
const nonce = generateNonce();
|
||||
token = await signEthereumTypedData(account, nonce);
|
||||
setWeb3AuthToken(token);
|
||||
}
|
||||
const redirectUri = `${getAuthUrl(application, provider, method)}&web3AuthTokenKey=${getWeb3AuthTokenKey(account)}`;
|
||||
goToLink(redirectUri);
|
||||
} catch (err) {
|
||||
showMessage("error", `${i18next.t("login:Failed to obtain MetaMask authorization")}: ${err.message}`);
|
||||
}
|
||||
}
|
@ -19,6 +19,9 @@ import * as UserBackend from "../backend/UserBackend";
|
||||
import * as Setting from "../Setting";
|
||||
import * as Provider from "../auth/Provider";
|
||||
import * as AuthBackend from "../auth/AuthBackend";
|
||||
import {goToWeb3Url} from "../auth/ProviderButton";
|
||||
import {delWeb3AuthToken} from "../auth/Web3Auth";
|
||||
import AccountAvatar from "../account/AccountAvatar";
|
||||
|
||||
class OAuthWidget extends React.Component {
|
||||
constructor(props) {
|
||||
@ -88,12 +91,15 @@ class OAuthWidget extends React.Component {
|
||||
return user.properties[key];
|
||||
}
|
||||
|
||||
unlinkUser(providerType) {
|
||||
unlinkUser(providerType, linkedValue) {
|
||||
const body = {
|
||||
providerType: providerType,
|
||||
// should add the unlink user's info, cause the user may not be logged in, but a admin want to unlink the user.
|
||||
user: this.props.user,
|
||||
};
|
||||
if (providerType === "MetaMask") {
|
||||
delWeb3AuthToken(linkedValue);
|
||||
}
|
||||
AuthBackend.unlink(body)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
@ -151,7 +157,7 @@ class OAuthWidget extends React.Component {
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={24 - this.props.labelSpan} >
|
||||
<img style={{marginRight: "10px"}} width={30} height={30} src={avatarUrl} alt={name} referrerPolicy="no-referrer" />
|
||||
<AccountAvatar style={{marginRight: "10px"}} size={30} src={avatarUrl} alt={name} referrerPolicy="no-referrer" />
|
||||
<span style={{width: this.props.labelSpan === 3 ? "300px" : "200px", display: (Setting.isMobile()) ? "inline" : "inline-block"}}>
|
||||
{
|
||||
linkedValue === "" ? (
|
||||
@ -169,11 +175,15 @@ class OAuthWidget extends React.Component {
|
||||
</span>
|
||||
{
|
||||
linkedValue === "" ? (
|
||||
<a key={provider.displayName} href={user.id !== account.id ? null : Provider.getAuthUrl(application, provider, "link")}>
|
||||
<Button style={{marginLeft: "20px", width: linkButtonWidth}} type="primary" disabled={user.id !== account.id}>{i18next.t("user:Link")}</Button>
|
||||
</a>
|
||||
provider.category === "Web3" ? (
|
||||
<Button style={{marginLeft: "20px", width: linkButtonWidth}} type="primary" disabled={user.id !== account.id} onClick={() => goToWeb3Url(application, provider, "link")}>{i18next.t("user:Link")}</Button>
|
||||
) : (
|
||||
<a key={provider.displayName} href={user.id !== account.id ? null : Provider.getAuthUrl(application, provider, "link")}>
|
||||
<Button style={{marginLeft: "20px", width: linkButtonWidth}} type="primary" disabled={user.id !== account.id}>{i18next.t("user:Link")}</Button>
|
||||
</a>
|
||||
)
|
||||
) : (
|
||||
<Button disabled={!providerItem.canUnlink && !account.isGlobalAdmin} style={{marginLeft: "20px", width: linkButtonWidth}} onClick={() => this.unlinkUser(provider.type)}>{i18next.t("user:Unlink")}</Button>
|
||||
<Button disabled={!providerItem.canUnlink && !account.isGlobalAdmin} style={{marginLeft: "20px", width: linkButtonWidth}} onClick={() => this.unlinkUser(provider.type, linkedValue)}>{i18next.t("user:Unlink")}</Button>
|
||||
)
|
||||
}
|
||||
</Col>
|
||||
|
@ -392,9 +392,11 @@
|
||||
"Auto sign in": "Automatische Anmeldung",
|
||||
"Continue with": "Weitermachen mit",
|
||||
"Email or phone": "E-Mail oder Telefon",
|
||||
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
|
||||
"Forgot password?": "Passwort vergessen?",
|
||||
"Loading": "Laden",
|
||||
"Logging out...": "Ausloggen...",
|
||||
"MetaMask plugin not detected": "MetaMask plugin not detected",
|
||||
"No account?": "Kein Konto?",
|
||||
"Or sign in with another account": "Oder mit einem anderen Konto anmelden",
|
||||
"Please input your Email or Phone!": "Bitte geben Sie Ihre E-Mail oder Telefonnummer ein!",
|
||||
|
@ -392,9 +392,11 @@
|
||||
"Auto sign in": "Auto sign in",
|
||||
"Continue with": "Continue with",
|
||||
"Email or phone": "Email or phone",
|
||||
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
|
||||
"Forgot password?": "Forgot password?",
|
||||
"Loading": "Loading",
|
||||
"Logging out...": "Logging out...",
|
||||
"MetaMask plugin not detected": "MetaMask plugin not detected",
|
||||
"No account?": "No account?",
|
||||
"Or sign in with another account": "Or sign in with another account",
|
||||
"Please input your Email or Phone!": "Please input your Email or Phone!",
|
||||
|
@ -392,9 +392,11 @@
|
||||
"Auto sign in": "Inicio de sesión automático",
|
||||
"Continue with": "Continúe con",
|
||||
"Email or phone": "Correo electrónico o teléfono",
|
||||
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
|
||||
"Forgot password?": "¿Olvidaste tu contraseña?",
|
||||
"Loading": "Cargando",
|
||||
"Logging out...": "Cerrando sesión...",
|
||||
"MetaMask plugin not detected": "MetaMask plugin not detected",
|
||||
"No account?": "¿No tienes cuenta?",
|
||||
"Or sign in with another account": "O inicia sesión con otra cuenta",
|
||||
"Please input your Email or Phone!": "¡Por favor introduzca su correo electrónico o teléfono!",
|
||||
|
@ -392,9 +392,11 @@
|
||||
"Auto sign in": "Connexion automatique",
|
||||
"Continue with": "Continuer avec",
|
||||
"Email or phone": "Email ou téléphone",
|
||||
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
|
||||
"Forgot password?": "Mot de passe oublié ?",
|
||||
"Loading": "Chargement",
|
||||
"Logging out...": "Déconnexion...",
|
||||
"MetaMask plugin not detected": "MetaMask plugin not detected",
|
||||
"No account?": "Aucun compte ?",
|
||||
"Or sign in with another account": "Ou connectez-vous avec un autre compte",
|
||||
"Please input your Email or Phone!": "S'il vous plaît, entrez votre adresse e-mail ou votre numéro de téléphone !",
|
||||
|
@ -392,9 +392,11 @@
|
||||
"Auto sign in": "Masuk otomatis",
|
||||
"Continue with": "Lanjutkan dengan",
|
||||
"Email or phone": "Email atau telepon",
|
||||
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
|
||||
"Forgot password?": "Lupa kata sandi?",
|
||||
"Loading": "Memuat",
|
||||
"Logging out...": "Keluar...",
|
||||
"MetaMask plugin not detected": "MetaMask plugin not detected",
|
||||
"No account?": "Tidak memiliki akun?",
|
||||
"Or sign in with another account": "Atau masuk dengan akun lain",
|
||||
"Please input your Email or Phone!": "Silahkan masukkan email atau nomor telepon Anda!",
|
||||
|
@ -392,9 +392,11 @@
|
||||
"Auto sign in": "自動サインイン",
|
||||
"Continue with": "続ける",
|
||||
"Email or phone": "メールまたは電話",
|
||||
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
|
||||
"Forgot password?": "パスワードを忘れましたか?",
|
||||
"Loading": "ローディング",
|
||||
"Logging out...": "ログアウト中...",
|
||||
"MetaMask plugin not detected": "MetaMask plugin not detected",
|
||||
"No account?": "アカウントがありませんか?",
|
||||
"Or sign in with another account": "別のアカウントでサインインする",
|
||||
"Please input your Email or Phone!": "あなたのメールアドレスまたは電話番号を入力してください!",
|
||||
|
@ -392,9 +392,11 @@
|
||||
"Auto sign in": "자동 로그인",
|
||||
"Continue with": "계속하다",
|
||||
"Email or phone": "이메일 또는 전화",
|
||||
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
|
||||
"Forgot password?": "비밀번호를 잊으셨나요?",
|
||||
"Loading": "로딩 중입니다",
|
||||
"Logging out...": "로그아웃 중...",
|
||||
"MetaMask plugin not detected": "MetaMask plugin not detected",
|
||||
"No account?": "계정이 없나요?",
|
||||
"Or sign in with another account": "다른 계정으로 로그인하세요",
|
||||
"Please input your Email or Phone!": "이메일 또는 전화번호를 입력해주세요!",
|
||||
|
@ -392,9 +392,11 @@
|
||||
"Auto sign in": "Entrar automaticamente",
|
||||
"Continue with": "Continuar com",
|
||||
"Email or phone": "Email ou telefone",
|
||||
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
|
||||
"Forgot password?": "Esqueceu a senha?",
|
||||
"Loading": "Carregando",
|
||||
"Logging out...": "Saindo...",
|
||||
"MetaMask plugin not detected": "MetaMask plugin not detected",
|
||||
"No account?": "Não possui uma conta?",
|
||||
"Or sign in with another account": "Ou entre com outra conta",
|
||||
"Please input your Email or Phone!": "Por favor, informe seu email ou telefone!",
|
||||
|
@ -392,9 +392,11 @@
|
||||
"Auto sign in": "Автоматическая авторизация",
|
||||
"Continue with": "Продолжайте с",
|
||||
"Email or phone": "Электронная почта или телефон",
|
||||
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
|
||||
"Forgot password?": "Забыли пароль?",
|
||||
"Loading": "Загрузка",
|
||||
"Logging out...": "Выход...",
|
||||
"MetaMask plugin not detected": "MetaMask plugin not detected",
|
||||
"No account?": "Нет аккаунта?",
|
||||
"Or sign in with another account": "Или войти с другой учетной записью",
|
||||
"Please input your Email or Phone!": "Пожалуйста, введите свой адрес электронной почты или номер телефона!",
|
||||
|
@ -392,9 +392,11 @@
|
||||
"Auto sign in": "Tự động đăng nhập",
|
||||
"Continue with": "Tiếp tục với",
|
||||
"Email or phone": "Email hoặc điện thoại",
|
||||
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
|
||||
"Forgot password?": "Quên mật khẩu?",
|
||||
"Loading": "Đang tải",
|
||||
"Logging out...": "Đăng xuất ...",
|
||||
"MetaMask plugin not detected": "MetaMask plugin not detected",
|
||||
"No account?": "Không có tài khoản?",
|
||||
"Or sign in with another account": "Hoặc đăng nhập bằng tài khoản khác",
|
||||
"Please input your Email or Phone!": "Vui lòng nhập địa chỉ Email hoặc số điện thoại của bạn!",
|
||||
|
@ -392,9 +392,11 @@
|
||||
"Auto sign in": "下次自动登录",
|
||||
"Continue with": "使用以下账号继续",
|
||||
"Email or phone": "Email或手机号",
|
||||
"Failed to obtain MetaMask authorization": "获取MetaMask授权失败",
|
||||
"Forgot password?": "忘记密码?",
|
||||
"Loading": "加载中",
|
||||
"Logging out...": "正在退出登录...",
|
||||
"MetaMask plugin not detected": "未检测到MetaMask插件",
|
||||
"No account?": "没有账号?",
|
||||
"Or sign in with another account": "或者,登录其他账号",
|
||||
"Please input your Email or Phone!": "请输入您的Email或手机号!",
|
||||
@ -746,6 +748,8 @@
|
||||
"Token URL - Tooltip": "自定义OAuth的Token URL",
|
||||
"Type": "类型",
|
||||
"Type - Tooltip": "类型",
|
||||
"User mapping": "User mapping",
|
||||
"User mapping - Tooltip": "User mapping - Tooltip",
|
||||
"UserInfo URL": "UserInfo URL",
|
||||
"UserInfo URL - Tooltip": "自定义OAuth的UserInfo URL",
|
||||
"admin (Shared)": "admin(共享)"
|
||||
|
@ -110,7 +110,7 @@ class ProviderTable extends React.Component {
|
||||
key: "canSignUp",
|
||||
width: "120px",
|
||||
render: (text, record, index) => {
|
||||
if (record.provider?.category !== "OAuth") {
|
||||
if (!["OAuth", "Web3"].includes(record.provider?.category)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -127,7 +127,7 @@ class ProviderTable extends React.Component {
|
||||
key: "canSignIn",
|
||||
width: "120px",
|
||||
render: (text, record, index) => {
|
||||
if (record.provider?.category !== "OAuth") {
|
||||
if (!["OAuth", "Web3"].includes(record.provider?.category)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -144,7 +144,7 @@ class ProviderTable extends React.Component {
|
||||
key: "canUnlink",
|
||||
width: "120px",
|
||||
render: (text, record, index) => {
|
||||
if (record.provider?.category !== "OAuth") {
|
||||
if (!["OAuth", "Web3"].includes(record.provider?.category)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -161,7 +161,7 @@ class ProviderTable extends React.Component {
|
||||
key: "prompted",
|
||||
width: "120px",
|
||||
render: (text, record, index) => {
|
||||
if (record.provider?.category !== "OAuth") {
|
||||
if (!["OAuth", "Web3"].includes(record.provider?.category)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user