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