From 10dccc6faa8c95b34a96cdfeae6daa335fe9b76d Mon Sep 17 00:00:00 2001 From: Bobby Date: Mon, 5 Sep 2022 19:25:07 -0400 Subject: Added captcha to register page --- Dockerfile | 4 ++ blog/urls.py | 1 + blog/views.py | 43 ++++++++++++++++++++- requirements.txt | 1 + static/css/main.css | 16 ++++++++ static/images/icons/button_refresh.jpeg | Bin 0 -> 26372 bytes static/js/captcha.js | 15 +++++++ templates/blog/register.html | 16 ++++++++ users/migrations/0003_captchastore.py | 22 +++++++++++ ...aptchastore_id_alter_captchastore_csrf_token.py | 22 +++++++++++ users/models.py | 7 ++++ 11 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 static/images/icons/button_refresh.jpeg create mode 100644 static/js/captcha.js create mode 100644 users/migrations/0003_captchastore.py create mode 100644 users/migrations/0004_remove_captchastore_id_alter_captchastore_csrf_token.py diff --git a/Dockerfile b/Dockerfile index 3c91da9e..12d04373 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,10 @@ RUN pip install -r requirements.txt COPY . . +# Migrations +RUN python manage.py makemigrations +RUN python manage.py migrate + RUN python manage.py collectstatic --noinput diff --git a/blog/urls.py b/blog/urls.py index 568d0cb9..1884e810 100644 --- a/blog/urls.py +++ b/blog/urls.py @@ -8,5 +8,6 @@ urlpatterns = [ path('account/', RedirectView.as_view(pattern_name='account', permanent=False)), path('my/account', views.account, name='account'), path('register/', views.register, name='register'), + path('register/refresh_captcha/', name='refresh_captcha', view=views.refresh_captcha), # path('my/homepage', views.homepage, name='homepage'), ] diff --git a/blog/views.py b/blog/views.py index b5c54182..c656f020 100644 --- a/blog/views.py +++ b/blog/views.py @@ -1,7 +1,14 @@ +from http.client import HTTPResponse from django.shortcuts import render, redirect -from users.models import UserProfile +from django.http import HttpResponse +from users.models import UserProfile, CaptchaStore from urllib.parse import urlparse import hashlib +from captcha.image import ImageCaptcha +from random import choice +from string import ascii_letters, digits +import base64 +import json # Create your views here. @@ -32,5 +39,37 @@ def homepage(request): return render(request, 'blog/homepage.html', {'title': 'Homepage'}) +def get_base64_captcha(): + image = ImageCaptcha() + random_string = ''.join([choice(ascii_letters + digits) for n in range(6)]) + data = image.generate(random_string) + base64_data = "data:image/png;base64," + base64.b64encode(data.getvalue()).decode() + return base64_data, random_string + def register(request): - return render(request, 'blog/register.html', {'title': 'Register New User'}) + csrf_token = request.META.get('CSRF_COOKIE') + base64_data, random_string = get_base64_captcha() + try: + # Delete old captcha + CaptchaStore.objects.get(csrf_token=csrf_token).delete() + except CaptchaStore.DoesNotExist: + pass + # Create new captcha + CaptchaStore.objects.create(captcha_string=random_string, csrf_token=csrf_token) + return render(request, 'blog/register.html', {'title': 'Register New User', 'captcha': base64_data}) + + +def refresh_captcha(request): + csrf_token = request.META.get('CSRF_COOKIE') + if not csrf_token or not request.META.get('HTTP_REFERER') or request.META.get('HTTP_REFERER').split('/')[-2] != 'register': + response_data = {'status': 'error', 'message': 'Unauthorized!'} + return HttpResponse(json.dumps(response_data), content_type="application/json", status=401) + base64_data, random_string = get_base64_captcha() + try: + CaptchaStore.objects.get(csrf_token=csrf_token).delete() + except CaptchaStore.DoesNotExist: + pass + + CaptchaStore.objects.create(captcha_string=random_string, csrf_token=csrf_token) + response_data = {'captcha': base64_data} + return HttpResponse(json.dumps(response_data), content_type="application/json") diff --git a/requirements.txt b/requirements.txt index 58cda50f..35143047 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ gunicorn==20.1.0 whitenoise==6.2.0 six==1.16.0 cryptocode==0.1 +captcha==0.4 diff --git a/static/css/main.css b/static/css/main.css index a524db34..32afdeb8 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -153,6 +153,22 @@ nav > ul > li { display: none; } +#captcha { + display: flex; + flex-direction: row; +} + +#captcha > img:nth-child(1) { + height: 60px; + margin-right: 10px; +} + +#captcha > img:nth-child(2) { + height: 30px; + transform: translateY(15px); + cursor: pointer; +} + /* Optimize for phones */ @media only screen and (max-width: 480px) { body { diff --git a/static/images/icons/button_refresh.jpeg b/static/images/icons/button_refresh.jpeg new file mode 100644 index 00000000..1d6b5567 Binary files /dev/null and b/static/images/icons/button_refresh.jpeg differ diff --git a/static/js/captcha.js b/static/js/captcha.js new file mode 100644 index 00000000..93be88da --- /dev/null +++ b/static/js/captcha.js @@ -0,0 +1,15 @@ +const refreshCaptchaButton = document.getElementById('refresh_captcha'); + +refreshCaptchaButton.addEventListener('click', function() { + const refreshCaptchaURl = refreshCaptchaButton.getAttribute('data-refresh-captcha-url'); + + const xhr = new XMLHttpRequest(); + xhr.open('GET', refreshCaptchaURl, true); + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + xhr.onload = function() { + const data = JSON.parse(this.responseText); + const captchaImage = document.getElementById('captcha_image'); + captchaImage.src = data['captcha']; + } + xhr.send(); +}); diff --git a/templates/blog/register.html b/templates/blog/register.html index aa0600da..44fbc116 100644 --- a/templates/blog/register.html +++ b/templates/blog/register.html @@ -41,9 +41,25 @@ {% endif %} {% endfor %} +
+ +
+ {% load static %} + captcha + Refresh Captcha +
+ + {% for message in messages %} + {% if 'captchaError' in message.tags %} + {{ message.message }} + {% endif %} + {% endfor %} +
+ {% endblock %} + diff --git a/users/migrations/0003_captchastore.py b/users/migrations/0003_captchastore.py new file mode 100644 index 00000000..9aebc0aa --- /dev/null +++ b/users/migrations/0003_captchastore.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.6 on 2022-09-05 22:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_userprofile_email_verified'), + ] + + operations = [ + migrations.CreateModel( + name='CaptchaStore', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('captcha_string', models.CharField(max_length=6)), + ('csrf_token', models.CharField(max_length=100)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/users/migrations/0004_remove_captchastore_id_alter_captchastore_csrf_token.py b/users/migrations/0004_remove_captchastore_id_alter_captchastore_csrf_token.py new file mode 100644 index 00000000..1dd16bcc --- /dev/null +++ b/users/migrations/0004_remove_captchastore_id_alter_captchastore_csrf_token.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.6 on 2022-09-05 22:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0003_captchastore'), + ] + + operations = [ + migrations.RemoveField( + model_name='captchastore', + name='id', + ), + migrations.AlterField( + model_name='captchastore', + name='csrf_token', + field=models.CharField(max_length=100, primary_key=True, serialize=False), + ), + ] diff --git a/users/models.py b/users/models.py index 808e197a..dab44f38 100644 --- a/users/models.py +++ b/users/models.py @@ -17,3 +17,10 @@ class UserProfile(models.Model): def __str__(self): return self.user.username +class CaptchaStore(models.Model): + captcha_string = models.CharField(max_length=6) + csrf_token = models.CharField(max_length=100, primary_key=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.captcha_string -- cgit v1.2.3