Feat: recaptcha

This commit is contained in:
HFO4 2020-04-11 10:02:19 +08:00
parent 43c9ce1d26
commit cfb1206e13
9 changed files with 335 additions and 12 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -58,7 +58,9 @@ const defaultStatus = InitSiteConfig({
emptyIcon: "#e8e8e8"
}
}
}
},
captcha_IsUseReCaptcha: false,
captcha_ReCaptchaKey: "defaultKey"
},
navigator: {
path: "/",

View File

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