Validate emails

This commit is contained in:
Yohann D'ANELLO 2020-09-20 21:24:52 +02:00
parent 83d396a6dc
commit 30fa8b7840
10 changed files with 244 additions and 46 deletions

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1 on 2020-09-19 20:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0002_auto_20200919_2015'),
]
operations = [
migrations.AddField(
model_name='tfjmuser',
name='email_confirmed',
field=models.BooleanField(default=False, verbose_name='email confirmed'),
),
]

View File

@ -2,12 +2,17 @@ import os
from datetime import date from datetime import date
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
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 tournament.models import Team, Tournament from tournament.models import Team, Tournament
from .tokens import email_validation_token
class TFJMUser(AbstractUser): class TFJMUser(AbstractUser):
""" """
@ -144,6 +149,11 @@ class TFJMUser(AbstractUser):
verbose_name=_("year"), verbose_name=_("year"),
) )
email_confirmed = models.BooleanField(
verbose_name=_("email confirmed"),
default=False,
)
@property @property
def participates(self): def participates(self):
""" """
@ -179,6 +189,27 @@ class TFJMUser(AbstractUser):
def __str__(self): def __str__(self):
return self.first_name + " " + self.last_name return self.first_name + " " + self.last_name
def send_email_validation_link(self):
subject = "[TFJM²] " + str(_("Activate your Note Kfet account"))
token = email_validation_token.make_token(self)
uid = urlsafe_base64_encode(force_bytes(self.pk))
site = Site.objects.first()
message = loader.render_to_string('registration/mails/email_validation_email.txt',
{
'user': self,
'domain': site.domain,
'token': token,
'uid': uid,
})
html = loader.render_to_string('registration/mails/email_validation_email.html',
{
'user': self,
'domain': site.domain,
'token': token,
'uid': uid,
})
self.email_user(subject, message, html_message=html)
class Document(PolymorphicModel): class Document(PolymorphicModel):
""" """

26
apps/member/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(user.email_confirmed) + str(login_timestamp) + str(timestamp)
email_validation_token = AccountActivationTokenGenerator()

View File

