From 6f708be8c1e0cdee189fc3a6fd28fc3295c24f6d Mon Sep 17 00:00:00 2001
From: "J.-S. Caux" <J.S.Caux@uva.nl>
Date: Sat, 23 Jan 2016 16:56:59 +0100
Subject: [PATCH] Enforce (secure) email validation upon registration request

---
 scipost/models.py                             |  2 +
 scipost/templates/scipost/activation_ack.html | 12 +++
 .../templates/scipost/already_activated.html  | 12 +++
 scipost/templates/scipost/personal_page.html  |  1 +
 .../scipost/request_new_activation_link.html  | 12 +++
 .../request_new_activation_link_ack.html      | 12 +++
 scipost/urls.py                               |  6 ++
 scipost/views.py                              | 86 +++++++++++++++++--
 8 files changed, 138 insertions(+), 5 deletions(-)
 create mode 100644 scipost/templates/scipost/activation_ack.html
 create mode 100644 scipost/templates/scipost/already_activated.html
 create mode 100644 scipost/templates/scipost/request_new_activation_link.html
 create mode 100644 scipost/templates/scipost/request_new_activation_link_ack.html

diff --git a/scipost/models.py b/scipost/models.py
index 7e7f1cb44..9876354cd 100644
--- a/scipost/models.py
+++ b/scipost/models.py
@@ -45,6 +45,8 @@ title_dict = dict(TITLE_CHOICES)
 class Contributor(models.Model):
     """ All users of SciPost are Contributors. Permissions determine the sub-types. """
     user = models.OneToOneField(User)
+    activation_key = models.CharField(max_length=40, default='')
+    key_expires = models.DateTimeField(default=timezone.now)
     rank = models.SmallIntegerField(default=0, choices=CONTRIBUTOR_RANKS)
     title = models.CharField(max_length=4, choices=TITLE_CHOICES)
     # username, password, email, first_name and last_name are inherited from User
diff --git a/scipost/templates/scipost/activation_ack.html b/scipost/templates/scipost/activation_ack.html
new file mode 100644
index 000000000..9f1049e21
--- /dev/null
+++ b/scipost/templates/scipost/activation_ack.html
@@ -0,0 +1,12 @@
+{% extends 'scipost/base.html' %}
+
+{% block pagetitle %}: activation (email confirmed){% endblock pagetitle %}
+
+{% block bodysup %}
+
+<section>
+  <h1>Your email address has been confirmed</h1>
+  <p>Your SciPost account will soon be vetted.</p>
+</section>
+
+{% endblock bodysup %}
diff --git a/scipost/templates/scipost/already_activated.html b/scipost/templates/scipost/already_activated.html
new file mode 100644
index 000000000..e2b5be6a5
--- /dev/null
+++ b/scipost/templates/scipost/already_activated.html
@@ -0,0 +1,12 @@
+{% extends 'scipost/base.html' %}
+
+{% block pagetitle %}: already activated (email confirmed){% endblock pagetitle %}
+
+{% block bodysup %}
+
+<section>
+  <h1>Your email has already been confirmed.</h1>
+  <p>Please wait for vetting of your registration. We shall strive to send you an update by email within 24 hours.</p>
+</section>
+
+{% endblock bodysup %}
diff --git a/scipost/templates/scipost/personal_page.html b/scipost/templates/scipost/personal_page.html
index 143b38be1..e8998d97a 100644
--- a/scipost/templates/scipost/personal_page.html
+++ b/scipost/templates/scipost/personal_page.html
@@ -37,6 +37,7 @@
     <h3>Registration actions</h3>
     <ul>
       <li><a href="{% url 'scipost:vet_registration_requests' %}">Vet Registration requests</a> ({{ nr_reg_to_vet }})</li>
+      <li>Awaiting validation ({{ nr_reg_awaiting_validation }}) (no action necessary)</li>
     </ul>
   </div>
   {% endif %}
