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 django.contrib.auth.models import AbstractUser
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 tournament.models import Team, Tournament
from .tokens import email_validation_token
class TFJMUser(AbstractUser):
"""
@ -144,6 +149,11 @@ class TFJMUser(AbstractUser):
verbose_name=_("year"),
)
email_confirmed = models.BooleanField(
verbose_name=_("email confirmed"),
default=False,
)
@property
def participates(self):
"""
@ -179,6 +189,27 @@ class TFJMUser(AbstractUser):
def __str__(self):
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):
"""

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 .views import CreateUserView, MyAccountView, UserDetailView, AddTeamView, JoinTeamView, MyTeamView,\
ProfileListView, OrphanedProfileListView, OrganizersListView, ResetAdminView
from .views import CreateUserView, MyAccountView, UserDetailView, AddTeamView, JoinTeamView, MyTeamView, \
ProfileListView, OrphanedProfileListView, OrganizersListView, ResetAdminView, UserValidationEmailSentView, \
UserResendValidationEmailView, UserValidateView
app_name = "member"
urlpatterns = [
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("information/<int:pk>/", UserDetailView.as_view(), name="information"),
path("add-team/", AddTeamView.as_view(), name="add_team"),

View File

@ -1,18 +1,20 @@
import random
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin, AccessMixin
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.http import FileResponse, Http404
from django.shortcuts import redirect
from django.shortcuts import redirect, resolve_url
from django.urls import reverse_lazy
from django.utils import timezone
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.views import View
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 tournament.forms import TeamForm, JoinTeam
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 .models import TFJMUser, Document, Solution, MotivationLetter, Synthesis
from .tables import UserTable
from .tokens import email_validation_token
class CreateUserView(CreateView):
@ -36,8 +39,87 @@ class CreateUserView(CreateView):
def dispatch(self, 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):
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):

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>."
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
msgid ""
"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"
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
msgid ""
"This link is only valid for a couple of days, after that you will need to "
"contact us to validate your email."
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
msgid "Thanks"
msgstr "Merci"
#: templates/registration/mails/email_validation_email.html:15
msgid "The Note Kfet team."
msgstr ""
#: templates/registration/password_change_done.html:8
msgid "Your password was changed."
msgstr "Votre mot de passe a été changé"

View File

@ -1,15 +1,27 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block content %}
{% if validlink %}
{% trans "Your email have successfully been validated." %}
{% if user_object.profile.registration_valid %}
<div class="card bg-light">
<h3 class="card-header text-center">
{{ 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 %}
</p>
{% 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 %}
{% else %}
{% trans "The link was invalid. The token may have expired. Please send us an email to activate your account." %}
{% endif %}
</div>
</div>
{% endblock %}

View File

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

View File

@ -1,15 +1,36 @@
{% 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." %}