From b1bb28d7bf95acc2c6b436761b695acc4831fa87 Mon Sep 17 00:00:00 2001 From: giovanism Date: Sat, 7 Mar 2020 14:57:08 +0700 Subject: [PATCH 01/22] [RED] Add test `is_verified` field on User registration --- backend/main/test_views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/main/test_views.py b/backend/main/test_views.py index 53bb3fe..fcc618c 100644 --- a/backend/main/test_views.py +++ b/backend/main/test_views.py @@ -25,6 +25,7 @@ class RegisterAPITestCase(APITestCase): user = User.objects.get(email=data['email']) self.assertEqual(user.email, response.data['email']) + self.assertEqual(user.is_verified, False) def test_register_fail(self): data = { -- GitLab From 6f0b7e466f68343ab719bcd8886df233aa788310 Mon Sep 17 00:00:00 2001 From: giovanism Date: Sat, 7 Mar 2020 14:59:52 +0700 Subject: [PATCH 02/22] [GREEN] Add is_verified field on User model --- .../main/migrations/0002_user_is_verified.py | 18 ++++++++++++++++++ backend/main/models.py | 1 + 2 files changed, 19 insertions(+) create mode 100644 backend/main/migrations/0002_user_is_verified.py diff --git a/backend/main/migrations/0002_user_is_verified.py b/backend/main/migrations/0002_user_is_verified.py new file mode 100644 index 0000000..1464e61 --- /dev/null +++ b/backend/main/migrations/0002_user_is_verified.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.3 on 2020-03-07 07:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='is_verified', + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/main/models.py b/backend/main/models.py index c48100c..72c94e4 100644 --- a/backend/main/models.py +++ b/backend/main/models.py @@ -47,6 +47,7 @@ class User(AbstractUser): username = None email = models.EmailField(_('email address'), unique=True) + is_verified = models.BooleanField(default=False) class Sex(models.TextChoices): -- GitLab From ef032d8afb6056b2c3a337aa94a12db5ea2e00c0 Mon Sep 17 00:00:00 2001 From: giovanism Date: Sat, 7 Mar 2020 15:10:55 +0700 Subject: [PATCH 03/22] [CHORES] Fix minor style error --- backend/main/serializers.py | 9 ++++++++- backend/main/tests.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/main/serializers.py b/backend/main/serializers.py index ee6441a..f6a81c3 100644 --- a/backend/main/serializers.py +++ b/backend/main/serializers.py @@ -14,4 +14,11 @@ class RegistrationSerializer(serializers.ModelSerializer): class Meta: model = User fields = ['email', 'password'] - extra_kwargs = {'password': {'write_only': True}} + extra_kwargs = { + 'password': { + 'write_only': True, + 'style': { + 'input_type': 'password' + } + } + } diff --git a/backend/main/tests.py b/backend/main/tests.py index 7e507a4..e022a3e 100644 --- a/backend/main/tests.py +++ b/backend/main/tests.py @@ -47,7 +47,7 @@ class SecretTests(APITestCase): http_authorization = b'Bearer ' + access_token.encode('utf-8') response = self.client.get('/secret/', - HTTP_AUTHORIZATION=http_authorization) + HTTP_AUTHORIZATION=http_authorization) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertDictEqual( -- GitLab From 124cd73b0a84d27ecaa810c3af289b0a65615026 Mon Sep 17 00:00:00 2001 From: giovanism Date: Sat, 7 Mar 2020 15:35:12 +0700 Subject: [PATCH 04/22] [RED] Add test for email verification send_mail() --- backend/main/test_views.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/main/test_views.py b/backend/main/test_views.py index fcc618c..2c5fd0e 100644 --- a/backend/main/test_views.py +++ b/backend/main/test_views.py @@ -1,3 +1,4 @@ +from django.core import mail from rest_framework import status from rest_framework.test import APITestCase from rest_framework.exceptions import ErrorDetail @@ -46,6 +47,15 @@ class RegisterAPITestCase(APITestCase): }, response.data) + def test_register_email_sent(self): + data = { + 'email': 'donald@duckduckgo.org', + 'password': '5up3r_53cuer' + } + + response = self.client.post('/auth/register/', data=data) + self.assertEqual(mail.outbox[0].to[0], data['email']) + class AccessTokenAPITestCase(APITestCase): -- GitLab From f1566da9fd236dfe7b12f4196d51fc42418d9590 Mon Sep 17 00:00:00 2001 From: giovanism Date: Sat, 7 Mar 2020 15:47:38 +0700 Subject: [PATCH 05/22] [GREEN] Update RegisterView to send_mail --- backend/main/serializers.py | 48 +++++++++++++++++++++++++++++++++++-- backend/main/tokens.py | 31 ++++++++++++++++++++++++ backend/main/views.py | 45 +++++++++++++++++++++++++++++++++- 3 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 backend/main/tokens.py diff --git a/backend/main/serializers.py b/backend/main/serializers.py index f6a81c3..46aeba9 100644 --- a/backend/main/serializers.py +++ b/backend/main/serializers.py @@ -1,9 +1,17 @@ -from rest_framework import serializers +from django.utils.translation import gettext_lazy as _ +from rest_framework.serializers import ( + CharField, + Serializer, + ModelSerializer, + ValidationError, +) +from rest_framework_authlib.errors import TokenError from .models import User +from .tokens import EmailVerificationToken -class RegistrationSerializer(serializers.ModelSerializer): +class RegistrationSerializer(ModelSerializer): def create(self, validated_data): user = User.objects.create_user(validated_data[User.USERNAME_FIELD], @@ -22,3 +30,39 @@ class RegistrationSerializer(serializers.ModelSerializer): } } } + + +class EmailVerificationTokenSerializer(Serializer): + email_verification = CharField() + + def validate_email_verification(self, value): + if isinstance(value, EmailVerificationToken): + self._token = value + else: + self._token = EmailVerificationToken(value, validate=False) + + try: + self._token.validate() + except TokenError as err: + raise ValidationError(err.args) + + user_query = User.objects.filter(email=self._token['email']) + if not user_query.exists(): + raise ValidationError( + _("User with email doesn't exist"), + code='email_not_exists') + + self._user = user_query.get() + if self._user.is_verified: + raise ValidationError( + _("User is already verified"), + code='user_already_verified') + + return value + + def verify(self): + if not hasattr(self, '_token') or not hasattr(self, '_user'): + self.is_valid(raise_exception=True) + + self._user.is_verified = True + self._user.save() diff --git a/backend/main/tokens.py b/backend/main/tokens.py new file mode 100644 index 0000000..79d1d31 --- /dev/null +++ b/backend/main/tokens.py @@ -0,0 +1,31 @@ +from datetime import timedelta +from django.core import exceptions +from django.core import validators +from rest_framework_authlib.errors import TokenError +from rest_framework_authlib.tokens import TokenBase + +from .models import User + + +class EmailVerificationToken(TokenBase): + token_schema = 'email_verification' + lifetime = timedelta(days=365) + + def validate(self): + super().validate() + self.validate_email() + + def validate_email(self): + try: + validators.validate_email(self['email']) + except KeyError: + raise TokenError(_('Token has no email field')) + except exceptions.ValidationError as err: + raise TokenError(err.message) + + @classmethod + def for_user(cls, user): + token = cls() + token['email'] = user.email + + return token diff --git a/backend/main/views.py b/backend/main/views.py index 6d49a8c..f7d5ee1 100644 --- a/backend/main/views.py +++ b/backend/main/views.py @@ -1,9 +1,17 @@ +from django.conf import settings +from django.core import mail +from django.http import HttpResponseRedirect +from django.template.loader import render_to_string from rest_framework import generics from rest_framework import views from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from . import serializers +from . import ( + models, + serializers, + tokens, +) # Create your views here. @@ -49,3 +57,38 @@ class RegisterView(generics.CreateAPIView): Request will be validated and the User object will be created. """ serializer_class = serializers.RegistrationSerializer + + def create(self, request, *args, **kwargs): + response = super().create(request, *args, **kwargs) + + try: + user = models.User.objects.get(email=request.data['email']) + except models.User.DoesNotExists as err: + # I guess this will never be evaluated + raise serializers.ValidationError(err.args) + + self.send_mail(user) + + return response + + def send_mail(self, user): + token = tokens.EmailVerificationToken.for_user(user) + serializer = serializers.EmailVerificationTokenSerializer( + data={'email_verification': token}) + + context = { + 'user': user, + 'token': token, + 'serializer': serializer, + } + + plain_body = render_to_string('email_verification.txt', context=context) + html_body = render_to_string('email_verification.html', context=context) + + mail.send_mail( + "[D'Blood] Please verify your email address", + plain_body, + settings.DEFAULT_FROM_EMAIL, + [user.email], + html_message=html_body, + ) -- GitLab From 10c988a5f02486cb0f94d291ee3c0223e26de311 Mon Sep 17 00:00:00 2001 From: giovanism Date: Sat, 7 Mar 2020 15:55:39 +0700 Subject: [PATCH 06/22] [GREEN] Add missing templates --- .../main/templates/email_verification.html | 130 ++++++++++++++++++ backend/main/templates/email_verification.txt | 17 +++ 2 files changed, 147 insertions(+) create mode 100644 backend/main/templates/email_verification.html create mode 100644 backend/main/templates/email_verification.txt diff --git a/backend/main/templates/email_verification.html b/backend/main/templates/email_verification.html new file mode 100644 index 0000000..b4f0ee5 --- /dev/null +++ b/backend/main/templates/email_verification.html @@ -0,0 +1,130 @@ + + + + + D'Blood Email Verification + + + + + + + + + + + + + + +
+ Creating Email Magic +
+ + + + + + + + + + + + + +
+ Verify your email address +
+ Hi! +
+ You have just registered D'Blood User Account using this email + address. Please verify your email by clicking the "Verify" + button. +
+
+ + +
+
+
+ + + + + +
+ ® Someone, somewhere 2013
+ + Unsubscribe + + to this newsletter instantly +
+ + + + + + +
+ + Twitter + + +   + + + Facebook + +
+
+
+ + diff --git a/backend/main/templates/email_verification.txt b/backend/main/templates/email_verification.txt new file mode 100644 index 0000000..8d81673 --- /dev/null +++ b/backend/main/templates/email_verification.txt @@ -0,0 +1,17 @@ +Hi, + +You have just registered D'Blood User Account using this email address. +Please verify your email by clicking the "Verify" button on the HTML version +of this email. + +Or you can copy-paste one of these provided email verification command into your +powershell or terminal. + +```powershell +``` + +```sh +``` + +Sincerely yours, +D'Blood Team -- GitLab From e6f76c0212fa1bf68c60418f29e1caa0430e4b04 Mon Sep 17 00:00:00 2001 From: giovanism Date: Sat, 7 Mar 2020 16:24:16 +0700 Subject: [PATCH 07/22] [CHORES] Add anymail email settings --- backend/dblood/settings.py | 14 ++++++++++++++ backend/requirements.txt | 1 + 2 files changed, 15 insertions(+) diff --git a/backend/dblood/settings.py b/backend/dblood/settings.py index 40e7367..f55fda1 100644 --- a/backend/dblood/settings.py +++ b/backend/dblood/settings.py @@ -46,6 +46,7 @@ INSTALLED_APPS = [ 'rest_framework_authlib', 'main', 'stok_darah', + 'anymail', ] MIDDLEWARE = [ @@ -157,3 +158,16 @@ REST_FRAMEWORK = { 'rest_framework_authlib.authentication.JWTAuthentication', ) } + +# Email settings + +ANYMAIL = { + 'MAILGUN_API_KEY': os.getenv('MAILGUN_API_KEY'), + 'MAILGUN_SENDER_DOMAIN': os.getenv('MAILGUN_SENDER_DOMAIN'), +} + +DEFAULT_FROM_EMAIL = 'noreply@dblood.depok.go.id' + +EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' + +SERVER_EMAIL = 'server@dblood.depok.go.id' diff --git a/backend/requirements.txt b/backend/requirements.txt index d3794ce..66ad53d 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,5 +3,6 @@ Authlib==0.14.1 dj-database-url==0.5.0 Django==3.0.3 +django-anymail==7.0.0 djangorestframework==3.11.0 gunicorn==20.0.4 -- GitLab From ed177b01c9538f1430c07950c29648f6cda564bc Mon Sep 17 00:00:00 2001 From: giovanism Date: Sat, 7 Mar 2020 17:33:11 +0700 Subject: [PATCH 08/22] [RED] Add test EmailVerification render_to_string --- backend/main/test_views.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/backend/main/test_views.py b/backend/main/test_views.py index 2c5fd0e..146dcdf 100644 --- a/backend/main/test_views.py +++ b/backend/main/test_views.py @@ -1,9 +1,12 @@ +from django.template.loader import render_to_string from django.core import mail from rest_framework import status from rest_framework.test import APITestCase from rest_framework.exceptions import ErrorDetail from .models import User +from .tokens import EmailVerificationToken +from .serializers import EmailVerificationTokenSerializer class RegisterAPITestCase(APITestCase): @@ -56,6 +59,24 @@ class RegisterAPITestCase(APITestCase): response = self.client.post('/auth/register/', data=data) self.assertEqual(mail.outbox[0].to[0], data['email']) + def test_register_email_html_template(self): + data = { + 'email': 'donald@duckduckgo.org', + 'password': '5up3r_53cuer' + } + + user = User.objects.create_user(**data) + token = EmailVerificationToken.for_user(user) + serializer = EmailVerificationTokenSerializer(token) + + context = {'user': user, 'token': token, 'serializer': serializer} + + plain_body = render_to_string('email_verification.txt', context=context) + html_body = render_to_string('email_verification.html', context=context) + + self.assertInHTML(str(token), html_body) + self.assertIn(str(token), plain_body) + class AccessTokenAPITestCase(APITestCase): -- GitLab From 1c495d4406f89cf8746530000e481dc0b17fcae8 Mon Sep 17 00:00:00 2001 From: giovanism Date: Sat, 7 Mar 2020 20:04:16 +0700 Subject: [PATCH 09/22] [CHORES] Update templates and send_mail() --- .../main/templates/email_verification.html | 7 ++++--- backend/main/templates/email_verification.txt | 7 ++++++- backend/main/views.py | 19 +++++++++++-------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/backend/main/templates/email_verification.html b/backend/main/templates/email_verification.html index b4f0ee5..b7afbad 100644 --- a/backend/main/templates/email_verification.html +++ b/backend/main/templates/email_verification.html @@ -1,3 +1,4 @@ +{% load rest_framework %} @@ -38,7 +39,7 @@ - Hi! + Hi, {{ emailed_user.first_name }}! @@ -55,13 +56,13 @@ style="color: #153643; font-family: Arial, sans-serif; font-size: 16px; line-height: 20px; text-align: center;" >
-- GitLab From 933da4514768ed0a42a8410662ee454bdbc46c85 Mon Sep 17 00:00:00 2001 From: giovanism Date: Mon, 9 Mar 2020 13:12:29 +0700 Subject: [PATCH 22/22] [CHORES] Remove legacy method --- frontend/src/api.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/frontend/src/api.js b/frontend/src/api.js index 36e6f46..c3bb2ba 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -1,10 +1,5 @@ import axios from "axios" import { BASE_API_URL } from "./config" -export const getJadwalDonor = date => - axios.get("http://www.mocky.io/v2/5e5392572e0000b50c2dac3b", { - params: { date }, - }) - export const getListJadwalDonor = date => - axios.get(`${BASE_API_URL}/donor/jadwal/?date=${date}`, { params: { date } }) + axios.get(`${BASE_API_URL}/donor/jadwal/?date=${date}`}) -- GitLab