diff --git a/scipost/templates/scipost/request_new_activation_link.html b/scipost/templates/scipost/request_new_activation_link.html
new file mode 100644
index 000000000..d17db11c8
--- /dev/null
+++ b/scipost/templates/scipost/request_new_activation_link.html
@@ -0,0 +1,12 @@
+{% extends 'scipost/base.html' %}
+
+{% block pagetitle %}: request new activation link{% endblock pagetitle %}
+
+{% block bodysup %}
+
+<section>
+  <h1>Request a new activation link</h1>
+  <p>Your previous activation link has expired. <a href="{% url 'scipost:request_new_activation_link' oldkey=oldkey %}">Click here</a> to have us email you a new one.</p>
+</section>
+
+{% endblock bodysup %}
diff --git a/scipost/templates/scipost/request_new_activation_link_ack.html b/scipost/templates/scipost/request_new_activation_link_ack.html
new file mode 100644
index 000000000..406d4892c
--- /dev/null
+++ b/scipost/templates/scipost/request_new_activation_link_ack.html
@@ -0,0 +1,12 @@
+{% extends 'scipost/base.html' %}
+
+{% block pagetitle %}: request new activation link (email) ack{% endblock pagetitle %}
+
+{% block bodysup %}
+
+<section>
+  <h1>We have emailed you a new activation link.</h1>
+  <p>Please acknowledge it within its 48 hours validity window if you want us to proceed with vetting your registraion.</p>
+</section>
+
+{% endblock bodysup %}
diff --git a/scipost/urls.py b/scipost/urls.py
index 82c864196..2459ab3e0 100644
--- a/scipost/urls.py
+++ b/scipost/urls.py
@@ -17,6 +17,12 @@ urlpatterns = [
     url(r'^thanks_for_registering$', views.thanks_for_registering, name='thanks for registering'),
     url(r'^vet_registration_requests$', views.vet_registration_requests, name='vet_registration_requests'),
     url(r'^vet_registration_request_ack/(?P<contributor_id>[0-9]+)$', views.vet_registration_request_ack, name='vet_registration_request_ack'),
+    url(r'^activation/(?P<key>.+)$', views.activation, name='activation'),
+    url(r'^activation_ack$', views.activation_ack, name='activation_ack'),
+    url(r'^request_new_activation_link/(?P<oldkey>.+)$', views.request_new_activation_link, name='request_new_activation_link'),
+    #url(r'^request_new_activation_link$', views.request_new_activation_link, name='request_new_activation_link'),
+    url(r'^request_new_activation_link_ack$', views.request_new_activation_link_ack, name='request_new_activation_link_ack'),
+    url(r'^already_activated$', views.already_activated, name='already_activated'),
     ## Authentication
     url(r'^login$', views.login_view, name='login'),
     url(r'^logout$', views.logout_view, name='logout'),
diff --git a/scipost/views.py b/scipost/views.py
index ef7aa8921..ccd75ee4b 100644
--- a/scipost/views.py
+++ b/scipost/views.py
@@ -1,4 +1,8 @@
 import datetime
+import hashlib
+import random
+import string
+
 from django.utils import timezone
 from django.shortcuts import get_object_or_404, render
 from django.contrib.auth import authenticate, login, logout
@@ -52,6 +56,8 @@ title_dict = dict(TITLE_CHOICES)
 reg_ref_dict = dict(REGISTRATION_REFUSAL_CHOICES)
 
 def register(request):
+    if request.user.is_authenticated():
+        return HttpResponseRedirect('personal_page')
     # If POST, process the form data
     if request.method == 'POST':
         # create a form instance and populate it with the form data
@@ -72,18 +78,32 @@ def register(request):
                 username = form.cleaned_data['username'],
                 password = form.cleaned_data['password']
                 )
+            # Set to inactive until activation via email link
+            user.is_active = False 
+            user.save()
             contributor = Contributor (
                 user=user, 
                 title = form.cleaned_data['title'],
                 orcid_id = form.cleaned_data['orcid_id'],
                 nationality = form.cleaned_data['nationality'],
                 country_of_employment = form.cleaned_data['country_of_employment'],
-                #address = form.cleaned_data['address'],
+                address = form.cleaned_data['address'],
                 affiliation = form.cleaned_data['affiliation'],
                 personalwebpage = form.cleaned_data['personalwebpage'],
                 )
             contributor.save()
-            email_text = 'Dear ' + title_dict[contributor.title] + ' ' + contributor.user.last_name + ', \n\nYour request for registration to the SciPost publication portal has been received, and will be processed soon. Many thanks for your interest.  \n\nThe SciPost Team.'
+            # Generate email activation key and link
+            salt = ""
+            for i in range(5):
+                salt = salt + random.choice(string.ascii_letters)
+            #salt = hashlib.sha1(str(random.random()).encode('utf8')).hexdigest()[:5]
+            salt = salt.encode('utf8')
+            usernamesalt = contributor.user.username
+            usernamesalt = usernamesalt.encode('utf8')
+            contributor.activation_key = hashlib.sha1(salt+usernamesalt).hexdigest()
+            contributor.key_expires = datetime.datetime.strftime(datetime.datetime.now() + datetime.timedelta(days=2), "%Y-%m-%d %H:%M:%S")
+            contributor.save()
+            email_text = 'Dear ' + title_dict[contributor.title] + ' ' + contributor.user.last_name + ', \n\nYour request for registration to the SciPost publication portal has been received. You now need to validate your email by visiting this link within the next 48 hours: \n\n' + 'https://scipost.org/activation/' + contributor.activation_key + '\n\nYour registration will thereafter be vetted. Many thanks for your interest.  \n\nThe SciPost Team.'
             emailmessage = EmailMessage('SciPost registration request received', email_text, 'registration@scipost.org', [contributor.user.email, 'registration@scipost.org'], reply_to=['registration@scipost.org'])
             emailmessage.send(fail_silently=False)
             return HttpResponseRedirect('thanks_for_registering')
@@ -99,9 +119,61 @@ def thanks_for_registering(request):
     return render(request, 'scipost/thanks_for_registering.html')
 
 
