diff --git a/src/actions/fetch.js b/src/actions/fetch.js index 7a291cb..a6cd5cf 100644 --- a/src/actions/fetch.js +++ b/src/actions/fetch.js @@ -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, + }, ); } diff --git a/src/components/Alert.jsx b/src/components/Alert.jsx index ccf4959..41ff687 100644 --- a/src/components/Alert.jsx +++ b/src/components/Alert.jsx @@ -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 = () => {

{(alertType === 'captcha') - ? + ? : ( -   - -
- )} - + +
+ ); }; diff --git a/src/components/GlobalCaptcha.jsx b/src/components/GlobalCaptcha.jsx new file mode 100644 index 0000000..c79815e --- /dev/null +++ b/src/components/GlobalCaptcha.jsx @@ -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 ( +
{ + 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) => ( +

+ {t`Error`}: {error} +

+ ))} + +

+ +   + +

+ + ); +}; + +export default React.memo(GlobalCaptcha); diff --git a/src/components/windows/Register.jsx b/src/components/windows/Register.jsx index bad3a7d..9362d78 100644 --- a/src/components/windows/Register.jsx +++ b/src/components/windows/Register.jsx @@ -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 }) => {

{t`Error`} : {error}

))} +

{t`Name`}:

setName(evt.target.value)} type="text" placeholder={t`Name`} - />
+ /> +

{t`Email`}:

setEmail(evt.target.value)} type="text" placeholder={t`Email`} - />
+ /> +

{t`Password`}:

setPassword(evt.target.value)} type="password" placeholder={t`Password`} - />
+ /> +

{t`Confirm Password`}:

setConfirmPassword(evt.target.value)} type="password" placeholder={t`Confirm Password`} - />
+ /> +

{t`Captcha`}:

+ diff --git a/src/routes/api/auth/register.js b/src/routes/api/auth/register.js index 06d193a..a31d1ea 100644 --- a/src/routes/api/auth/register.js +++ b/src/routes/api/auth/register.js @@ -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; diff --git a/src/routes/api/captcha.js b/src/routes/api/captcha.js index fb7d1bd..efa8c3d 100644 --- a/src/routes/api/captcha.js +++ b/src/routes/api/captcha.js @@ -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: diff --git a/src/styles/default.css b/src/styles/default.css index 94e64ab..f68d856 100644 --- a/src/styles/default.css +++ b/src/styles/default.css @@ -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; diff --git a/src/utils/captcha.js b/src/utils/captcha.js index 2da30ad..75f3047 100644 --- a/src/utils/captcha.js +++ b/src/utils/captcha.js @@ -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; }