Validate emails
This commit is contained in:
parent
83d396a6dc
commit
30fa8b7840
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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()
|
|
@ -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"),
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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é"
|
||||||
|
|
|
@ -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 %}
|
||||||
|
<div class="card bg-light">
|
||||||
|
<h3 class="card-header text-center">
|
||||||
|
{{ title }}
|
||||||
|
</h3>
|
||||||
|
<div class="card-body">
|
||||||
{% if validlink %}
|
{% if validlink %}
|
||||||
|
<p>
|
||||||
{% trans "Your email have successfully been validated." %}
|
{% trans "Your email have successfully been validated." %}
|
||||||
{% if user_object.profile.registration_valid %}
|
</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>
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
{% trans "The link was invalid. The token may have expired. Please send us an email to activate your account." %}
|
{% trans "The link was invalid. The token may have expired. Please send us an email to activate your account." %}
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -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 %}
|
|
@ -1,15 +1,36 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<p>
|
||||||
{% trans "Hi" %} {{ user.username }},
|
{% trans "Hi" %} {{ user.username }},
|
||||||
|
</p>
|
||||||
|
|
||||||
{% trans "You recently registered on the Note Kfet. Please click on the link below to confirm your registration." %}
|
<p>
|
||||||
|
{% trans "You recently registered on the TFJM² platform. Please click on the link below to confirm your registration." %}
|
||||||
|
</p>
|
||||||
|
|
||||||
https://{{ domain }}{% url 'registration:email_validation' uidb64=uid token=token %}
|
<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>
|
||||||
|
|
||||||
|
<p>
|
||||||
{% 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." %}
|
||||||
|
</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 "Thanks" %},
|
{% trans "Thanks" %},
|
||||||
|
</p>
|
||||||
|
|
||||||
{% trans "The Note Kfet team." %}
|
--
|
||||||
|
<p>
|
||||||
|
{% trans "The CNO." %}<br>
|
||||||
|
</p>
|
||||||
|
|
|
@ -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." %}
|
Loading…
Reference in New Issue