1
0
mirror of https://gitlab.com/animath/si/plateforme-corres2math.git synced 2025-02-06 13:33:00 +00:00

Confirm email addresses

This commit is contained in:
Yohann D'ANELLO 2020-09-22 19:37:37 +02:00
parent 3741557200
commit ae56203970
14 changed files with 238 additions and 43 deletions

View File

@ -1,9 +1,10 @@
FROM python:3-alpine FROM python:3-alpine
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
ENV DJANGO_ALLOW_ASYNC_UNSAFE 1
# Install LaTeX requirements # 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 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 ln -sf /code/nginx_corres2math.conf /etc/nginx/conf.d/corres2math.conf
RUN rm /etc/nginx/conf.d/default.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 # With a bashrc, the shell is better
RUN ln -s /code/.bashrc /root/.bashrc RUN ln -s /code/.bashrc /root/.bashrc

View File

@ -6,6 +6,7 @@ class RegistrationConfig(AppConfig):
name = 'registration' name = 'registration'
def ready(self): 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(set_username, "auth.User")
pre_save.connect(send_email_link, "auth.User")
post_save.connect(create_admin_registration, "auth.User") post_save.connect(create_admin_registration, "auth.User")

View File

@ -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'),
),
]

View File

@ -1,7 +1,13 @@
from django.contrib.sites.models import Site
from django.db import models 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 django.utils.translation import gettext_lazy as _
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
from corres2math.tokens import email_validation_token
class Registration(PolymorphicModel): class Registration(PolymorphicModel):
user = models.OneToOneField( user = models.OneToOneField(
@ -15,6 +21,36 @@ class Registration(PolymorphicModel):
verbose_name=_("Grant Animath to contact me in the future about other actions"), 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 @property
def type(self): def type(self):
raise NotImplementedError raise NotImplementedError

View File

@ -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, **_): def set_username(instance, **_):
instance.username = instance.email 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, **_): def create_admin_registration(instance, **_):
if instance.is_superuser: if instance.is_superuser:
AdminRegistration.objects.get_or_create(user=instance) AdminRegistration.objects.get_or_create(user=instance)

View File

@ -1,9 +1,13 @@
from django.urls import path from django.urls import path
from .views import SignupView from .views import SignupView, UserValidationEmailSentView, UserResendValidationEmailView, UserValidateView
app_name = "registration" app_name = "registration"
urlpatterns = [ urlpatterns = [
path("signup", SignupView.as_view(), name="signup"), path("signup", SignupView.as_view(), name="signup"),
path('validate_email/sent/', UserValidationEmailSentView.as_view(), name='email_validation_sent'),
path('validate_email/resend/<int:pk>/', UserResendValidationEmailView.as_view(),
name='email_validation_resend'),
path('validate_email/<uidb64>/<token>/', UserValidateView.as_view(), name='email_validation'),
] ]

View File

@ -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.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db import transaction from django.db import transaction
from django.shortcuts import resolve_url, redirect
from django.urls import reverse_lazy 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 from .forms import SignupForm, StudentRegistrationForm, CoachRegistrationForm
@ -39,3 +46,78 @@ class SignupView(CreateView):
def get_success_url(self): def get_success_url(self):
return reverse_lazy("index") 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')

View File

@ -1,5 +1,5 @@
# m h dom mon dow user command # min hour day month weekday command
# Envoyer les mails en attente # Envoyer les mails en attente
* * * * * root cd /code && python manage.py send_mail -c 1 * * * * * cd /code && python manage.py send_mail -c 1
* * * * * root cd /code && python manage.py retry_deferred -c 1 * * * * * cd /code && python manage.py retry_deferred -c 1
00 0 * * * root cd /code && python manage.py purge_mail_log 7 -c 1 0 0 * * * cd /code && python manage.py purge_mail_log 7 -c 1

View File

@ -1,5 +1,4 @@
# Database # Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases # https://docs.djangoproject.com/en/3.0/ref/settings/#databases
EMAIL_BACKEND = 'mailer.backend.DbBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
MAILER_EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

26
corres2math/tokens.py Normal file
View File

@ -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()

View File