+def activation(request, key):
+    activation_expired = False
+    already_active = False
+    contributor = get_object_or_404(Contributor, activation_key=key)
+    if contributor.user.is_active == False:
+        if timezone.now() > contributor.key_expires:
+            activation_expired = True
+            id_user = contributor.user.id
+            context = {'oldkey': key}
+            return render(request, 'scipost/request_new_activation_link.html', context)
+        else: 
+            contributor.user.is_active = True
+            contributor.user.save()
+            return render(request, 'scipost/activation_ack.html')
+    else:
+        already_active = True
+        return render(request, 'scipost/already_activated.html')
+    # will never come beyond here
+    return render(request, 'scipost/index.html')
+
+
+def activation_ack(request):
+    return render(request, 'scipost/activation_ack.html')
+
+
+def request_new_activation_link(request, oldkey):
+    contributor = get_object_or_404(Contributor, activation_key=oldkey)
+    # Generate a new email activation key and link
+    salt = ""
+    for i in range(5):
+        salt = salt + random.choice(string.ascii_letters)
+            #salt = hashlib.sha1(str(random.random()).encode('utf8')).hexdigest()[:5]
+    salt = salt.encode('utf8')
+    usernamesalt = contributor.user.username
+    usernamesalt = usernamesalt.encode('utf8')
+    contributor.activation_key = hashlib.sha1(salt+usernamesalt).hexdigest()
+    contributor.key_expires = datetime.datetime.strftime(datetime.datetime.now() + datetime.timedelta(days=2), "%Y-%m-%d %H:%M:%S")
+    contributor.save()
+    email_text = 'Dear ' + title_dict[contributor.title] + ' ' + contributor.user.last_name + ', \n\nYour request for a new email activation link for registration to the SciPost publication portal has been received. You now need to visit this link within the next 48 hours: \n\n' + 'https://scipost.org/activation/' + contributor.activation_key + '\n\nYour registration will thereafter be vetted. Many thanks for your interest.  \n\nThe SciPost Team.'
+    emailmessage = EmailMessage('SciPost registration: new email activation link', email_text, 'registration@scipost.org', [contributor.user.email, 'registration@scipost.org'], reply_to=['registration@scipost.org'])
+    emailmessage.send(fail_silently=False)
+    return render (request, 'scipost/request_new_activation_link_ack.html')
+
+
+def request_new_activation_link_ack(request):
+    return render (request, 'scipost/request_new_activation_link_ack.html')
+
+
+def already_activated(request):
+    return render (request, 'scipost/already_activated.html')
+
+
 def vet_registration_requests(request):
     contributor = Contributor.objects.get(user=request.user)
-    contributor_to_vet = Contributor.objects.filter(rank=0).first() # limit to one at a time
+    contributor_to_vet = Contributor.objects.filter(user__is_active=True, rank=0).first() # limit to one at a time
     form = VetRegistrationForm()
     context = {'contributor': contributor, 'contributor_to_vet': contributor_to_vet, 'form': form }
     return render(request, 'scipost/vet_registration_requests.html', context)
@@ -163,10 +235,14 @@ def personal_page(request):
         contributor = Contributor.objects.get(user=request.user)
         # if an editor, count the number of actions required:
         nr_reg_to_vet = 0
+        nr_reg_awaiting_validation = 0
         nr_submissions_to_process = 0
         if contributor.rank >= 4:
+            now = timezone.now()
+            intwodays = now + timezone.timedelta(days=2)
             # count the number of pending registration request
-            nr_reg_to_vet = Contributor.objects.filter(rank=0).count()
+            nr_reg_to_vet = Contributor.objects.filter(user__is_active=True, rank=0).count()
+            nr_reg_awaiting_validation = Contributor.objects.filter(user__is_active=False, key_expires__gte=now, key_expires__lte=intwodays, rank=0).count()
             nr_submissions_to_process = Submission.objects.filter(vetted=False).count()
         nr_commentary_page_requests_to_vet = 0
         nr_comments_to_vet = 0
@@ -177,7 +253,7 @@ def personal_page(request):
             nr_comments_to_vet = Comment.objects.filter(status=0).count()
             nr_author_replies_to_vet = AuthorReply.objects.filter(status=0).count()
             nr_reports_to_vet = Report.objects.filter(status=0).count()
-        context = {'contributor': contributor, 'nr_reg_to_vet': nr_reg_to_vet, 'nr_commentary_page_requests_to_vet': nr_commentary_page_requests_to_vet, 'nr_comments_to_vet': nr_comments_to_vet, 'nr_author_replies_to_vet': nr_author_replies_to_vet, 'nr_reports_to_vet': nr_reports_to_vet, 'nr_submissions_to_process': nr_submissions_to_process }
+        context = {'contributor': contributor, 'nr_reg_to_vet': nr_reg_to_vet, 'nr_reg_awaiting_validation': nr_reg_awaiting_validation, 'nr_commentary_page_requests_to_vet': nr_commentary_page_requests_to_vet, 'nr_comments_to_vet': nr_comments_to_vet, 'nr_author_replies_to_vet': nr_author_replies_to_vet, 'nr_reports_to_vet': nr_reports_to_vet, 'nr_submissions_to_process': nr_submissions_to_process }
         return render(request, 'scipost/personal_page.html', context)
     else:
         form = AuthenticationForm()
-- 
GitLab