@ -1,12 +1,17 @@
from django.urls import path from django.urls import path
from .views import CreateUserView, MyAccountView, UserDetailView, AddTeamView, JoinTeamView, MyTeamView,\ from .views import CreateUserView, MyAccountView, UserDetailView, AddTeamView, JoinTeamView, MyTeamView, \
ProfileListView, OrphanedProfileListView, OrganizersListView, ResetAdminView ProfileListView, OrphanedProfileListView, OrganizersListView, ResetAdminView, UserValidationEmailSentView, \
UserResendValidationEmailView, UserValidateView
app_name = "member" app_name = "member"
urlpatterns = [ urlpatterns = [
path('signup/', CreateUserView.as_view(), name="signup"), path('signup/', CreateUserView.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'),
path("my-account/", MyAccountView.as_view(), name="my_account"), path("my-account/", MyAccountView.as_view(), name="my_account"),
path("information/<int:pk>/", UserDetailView.as_view(), name="information"), path("information/<int:pk>/", UserDetailView.as_view(), name="information"),
path("add-team/", AddTeamView.as_view(), name="add_team"), path("add-team/", AddTeamView.as_view(), name="add_team"),

View File

@ -1,18 +1,20 @@
import random import random
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin, AccessMixin from django.contrib.auth.mixins import LoginRequiredMixin, AccessMixin
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied, ValidationError
from django.db.models import Q from django.db.models import Q
from django.http import FileResponse, Http404 from django.http import FileResponse, Http404
from django.shortcuts import redirect from django.shortcuts import redirect, resolve_url
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.http import urlsafe_base64_decode
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from django.views.decorators.debug import sensitive_post_parameters from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import CreateView, UpdateView, DetailView, FormView from django.views.generic import CreateView, UpdateView, DetailView, FormView, TemplateView
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from tournament.forms import TeamForm, JoinTeam from tournament.forms import TeamForm, JoinTeam
from tournament.models import Team, Tournament, Pool from tournament.models import Team, Tournament, Pool
@ -21,6 +23,7 @@ from tournament.views import AdminMixin, TeamMixin, OrgaMixin
from .forms import SignUpForm, TFJMUserForm, AdminUserForm, CoachUserForm from .forms import SignUpForm, TFJMUserForm, AdminUserForm, CoachUserForm
from .models import TFJMUser, Document, Solution, MotivationLetter, Synthesis from .models import TFJMUser, Document, Solution, MotivationLetter, Synthesis
from .tables import UserTable from .tables import UserTable
from .tokens import email_validation_token
class CreateUserView(CreateView): class CreateUserView(CreateView):
@ -36,8 +39,87 @@ class CreateUserView(CreateView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
form.instance.send_email_validation_link()
return super().form_valid(form)
def get_success_url(self): def get_success_url(self):
return reverse_lazy('index') return reverse_lazy('member:email_validation_sent')
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.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 = TFJMUser.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, TFJMUser.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 = TFJMUser
extra_context = {"title": _("Resend email validation link")}
def get(self, request, *args, **kwargs):
user = self.get_object()
user.profile.send_email_validation_link()
url = 'member:user_detail' if user.profile.registration_valid else 'member:future_user_detail'
return redirect(url, user.id)
class MyAccountView(LoginRequiredMixin, UpdateView): class MyAccountView(LoginRequiredMixin, UpdateView):

View File

@ -917,11 +917,6 @@ msgstr "Votre adresse e-mail a bien été validée."
msgid "You can now <a href=\"%(login_url)s\">log in</a>." msgid "You can now <a href=\"%(login_url)s\">log in</a>."
msgstr "Vous pouvez désormais <a href=\"%(login_url)s\">vous connecter</a>" msgstr "Vous pouvez désormais <a href=\"%(login_url)s\">vous connecter</a>"
#: templates/registration/email_validation_complete.html:10
msgid ""
"You must pay now your membership in the Kfet to complete your registration."
msgstr ""
#: templates/registration/email_validation_complete.html:13 #: templates/registration/email_validation_complete.html:13
msgid "" msgid ""
"The link was invalid. The token may have expired. Please send us an email to " "The link was invalid. The token may have expired. Please send us an email to "
@ -956,32 +951,16 @@ msgstr "Mot de passe oublié ?"
msgid "Hi" msgid "Hi"
msgstr "Bonjour" msgstr "Bonjour"
#: templates/registration/mails/email_validation_email.html:5
msgid ""
"You recently registered on the Note Kfet. Please click on the link below to "
"confirm your registration."
msgstr ""
#: templates/registration/mails/email_validation_email.html:9 #: templates/registration/mails/email_validation_email.html:9
msgid "" 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 ""
#: templates/registration/mails/email_validation_email.html:11
msgid ""
"After that, you'll have to wait that someone validates your account before "
"you can log in. You will need to pay your membership in the Kfet."
msgstr ""
#: templates/registration/mails/email_validation_email.html:13 #: templates/registration/mails/email_validation_email.html:13
msgid "Thanks" msgid "Thanks"
msgstr "Merci" msgstr "Merci"
#: templates/registration/mails/email_validation_email.html:15
msgid "The Note Kfet team."
msgstr ""
#: templates/registration/password_change_done.html:8 #: templates/registration/password_change_done.html:8
msgid "Your password was changed." msgid "Your password was changed."
msgstr "Votre mot de passe a été changé" msgstr "Votre mot de passe a été changé"

View File

@ -1,15 +1,27 @@
{% extends "base.html" %} {% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
{% if validlink %} <div class="card bg-light">
{% trans "Your email have successfully been validated." %} <h3 class="card-header text-center">
{% if user_object.profile.registration_valid %} {{ title }}
</h3>
<div class="card-body">
{% if validlink %}
<p>
{% trans "Your email have successfully been validated." %}
</p>
<p>
{% blocktrans %}You can now <a href="{{ login_url }}">log in</a>.{% endblocktrans %} {% blocktrans %}You can now <a href="{{ login_url }}">log in</a>.{% endblocktrans %}
</p>
{% else %} {% else %}
{% trans "You must pay now your membership in the Kfet to complete your registration." %} <p>
{% trans "The link was invalid. The token may have expired. Please send us an email to activate your account." %}
</p>
{% endif %} {% endif %}
{% else %} </div>
{% trans "The link was invalid. The token may have expired. Please send us an email to activate your account." %} </div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -1,7 +1,18 @@
{% extends "base.html" %} {% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block content %} {% block content %}
<h2>Account Activation</h2> <div class="card bg-light">
<h3 class="card-header text-center">
An email has been sent. Please click on the link to activate your account. {% trans "Account activation" %}
</h3>
<div class="card-body">
<p>
{% trans "An email has been sent. Please click on the link to activate your account." %}
</p>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,15 +1,36 @@
{% load i18n %} {% load i18n %}
{% trans "Hi" %} {{ user.username }}, <!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
{% trans "You recently registered on the Note Kfet. Please click on the link below to confirm your registration." %} <p>
{% trans "Hi" %} {{ user.username }},
</p>
https://{{ domain }}{% url 'registration:email_validation' uidb64=uid token=token %} <p>
{% trans "You recently registered on the TFJM² platform. Please click on the link below to confirm your registration." %}
</p>
{% trans "This link is only valid for a couple of days, after that you will need to contact us to validate your email." %} <p>
<a href="https://{{ domain }}{% url 'member:email_validation' uidb64=uid token=token %}">
https://{{ domain }}{% url 'member:email_validation' uidb64=uid token=token %}
</a>
</p>
{% trans "After that, you'll have to wait that someone validates your account before you can log in. You will need to pay your membership in the Kfet." %} <p>
{% trans "This link is only valid for a couple of days, after that you will need to contact us to validate your email." %}
</p>
{% trans "Thanks" %}, <p>
{% trans "Thanks" %},
</p>
{% trans "The Note Kfet team." %} --
<p>
{% trans "The CNO." %}<br>
</p>

View File

@ -0,0 +1,13 @@
{% load i18n %}
{% trans "Hi" %} {{ user.username }},
{% trans "You recently registered on the TFJM² platform. Please click on the link below to confirm your registration." %}
https://{{ domain }}{% url 'member: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." %}