From 0c34b56ff3e9887a592363ce92753a6a8383aa45 Mon Sep 17 00:00:00 2001 From: Yasin Date: Thu, 9 Jan 2014 18:09:18 +0100 Subject: [PATCH] ForgotPassword: DONE-Using the Django authentication system- Adapted to use manifold backend (not working because in actions.py manifold_update_user is not working with execute_admin_query- needed to be fixed) --- portal/django_passresetview.py | 194 ++++++++++++++++++ portal/forms.py | 101 +++++++++ portal/urls.py | 18 +- .../.password_reset_confirm.html.swp | Bin 0 -> 12288 bytes .../registration/password_reset_complete.html | 17 ++ .../registration/password_reset_confirm.html | 33 +++ .../registration/password_reset_done.html | 14 ++ .../registration/password_reset_email.html | 14 ++ .../registration/password_reset_form.html | 21 ++ 9 files changed, 409 insertions(+), 3 deletions(-) create mode 100644 portal/django_passresetview.py create mode 100644 ui/templates/registration/.password_reset_confirm.html.swp create mode 100644 ui/templates/registration/password_reset_complete.html create mode 100644 ui/templates/registration/password_reset_confirm.html create mode 100644 ui/templates/registration/password_reset_done.html create mode 100644 ui/templates/registration/password_reset_email.html create mode 100644 ui/templates/registration/password_reset_form.html diff --git a/portal/django_passresetview.py b/portal/django_passresetview.py new file mode 100644 index 00000000..1126f106 --- /dev/null +++ b/portal/django_passresetview.py @@ -0,0 +1,194 @@ +try: + from urllib.parse import urlparse, urlunparse +except ImportError: # Python 2 + from urlparse import urlparse, urlunparse + +from django.conf import settings +from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect, QueryDict +from django.template.response import TemplateResponse +from django.utils.http import base36_to_int, is_safe_url +from django.utils.translation import ugettext as _ +from django.shortcuts import resolve_url +from django.views.decorators.debug import sensitive_post_parameters +from django.views.decorators.cache import never_cache +from django.views.decorators.csrf import csrf_protect + +# Avoid shadowing the login() and logout() views below. +from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login, logout as auth_logout, get_user_model +from django.contrib.auth.decorators import login_required +from portal.forms import PasswordResetForm, SetPasswordForm +from django.contrib.auth.tokens import default_token_generator +from django.contrib.sites.models import get_current_site +from django.contrib.auth.hashers import UNUSABLE_PASSWORD, identify_hasher + +## +import os.path, re +import json + +from random import choice + +from django.core.mail import send_mail +from django.contrib import messages +from django.views.generic import View +from django.shortcuts import render +from django.http import HttpResponse, HttpResponseRedirect + +from unfold.loginrequired import FreeAccessView +from ui.topmenu import topmenu_items_live + +from manifold.manifoldapi import execute_admin_query +from manifold.core.query import Query +from portal.actions import manifold_update_user + +from portal.forms import PassResetForm +from portal.actions import manifold_update_user + + + +# 4 views for password reset: +# - password_reset sends the mail +# - password_reset_done shows a success message for the above +# - password_reset_confirm checks the link the user clicked and +# prompts for a new password +# - password_reset_complete shows a success message for the above + +@csrf_protect +def password_reset(request, is_admin_site=False, + template_name='registration/password_reset_form.html', + email_template_name='registration/password_reset_email.html', + subject_template_name='registration/password_reset_subject.txt', + password_reset_form=PasswordResetForm, + token_generator=default_token_generator, + post_reset_redirect=None, + from_email=None, + current_app=None, + extra_context=None): + if post_reset_redirect is None: + post_reset_redirect = reverse('portal.django_passresetview.password_reset_done') + if request.method == "POST": + form = password_reset_form(request.POST) + if form.is_valid(): + + ### email check in manifold DB ### + email = form.cleaned_data['email'] # email inserted on the form + user_query = Query().get('local:user').select('user_id','email') + user_details = execute_admin_query(request, user_query) + flag = 0 + for user_detail in user_details: + if user_detail['email']==email: + flag = 1 + break + + if flag == 0: + messages.error(request, 'Sorry, this email is not registered.') + return render(request, 'registration/password_reset_form.html', { + 'form': form, + }) + ### end of email check in manifold ### + + opts = { + 'use_https': request.is_secure(), + 'token_generator': token_generator, + 'from_email': from_email, + 'email_template_name': email_template_name, + 'subject_template_name': subject_template_name, + 'request': request, + } + if is_admin_site: + opts = dict(opts, domain_override=request.get_host()) + form.save(**opts) + return HttpResponseRedirect(post_reset_redirect) + else: + form = password_reset_form() + context = { + 'form': form, + } + if extra_context is not None: + context.update(extra_context) + return TemplateResponse(request, template_name, context, + current_app=current_app) + + +def password_reset_done(request, + template_name='registration/password_reset_done.html', + current_app=None, extra_context=None): + context = {} + if extra_context is not None: + context.update(extra_context) + return TemplateResponse(request, template_name, context, + current_app=current_app) + + +# Doesn't need csrf_protect since no-one can guess the URL +@sensitive_post_parameters() +@never_cache +def password_reset_confirm(request, uidb36=None, token=None, + template_name='registration/password_reset_confirm.html', + token_generator=default_token_generator, + set_password_form=SetPasswordForm, + post_reset_redirect=None, + current_app=None, extra_context=None): + """ + View that checks the hash in a password reset link and presents a + form for entering a new password. + """ + UserModel = get_user_model() + assert uidb36 is not None and token is not None # checked by URLconf + if post_reset_redirect is None: + post_reset_redirect = reverse('portal.django_passresetview.password_reset_complete') + try: + uid_int = base36_to_int(uidb36) + user = UserModel._default_manager.get(pk=uid_int) + except (ValueError, OverflowError, UserModel.DoesNotExist): + user = None + + if user is not None and token_generator.check_token(user, token): + validlink = True + if request.method == 'POST': + form = set_password_form(user, request.POST) + if form.is_valid(): + + ### manifold pass update ### + #password = form.cleaned_data('password1') + password=request.POST['new_password1'] + user_query = Query().get('local:user').select('user_id','email','password') + user_details = execute_admin_query(request, user_query) + for user_detail in user_details: + if user_detail['email'] == user.email: + user_detail['password'] = password + #updating password in local:user + user_params = { 'password': user_detail['password']} + manifold_update_user(request,user.email,user_params) + ### end of manifold pass update ### + + + form.save() + return HttpResponseRedirect(post_reset_redirect) + else: + form = set_password_form(None) + else: + validlink = False + form = None + context = { + 'form': form, + 'validlink': validlink, + } + if extra_context is not None: + context.update(extra_context) + return TemplateResponse(request, template_name, context, + current_app=current_app) + + +def password_reset_complete(request, + template_name='registration/password_reset_complete.html', + current_app=None, extra_context=None): + context = { + 'login_url': resolve_url(settings.LOGIN_URL) + } + if extra_context is not None: + context.update(extra_context) + return TemplateResponse(request, template_name, context, + current_app=current_app) + + diff --git a/portal/forms.py b/portal/forms.py index 848f1c3e..df5c1a67 100644 --- a/portal/forms.py +++ b/portal/forms.py @@ -26,6 +26,15 @@ from portal.models import PendingUser, PendingSlice #from crispy_forms.helper import FormHelper #from crispy_forms.layout import Submit from django.utils.translation import ugettext_lazy as _ +from django.contrib.auth.tokens import default_token_generator +from django.contrib.auth import authenticate, get_user_model +from django.contrib.auth.hashers import UNUSABLE_PASSWORD, identify_hasher +from django.contrib.sites.models import get_current_site +from django.utils.http import int_to_base36 +from django.template import loader + + + # xxx painful, but... # bootstrap3 requires the fields to be tagged class='form-control' @@ -110,4 +119,96 @@ class SliceRequestForm(forms.Form): widget = forms.Select(attrs={'class':'form-control'}), choices = authority_hrn, help_text = "An authority responsible for vetting your slice") + + +class PasswordResetForm(forms.Form): + error_messages = { + 'unknown': _("That email address doesn't have an associated " + "user account. Are you sure you've registered?"), + 'unusable': _("The user account associated with this email " + "address cannot reset the password."), + } + email = forms.EmailField(label=_("Email"), max_length=254) + + def clean_email(self): + """ + Validates that an active user exists with the given email address. + """ + UserModel = get_user_model() + email = self.cleaned_data["email"] + self.users_cache = UserModel._default_manager.filter(email__iexact=email) + if not len(self.users_cache): + raise forms.ValidationError(self.error_messages['unknown']) + if not any(user.is_active for user in self.users_cache): + # none of the filtered users are active + raise forms.ValidationError(self.error_messages['unknown']) + if any((user.password == UNUSABLE_PASSWORD) + for user in self.users_cache): + raise forms.ValidationError(self.error_messages['unusable']) + return email + + def save(self, domain_override=None, + subject_template_name='registration/password_reset_subject.txt', + email_template_name='registration/password_reset_email.html', + use_https=False, token_generator=default_token_generator, + from_email=None, request=None): + """ + Generates a one-use only link for resetting password and sends to the + user. + """ + from django.core.mail import send_mail + for user in self.users_cache: + if not domain_override: + current_site = get_current_site(request) + site_name = current_site.name + domain = current_site.domain + else: + site_name = domain = domain_override + c = { + 'email': user.email, + 'domain': domain, + 'site_name': site_name, + 'uid': int_to_base36(user.pk), + 'user': user, + 'token': token_generator.make_token(user), + 'protocol': use_https and 'https' or 'http', + } + subject = loader.render_to_string(subject_template_name, c) + # Email subject *must not* contain newlines + subject = ''.join(subject.splitlines()) + email = loader.render_to_string(email_template_name, c) + send_mail(subject, email, from_email, [user.email]) + + +class SetPasswordForm(forms.Form): + """ + A form that lets a user change set his/her password without entering the + old password + """ + error_messages = { + 'password_mismatch': _("The two password fields didn't match."), + } + new_password1 = forms.CharField(label=_("New password"), + widget=forms.PasswordInput) + new_password2 = forms.CharField(label=_("New password confirmation"), + widget=forms.PasswordInput) + + def __init__(self, user, *args, **kwargs): + self.user = user + super(SetPasswordForm, self).__init__(*args, **kwargs) + + def clean_new_password2(self): + password1 = self.cleaned_data.get('new_password1') + password2 = self.cleaned_data.get('new_password2') + if password1 and password2: + if password1 != password2: + raise forms.ValidationError( + self.error_messages['password_mismatch']) + return password2 + + def save(self, commit=True): + self.user.set_password(self.cleaned_data['new_password1']) + if commit: + self.user.save() + return self.user diff --git a/portal/urls.py b/portal/urls.py index 87ff1e1e..bcd56e5f 100644 --- a/portal/urls.py +++ b/portal/urls.py @@ -37,7 +37,7 @@ from portal.passresetview import PassResetView # hopefully these should move in dedicated source files too from portal.views import PresViewView, pres_view_static, pres_view_methods, pres_view_animation from portal.views import ValidatePendingView - +from portal.django_passresetview import password_reset, password_reset_done, password_reset_confirm, password_reset_complete # DEPRECATED #named_register_forms = ( # DEPRECATED # ("step1", RegisterUserForm), @@ -66,7 +66,7 @@ urlpatterns = patterns('', url(r'^account/account_process/?$', account_process), url(r'^register/?$', RegistrationView.as_view(), name='registration'), url(r'^contact/?$', ContactView.as_view(), name='contact'), - url(r'^pass_reset/?$', PassResetView.as_view(), name='pass_rest'), + #url(r'^pass_reset/?$', PassResetView.as_view(), name='pass_rest'), # Slice request url(r'^slice_request/?$', SliceRequestView.as_view(), name='slice_request'), # Validate pending requests @@ -82,6 +82,18 @@ urlpatterns = patterns('', #url(r'^slice/request/?$', views.slice_request, name='slice_request'), # Slice confirmation #url(r'^slice/validate/?$', views.slice_validate, name='slice_validate'), + url(r'^pass_reset/$', + 'portal.django_passresetview.password_reset', + {'post_reset_redirect' : '/portal/password/reset/done/'}), + (r'^password/reset/done/$', + 'portal.django_passresetview.password_reset_done'), + (r'^password/reset/(?P[0-9A-Za-z]+)-(?P.+)/$', + 'portal.django_passresetview.password_reset_confirm', + {'post_reset_redirect' : '/portal/password/done/'}), + (r'^password/done/$', + 'portal.django_passresetview.password_reset_complete'), + # ... + ) # (r'^accounts/', include('registration.backends.default.urls')), @@ -89,4 +101,4 @@ urlpatterns = patterns('', # DEPRECATED # url(r'^$', views.index, name='index'), # DEPRECATED # url(r"^registerwizard/(?P[-\w]+)/$", register_wizard, # DEPRECATED # name="register_wizard_step"), -# DEPRECATED # url(r"^registerwizard/$", register_wizard, name="register_wizard") +# DEPRECATED # url(r"^registerwizard/$", regster_wizard, name="register_wizard") diff --git a/ui/templates/registration/.password_reset_confirm.html.swp b/ui/templates/registration/.password_reset_confirm.html.swp new file mode 100644 index 0000000000000000000000000000000000000000..f4dc4e79b04c274ae24be706504144b376bc0dfb GIT binary patch literal 12288 zcmeI2yKWOf6ow~AKmpfC(@GCcp%k z025#WOn?a-Z2}>m5NfC(@GCcp%a2mzT0 z@!^yZ%cqeL{{Jt31NeGYh|ka`Xd8MDy@TFBuc4RF3+OTQ2>O0Th;PtG=q=QM&O@)J zg;;^^K)0cr&<*G$bOPE$%x92=Qs^NfC(@GCcp%k02BDH32e@g zvPsuu@jA_HiG8=@d)+JNL^E$=9__x?Q*^(t=vT)tRUN-%v|Xox^rY>EG&;FLXdL#s zYlhTSu`IkI9jK25(l{lPa#&fycv2%;HA-TIQ%?%z14*)KurER_f{$iBZ^KK!v1?{_ zb=-P9Hd0t$#If?eSD4vq3s&Rn3vx9GQRPzaWiD-(IW@J-i$GzRt5)QTZmL5>NT9&& zPQJ5?eQDPerNbRh<;fA%+m)eaPO*WDR)oygtV(F06BV_aMs}4U1Zzb)=^&@hFKYAA zL8ebKWL$QRx1xwr75Z7yicm43iXMB{>x66_1#+PF#-*ZcP>zOK;r7-_7@$#|d`1IB zv9z?IobC37vOxEGvYQlR_6?#?prAw5{=v6}Vdw{*l t`tE39dznc(sYLYga+68;X{% trans 'Password reset complete' %} + +

