mirror of
https://github.com/lucaspalomodevelop/indico-plugins.git
synced 2026-03-12 23:27:22 +00:00
CloudCaptchas: Add hCaptcha support (#186)
* Add hCaptcha support * Rename plugin to cloud_captchas
This commit is contained in:
parent
8a9edc61d0
commit
535bb975fc
4
cloud_captchas/MANIFEST.in
Normal file
4
cloud_captchas/MANIFEST.in
Normal file
@ -0,0 +1,4 @@
|
||||
graft indico_cloud_captchas/static
|
||||
graft indico_cloud_captchas/translations
|
||||
|
||||
global-exclude *.pyc __pycache__ .keep
|
||||
27
cloud_captchas/README.md
Normal file
27
cloud_captchas/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Cloud CAPTCHAs Plugin
|
||||
|
||||
This plugin replaces the built-in CAPTCHA with Google's reCAPTCHA v2 or hCaptcha.
|
||||
|
||||
This plugin also serves as an example for developers who want to use a different
|
||||
CAPTCHA for their Indico instance.
|
||||
|
||||

|
||||
|
||||
|
||||
## Setup
|
||||
|
||||
The plugin requires you to set the site key and secret key on the plugin settings page.
|
||||
|
||||
When using reCaptcha these keys can be created on the [reCAPTCHA admin dashboard][recaptcha-create].
|
||||
Choose **reCAPTCHA v2** and **"I'm not a robot" Checkbox**.
|
||||
|
||||
When using hCaptcha the keys can be created on the [hCaptcha dashboard][hcaptcha-dashboard].
|
||||
|
||||
## Changelog
|
||||
|
||||
### 3.2
|
||||
|
||||
- Initial release for Indico 3.2
|
||||
|
||||
[recaptcha-create]: https://www.google.com/recaptcha/admin/create
|
||||
[hcaptcha-dashboard]: https://dashboard.hcaptcha.com/overview
|
||||
@ -8,4 +8,4 @@
|
||||
from indico.util.i18n import make_bound_gettext
|
||||
|
||||
|
||||
_ = make_bound_gettext('recaptcha')
|
||||
_ = make_bound_gettext('cloud_captchas')
|
||||
@ -5,6 +5,7 @@
|
||||
// them and/or modify them under the terms of the MIT License;
|
||||
// see the LICENSE file for more details.
|
||||
|
||||
import HCaptcha from '@hcaptcha/react-hcaptcha';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {useState, useEffect, useRef, useCallback} from 'react';
|
||||
import {useFormState, useForm} from 'react-final-form';
|
||||
@ -14,44 +15,55 @@ import {Message, Form} from 'semantic-ui-react';
|
||||
import {FinalField} from 'indico/react/forms';
|
||||
import {Translate} from 'indico/react/i18n';
|
||||
|
||||
import './Captcha.module.scss';
|
||||
import './CloudCaptcha.module.scss';
|
||||
|
||||
export default function Captcha({name, settings: {siteKey}, wtf}) {
|
||||
export default function CloudCaptcha({name, settings: {siteKey, hCaptcha}, wtf}) {
|
||||
return wtf ? (
|
||||
<WTFCaptcha name={name} siteKey={siteKey} />
|
||||
<WTFCloudCaptcha name={name} siteKey={siteKey} hCaptcha={hCaptcha} />
|
||||
) : (
|
||||
<FinalCaptcha name={name} siteKey={siteKey} />
|
||||
<FinalCloudCaptcha name={name} siteKey={siteKey} hCaptcha={hCaptcha} />
|
||||
);
|
||||
}
|
||||
|
||||
Captcha.propTypes = {
|
||||
CloudCaptcha.propTypes = {
|
||||
name: PropTypes.string,
|
||||
wtf: PropTypes.bool,
|
||||
settings: PropTypes.shape({
|
||||
siteKey: PropTypes.string.isRequired,
|
||||
hCaptcha: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
Captcha.defaultProps = {
|
||||
CloudCaptcha.defaultProps = {
|
||||
name: 'captcha',
|
||||
wtf: false,
|
||||
};
|
||||
|
||||
function CaptchaField({onChange, siteKey, reCaptchaRef}) {
|
||||
return <ReCAPTCHA sitekey={siteKey} onChange={onChange} ref={reCaptchaRef} />;
|
||||
function CloudCaptchaField({onChange, siteKey, hCaptcha, reCaptchaRef}) {
|
||||
return hCaptcha ? (
|
||||
<HCaptcha
|
||||
sitekey={siteKey}
|
||||
onVerify={onChange}
|
||||
onExpire={() => onChange(null)}
|
||||
ref={reCaptchaRef}
|
||||
/>
|
||||
) : (
|
||||
<ReCAPTCHA sitekey={siteKey} onChange={onChange} ref={reCaptchaRef} />
|
||||
);
|
||||
}
|
||||
|
||||
CaptchaField.propTypes = {
|
||||
CloudCaptchaField.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
siteKey: PropTypes.string.isRequired,
|
||||
hCaptcha: PropTypes.bool.isRequired,
|
||||
reCaptchaRef: PropTypes.object,
|
||||
};
|
||||
|
||||
CaptchaField.defaultProps = {
|
||||
CloudCaptchaField.defaultProps = {
|
||||
reCaptchaRef: undefined,
|
||||
};
|
||||
|
||||
function WTFCaptcha({name, siteKey}) {
|
||||
function WTFCloudCaptcha({name, siteKey, hCaptcha}) {
|
||||
const fieldRef = useRef(null);
|
||||
const [response, setResponse] = useState('');
|
||||
const [hasError, setError] = useState(false);
|
||||
@ -88,7 +100,7 @@ function WTFCaptcha({name, siteKey}) {
|
||||
<Form as="div" styleName="captcha">
|
||||
<input type="hidden" name={name} value={response} ref={fieldRef} />
|
||||
<Form.Field error={hasError}>
|
||||
<CaptchaField siteKey={siteKey} onChange={handleChange} />
|
||||
<CloudCaptchaField siteKey={siteKey} onChange={handleChange} hCaptcha={hCaptcha} />
|
||||
</Form.Field>
|
||||
</Form>
|
||||
</div>
|
||||
@ -96,12 +108,13 @@ function WTFCaptcha({name, siteKey}) {
|
||||
);
|
||||
}
|
||||
|
||||
WTFCaptcha.propTypes = {
|
||||
WTFCloudCaptcha.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
siteKey: PropTypes.string.isRequired,
|
||||
hCaptcha: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
function FinalCaptcha({name, siteKey}) {
|
||||
function FinalCloudCaptcha({name, siteKey, hCaptcha}) {
|
||||
const reCaptchaRef = useRef(null);
|
||||
const form = useForm();
|
||||
const {submitErrors} = useFormState({
|
||||
@ -132,8 +145,9 @@ function FinalCaptcha({name, siteKey}) {
|
||||
<FinalField
|
||||
name={name}
|
||||
required
|
||||
component={CaptchaField}
|
||||
component={CloudCaptchaField}
|
||||
siteKey={siteKey}
|
||||
hCaptcha={hCaptcha}
|
||||
reCaptchaRef={reCaptchaRef}
|
||||
/>
|
||||
</Form>
|
||||
@ -142,7 +156,8 @@ function FinalCaptcha({name, siteKey}) {
|
||||
);
|
||||
}
|
||||
|
||||
FinalCaptcha.propTypes = {
|
||||
FinalCloudCaptcha.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
siteKey: PropTypes.string.isRequired,
|
||||
hCaptcha: PropTypes.bool.isRequired,
|
||||
};
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
import {bindTranslateComponents} from 'indico/react/i18n';
|
||||
|
||||
const {Translate, PluralTranslate} = bindTranslateComponents('cern_access');
|
||||
const {Translate, PluralTranslate} = bindTranslateComponents('cloud_captchas');
|
||||
|
||||
export {Translate, PluralTranslate};
|
||||
export {Singular, Plural, Param} from 'react-jsx-i18n';
|
||||
@ -7,6 +7,6 @@
|
||||
|
||||
import {registerPluginComponent} from 'indico/utils/plugins';
|
||||
|
||||
import Captcha from './Captcha';
|
||||
import CloudCaptcha from './CloudCaptcha';
|
||||
|
||||
registerPluginComponent('recaptcha', 'captcha', Captcha);
|
||||
registerPluginComponent('cloud_captchas', 'captcha', CloudCaptcha);
|
||||
143
cloud_captchas/indico_cloud_captchas/plugin.py
Normal file
143
cloud_captchas/indico_cloud_captchas/plugin.py
Normal file
@ -0,0 +1,143 @@
|
||||
# This file is part of the Indico plugins.
|
||||
# Copyright (C) 2002 - 2022 CERN
|
||||
#
|
||||
# The Indico plugins are free software; you can redistribute
|
||||
# them and/or modify them under the terms of the MIT License;
|
||||
# see the LICENSE file for more details.
|
||||
|
||||
import requests
|
||||
from requests.exceptions import HTTPError, RequestException
|
||||
from wtforms.fields import StringField
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
from indico.core.plugins import IndicoPlugin
|
||||
from indico.modules.api.forms import IndicoEnumSelectField
|
||||
from indico.modules.core.plugins import CaptchaPluginMixin
|
||||
from indico.modules.users import EnumConverter
|
||||
from indico.util.enum import RichIntEnum
|
||||
from indico.web.forms.base import IndicoForm
|
||||
from indico.web.forms.validators import HiddenUnless
|
||||
from indico.web.views import WPBase
|
||||
|
||||
from indico_cloud_captchas import _
|
||||
|
||||
|
||||
class CaptchaProvider(RichIntEnum):
|
||||
__titles__ = [_("None (use Indico's built-in CAPTCHA)"), 'reCAPTCHA', 'hCaptcha']
|
||||
none = 0
|
||||
recaptcha = 1
|
||||
hcaptcha = 2
|
||||
|
||||
|
||||
class CloudCaptchasSettingsForm(IndicoForm):
|
||||
provider = IndicoEnumSelectField(
|
||||
_('Type'), enum=CaptchaProvider,
|
||||
description=_('Select which CAPTCHA provider you want to use')
|
||||
)
|
||||
# recaptcha
|
||||
recaptcha_site_key = StringField(
|
||||
_('reCAPTCHA site key'),
|
||||
[HiddenUnless('provider', CaptchaProvider.recaptcha, preserve_data=True), DataRequired()],
|
||||
description=_('The site key available in the reCAPTCHA admin dashboard')
|
||||
)
|
||||
recaptcha_secret_key = StringField(
|
||||
_('reCAPTCHA secret key'),
|
||||
[HiddenUnless('provider', CaptchaProvider.recaptcha, preserve_data=True), DataRequired()],
|
||||
description=_('The secret key available in the reCAPTCHA admin dashboard')
|
||||
)
|
||||
# hcaptcha
|
||||
hcaptcha_site_key = StringField(
|
||||
_('hCaptcha site key'),
|
||||
[HiddenUnless('provider', CaptchaProvider.hcaptcha, preserve_data=True), DataRequired()],
|
||||
description=_('The site key available in the hCaptcha admin dashboard')
|
||||
)
|
||||
hcaptcha_secret_key = StringField(
|
||||
_('reCAPTCHA secret key'),
|
||||
[HiddenUnless('provider', CaptchaProvider.hcaptcha, preserve_data=True), DataRequired()],
|
||||
description=_('The secret key available in the hCaptcha admin dashboard')
|
||||
)
|
||||
|
||||
|
||||
class CloudCaptchasPlugin(CaptchaPluginMixin, IndicoPlugin):
|
||||
"""Cloud CAPTCHAs
|
||||
|
||||
Replaces Indico's default CAPTCHA with reCAPTCHA or hCaptcha.
|
||||
"""
|
||||
|
||||
configurable = True
|
||||
settings_form = CloudCaptchasSettingsForm
|
||||
default_settings = {
|
||||
'provider': CaptchaProvider.none,
|
||||
'recaptcha_site_key': '',
|
||||
'recaptcha_secret_key': '',
|
||||
'hcaptcha_site_key': '',
|
||||
'hcaptcha_secret_key': '',
|
||||
}
|
||||
settings_converters = {
|
||||
'provider': EnumConverter(CaptchaProvider),
|
||||
}
|
||||
|
||||
def init(self):
|
||||
super().init()
|
||||
# TODO split hcaptcha/recaptcha (hcaptcha is pretty big since it includes react)
|
||||
self.inject_bundle('main.js', WPBase, condition=lambda: self.settings.get('provider') != CaptchaProvider.none)
|
||||
self.inject_bundle('main.css', WPBase, condition=lambda: self.settings.get('provider') != CaptchaProvider.none)
|
||||
|
||||
def is_captcha_available(self):
|
||||
provider = self.settings.get('provider')
|
||||
if provider == CaptchaProvider.recaptcha:
|
||||
return bool(self.settings.get('recaptcha_site_key') and self.settings.get('recaptcha_secret_key'))
|
||||
elif provider == CaptchaProvider.hcaptcha:
|
||||
return bool(self.settings.get('hcaptcha_site_key') and self.settings.get('hcaptcha_secret_key'))
|
||||
return False
|
||||
|
||||
def _validate_recaptcha(self, answer):
|
||||
data = {
|
||||
'secret': self.settings.get('recaptcha_secret_key'),
|
||||
'response': answer
|
||||
}
|
||||
if resp := self._validate_http_post('https://www.google.com/recaptcha/api/siteverify', data):
|
||||
return resp.json()['success']
|
||||
return False
|
||||
|
||||
def _validate_hcaptcha(self, answer):
|
||||
data = {
|
||||
'sitekey': self.settings.get('hcaptcha_site_key'),
|
||||
'secret': self.settings.get('hcaptcha_secret_key'),
|
||||
'response': answer
|
||||
}
|
||||
if resp := self._validate_http_post('https://hcaptcha.com/siteverify', data):
|
||||
return resp.json()['success']
|
||||
return False
|
||||
|
||||
def _validate_http_post(self, url, data):
|
||||
try:
|
||||
resp = requests.post(url, data=data)
|
||||
resp.raise_for_status()
|
||||
except HTTPError as exc:
|
||||
self.logger.error('Failed to validate CAPTCHA: %s', exc.response.text)
|
||||
return None
|
||||
except RequestException as exc:
|
||||
self.logger.error('Failed to validate CAPTCHA: %s', exc)
|
||||
return None
|
||||
return resp
|
||||
|
||||
def validate_captcha(self, answer):
|
||||
if self.settings.get('provider') == CaptchaProvider.recaptcha:
|
||||
return self._validate_recaptcha(answer)
|
||||
elif self.settings.get('provider') == CaptchaProvider.hcaptcha:
|
||||
return self._validate_hcaptcha(answer)
|
||||
# should never happen
|
||||
return False
|
||||
|
||||
def get_captcha_settings(self):
|
||||
if self.settings.get('provider') == CaptchaProvider.recaptcha:
|
||||
return {
|
||||
'siteKey': self.settings.get('recaptcha_site_key'),
|
||||
'hCaptcha': False,
|
||||
}
|
||||
elif self.settings.get('provider') == CaptchaProvider.hcaptcha:
|
||||
return {
|
||||
'siteKey': self.settings.get('hcaptcha_site_key'),
|
||||
'hCaptcha': True,
|
||||
}
|
||||
@ -1,16 +1,38 @@
|
||||
{
|
||||
"name": "recaptcha",
|
||||
"version": "1.0.0",
|
||||
"name": "indico-plugin-cloud-captchas",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "recaptcha",
|
||||
"version": "1.0.0",
|
||||
"name": "indico-plugin-cloud-captchas",
|
||||
"dependencies": {
|
||||
"@hcaptcha/react-hcaptcha": "^1.4.4",
|
||||
"react-google-recaptcha": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.18.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz",
|
||||
"integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@hcaptcha/react-hcaptcha": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.4.4.tgz",
|
||||
"integrity": "sha512-Aen217LDnf5ywbPSwBG5CsoqBLIHIAS9lhj3zQjXJuO13doQ6/ubkCWNuY8jmwYLefoFt3V3MrZmCdKDaFoTuQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.3.0",
|
||||
"react-dom": ">= 16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
@ -77,6 +99,19 @@
|
||||
"react": ">=16.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-google-recaptcha": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-2.1.0.tgz",
|
||||
@ -93,9 +128,39 @@
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.9",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
|
||||
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
|
||||
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.18.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz",
|
||||
"integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
},
|
||||
"@hcaptcha/react-hcaptcha": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.4.4.tgz",
|
||||
"integrity": "sha512-Aen217LDnf5ywbPSwBG5CsoqBLIHIAS9lhj3zQjXJuO13doQ6/ubkCWNuY8jmwYLefoFt3V3MrZmCdKDaFoTuQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.17.9"
|
||||
}
|
||||
},
|
||||
"hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
@ -150,6 +215,16 @@
|
||||
"prop-types": "^15.5.0"
|
||||
}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.0"
|
||||
}
|
||||
},
|
||||
"react-google-recaptcha": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-2.1.0.tgz",
|
||||
@ -163,6 +238,20 @@
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.9",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
|
||||
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
|
||||
},
|
||||
"scheduler": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
|
||||
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
cloud_captchas/package.json
Normal file
8
cloud_captchas/package.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "indico-plugin-cloud-captchas",
|
||||
"dependencies": {
|
||||
"@hcaptcha/react-hcaptcha": "^1.4.4",
|
||||
"react-google-recaptcha": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@ -1,5 +1,5 @@
|
||||
[metadata]
|
||||
name = indico-plugin-recaptcha
|
||||
name = indico-plugin-cloud-captchas
|
||||
version = 3.2-dev
|
||||
description = Google reCAPTCHA plugin for Indico
|
||||
long_description = file: README.md
|
||||
@ -24,7 +24,7 @@ install_requires =
|
||||
|
||||
[options.entry_points]
|
||||
indico.plugins =
|
||||
recaptcha = indico_recaptcha.plugin:ReCaptchaPlugin
|
||||
cloud_captchas = indico_cloud_captchas.plugin:CloudCaptchasPlugin
|
||||
|
||||
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
graft indico_recaptcha/static
|
||||
graft indico_recaptcha/translations
|
||||
|
||||
global-exclude *.pyc __pycache__ .keep
|
||||
@ -1,23 +0,0 @@
|
||||
# Google reCAPTCHA Plugin
|
||||
|
||||
This plugin replaces the built-in CAPTCHA with Google's reCAPTCHA v2.
|
||||
|
||||
This plugin also serves as an example for developers who want to use a
|
||||
different CAPTCHA for their Indico instance.
|
||||
|
||||

|
||||
|
||||
|
||||
## Setup
|
||||
|
||||
The plugin requires you to set the reCAPTCHA site key and secret key on the plugin
|
||||
settings page. These keys can be created on the [reCAPTCHA admin dashboard][recaptcha-create].
|
||||
Choose **reCAPTCHA v2** and **"I'm not a robot" Checkbox**.
|
||||
|
||||
## Changelog
|
||||
|
||||
### 3.2
|
||||
|
||||
- Initial release for Indico 3.2
|
||||
|
||||
[recaptcha-create]: https://www.google.com/recaptcha/admin/create
|
||||
@ -1,64 +0,0 @@
|
||||
# This file is part of the Indico plugins.
|
||||
# Copyright (C) 2002 - 2022 CERN
|
||||
#
|
||||
# The Indico plugins are free software; you can redistribute
|
||||
# them and/or modify them under the terms of the MIT License;
|
||||
# see the LICENSE file for more details.
|
||||
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
from wtforms.fields import BooleanField, StringField
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
from indico.core.plugins import IndicoPlugin
|
||||
from indico.modules.core.plugins import CaptchaPluginMixin
|
||||
from indico.web.forms.base import IndicoForm
|
||||
from indico.web.forms.widgets import SwitchWidget
|
||||
from indico.web.views import WPBase
|
||||
|
||||
from indico_recaptcha import _
|
||||
|
||||
|
||||
class ReCaptchaSettingsForm(IndicoForm):
|
||||
enabled = BooleanField(_('Enabled'), widget=SwitchWidget(), description=_('Whether to enable the access overrides'))
|
||||
site_key = StringField(_('Site key'), [DataRequired()],
|
||||
description=_('The site key available in the reCAPTCHA admin dashboard'))
|
||||
secret_key = StringField(_('Secret key'), [DataRequired()],
|
||||
description=_('The secret key available in the reCAPTCHA admin dashboard'))
|
||||
|
||||
|
||||
class ReCaptchaPlugin(CaptchaPluginMixin, IndicoPlugin):
|
||||
"""Google reCAPTCHA
|
||||
|
||||
Replaces Indico's default CAPTCHA with Google reCAPTCHA.
|
||||
"""
|
||||
|
||||
configurable = True
|
||||
settings_form = ReCaptchaSettingsForm
|
||||
default_settings = {
|
||||
'enabled': False,
|
||||
'site_key': '',
|
||||
'secret_key': '',
|
||||
}
|
||||
|
||||
def init(self):
|
||||
super().init()
|
||||
self.inject_bundle('main.js', WPBase, condition=lambda: self.settings.get('enabled'))
|
||||
self.inject_bundle('main.css', WPBase, condition=lambda: self.settings.get('enabled'))
|
||||
|
||||
def is_captcha_available(self):
|
||||
return self.settings.get('enabled') and bool(self.settings.get('site_key'))
|
||||
|
||||
def validate_captcha(self, answer):
|
||||
secret = self.settings.get('secret_key')
|
||||
resp = requests.post('https://www.google.com/recaptcha/api/siteverify',
|
||||
data={'secret': secret, 'response': answer})
|
||||
try:
|
||||
resp.raise_for_status()
|
||||
except RequestException as exc:
|
||||
self.logger.error('Failed to validate CAPTCHA: %s', exc.response.text)
|
||||
return False
|
||||
return resp.json()['success']
|
||||
|
||||
def get_captcha_settings(self):
|
||||
return {'siteKey': self.settings.get('site_key')}
|
||||
@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "recaptcha",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"repository": "https://github.com/indico/indico-plugins",
|
||||
"author": "Indico Team <indico-team@cern.ch>",
|
||||
"dependencies": {
|
||||
"react-google-recaptcha": "^2.1.0"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user