Send an e-mail verification to a new registered user

This commit is contained in:
Yohann D'ANELLO 2020-04-05 04:26:42 +02:00
parent 72e5df0cf5
commit 26281af673
8 changed files with 179 additions and 14 deletions

View File

@ -4,6 +4,7 @@
from django import forms from django import forms
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
from permission.models import PermissionMask from permission.models import PermissionMask
@ -23,10 +24,14 @@ class SignUpForm(UserCreationForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['username'].widget.attrs.pop("autofocus", None) self.fields['username'].widget.attrs.pop("autofocus", None)
self.fields['first_name'].widget.attrs.update({"autofocus": "autofocus"}) self.fields['first_name'].widget.attrs.update({"autofocus": "autofocus"})
self.fields['first_name'].required = True
self.fields['last_name'].required = True
self.fields['email'].required = True
self.fields['email'].help_text = _("This address must be valid.")
class Meta: class Meta:
model = User model = User
fields = ['first_name', 'last_name', 'username', 'email'] fields = ('first_name', 'last_name', 'username', 'email', )
class ProfileForm(forms.ModelForm): class ProfileForm(forms.ModelForm):
@ -37,7 +42,7 @@ class ProfileForm(forms.ModelForm):
class Meta: class Meta:
model = Profile model = Profile
fields = '__all__' fields = '__all__'
exclude = ['user'] exclude = ('user', 'email_confirmed', )
class ClubForm(forms.ModelForm): class ClubForm(forms.ModelForm):

View File

@ -45,6 +45,12 @@ class Profile(models.Model):
) )
paid = models.BooleanField( paid = models.BooleanField(
verbose_name=_("paid"), verbose_name=_("paid"),
help_text=_("Tells if the user receive a salary."),
default=False,
)
email_confirmed = models.BooleanField(
verbose_name=_("email confirmed"),
default=False, default=False,
) )

30
apps/member/tokens.py Normal file
View File

@ -0,0 +1,30 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
# Copied from https://gitlab.crans.org/bombar/codeflix/-/blob/master/codeflix/codeflix/tokens.py
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.profile.email_confirmed) + str(login_timestamp) + str(timestamp)
account_activation_token = AccountActivationTokenGenerator()

View File