{% trans "Your password has been set. You may go ahead and log in now." %}

+ +

{% trans 'Log in' %}

+ +{% endblock %} +{% endblock %} + diff --git a/ui/templates/registration/password_reset_confirm.html b/ui/templates/registration/password_reset_confirm.html new file mode 100644 index 00000000..e683a3f9 --- /dev/null +++ b/ui/templates/registration/password_reset_confirm.html @@ -0,0 +1,33 @@ +{% extends "layout-unfold1.html" %} +{% load i18n %} + +{% block unfold_main %} + + +{% block content %} + +{% if validlink %} + +

{% trans 'Enter new password' %}

+ +

{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}

+ +
{% csrf_token %} +{{ form.new_password1.errors }} +

{{ form.new_password1 }}

+{{ form.new_password2.errors }} +

{{ form.new_password2 }}

+

+
+ +{% else %} + +

{% trans 'Password reset unsuccessful' %}

+ +

{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}

+ +{% endif %} + +{% endblock %} +{% endblock %} + diff --git a/ui/templates/registration/password_reset_done.html b/ui/templates/registration/password_reset_done.html new file mode 100644 index 00000000..58afd0ee --- /dev/null +++ b/ui/templates/registration/password_reset_done.html @@ -0,0 +1,14 @@ +{% extends "layout-unfold1.html" %} +{% load i18n %} + +{% block unfold_main %} + +{% block content %} + +

{% trans 'Password reset successful' %}

+ +

{% trans "We've emailed you instructions for setting your password to the email address you submitted. You should be receiving it shortly." %}

+ +{% endblock %} +{% endblock %} + diff --git a/ui/templates/registration/password_reset_email.html b/ui/templates/registration/password_reset_email.html new file mode 100644 index 00000000..51431c73 --- /dev/null +++ b/ui/templates/registration/password_reset_email.html @@ -0,0 +1,14 @@ +{% load i18n %}{% autoescape off %} +{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} + +{% trans "Please go to the following page and choose a new password:" %} +{% block reset_link %} +{{ protocol }}://{{ domain }}{% url 'portal.django_passresetview.password_reset_confirm' uidb36=uid token=token %} +{% endblock %} +{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }} + +{% trans "Thanks for using our site!" %} + +{% blocktrans %}The {{ site_name }} team{% endblocktrans %} + +{% endautoescape %} diff --git a/ui/templates/registration/password_reset_form.html b/ui/templates/registration/password_reset_form.html new file mode 100644 index 00000000..dc188cd7 --- /dev/null +++ b/ui/templates/registration/password_reset_form.html @@ -0,0 +1,21 @@ +{% extends "layout-unfold1.html" %} +{% load i18n %} + +{% block unfold_main %} + + + +{% block content %} + +

{% trans "Onelab Password reset wizard" %}

+ +

{% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}

+ +
{% csrf_token %} +{{ form.email.errors }} +

{{ form.email }}

+
+ +{% endblock %} +{% endblock %} + -- 2.43.0