make captcha more general and add captcha to signup form

This commit is contained in:
HF 2022-01-11 10:14:08 +01:00
parent 2bc1aa9591
commit c29578dfaf
9 changed files with 234 additions and 119 deletions

View File

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

View File

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

View File

@ -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>:&nbsp;{error}
</p>
))}
<>
<p className="modaltext">
{t`Type the characters from the following image:`}
&nbsp;
@ -69,30 +64,61 @@ const Captcha = ({ callback, close }) => {
</span>
</p>
<br />
{errors.map((error) => (
<p key={error} className="errormessage">
<span>{t`Error`}</span>:&nbsp;{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:`}&nbsp;
@ -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>
&nbsp;
<button
type="submit"
style={{ fontSize: 16 }}
>
{t`Send`}
</button>
</div>
)}
</form>
<input type="hidden" name="captchaid" value={captchaData.id || '0'} />
<br />
</>
);
};

View File

@ -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>:&nbsp;{error}
</p>
))}
<Captcha autoload key={captKey} />
<p>
<button
type="button"
onClick={close}
style={{ fontSize: 16 }}
>
{t`Cancel`}
</button>
&nbsp;
<button
type="submit"
style={{ fontSize: 16 }}
>
{t`Send`}
</button>
</p>
</form>
);
};
export default React.memo(GlobalCaptcha);

View File

@ -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>
:&nbsp;{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>

View File

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

View File

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

View File

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

View File

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