make captcha more general and add captcha to signup form
This commit is contained in:
parent
2bc1aa9591
commit
c29578dfaf
|
@ -251,10 +251,12 @@ export function requestLogin(nameoremail, password) {
|
|||
);
|
||||
}
|
||||
|
||||
export function requestRegistration(name, email, password) {
|
||||
export function requestRegistration(name, email, password, captcha, captchaid) {
|
||||
return makeAPIPOSTRequest(
|
||||
'api/auth/register',
|
||||
{ name, email, password },
|
||||
{
|
||||
name, email, password, captcha, captchaid,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import Captcha from './Captcha';
|
||||
import GlobalCaptcha from './GlobalCaptcha';
|
||||
import { closeAlert } from '../actions';
|
||||
|
||||
const Alert = () => {
|
||||
|
@ -55,7 +55,7 @@ const Alert = () => {
|
|||
</p>
|
||||
<div>
|
||||
{(alertType === 'captcha')
|
||||
? <Captcha close={close} />
|
||||
? <GlobalCaptcha close={close} />
|
||||
: (
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
/*
|
||||
* Form to ask for captcha.
|
||||
* If callback is provided, it sets the captcha text to it.
|
||||
* If callback is not provided, it provides a button to send the
|
||||
* captcha itself
|
||||
* @flow
|
||||
* Offers input for captchas, parent needs to provide a form and
|
||||
* get "captcha" and "captchaid" values
|
||||
*/
|
||||
|
||||
/* eslint-disable jsx-a11y/no-autofocus */
|
||||
|
@ -12,7 +10,6 @@ import React, { useState, useEffect } from 'react';
|
|||
import { t } from 'ttag';
|
||||
|
||||
import { IoReloadCircleSharp } from 'react-icons/io5';
|
||||
import { requestSolveCaptcha } from '../actions/fetch';
|
||||
|
||||
async function getUrlAndId() {
|
||||
const url = window.ssv.captchaurl;
|
||||
|
@ -27,40 +24,38 @@ async function getUrlAndId() {
|
|||
return null;
|
||||
}
|
||||
|
||||
const Captcha = ({ callback, close }) => {
|
||||
/*
|
||||
* autoload: Load captcha immediately and autofocus input textbox
|
||||
* width: width of the captcha image
|
||||
*/
|
||||
const Captcha = ({ autoload, width }) => {
|
||||
const [captchaData, setCaptchaData] = useState({});
|
||||
const [text, setText] = useState('');
|
||||
const [errors, setErrors] = useState([]);
|
||||
const [imgLoaded, setImgLoaded] = useState(false);
|
||||
|
||||
useEffect(async () => {
|
||||
const [svgUrl, captchaid] = await getUrlAndId();
|
||||
const reloadCaptcha = async () => {
|
||||
if (imgLoaded) {
|
||||
setImgLoaded(false);
|
||||
}
|
||||
const captchaResponse = await getUrlAndId();
|
||||
if (!captchaResponse) {
|
||||
setErrors([t`Could not load captcha`]);
|
||||
return;
|
||||
}
|
||||
const [svgUrl, captchaid] = captchaResponse;
|
||||
setCaptchaData({ url: svgUrl, id: captchaid });
|
||||
};
|
||||
|
||||
useEffect(async () => {
|
||||
if (autoload) {
|
||||
reloadCaptcha();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const contWidth = width || 100;
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const { errors: resErrors } = await requestSolveCaptcha(
|
||||
text,
|
||||
captchaData.id,
|
||||
);
|
||||
if (resErrors) {
|
||||
const [svgUrl, captchaid] = await getUrlAndId();
|
||||
setCaptchaData({ url: svgUrl, id: captchaid });
|
||||
setText('');
|
||||
setErrors(resErrors);
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{errors.map((error) => (
|
||||
<p key={error} className="errormessage">
|
||||
<span>{t`Error`}</span>: {error}
|
||||
</p>
|
||||
))}
|
||||
<>
|
||||
<p className="modaltext">
|
||||
{t`Type the characters from the following image:`}
|
||||
|
||||
|
@ -69,30 +64,61 @@ const Captcha = ({ callback, close }) => {
|
|||
</span>
|
||||
</p>
|
||||
<br />
|
||||
{errors.map((error) => (
|
||||
<p key={error} className="errormessage">
|
||||
<span>{t`Error`}</span>: {error}
|
||||
</p>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
paddingTop: '60%',
|
||||
width: `${contWidth}%`,
|
||||
paddingTop: `${Math.floor(contWidth * 0.6)}%`,
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
backgroundColor: '#e0e0e0',
|
||||
}}
|
||||
>
|
||||
{(captchaData.url) && (
|
||||
<img
|
||||
style={{
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
opacity: (imgLoaded) ? 1 : 0,
|
||||
transform: 'translate(-50%,-50%)',
|
||||
transition: '100ms',
|
||||
}}
|
||||
src={captchaData.url}
|
||||
alt="CAPTCHA"
|
||||
onLoad={() => { setImgLoaded(true); }}
|
||||
onError={() => setErrors([t`Could not load captcha`])}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%,-50%)',
|
||||
}}
|
||||
>
|
||||
{(captchaData.url)
|
||||
? (
|
||||
<img
|
||||
style={{
|
||||
width: '100%',
|
||||
opacity: (imgLoaded) ? 1 : 0,
|
||||
transition: '100ms',
|
||||
}}
|
||||
src={captchaData.url}
|
||||
alt="CAPTCHA"
|
||||
onLoad={() => {
|
||||
setErrors([]);
|
||||
setImgLoaded(true);
|
||||
}}
|
||||
onError={() => {
|
||||
setErrors([t`Could not load captcha`]);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title={t`Load Captcha`}
|
||||
className="modallink"
|
||||
onClick={reloadCaptcha}
|
||||
onKeyPress={reloadCaptcha}
|
||||
>
|
||||
{t`Click to Load Captcha`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="modaltext">
|
||||
{t`Can't read? Reload:`}
|
||||
|
@ -102,54 +128,29 @@ const Captcha = ({ callback, close }) => {
|
|||
title={t`Reload`}
|
||||
className="modallink"
|
||||
style={{ fontSize: 28 }}
|
||||
onClick={async () => {
|
||||
setImgLoaded(false);
|
||||
const [svgUrl, captchaid] = await getUrlAndId();
|
||||
setCaptchaData({ url: svgUrl, id: captchaid });
|
||||
}}
|
||||
onClick={reloadCaptcha}
|
||||
>
|
||||
<IoReloadCircleSharp />
|
||||
</span>
|
||||
</p>
|
||||
<input
|
||||
name="captcha"
|
||||
placeholder={t`Enter Characters`}
|
||||
type="text"
|
||||
value={text}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
autoFocus
|
||||
autoFocus={autoload}
|
||||
style={{
|
||||
width: '6em',
|
||||
fontSize: 21,
|
||||
margin: 5,
|
||||
}}
|
||||
onChange={(evt) => {
|
||||
const txt = evt.target.value;
|
||||
setText(txt);
|
||||
if (callback) callback(txt);
|
||||
}}
|
||||
/>
|
||||
{(!callback) && (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={close}
|
||||
style={{ fontSize: 16 }}
|
||||
>
|
||||
{t`Cancel`}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
style={{ fontSize: 16 }}
|
||||
>
|
||||
{t`Send`}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
<input type="hidden" name="captchaid" value={captchaData.id || '0'} />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Global Captcha that is valid sitewide
|
||||
* via api/captcha
|
||||
* Displayed in an Alert
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { t } from 'ttag';
|
||||
|
||||
import Captcha from './Captcha';
|
||||
import { requestSolveCaptcha } from '../actions/fetch';
|
||||
|
||||
const GlobalCaptcha = ({ close }) => {
|
||||
const [errors, setErrors] = useState([]);
|
||||
// used to be able to force Captcha rerender on error
|
||||
const [captKey, setCaptKey] = useState(Date.now());
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const text = e.target.captcha.value;
|
||||
const captchaid = e.target.captchaid.value;
|
||||
const { errors: resErrors } = await requestSolveCaptcha(
|
||||
text,
|
||||
captchaid,
|
||||
);
|
||||
if (resErrors) {
|
||||
setCaptKey(Date.now());
|
||||
setErrors(resErrors);
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{errors.map((error) => (
|
||||
<p key={error} className="errormessage">
|
||||
<span>{t`Error`}</span>: {error}
|
||||
</p>
|
||||
))}
|
||||
<Captcha autoload key={captKey} />
|
||||
<p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={close}
|
||||
style={{ fontSize: 16 }}
|
||||
>
|
||||
{t`Cancel`}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
style={{ fontSize: 16 }}
|
||||
>
|
||||
{t`Send`}
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(GlobalCaptcha);
|
|
@ -6,6 +6,7 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { t } from 'ttag';
|
||||
import Captcha from '../Captcha';
|
||||
import {
|
||||
validateEMail, validateName, validatePassword,
|
||||
} from '../../utils/validation';
|
||||
|
@ -29,27 +30,27 @@ function validate(name, email, password, confirmPassword) {
|
|||
return errors;
|
||||
}
|
||||
|
||||
const inputStyles = {
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
maxWidth: '35em',
|
||||
};
|
||||
|
||||
const Register = ({ windowId }) => {
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [submitting, setSubmitting] = useState('');
|
||||
const [errors, setErrors] = useState([]);
|
||||
// used to be able to force Captcha rerender on error
|
||||
const [captKey, setCaptKey] = useState(Date.now());
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const handleSubmit = async (evt) => {
|
||||
evt.preventDefault();
|
||||
if (submitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = evt.target.name.value;
|
||||
const email = evt.target.email.value;
|
||||
const password = evt.target.password.value;
|
||||
const confirmPassword = evt.target.confirmpassword.value;
|
||||
const captcha = evt.target.captcha.value;
|
||||
const captchaid = evt.target.captchaid.value;
|
||||
|
||||
const valErrors = validate(name, email, password, confirmPassword);
|
||||
if (valErrors.length > 0) {
|
||||
setErrors(valErrors);
|
||||
|
@ -61,9 +62,12 @@ const Register = ({ windowId }) => {
|
|||
name,
|
||||
email,
|
||||
password,
|
||||
captcha,
|
||||
captchaid,
|
||||
);
|
||||
setSubmitting(false);
|
||||
if (respErrors) {
|
||||
setCaptKey(Date.now());
|
||||
setErrors(respErrors);
|
||||
return;
|
||||
}
|
||||
|
@ -83,38 +87,40 @@ const Register = ({ windowId }) => {
|
|||
<p key={error} className="errormessage"><span>{t`Error`}</span>
|
||||
: {error}</p>
|
||||
))}
|
||||
<p className="modaltitle">{t`Name`}:</p>
|
||||
<input
|
||||
style={inputStyles}
|
||||
value={name}
|
||||
name="name"
|
||||
className="reginput"
|
||||
autoComplete="username"
|
||||
onChange={(evt) => setName(evt.target.value)}
|
||||
type="text"
|
||||
placeholder={t`Name`}
|
||||
/><br />
|
||||
/>
|
||||
<p className="modaltitle">{t`Email`}:</p>
|
||||
<input
|
||||
style={inputStyles}
|
||||
value={email}
|
||||
name="email"
|
||||
className="reginput"
|
||||
autoComplete="email"
|
||||
onChange={(evt) => setEmail(evt.target.value)}
|
||||
type="text"
|
||||
placeholder={t`Email`}
|
||||
/><br />
|
||||
/>
|
||||
<p className="modaltitle">{t`Password`}:</p>
|
||||
<input
|
||||
style={inputStyles}
|
||||
value={password}
|
||||
name="password"
|
||||
className="reginput"
|
||||
autoComplete="new-password"
|
||||
onChange={(evt) => setPassword(evt.target.value)}
|
||||
type="password"
|
||||
placeholder={t`Password`}
|
||||
/><br />
|
||||
/>
|
||||
<p className="modaltitle">{t`Confirm Password`}:</p>
|
||||
<input
|
||||
style={inputStyles}
|
||||
value={confirmPassword}
|
||||
name="confirmpassword"
|
||||
className="reginput"
|
||||
autoComplete="new-password"
|
||||
onChange={(evt) => setConfirmPassword(evt.target.value)}
|
||||
type="password"
|
||||
placeholder={t`Confirm Password`}
|
||||
/><br />
|
||||
/>
|
||||
<p className="modaltitle">{t`Captcha`}:</p>
|
||||
<Captcha autoload={false} width={60} key={captKey} />
|
||||
<button type="submit">
|
||||
{(submitting) ? '...' : t`Submit`}
|
||||
</button>
|
||||
|
|
|
@ -17,8 +17,11 @@ import {
|
|||
validateName,
|
||||
validatePassword,
|
||||
} from '../../../utils/validation';
|
||||
import {
|
||||
checkCaptchaSolution,
|
||||
} from '../../../utils/captcha';
|
||||
|
||||
async function validate(email, name, password, t, gettext) {
|
||||
async function validate(email, name, password, captcha, captchaid, t, gettext) {
|
||||
const errors = [];
|
||||
const emailerror = gettext(validateEMail(email));
|
||||
if (emailerror) errors.push(emailerror);
|
||||
|
@ -27,6 +30,8 @@ async function validate(email, name, password, t, gettext) {
|
|||
const passworderror = gettext(validatePassword(password));
|
||||
if (passworderror) errors.push(passworderror);
|
||||
|
||||
if (!captcha || !captchaid) errors.push(t`No Captcha given`);
|
||||
|
||||
let reguser = await RegUser.findOne({ where: { email } });
|
||||
if (reguser) errors.push(t`E-Mail already in use.`);
|
||||
reguser = await RegUser.findOne({ where: { name } });
|
||||
|
@ -36,9 +41,34 @@ async function validate(email, name, password, t, gettext) {
|
|||
}
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
const { email, name, password } = req.body;
|
||||
const {
|
||||
email, name, password, captcha, captchaid,
|
||||
} = req.body;
|
||||
const { t, gettext } = req.ttag;
|
||||
const errors = await validate(email, name, password, t, gettext);
|
||||
const errors = await validate(
|
||||
email, name, password, captcha, captchaid, t, gettext,
|
||||
);
|
||||
|
||||
const ip = getIPFromRequest(req);
|
||||
if (!errors.length) {
|
||||
const captchaPass = await checkCaptchaSolution(
|
||||
captcha, ip, true, captchaid,
|
||||
);
|
||||
switch (captchaPass) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
errors.push(t`You took too long, try again.`);
|
||||
break;
|
||||
case 2:
|
||||
errors.push(t`You failed your captcha`);
|
||||
break;
|
||||
default:
|
||||
errors.push(t`Unknown Captcha Error`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
res.status(400);
|
||||
res.json({
|
||||
|
@ -63,7 +93,6 @@ export default async (req: Request, res: Response) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const ip = getIPFromRequest(req);
|
||||
logger.info(`Created new user ${name} ${email} ${ip}`);
|
||||
|
||||
const { user, lang } = req;
|
||||
|
|
|
@ -28,7 +28,7 @@ export default async (req: Request, res: Response) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const ret = await checkCaptchaSolution(text, ip, id);
|
||||
const ret = await checkCaptchaSolution(text, ip, false, id);
|
||||
|
||||
switch (ret) {
|
||||
case 0:
|
||||
|
|
|
@ -805,6 +805,16 @@ tr:nth-child(even) {
|
|||
background-color: #ffa9a9cc;
|
||||
}
|
||||
|
||||
.reginput {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-width: 35em;
|
||||
}
|
||||
|
||||
.errormessage {
|
||||
color: #b73c3c;
|
||||
}
|
||||
|
||||
.cooldownbox {
|
||||
top: 16px;
|
||||
width: 48px;
|
||||
|
|
|
@ -90,6 +90,8 @@ export function setCaptchaSolution(
|
|||
*
|
||||
* @param text Solution of captcha
|
||||
* @param ip
|
||||
* @param onetime If the captcha is just one time or should be remembered
|
||||
* for this ip
|
||||
* @return 0 if solution right
|
||||
* 1 if timed out
|
||||
* 2 if wrong
|
||||
|
@ -97,6 +99,7 @@ export function setCaptchaSolution(
|
|||
export async function checkCaptchaSolution(
|
||||
text,
|
||||
ip,
|
||||
onetime = false,
|
||||
captchaid = null,
|
||||
) {
|
||||
const ipn = getIPv6Subnet(ip);
|
||||
|
@ -107,8 +110,10 @@ export async function checkCaptchaSolution(
|
|||
const solution = await redis.getAsync(key);
|
||||
if (solution) {
|
||||
if (evaluateResult(solution.toString('utf8'), text)) {
|
||||
const solvkey = `human:${ipn}`;
|
||||
await redis.setAsync(solvkey, '', 'EX', TTL_CACHE);
|
||||
if (!onetime) {
|
||||
const solvkey = `human:${ipn}`;
|
||||
await redis.setAsync(solvkey, '', 'EX', TTL_CACHE);
|
||||
}
|
||||
logger.info(`CAPTCHA ${ip} successfully solved captcha`);
|
||||
return 0;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue