From: Yasin Date: Thu, 9 Jan 2014 17:09:18 +0000 (+0100) Subject: ForgotPassword: DONE-Using the Django authentication system- Adapted to use manifold... X-Git-Tag: myslice-0.3-0~38^2~10 X-Git-Url: http://git.onelab.eu/?p=myslice.git;a=commitdiff_plain;h=0c34b56ff3e9887a592363ce92753a6a8383aa45 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) --- 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 00000000..f4dc4e79 Binary files /dev/null and b/ui/templates/registration/.password_reset_confirm.html.swp differ diff --git a/ui/templates/registration/password_reset_complete.html b/ui/templates/registration/password_reset_complete.html new file mode 100644 index 00000000..a5663077 --- /dev/null +++ b/ui/templates/registration/password_reset_complete.html @@ -0,0 +1,17 @@ +{% extends "layout-unfold1.html" %} +{% load i18n %} + +{% block unfold_main %} + + +{% block content %} + +

{% 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 %} +