@ -1,5 +1,7 @@
#!/bin/sh #!/bin/sh
crond -l 0
python manage.py compilemessages python manage.py compilemessages
python manage.py migrate python manage.py migrate

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Corres2math\n" "Project-Id-Version: Corres2math\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Yohann D'ANELLO <yohann.danello@animath.fr>\n" "Last-Translator: Yohann D'ANELLO <yohann.danello@animath.fr>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -25,7 +25,7 @@ msgstr "API"
msgid "Logs" msgid "Logs"
msgstr "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" msgid "user"
msgstr "utilisateur" msgstr "utilisateur"
@ -115,7 +115,8 @@ msgid ""
"Give the authorisation to publish the video on the main website to promote " "Give the authorisation to publish the video on the main website to promote "
"the action." "the action."
msgstr "" 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 #: apps/participation/models.py:35
#, python-brace-format #, python-brace-format
@ -123,7 +124,7 @@ msgid "Team {name} ({trigram})"
msgstr "Équipe {name} ({trigram})" msgstr "Équipe {name} ({trigram})"
#: apps/participation/models.py:38 apps/participation/models.py:49 #: 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" msgid "team"
msgstr "équipe" msgstr "équipe"
@ -202,85 +203,93 @@ msgstr "rôle"
msgid "participant" msgid "participant"
msgstr "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" msgid "coach"
msgstr "encadrant" msgstr "encadrant"
#: apps/registration/models.py:15 #: apps/registration/models.py:21
msgid "Grant Animath to contact me in the future about other actions" msgid "Grant Animath to contact me in the future about other actions"
msgstr "" msgstr ""
"Autoriser Animath à me recontacter à l'avenir à propos d'autres actions" "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 #, python-brace-format
msgid "registration of {first_name} {last_name}" msgid "registration of {first_name} {last_name}"
msgstr "inscription de {first_name} {last_name}" msgstr "inscription de {first_name} {last_name}"
#: apps/registration/models.py:27 #: apps/registration/models.py:59
msgid "registration" msgid "registration"
msgstr "inscription" msgstr "inscription"
#: apps/registration/models.py:28 #: apps/registration/models.py:60
msgid "registrations" msgid "registrations"
msgstr "inscriptions" msgstr "inscriptions"
#: apps/registration/models.py:43 #: apps/registration/models.py:75
msgid "12th grade" msgid "12th grade"
msgstr "Terminale" msgstr "Terminale"
#: apps/registration/models.py:44 #: apps/registration/models.py:76
msgid "11th grade" msgid "11th grade"
msgstr "Première" msgstr "Première"
#: apps/registration/models.py:45 #: apps/registration/models.py:77
msgid "10th grade or lower" msgid "10th grade or lower"
msgstr "Seconde ou inférieur" msgstr "Seconde ou inférieur"
#: apps/registration/models.py:47 #: apps/registration/models.py:79
msgid "student class" msgid "student class"
msgstr "classe" msgstr "classe"
#: apps/registration/models.py:52 #: apps/registration/models.py:84
msgid "school" msgid "school"
msgstr "école" msgstr "école"
#: apps/registration/models.py:57 #: apps/registration/models.py:89
msgid "student" msgid "student"
msgstr "étudiant" msgstr "étudiant"
#: apps/registration/models.py:60 #: apps/registration/models.py:92
msgid "student registration" msgid "student registration"
msgstr "inscription d'élève" msgstr "inscription d'élève"
#: apps/registration/models.py:61 #: apps/registration/models.py:93
msgid "student registrations" msgid "student registrations"
msgstr "inscriptions d'élève" msgstr "inscriptions d'élève"
#: apps/registration/models.py:75 #: apps/registration/models.py:107
msgid "professional activity" msgid "professional activity"
msgstr "activité professionnelle" msgstr "activité professionnelle"
#: apps/registration/models.py:83 #: apps/registration/models.py:115
msgid "coach registration" msgid "coach registration"
msgstr "inscription d'encadrant" msgstr "inscription d'encadrant"
#: apps/registration/models.py:84 #: apps/registration/models.py:116
msgid "coach registrations" msgid "coach registrations"
msgstr "inscriptions d'encadrants" msgstr "inscriptions d'encadrants"
#: apps/registration/models.py:89 #: apps/registration/models.py:121
msgid "role of the administrator" msgid "role of the administrator"
msgstr "rôle de l'administrateur" msgstr "rôle de l'administrateur"
#: apps/registration/models.py:94 #: apps/registration/models.py:126
msgid "admin" msgid "admin"
msgstr "admin" msgstr "admin"
#: apps/registration/models.py:97 #: apps/registration/models.py:129
msgid "admin registration" msgid "admin registration"
msgstr "inscription d'administrateur" msgstr "inscription d'administrateur"
#: apps/registration/models.py:98 #: apps/registration/models.py:130
msgid "admin registrations" msgid "admin registrations"
msgstr "inscriptions d'administrateur" msgstr "inscriptions d'administrateur"
@ -443,6 +452,8 @@ msgid ""
"You recently registered on the Correspondances platform. Please click on the " "You recently registered on the Correspondances platform. Please click on the "
"link below to confirm your registration." "link below to confirm your registration."
msgstr "" 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.html:26
#: templates/registration/mails/email_validation_email.txt:9 #: 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 " "This link is only valid for a couple of days, after that you will need to "
"contact us to validate your email." "contact us to validate your email."
msgstr "" 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.html:30
#: templates/registration/mails/email_validation_email.txt:11 #: 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.html:35
#: templates/registration/mails/email_validation_email.txt:13 #: templates/registration/mails/email_validation_email.txt:13
msgid "The CNO." msgid "The Correspondances team."
msgstr "" msgstr "L'équipe des Correspondances"
#: templates/registration/password_change_done.html:8 #: templates/registration/password_change_done.html:8
msgid "Your password was changed." msgid "Your password was changed."

View File

@ -17,8 +17,8 @@
</p> </p>
<p> <p>
<a href="https://{{ domain }}{% url 'member:email_validation' uidb64=uid token=token %}"> <a href="https://{{ domain }}{% url 'registration:email_validation' uidb64=uid token=token %}">
https://{{ domain }}{% url 'member:email_validation' uidb64=uid token=token %} https://{{ domain }}{% url 'registration:email_validation' uidb64=uid token=token %}
</a> </a>
</p> </p>
@ -32,5 +32,5 @@
-- --
<p> <p>
{% trans "The CNO." %}<br> {% trans "The Correspondances team." %}<br>
</p> </p>

View File

@ -4,10 +4,10 @@
{% trans "You recently registered on the Correspondances platform. Please click on the link below to confirm your registration." %} {% 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 "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 "Thanks" %},
{% trans "The CNO." %} {% trans "The Correspondances team." %}