@ -8,6 +8,9 @@ from . import views
app_name = 'member' app_name = 'member'
urlpatterns = [ urlpatterns = [
path('signup/', views.UserCreateView.as_view(), name="signup"), path('signup/', views.UserCreateView.as_view(), name="signup"),
path('accounts/activate/sent', views.UserActivationEmailSentView.as_view(), name='account_activation_sent'),
path('accounts/activate/<uidb64>/<token>', views.UserActivateView.as_view(), name='account_activation'),
path('club/', views.ClubListView.as_view(), name="club_list"), path('club/', views.ClubListView.as_view(), name="club_list"),
path('club/create/', views.ClubCreateView.as_view(), name="club_create"), path('club/create/', views.ClubCreateView.as_view(), name="club_create"),

View File

@ -9,12 +9,18 @@ from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.views import LoginView from django.contrib.auth.views import LoginView
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Q from django.db.models import Q
from django.forms import HiddenInput from django.forms import HiddenInput
from django.shortcuts import redirect from django.shortcuts import redirect, resolve_url
from django.template import loader
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_protect
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
from django.views.generic.base import View from django.views.generic.base import View
from django.views.generic.edit import FormMixin from django.views.generic.edit import FormMixin
@ -30,6 +36,7 @@ from permission.views import ProtectQuerysetMixin
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm
from .models import Club, Membership from .models import Club, Membership
from .tables import ClubTable, UserTable, MembershipTable from .tables import ClubTable, UserTable, MembershipTable
from .tokens import account_activation_token
class CustomLoginView(LoginView): class CustomLoginView(LoginView):
@ -57,15 +64,90 @@ class UserCreateView(CreateView):
return context return context
def form_valid(self, form): def form_valid(self, form):
"""
If the form is valid, then the user is created with is_active set to False
so that the user cannot log in until the email has been validated.
"""
profile_form = ProfileForm(self.request.POST) profile_form = ProfileForm(self.request.POST)
if form.is_valid() and profile_form.is_valid(): if not profile_form.is_valid():
return self.form_invalid(form)
user = form.save(commit=False) user = form.save(commit=False)
user.is_active = False
user.profile = profile_form.save(commit=False) user.profile = profile_form.save(commit=False)
user.save() user.save()
user.profile.save() user.profile.save()
site = get_current_site(self.request)
subject = "Activate your {} account".format(site.name)
message = loader.render_to_string('registration/account_activation_email.html',
{
'user': user,
'domain': site.domain,
'site_name': "La Note Kfet",
'protocol': 'https',
'token': account_activation_token.make_token(user),
'uid': urlsafe_base64_encode(force_bytes(user.pk)).decode('UTF-8'),
})
user.email_user(subject, message)
return super().form_valid(form) return super().form_valid(form)
class UserActivateView(TemplateView):
title = _("Account Activation")
template_name = 'registration/account_activation_complete.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
"""
The dispatch method looks at the request to determine whether it is a GET, POST, etc,
and relays the request to a matching method if one is defined, or raises HttpResponseNotAllowed
if not. We chose to check the token in the dispatch method to mimic PasswordReset from
django.contrib.auth
"""
assert 'uidb64' in kwargs and 'token' in kwargs
self.validlink = False
user = self.get_user(kwargs['uidb64'])
token = kwargs['token']
if user is not None and account_activation_token.check_token(user, token):
self.validlink = True
user.is_active = True
user.profile.email_confirmed = True
user.save()
return super().dispatch(*args, **kwargs)
else:
# Display the "Account Activation unsuccessful" page.
return self.render_to_response(self.get_context_data())
def get_user(self, uidb64):
print(uidb64)
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['login_url'] = resolve_url(settings.LOGIN_URL)
if self.validlink:
context['validlink'] = True
else:
context.update({
'title': _('Account Activation unsuccessful'),
'validlink': False,
})
return context
class UserActivationEmailSentView(TemplateView):
template_name = 'registration/account_activation_email_sent.html'
title = _('Account activation email sent')
class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
model = User model = User
fields = ['first_name', 'last_name', 'username', 'email'] fields = ['first_name', 'last_name', 'username', 'email']
@ -75,14 +157,20 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
form = context['form']
form.fields['username'].widget.attrs.pop("autofocus", None)
form.fields['first_name'].widget.attrs.update({"autofocus": "autofocus"})
form.fields['first_name'].required = True
form.fields['last_name'].required = True
form.fields['email'].required = True
form.fields['email'].help_text = _("This address must be valid.")
context['profile_form'] = self.profile_form(instance=context['user_object'].profile) context['profile_form'] = self.profile_form(instance=context['user_object'].profile)
context['title'] = _("Update Profile") context['title'] = _("Update Profile")
return context return context
def get_form(self, form_class=None): def form_valid(self, form):
form = super().get_form(form_class)
if 'username' not in form.data:
return form
new_username = form.data['username'] new_username = form.data['username']
# Si l'utilisateur cherche à modifier son pseudo, le nouveau pseudo ne doit pas être proche d'un alias existant # Si l'utilisateur cherche à modifier son pseudo, le nouveau pseudo ne doit pas être proche d'un alias existant
note = NoteUser.objects.filter( note = NoteUser.objects.filter(
@ -90,9 +178,8 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
if note.exists() and note.get().user != self.object: if note.exists() and note.get().user != self.object:
form.add_error('username', form.add_error('username',
_("An alias with a similar name already exists.")) _("An alias with a similar name already exists."))
return form return super().form_invalid(form)
def form_valid(self, form):
profile_form = ProfileForm( profile_form = ProfileForm(
data=self.request.POST, data=self.request.POST,
instance=self.object.profile, instance=self.object.profile,

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h2>{% trans Activation %}</h2>
{% if validlink %}
{% blocktrans trimmed %}
Your account have successfully been activated. You can now <a href="{{ login_url }}">log in</a>.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
The link was invalid. The token may have expired. Please send us an email to activate your account.
{% endblocktrans %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,11 @@
Hi {{ user.username }},
Welcome to {{ site_name }}. Please click on the link below to confirm your registration.
{{ protocol }}://{{ domain }}{% url 'member:account_activation' uidb64=uid token=token %}
This link is only valid for a couple of days, after that you will need to contact us to validate your email.
Thanks,
{{ site_name }} team.

View File

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block content %}
<h2>Account Activation</h2>
An email has been sent. Please click on the link to activate your account.
{% endblock %}