From 46464f65446f90d26e5ec7f335f1c1e6c85d407f Mon Sep 17 00:00:00 2001 From: Jochen Klar Date: Tue, 21 Jan 2025 16:36:57 +0100 Subject: [PATCH] Add throttling to contact api endpoint --- rdmo/core/assets/js/api/BaseApi.js | 14 ++++++++++++++ rdmo/core/settings.py | 9 ++++++--- rdmo/core/throttling.py | 8 ++++++++ .../assets/js/interview/components/main/Contact.js | 9 ++++++++- rdmo/projects/viewsets.py | 4 +++- 5 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 rdmo/core/throttling.py diff --git a/rdmo/core/assets/js/api/BaseApi.js b/rdmo/core/assets/js/api/BaseApi.js index c7d6d76510..7f2c371110 100644 --- a/rdmo/core/assets/js/api/BaseApi.js +++ b/rdmo/core/assets/js/api/BaseApi.js @@ -19,6 +19,12 @@ function BadRequestError(errors) { this.errors = errors } +function ThrottlingError(errors) { + this.errors = { + throttling: errors.detail + } +} + class BaseApi { static get(url) { @@ -31,6 +37,10 @@ class BaseApi { return response.json().then(errors => { throw new BadRequestError(errors) }) + } else if (response.status === 429) { + return response.json().then(errors => { + throw new ThrottlingError(errors) + }) } else { throw new ApiError(response.statusText, response.status) } @@ -59,6 +69,10 @@ class BaseApi { return response.json().then(errors => { throw new ValidationError(errors) }) + } else if (response.status === 429) { + return response.json().then(errors => { + throw new ThrottlingError(errors) + }) } else { throw new ApiError(response.statusText, response.status) } diff --git a/rdmo/core/settings.py b/rdmo/core/settings.py index 63d3b17459..87d6c0f2c9 100644 --- a/rdmo/core/settings.py +++ b/rdmo/core/settings.py @@ -164,8 +164,8 @@ CACHES = { 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': 'rdmo_default' + 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', + 'LOCATION': 'django_cache' } } @@ -180,7 +180,10 @@ ), 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.JSONRenderer', - ) + ), + 'DEFAULT_THROTTLE_RATES': { + 'email': '1/minute' + } } SETTINGS_EXPORT = [ diff --git a/rdmo/core/throttling.py b/rdmo/core/throttling.py new file mode 100644 index 0000000000..215ac552c4 --- /dev/null +++ b/rdmo/core/throttling.py @@ -0,0 +1,8 @@ +from rest_framework.throttling import UserRateThrottle + + +class EmailThrottle(UserRateThrottle): + scope = 'email' + + def allow_request(self, request, view): + return request.method == 'GET' or super().allow_request(request, view) diff --git a/rdmo/projects/assets/js/interview/components/main/Contact.js b/rdmo/projects/assets/js/interview/components/main/Contact.js index 84d88109d1..7bb1369ebb 100644 --- a/rdmo/projects/assets/js/interview/components/main/Contact.js +++ b/rdmo/projects/assets/js/interview/components/main/Contact.js @@ -31,7 +31,7 @@ const Contact = ({ templates, contact, sendContact, closeContact }) => { bsSize: 'lg' }}> -
+
{ }
+ { + errors && errors.throttling && ( + + ) + }
diff --git a/rdmo/projects/viewsets.py b/rdmo/projects/viewsets.py index 0527882b2e..efd28c1b21 100644 --- a/rdmo/projects/viewsets.py +++ b/rdmo/projects/viewsets.py @@ -22,6 +22,7 @@ from rdmo.conditions.models import Condition from rdmo.core.permissions import HasModelPermission +from rdmo.core.throttling import EmailThrottle from rdmo.core.utils import human2bytes, is_truthy, return_file_response from rdmo.options.models import OptionSet from rdmo.questions.models import Catalog, Page, Question, QuestionSet @@ -329,7 +330,8 @@ def visibility(self, request, pk=None): raise Http404 @action(detail=True, methods=['get', 'post'], - permission_classes=(HasModelPermission | HasProjectPermission, )) + permission_classes=(HasModelPermission | HasProjectPermission, ), + throttle_classes=[EmailThrottle]) def contact(self, request, pk): if settings.PROJECT_CONTACT: if request.method == 'POST':