From fa38cccd3c48790d7fb861183c6a9387d9ad6084 Mon Sep 17 00:00:00 2001 From: Bombar Maxime <bombar@crans.org> Date: Fri, 14 Feb 2020 00:01:59 +0100 Subject: [PATCH] Allow new users to sign in. Implement an email validation via a custom token based on PasswordResetToken. Please apply migrations. --- codeflix/codeflix/forms.py | 11 ++ codeflix/codeflix/migrations/0001_initial.py | 25 ++++ codeflix/codeflix/migrations/__init__.py | 0 codeflix/codeflix/models.py | 21 +++ codeflix/codeflix/settings.py | 1 + codeflix/codeflix/tokens.py | 26 ++++ codeflix/codeflix/urls.py | 5 + codeflix/codeflix/views.py | 120 ++++++++++++++++++ codeflix/templates/base.html | 5 +- .../account_activation_complete.html | 8 ++ .../account_activation_email.html | 11 ++ .../account_activation_email_sent.html | 7 + codeflix/templates/registration/signup.html | 15 +++ 13 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 codeflix/codeflix/forms.py create mode 100644 codeflix/codeflix/migrations/0001_initial.py create mode 100644 codeflix/codeflix/migrations/__init__.py create mode 100644 codeflix/codeflix/models.py create mode 100644 codeflix/codeflix/tokens.py create mode 100644 codeflix/codeflix/views.py create mode 100644 codeflix/templates/registration/account_activation_complete.html create mode 100644 codeflix/templates/registration/account_activation_email.html create mode 100644 codeflix/templates/registration/account_activation_email_sent.html create mode 100644 codeflix/templates/registration/signup.html diff --git a/codeflix/codeflix/forms.py b/codeflix/codeflix/forms.py new file mode 100644 index 0000000..23f260f --- /dev/null +++ b/codeflix/codeflix/forms.py @@ -0,0 +1,11 @@ +from django.contrib.auth.forms import UserCreationForm +from django.contrib.auth.models import User + + +class SignUpForm(UserCreationForm): + """ + Extends the django generic UserCreationForm to get all the fields. + """ + class Meta: + model = User + fields = ['first_name', 'last_name', 'username', 'email'] diff --git a/codeflix/codeflix/migrations/0001_initial.py b/codeflix/codeflix/migrations/0001_initial.py new file mode 100644 index 0000000..9cf8689 --- /dev/null +++ b/codeflix/codeflix/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0.2 on 2020-02-13 22:35 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email_confirmed', models.BooleanField(default=False)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/codeflix/codeflix/migrations/__init__.py b/codeflix/codeflix/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codeflix/codeflix/models.py b/codeflix/codeflix/models.py new file mode 100644 index 0000000..62cd63b --- /dev/null +++ b/codeflix/codeflix/models.py @@ -0,0 +1,21 @@ +from django.contrib.auth.models import User +from django.db import models +from django.db.models.signals import post_save +from django.dispatch import receiver + +# Create your models here. + + +class Profile(models.Model): + """ + The Profile of a user. Adds new fields. + """ + user = models.OneToOneField(User, on_delete=models.CASCADE) + email_confirmed = models.BooleanField(default=False) + + +@receiver(post_save, sender=User) +def update_user_profile(sender, instance, created, **kwargs): + if created: + Profile.objects.create(user=instance) + instance.profile.save() diff --git a/codeflix/codeflix/settings.py b/codeflix/codeflix/settings.py index 4b7ffd3..d1bea51 100644 --- a/codeflix/codeflix/settings.py +++ b/codeflix/codeflix/settings.py @@ -39,6 +39,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'codeflix', 'codeforces', # For the frontend diff --git a/codeflix/codeflix/tokens.py b/codeflix/codeflix/tokens.py new file mode 100644 index 0000000..4f2e22b --- /dev/null +++ b/codeflix/codeflix/tokens.py @@ -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.profile.email_confirmed) + str(login_timestamp) + str(timestamp) + + +account_activation_token = AccountActivationTokenGenerator() diff --git a/codeflix/codeflix/urls.py b/codeflix/codeflix/urls.py index 19366cb..e0156a7 100644 --- a/codeflix/codeflix/urls.py +++ b/codeflix/codeflix/urls.py @@ -17,9 +17,14 @@ from django.contrib import admin from django.contrib.auth import views as auth_views from django.urls import include, path +from . import views urlpatterns = [ path('', auth_views.LoginView.as_view(template_name='index.html'), name='index'), path('accounts/', include('django.contrib.auth.urls')), + path('accounts/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('accounts/activate/done', views.UserActivateDoneView.as_view(), name='account_activation_done'), path('admin/', admin.site.urls), ] diff --git a/codeflix/codeflix/views.py b/codeflix/codeflix/views.py new file mode 100644 index 0000000..d35f9a9 --- /dev/null +++ b/codeflix/codeflix/views.py @@ -0,0 +1,120 @@ +from django.contrib.auth.models import User +from django.contrib.sites.shortcuts import get_current_site +from django.core.exceptions import ValidationError +from django.shortcuts import resolve_url +from django.template import loader +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.views.decorators.csrf import csrf_protect +from django.views.generic import CreateView, TemplateView + +from . import settings +from .forms import SignUpForm +from .tokens import account_activation_token + +# create your views here. + + +class UserCreateView(CreateView): + """ + A view used to register a new user. + """ + email_template_name = 'registration/account_activation_email.html' + form_class = SignUpForm + success_url = reverse_lazy('account_activation_sent') + template_name = 'registration/signup.html' + title = _("Sign up") + token_generator = account_activation_token + + 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. + """ + use_https = self.request.is_secure() + user = form.save(commit=False) + user.is_active = False + user.save() # Sends a signal to create the corresponding profile object. + 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': site.name, + 'protocol': 'https' if use_https else 'http', + 'token': self.token_generator.make_token(user), + 'uid': urlsafe_base64_encode(force_bytes(user.pk)), + }) + user.email_user(subject, message) + return super().form_valid(form) + + +class UserActivateView(TemplateView): + succes_url = reverse_lazy('account_activation_done') + title = _("Account Activation") + + @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): + 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) + if self.validlink: + context['validlink'] = True + else: + context.update({ + 'title': _('Account Activation unsuccessful'), + 'validlink': False, + }) + return context + + +class UserActivateDoneView(TemplateView): + template_name = 'registration/account_activation_complete.html' + title = _('Account activation complte') + + def get_context_data(self, **kwargs): + """ + Provides the template with the login_url. + """ + context = super().get_context_data(**kwargs) + context['login_url'] = resolve_url(settings.LOGIN_URL) + return context + + +class UserActivationEmailSentView(TemplateView): + template_name = 'registration/account_activation_email_sent.html' + title = _('Account activation email sent') diff --git a/codeflix/templates/base.html b/codeflix/templates/base.html index d4b1511..ccc8e05 100644 --- a/codeflix/templates/base.html +++ b/codeflix/templates/base.html @@ -33,9 +33,10 @@ <li class="nav-item"> <a class="nav-link" href="#"> Welcome {{ user.get_username }} </a> </li> - <a class="nav-link" href="{% url 'logout' %}">{% icon "sign-out-alt"%} Logout </a> + <a class="nav-link" href="{% url 'logout' %}">{% icon "sign-out-alt"%} Log Out </a> {% else %} - <a class="nav-link" href="{% url 'login' %}">{% icon "sign-in-alt" %} Login </a> + <a class="nav-link" href="{% url 'signup' %}">{% icon "user-plus" %} Sign Up </a> + <a class="nav-link" href="{% url 'login' %}">{% icon "sign-in-alt" %} Log In </a> {% endif %} </li> </ul> diff --git a/codeflix/templates/registration/account_activation_complete.html b/codeflix/templates/registration/account_activation_complete.html new file mode 100644 index 0000000..5a7a01c --- /dev/null +++ b/codeflix/templates/registration/account_activation_complete.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block content %} +<h2>Account Activation</h2> + +Your account have successfully been activated. You can now <a href="{{ login_url }}">log in</a>. + +{% endblock %} diff --git a/codeflix/templates/registration/account_activation_email.html b/codeflix/templates/registration/account_activation_email.html new file mode 100644 index 0000000..e6fb54a --- /dev/null +++ b/codeflix/templates/registration/account_activation_email.html @@ -0,0 +1,11 @@ +Hi {{ user.username }}, + +Welcome to {{ site_name }}. Please click on the link below to confirm your registration. + +{{ protocol }}://{{ domain }}{% url '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. diff --git a/codeflix/templates/registration/account_activation_email_sent.html b/codeflix/templates/registration/account_activation_email_sent.html new file mode 100644 index 0000000..ddf2d97 --- /dev/null +++ b/codeflix/templates/registration/account_activation_email_sent.html @@ -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 %} diff --git a/codeflix/templates/registration/signup.html b/codeflix/templates/registration/signup.html new file mode 100644 index 0000000..3bad287 --- /dev/null +++ b/codeflix/templates/registration/signup.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% load bootstrap4 %} + +{% block title %}Sign up{% endblock %} + +{% block content %} + <form method="post" action=""> + {% csrf_token %} + {% bootstrap_form form %} + <button class="btn btn-success" type="submit"> + <i class="fa fa-sign-in"></i> Sign up + </button> + </form> +{% endblock %} -- GitLab