From ae562039708b7839232383315f2d3019f5c3b7d8 Mon Sep 17 00:00:00 2001
From: Yohann D'ANELLO
Date: Tue, 22 Sep 2020 19:37:37 +0200
Subject: [PATCH] Confirm email addresses
---
Dockerfile | 5 +-
apps/registration/apps.py | 3 +-
.../0003_registration_email_confirmed.py | 18 ++++
apps/registration/models.py | 36 ++++++++
apps/registration/signals.py | 15 +++-
apps/registration/urls.py | 6 +-
apps/registration/views.py | 84 ++++++++++++++++++-
corres2math.cron | 8 +-
corres2math/settings_dev.py | 3 +-
corres2math/tokens.py | 26 ++++++
entrypoint.sh | 2 +
locale/fr/LC_MESSAGES/django.po | 65 ++++++++------
.../mails/email_validation_email.html | 6 +-
.../mails/email_validation_email.txt | 4 +-
14 files changed, 238 insertions(+), 43 deletions(-)
create mode 100644 apps/registration/migrations/0003_registration_email_confirmed.py
create mode 100644 corres2math/tokens.py
diff --git a/Dockerfile b/Dockerfile
index ba9dfc8..3f92665 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,9 +1,10 @@
FROM python:3-alpine
ENV PYTHONUNBUFFERED 1
+ENV DJANGO_ALLOW_ASYNC_UNSAFE 1
# Install LaTeX requirements
-RUN apk add --no-cache gettext texlive nginx gcc libc-dev libffi-dev postgresql-dev mariadb-connector-c-dev
+RUN apk add --no-cache gettext texlive nginx gcc libc-dev libffi-dev postgresql-dev
RUN apk add --no-cache bash
@@ -20,7 +21,7 @@ RUN ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/
RUN ln -sf /code/nginx_corres2math.conf /etc/nginx/conf.d/corres2math.conf
RUN rm /etc/nginx/conf.d/default.conf
-RUN cp /code/corres2math.cron /etc/crontabs/corres2math
+RUN crontab /code/corres2math.cron
# With a bashrc, the shell is better
RUN ln -s /code/.bashrc /root/.bashrc
diff --git a/apps/registration/apps.py b/apps/registration/apps.py
index 36bf8da..56b65e0 100644
--- a/apps/registration/apps.py
+++ b/apps/registration/apps.py
@@ -6,6 +6,7 @@ class RegistrationConfig(AppConfig):
name = 'registration'
def ready(self):
- from registration.signals import set_username, create_admin_registration
+ from registration.signals import set_username, send_email_link, create_admin_registration
pre_save.connect(set_username, "auth.User")
+ pre_save.connect(send_email_link, "auth.User")
post_save.connect(create_admin_registration, "auth.User")
diff --git a/apps/registration/migrations/0003_registration_email_confirmed.py b/apps/registration/migrations/0003_registration_email_confirmed.py
new file mode 100644
index 0000000..a9cc7ae
--- /dev/null
+++ b/apps/registration/migrations/0003_registration_email_confirmed.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.1 on 2020-09-22 16:56
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('registration', '0002_auto_20200921_1948'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='registration',
+ name='email_confirmed',
+ field=models.BooleanField(default=False, verbose_name='email confirmed'),
+ ),
+ ]
diff --git a/apps/registration/models.py b/apps/registration/models.py
index a8df9da..259788b 100644
--- a/apps/registration/models.py
+++ b/apps/registration/models.py
@@ -1,7 +1,13 @@
+from django.contrib.sites.models import Site
from django.db import models
+from django.template import loader
+from django.utils.encoding import force_bytes
+from django.utils.http import urlsafe_base64_encode
from django.utils.translation import gettext_lazy as _
from polymorphic.models import PolymorphicModel
+from corres2math.tokens import email_validation_token
+
class Registration(PolymorphicModel):
user = models.OneToOneField(
@@ -15,6 +21,36 @@ class Registration(PolymorphicModel):
verbose_name=_("Grant Animath to contact me in the future about other actions"),
)
+ email_confirmed = models.BooleanField(
+ default=False,
+ verbose_name=_("email confirmed"),
+ )
+
+ def save(self, *args, **kwargs):
+ self.send_email_validation_link()
+ return super().save(*args, **kwargs)
+
+ def send_email_validation_link(self):
+ subject = "[Corres2math] " + str(_("Activate your Correspondances account"))
+ token = email_validation_token.make_token(self.user)
+ uid = urlsafe_base64_encode(force_bytes(self.user.pk))
+ site = Site.objects.first()
+ message = loader.render_to_string('registration/mails/email_validation_email.txt',
+ {
+ 'user': self.user,
+ 'domain': site.domain,
+ 'token': token,
+ 'uid': uid,
+ })
+ html = loader.render_to_string('registration/mails/email_validation_email.html',
+ {
+ 'user': self.user,
+ 'domain': site.domain,
+ 'token': token,
+ 'uid': uid,
+ })
+ self.user.email_user(subject, message, html_message=html)
+
@property
def type(self):
raise NotImplementedError
diff --git a/apps/registration/signals.py b/apps/registration/signals.py
index 65fd535..2f5a0d6 100644
--- a/apps/registration/signals.py
+++ b/apps/registration/signals.py
@@ -1,10 +1,23 @@
-from registration.models import AdminRegistration
+from django.contrib.auth.models import User
+
+from registration.models import AdminRegistration, Registration
def set_username(instance, **_):
instance.username = instance.email
+def send_email_link(instance, **_):
+ if instance.pk:
+ old_instance = User.objects.get(pk=instance.pk)
+ if old_instance.email != instance.email:
+ registration = Registration.objects.get(user=instance)
+ registration.email_confirmed = False
+ registration.save()
+ registration.user = instance
+ registration.send_email_validation_link()
+
+
def create_admin_registration(instance, **_):
if instance.is_superuser:
AdminRegistration.objects.get_or_create(user=instance)
diff --git a/apps/registration/urls.py b/apps/registration/urls.py
index 514ea77..bf11ee6 100644
--- a/apps/registration/urls.py
+++ b/apps/registration/urls.py
@@ -1,9 +1,13 @@
from django.urls import path
-from .views import SignupView
+from .views import SignupView, UserValidationEmailSentView, UserResendValidationEmailView, UserValidateView
app_name = "registration"
urlpatterns = [
path("signup", SignupView.as_view(), name="signup"),
+ path('validate_email/sent/', UserValidationEmailSentView.as_view(), name='email_validation_sent'),
+ path('validate_email/resend//', UserResendValidationEmailView.as_view(),
+ name='email_validation_resend'),
+ path('validate_email///', UserValidateView.as_view(), name='email_validation'),
]
diff --git a/apps/registration/views.py b/apps/registration/views.py
index a0602e7..a58c98f 100644
--- a/apps/registration/views.py
+++ b/apps/registration/views.py
@@ -1,8 +1,15 @@
+from django.conf import settings
+from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
+from django.core.exceptions import ValidationError
from django.db import transaction
+from django.shortcuts import resolve_url, redirect
from django.urls import reverse_lazy
-from django.views.generic import CreateView
+from django.utils.http import urlsafe_base64_decode
+from django.utils.translation import gettext_lazy as _
+from django.views.generic import CreateView, TemplateView, DetailView
+from corres2math.tokens import email_validation_token
from .forms import SignupForm, StudentRegistrationForm, CoachRegistrationForm
@@ -39,3 +46,78 @@ class SignupView(CreateView):
def get_success_url(self):
return reverse_lazy("index")
+
+
+class UserValidateView(TemplateView):
+ """
+ A view to validate the email address.
+ """
+ title = _("Email validation")
+ template_name = 'registration/email_validation_complete.html'
+ extra_context = {"title": _("Validate email")}
+
+ def get(self, *args, **kwargs):
+ """
+ With a given token and user id (in params), validate the email address.
+ """
+ assert 'uidb64' in kwargs and 'token' in kwargs
+
+ self.validlink = False
+ user = self.get_user(kwargs['uidb64'])
+ token = kwargs['token']
+
+ # Validate the token
+ if user is not None and email_validation_token.check_token(user, token):
+ self.validlink = True
+ user.registration.email_confirmed = True
+ user.save()
+ return self.render_to_response(self.get_context_data(), status=200 if self.validlink else 400)
+
+ def get_user(self, uidb64):
+ """
+ Get user from the base64-encoded string.
+ """
+ try:
+ # urlsafe_base64_decode() decodes to bytestring
+ uid = urlsafe_base64_decode(uidb64).decode()
+ user = User.objects.get(pk=uid)
+ except (TypeError, ValueError, OverflowError, User.DoesNotExist, ValidationError):
+ user = None
+ return user
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context['user_object'] = self.get_user(self.kwargs["uidb64"])
+ context['login_url'] = resolve_url(settings.LOGIN_URL)
+ if self.validlink:
+ context['validlink'] = True
+ else:
+ context.update({
+ 'title': _('Email validation unsuccessful'),
+ 'validlink': False,
+ })
+ return context
+
+
+class UserValidationEmailSentView(TemplateView):
+ """
+ Display the information that the validation link has been sent.
+ """
+ template_name = 'registration/email_validation_email_sent.html'
+ extra_context = {"title": _('Email validation email sent')}
+
+
+class UserResendValidationEmailView(LoginRequiredMixin, DetailView):
+ """
+ Rensend the email validation link.
+ """
+ model = User
+ extra_context = {"title": _("Resend email validation link")}
+
+ def get(self, request, *args, **kwargs):
+ user = self.get_object()
+
+ user.profile.send_email_validation_link()
+
+ # TODO Change URL
+ return redirect('index')
diff --git a/corres2math.cron b/corres2math.cron
index 113ed0d..1e07e98 100644
--- a/corres2math.cron
+++ b/corres2math.cron
@@ -1,5 +1,5 @@
-# m h dom mon dow user command
+# min hour day month weekday command
# Envoyer les mails en attente
- * * * * * root cd /code && python manage.py send_mail -c 1
- * * * * * root cd /code && python manage.py retry_deferred -c 1
- 00 0 * * * root cd /code && python manage.py purge_mail_log 7 -c 1
+* * * * * cd /code && python manage.py send_mail -c 1
+* * * * * cd /code && python manage.py retry_deferred -c 1
+0 0 * * * cd /code && python manage.py purge_mail_log 7 -c 1
diff --git a/corres2math/settings_dev.py b/corres2math/settings_dev.py
index a52990e..bf6e856 100644
--- a/corres2math/settings_dev.py
+++ b/corres2math/settings_dev.py
@@ -1,5 +1,4 @@
# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
-EMAIL_BACKEND = 'mailer.backend.DbBackend'
-MAILER_EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
+EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
diff --git a/corres2math/tokens.py b/corres2math/tokens.py
new file mode 100644
index 0000000..c194980
--- /dev/null
+++ b/corres2math/tokens.py
@@ -0,0 +1,26 @@
+from django.contrib.auth.tokens import PasswordResetTokenGenerator
+
+
+class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
+ """
+ Create a unique token generator to confirm email addresses.
+ """
+ def _make_hash_value(self, user, timestamp):
+ """
+ Hash the user's primary key and some user state that's sure to change
+ after an account validation to produce a token that invalidated when
+ it's used:
+ 1. The user.profile.email_confirmed field will change upon an account
+ validation.
+ 2. The last_login field will usually be updated very shortly after
+ an account validation.
+ Failing those things, settings.PASSWORD_RESET_TIMEOUT_DAYS eventually
+ invalidates the token.
+ """
+ # Truncate microseconds so that tokens are consistent even if the
+ # database doesn't support microseconds.
+ login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None)
+ return str(user.pk) + str(user.email) + str(login_timestamp) + str(timestamp)
+
+
+email_validation_token = AccountActivationTokenGenerator()
diff --git a/entrypoint.sh b/entrypoint.sh
index 0029acc..8827c4d 100755
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -1,5 +1,7 @@
#!/bin/sh
+crond -l 0
+
python manage.py compilemessages
python manage.py migrate
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index 1c9b256..c48f172 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Corres2math\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-09-22 18:21+0200\n"
+"POT-Creation-Date: 2020-09-22 18:49+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Yohann D'ANELLO \n"
"Language-Team: LANGUAGE \n"
@@ -25,7 +25,7 @@ msgstr "API"
msgid "Logs"
msgstr "Logs"
-#: apps/logs/models.py:22 apps/registration/models.py:10
+#: apps/logs/models.py:22 apps/registration/models.py:16
msgid "user"
msgstr "utilisateur"
@@ -115,7 +115,8 @@ msgid ""
"Give the authorisation to publish the video on the main website to promote "
"the action."
msgstr ""
-"Donner l'autorisation de publier la vidéo sur le site principal pour promouvoir les Correspondances."
+"Donner l'autorisation de publier la vidéo sur le site principal pour "
+"promouvoir les Correspondances."
#: apps/participation/models.py:35
#, python-brace-format
@@ -123,7 +124,7 @@ msgid "Team {name} ({trigram})"
msgstr "Équipe {name} ({trigram})"
#: apps/participation/models.py:38 apps/participation/models.py:49
-#: apps/registration/models.py:38 apps/registration/models.py:71
+#: apps/registration/models.py:70 apps/registration/models.py:103
msgid "team"
msgstr "équipe"
@@ -202,85 +203,93 @@ msgstr "rôle"
msgid "participant"
msgstr "participant"
-#: apps/registration/forms.py:14 apps/registration/models.py:80
+#: apps/registration/forms.py:14 apps/registration/models.py:112
msgid "coach"
msgstr "encadrant"
-#: apps/registration/models.py:15
+#: apps/registration/models.py:21
msgid "Grant Animath to contact me in the future about other actions"
msgstr ""
"Autoriser Animath à me recontacter à l'avenir à propos d'autres actions"
-#: apps/registration/models.py:23
+#: apps/registration/models.py:26
+msgid "email confirmed"
+msgstr "email confirmé"
+
+#: apps/registration/models.py:30
+msgid "Activate your Correspondances account"
+msgstr "Activez votre compte des Correspondances"
+
+#: apps/registration/models.py:55
#, python-brace-format
msgid "registration of {first_name} {last_name}"
msgstr "inscription de {first_name} {last_name}"
-#: apps/registration/models.py:27
+#: apps/registration/models.py:59
msgid "registration"
msgstr "inscription"
-#: apps/registration/models.py:28
+#: apps/registration/models.py:60
msgid "registrations"
msgstr "inscriptions"
-#: apps/registration/models.py:43
+#: apps/registration/models.py:75
msgid "12th grade"
msgstr "Terminale"
-#: apps/registration/models.py:44
+#: apps/registration/models.py:76
msgid "11th grade"
msgstr "Première"
-#: apps/registration/models.py:45
+#: apps/registration/models.py:77
msgid "10th grade or lower"
msgstr "Seconde ou inférieur"
-#: apps/registration/models.py:47
+#: apps/registration/models.py:79
msgid "student class"
msgstr "classe"
-#: apps/registration/models.py:52
+#: apps/registration/models.py:84
msgid "school"
msgstr "école"
-#: apps/registration/models.py:57
+#: apps/registration/models.py:89
msgid "student"
msgstr "étudiant"
-#: apps/registration/models.py:60
+#: apps/registration/models.py:92
msgid "student registration"
msgstr "inscription d'élève"
-#: apps/registration/models.py:61
+#: apps/registration/models.py:93
msgid "student registrations"
msgstr "inscriptions d'élève"
-#: apps/registration/models.py:75
+#: apps/registration/models.py:107
msgid "professional activity"
msgstr "activité professionnelle"
-#: apps/registration/models.py:83
+#: apps/registration/models.py:115
msgid "coach registration"
msgstr "inscription d'encadrant"
-#: apps/registration/models.py:84
+#: apps/registration/models.py:116
msgid "coach registrations"
msgstr "inscriptions d'encadrants"
-#: apps/registration/models.py:89
+#: apps/registration/models.py:121
msgid "role of the administrator"
msgstr "rôle de l'administrateur"
-#: apps/registration/models.py:94
+#: apps/registration/models.py:126
msgid "admin"
msgstr "admin"
-#: apps/registration/models.py:97
+#: apps/registration/models.py:129
msgid "admin registration"
msgstr "inscription d'administrateur"
-#: apps/registration/models.py:98
+#: apps/registration/models.py:130
msgid "admin registrations"
msgstr "inscriptions d'administrateur"
@@ -443,6 +452,8 @@ msgid ""
"You recently registered on the Correspondances platform. Please click on the "
"link below to confirm your registration."
msgstr ""
+"Vous vous êtes inscrits sur la plateforme des Correspondances. Merci de "
+"cliquer sur le lien ci-dessous pour confirmer votre inscription."
#: templates/registration/mails/email_validation_email.html:26
#: templates/registration/mails/email_validation_email.txt:9
@@ -450,6 +461,8 @@ msgid ""
"This link is only valid for a couple of days, after that you will need to "
"contact us to validate your email."
msgstr ""
+"Ce lien n'est valide que pendant quelques jours, après cela vous devrez nous "
+"contacter pour valider votre email."
#: templates/registration/mails/email_validation_email.html:30
#: templates/registration/mails/email_validation_email.txt:11
@@ -458,8 +471,8 @@ msgstr "Merci"
#: templates/registration/mails/email_validation_email.html:35
#: templates/registration/mails/email_validation_email.txt:13
-msgid "The CNO."
-msgstr ""
+msgid "The Correspondances team."
+msgstr "L'équipe des Correspondances"
#: templates/registration/password_change_done.html:8
msgid "Your password was changed."
diff --git a/templates/registration/mails/email_validation_email.html b/templates/registration/mails/email_validation_email.html
index cd0bfed..416c496 100644
--- a/templates/registration/mails/email_validation_email.html
+++ b/templates/registration/mails/email_validation_email.html
@@ -17,8 +17,8 @@
-
- https://{{ domain }}{% url 'member:email_validation' uidb64=uid token=token %}
+
+ https://{{ domain }}{% url 'registration:email_validation' uidb64=uid token=token %}
@@ -32,5 +32,5 @@
--
- {% trans "The CNO." %}
+ {% trans "The Correspondances team." %}
diff --git a/templates/registration/mails/email_validation_email.txt b/templates/registration/mails/email_validation_email.txt
index b12d3fa..e9219c4 100644
--- a/templates/registration/mails/email_validation_email.txt
+++ b/templates/registration/mails/email_validation_email.txt
@@ -4,10 +4,10 @@
{% trans "You recently registered on the Correspondances platform. Please click on the link below to confirm your registration." %}
-https://{{ domain }}{% url 'member:email_validation' uidb64=uid token=token %}
+https://{{ domain }}{% url 'registration:email_validation' uidb64=uid token=token %}
{% trans "This link is only valid for a couple of days, after that you will need to contact us to validate your email." %}
{% trans "Thanks" %},
-{% trans "The CNO." %}
+{% trans "The Correspondances team." %}