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