mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-25 19:52:48 +00:00
Feat: recaptcha
This commit is contained in:
parent
43c9ce1d26
commit
cfb1206e13
|
|
@ -56,6 +56,7 @@
|
|||
"react-addons-update": "^15.6.2",
|
||||
"react-app-polyfill": "^1.0.4",
|
||||
"react-color": "^2.18.0",
|
||||
"react-async-script": "^1.1.1",
|
||||
"react-content-loader": "^5.0.2",
|
||||
"react-dev-utils": "^9.1.0",
|
||||
"react-dnd": "^9.5.1",
|
||||
|
|
|
|||
|
|
@ -45,7 +45,10 @@ export default function Access() {
|
|||
login_captcha: "0",
|
||||
reg_captcha: "0",
|
||||
forget_captcha: "0",
|
||||
authn_enabled: "0"
|
||||
authn_enabled: "0",
|
||||
captcha_IsUseReCaptcha: "0",
|
||||
captcha_ReCaptchaKey: "defaultKey",
|
||||
captcha_ReCaptchaSecret: "defaultSecret",
|
||||
});
|
||||
const [siteURL, setSiteURL] = useState("");
|
||||
const [groups, setGroups] = useState([]);
|
||||
|
|
@ -245,6 +248,67 @@ export default function Access() {
|
|||
</FormControl>
|
||||
</div>
|
||||
|
||||
<div className={classes.form}>
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={
|
||||
options.captcha_IsUseReCaptcha === "1"
|
||||
}
|
||||
onChange={handleChange(
|
||||
"captcha_IsUseReCaptcha"
|
||||
)}
|
||||
/>
|
||||
}
|
||||
label="使用ReCaptcha验证码"
|
||||
/>
|
||||
<FormHelperText id="component-helper-text">
|
||||
是否使用ReCaptcha验证码
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
{options.captcha_IsUseReCaptcha === "1" && (
|
||||
<>
|
||||
<div className={classes.form}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel htmlFor="component-helper">
|
||||
Site KEY
|
||||
</InputLabel>
|
||||
<Input
|
||||
required
|
||||
value={options.captcha_ReCaptchaKey}
|
||||
onChange={handleInputChange(
|
||||
"captcha_ReCaptchaKey"
|
||||
)}
|
||||
/>
|
||||
<FormHelperText id="component-helper-text">
|
||||
应用管理页面获取到的的 网站密钥
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
<div className={classes.form}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel htmlFor="component-helper">
|
||||
Secret
|
||||
</InputLabel>
|
||||
<Input
|
||||
required
|
||||
value={options.captcha_ReCaptchaSecret}
|
||||
onChange={handleInputChange(
|
||||
"captcha_ReCaptchaSecret"
|
||||
)}
|
||||
/>
|
||||
<FormHelperText id="component-helper-text">
|
||||
应用管理页面获取到的的 秘钥
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={classes.form}>
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { enableUploaderLoad } from "../../middleware/Init";
|
|||
import { Fingerprint, VpnKey } from "@material-ui/icons";
|
||||
import VpnIcon from "@material-ui/icons/VpnKeyOutlined";
|
||||
import {useLocation} from "react-router";
|
||||
import ReCaptcha from "./ReCaptcha";
|
||||
const useStyles = makeStyles(theme => ({
|
||||
layout: {
|
||||
width: "auto",
|
||||
|
|
@ -102,6 +103,8 @@ function LoginForm() {
|
|||
const loginCaptcha = useSelector(state => state.siteConfig.loginCaptcha);
|
||||
const title = useSelector(state => state.siteConfig.title);
|
||||
const authn = useSelector(state => state.siteConfig.authn);
|
||||
const useReCaptcha = useSelector(state => state.siteConfig.captcha_IsUseReCaptcha);
|
||||
const reCaptchaKey = useSelector(state => state.siteConfig.captcha_ReCaptchaKey);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const ToggleSnackbar = useCallback(
|
||||
|
|
@ -140,7 +143,7 @@ function LoginForm() {
|
|||
|
||||
useEffect(() => {
|
||||
setEmail(query.get("username"));
|
||||
if (loginCaptcha) {
|
||||
if (loginCaptcha && !useReCaptcha) {
|
||||
refreshCaptcha();
|
||||
}
|
||||
}, [location,loginCaptcha]);
|
||||
|
|
@ -244,7 +247,9 @@ function LoginForm() {
|
|||
.catch(error => {
|
||||
setLoading(false);
|
||||
ToggleSnackbar("top", "right", error.message, "warning");
|
||||
refreshCaptcha();
|
||||
if (!useReCaptcha) {
|
||||
refreshCaptcha();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -300,7 +305,7 @@ function LoginForm() {
|
|||
autoComplete
|
||||
/>
|
||||
</FormControl>
|
||||
{loginCaptcha && (
|
||||
{loginCaptcha && !useReCaptcha && (
|
||||
<div className={classes.captchaContainer}>
|
||||
<FormControl margin="normal" required fullWidth>
|
||||
<InputLabel htmlFor="captcha">
|
||||
|
|
@ -338,6 +343,24 @@ function LoginForm() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{loginCaptcha && useReCaptcha && (
|
||||
<div className={classes.captchaContainer}>
|
||||
<FormControl margin="normal" required fullWidth>
|
||||
<div>
|
||||
<ReCaptcha
|
||||
style={{ display: "inline-block" }}
|
||||
sitekey={reCaptchaKey}
|
||||
onChange={value =>
|
||||
setCaptcha(value)
|
||||
}
|
||||
id="captcha"
|
||||
name="captcha"
|
||||
/>
|
||||
</div>
|
||||
</FormControl>{" "}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import ReCAPTCHA from "./ReCaptchaWrapper";
|
||||
import makeAsyncScriptLoader from "react-async-script";
|
||||
|
||||
const callbackName = "onloadcallback";
|
||||
const globalName = "grecaptcha";
|
||||
|
||||
function getURL() {
|
||||
const hostname = "recaptcha.net";
|
||||
return `https://${hostname}/recaptcha/api.js?onload=${callbackName}&render=explicit`;
|
||||
}
|
||||
|
||||
export default makeAsyncScriptLoader(getURL, {
|
||||
callbackName,
|
||||
globalName,
|
||||
})(ReCAPTCHA);
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
export default class ReCAPTCHA extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.handleExpired = this.handleExpired.bind(this);
|
||||
this.handleErrored = this.handleErrored.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleRecaptchaRef = this.handleRecaptchaRef.bind(this);
|
||||
}
|
||||
|
||||
getValue() {
|
||||
if (this.props.grecaptcha && this._widgetId !== undefined) {
|
||||
return this.props.grecaptcha.getResponse(this._widgetId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getWidgetId() {
|
||||
if (this.props.grecaptcha && this._widgetId !== undefined) {
|
||||
return this._widgetId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
execute() {
|
||||
const { grecaptcha } = this.props;
|
||||
|
||||
if (grecaptcha && this._widgetId !== undefined) {
|
||||
return grecaptcha.execute(this._widgetId);
|
||||
} else {
|
||||
this._executeRequested = true;
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (this.props.grecaptcha && this._widgetId !== undefined) {
|
||||
this.props.grecaptcha.reset(this._widgetId);
|
||||
}
|
||||
}
|
||||
|
||||
handleExpired() {
|
||||
if (this.props.onExpired) {
|
||||
this.props.onExpired();
|
||||
} else {
|
||||
this.handleChange(null);
|
||||
}
|
||||
}
|
||||
|
||||
handleErrored() {
|
||||
if (this.props.onErrored) this.props.onErrored();
|
||||
}
|
||||
|
||||
handleChange(token) {
|
||||
if (this.props.onChange) this.props.onChange(token);
|
||||
}
|
||||
|
||||
explicitRender() {
|
||||
if (this.props.grecaptcha && this.props.grecaptcha.render && this._widgetId === undefined) {
|
||||
const wrapper = document.createElement("div");
|
||||
this._widgetId = this.props.grecaptcha.render(wrapper, {
|
||||
sitekey: this.props.sitekey,
|
||||
callback: this.handleChange,
|
||||
theme: this.props.theme,
|
||||
type: this.props.type,
|
||||
tabindex: this.props.tabindex,
|
||||
"expired-callback": this.handleExpired,
|
||||
"error-callback": this.handleErrored,
|
||||
size: this.props.size,
|
||||
stoken: this.props.stoken,
|
||||
hl: this.props.hl,
|
||||
badge: this.props.badge,
|
||||
});
|
||||
this.captcha.appendChild(wrapper);
|
||||
}
|
||||
if (this._executeRequested && this.props.grecaptcha && this._widgetId !== undefined) {
|
||||
this._executeRequested = false;
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.explicitRender();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.explicitRender();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._widgetId !== undefined) {
|
||||
this.delayOfCaptchaIframeRemoving();
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
delayOfCaptchaIframeRemoving() {
|
||||
const temporaryNode = document.createElement("div");
|
||||
document.body.appendChild(temporaryNode);
|
||||
temporaryNode.style.display = "none";
|
||||
|
||||
// move of the recaptcha to a temporary node
|
||||
while (this.captcha.firstChild) {
|
||||
temporaryNode.appendChild(this.captcha.firstChild);
|
||||
}
|
||||
|
||||
// delete the temporary node after reset will be done
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(temporaryNode);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
handleRecaptchaRef(elem) {
|
||||
this.captcha = elem;
|
||||
}
|
||||
|
||||
render() {
|
||||
// consume properties owned by the reCATPCHA, pass the rest to the div so the user can style it.
|
||||
/* eslint-disable no-unused-vars */
|
||||
const {
|
||||
sitekey,
|
||||
onChange,
|
||||
theme,
|
||||
type,
|
||||
tabindex,
|
||||
onExpired,
|
||||
onErrored,
|
||||
size,
|
||||
stoken,
|
||||
grecaptcha,
|
||||
badge,
|
||||
hl,
|
||||
...childProps
|
||||
} = this.props;
|
||||
/* eslint-enable no-unused-vars */
|
||||
return <div {...childProps} ref={this.handleRecaptchaRef} />;
|
||||
}
|
||||
}
|
||||
|
||||
ReCAPTCHA.displayName = "ReCAPTCHA";
|
||||
ReCAPTCHA.propTypes = {
|
||||
sitekey: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
grecaptcha: PropTypes.object,
|
||||
theme: PropTypes.oneOf(["dark", "light"]),
|
||||
type: PropTypes.oneOf(["image", "audio"]),
|
||||
tabindex: PropTypes.number,
|
||||
onExpired: PropTypes.func,
|
||||
onErrored: PropTypes.func,
|
||||
size: PropTypes.oneOf(["compact", "normal", "invisible"]),
|
||||
stoken: PropTypes.string,
|
||||
hl: PropTypes.string,
|
||||
badge: PropTypes.oneOf(["bottomright", "bottomleft", "inline"]),
|
||||
};
|
||||
ReCAPTCHA.defaultProps = {
|
||||
onChange: () => {},
|
||||
theme: "light",
|
||||
type: "image",
|
||||
tabindex: 0,
|
||||
size: "normal",
|
||||
badge: "bottomright",
|
||||
};
|
||||
|
|
@ -21,6 +21,7 @@ import {
|
|||
Typography
|
||||
} from "@material-ui/core";
|
||||
import EmailIcon from "@material-ui/icons/EmailOutlined";
|
||||
import ReCaptcha from "./ReCaptcha";
|
||||
const useStyles = makeStyles(theme => ({
|
||||
layout: {
|
||||
width: "auto",
|
||||
|
|
@ -97,6 +98,8 @@ function Register() {
|
|||
|
||||
const title = useSelector(state => state.siteConfig.title);
|
||||
const regCaptcha = useSelector(state => state.siteConfig.regCaptcha);
|
||||
const useReCaptcha = useSelector(state => state.siteConfig.captcha_IsUseReCaptcha);
|
||||
const reCaptchaKey = useSelector(state => state.siteConfig.captcha_ReCaptchaKey);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const ToggleSnackbar = useCallback(
|
||||
|
|
@ -157,12 +160,14 @@ function Register() {
|
|||
.catch(error => {
|
||||
setLoading(false);
|
||||
ToggleSnackbar("top", "right", error.message, "warning");
|
||||
refreshCaptcha();
|
||||
if (!useReCaptcha) {
|
||||
refreshCaptcha();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (regCaptcha) {
|
||||
if (regCaptcha && !useReCaptcha) {
|
||||
refreshCaptcha();
|
||||
}
|
||||
}, [regCaptcha]);
|
||||
|
|
@ -212,7 +217,7 @@ function Register() {
|
|||
value={input.password_repeat}
|
||||
autoComplete />
|
||||
</FormControl>
|
||||
{regCaptcha && (
|
||||
{regCaptcha && !useReCaptcha && (
|
||||
<div className={classes.captchaContainer}>
|
||||
<FormControl margin="normal" required fullWidth>
|
||||
<InputLabel htmlFor="captcha">
|
||||
|
|
@ -248,6 +253,25 @@ function Register() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{regCaptcha && useReCaptcha && (
|
||||
<div className={classes.captchaContainer}>
|
||||
<FormControl margin="normal" required fullWidth>
|
||||
<ReCaptcha
|
||||
style={{ display: "inline-block" }}
|
||||
sitekey={reCaptchaKey}
|
||||
onChange={value=>
|
||||
setInput({
|
||||
...input,
|
||||
captcha:value
|
||||
})
|
||||
}
|
||||
id="captcha"
|
||||
name="captcha"
|
||||
/>
|
||||
</FormControl>{" "}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
Typography
|
||||
} from "@material-ui/core";
|
||||
import KeyIcon from "@material-ui/icons/VpnKeyOutlined";
|
||||
import ReCaptcha from "./ReCaptcha";
|
||||
const useStyles = makeStyles(theme => ({
|
||||
layout: {
|
||||
width: "auto",
|
||||
|
|
@ -68,6 +69,8 @@ function Reset() {
|
|||
const [captchaData, setCaptchaData] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const forgetCaptcha = useSelector(state => state.siteConfig.forgetCaptcha);
|
||||
const useReCaptcha = useSelector(state => state.siteConfig.captcha_IsUseReCaptcha);
|
||||
const reCaptchaKey = useSelector(state => state.siteConfig.captcha_ReCaptchaKey);
|
||||
const dispatch = useDispatch();
|
||||
const ToggleSnackbar = useCallback(
|
||||
(vertical, horizontal, msg, color) =>
|
||||
|
|
@ -97,7 +100,7 @@ function Reset() {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (forgetCaptcha) {
|
||||
if (forgetCaptcha && !useReCaptcha) {
|
||||
refreshCaptcha();
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
|
|
@ -119,7 +122,9 @@ function Reset() {
|
|||
.catch(error => {
|
||||
setLoading(false);
|
||||
ToggleSnackbar("top", "right", error.message, "warning");
|
||||
refreshCaptcha();
|
||||
if (!useReCaptcha){
|
||||
refreshCaptcha();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -147,7 +152,7 @@ function Reset() {
|
|||
autoFocus
|
||||
/>
|
||||
</FormControl>
|
||||
{forgetCaptcha && (
|
||||
{forgetCaptcha && !useReCaptcha && (
|
||||
<div className={classes.captchaContainer}>
|
||||
<FormControl margin="normal" required fullWidth>
|
||||
<InputLabel htmlFor="captcha">
|
||||
|
|
@ -178,6 +183,24 @@ function Reset() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{forgetCaptcha && useReCaptcha && (
|
||||
<div className={classes.captchaContainer}>
|
||||
<FormControl margin="normal" required fullWidth>
|
||||
<ReCaptcha
|
||||
style={{ display: "inline-block" }}
|
||||
sitekey={reCaptchaKey}
|
||||
onChange={value=>
|
||||
setInput({
|
||||
...input,
|
||||
captcha:value
|
||||
})
|
||||
}
|
||||
id="captcha"
|
||||
name="captcha"
|
||||
/>
|
||||
</FormControl>{" "}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
|
|
|
|||
|
|
@ -58,7 +58,9 @@ const defaultStatus = InitSiteConfig({
|
|||
emptyIcon: "#e8e8e8"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
captcha_IsUseReCaptcha: false,
|
||||
captcha_ReCaptchaKey: "defaultKey"
|
||||
},
|
||||
navigator: {
|
||||
path: "/",
|
||||
|
|
|
|||
10
yarn.lock
10
yarn.lock
|
|
@ -8525,7 +8525,7 @@ prompts@^2.0.1:
|
|||
kleur "^3.0.3"
|
||||
sisteransi "^1.0.3"
|
||||
|
||||
prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||
prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||
version "15.7.2"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
||||
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
||||
|
|
@ -8726,6 +8726,14 @@ react-app-polyfill@^1.0.4:
|
|||
regenerator-runtime "0.13.3"
|
||||
whatwg-fetch "3.0.0"
|
||||
|
||||
react-async-script@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.1.1.tgz#f481c6c5f094bf4b94a9d52da0d0dda2e1a74bdf"
|
||||
integrity sha512-pmgS3O7JcX4YtH/Xy//NXylpD5CNb5T4/zqlVUV3HvcuyOanatvuveYoxl3X30ZSq/+q/+mSXcNS8xDVQJpSeA==
|
||||
dependencies:
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
prop-types "^15.5.0"
|
||||
|
||||
react-color@^2.18.0:
|
||||
version "2.18.0"
|
||||
resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.18.0.tgz#34956f0bac394f6c3bc01692fd695644cc775ffd"
|
||||
|
|
|
|||
Loading…
Reference in New Issue