Added a change password page, and linked to it from the account profile page and user menu. Added zxcvbn-based browser-side password strength estimation on the various password setting, re-setting, and changing forms. Added a change password test. Changed ietfauth/urls.py to not use the deprecated string form for views in urlpatterns.
- Legacy-Id: 12798
This commit is contained in:
parent
93efc4470a
commit
7dea44e626
|
@ -1,4 +1,5 @@
|
||||||
import re
|
import re
|
||||||
|
from unidecode import unidecode
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -8,7 +9,7 @@ from django.contrib.auth.models import User
|
||||||
from django.utils.html import mark_safe
|
from django.utils.html import mark_safe
|
||||||
from django.core.urlresolvers import reverse as urlreverse
|
from django.core.urlresolvers import reverse as urlreverse
|
||||||
|
|
||||||
from unidecode import unidecode
|
from django_password_strength.widgets import PasswordStrengthInput, PasswordConfirmationInput
|
||||||
|
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
|
||||||
|
@ -31,8 +32,8 @@ class RegistrationForm(forms.Form):
|
||||||
|
|
||||||
|
|
||||||
class PasswordForm(forms.Form):
|
class PasswordForm(forms.Form):
|
||||||
password = forms.CharField(widget=forms.PasswordInput)
|
password = forms.CharField(widget=PasswordStrengthInput)
|
||||||
password_confirmation = forms.CharField(widget=forms.PasswordInput,
|
password_confirmation = forms.CharField(widget=PasswordConfirmationInput,
|
||||||
help_text="Enter the same password as above, for verification.")
|
help_text="Enter the same password as above, for verification.")
|
||||||
|
|
||||||
def clean_password_confirmation(self):
|
def clean_password_confirmation(self):
|
||||||
|
@ -166,3 +167,28 @@ class WhitelistForm(forms.ModelForm):
|
||||||
exclude = ['by', 'time' ]
|
exclude = ['by', 'time' ]
|
||||||
|
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePasswordForm(forms.Form):
|
||||||
|
current_password = forms.CharField(widget=forms.PasswordInput)
|
||||||
|
|
||||||
|
|
||||||
|
new_password = forms.CharField(widget=PasswordStrengthInput)
|
||||||
|
new_password_confirmation = forms.CharField(widget=PasswordConfirmationInput)
|
||||||
|
|
||||||
|
def __init__(self, user, data=None):
|
||||||
|
self.user = user
|
||||||
|
super(ChangePasswordForm, self).__init__(data)
|
||||||
|
|
||||||
|
def clean_current_password(self):
|
||||||
|
password = self.cleaned_data.get('current_password', None)
|
||||||
|
if not self.user.check_password(password):
|
||||||
|
raise ValidationError('Invalid password')
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
new_password = self.cleaned_data.get('new_password', None)
|
||||||
|
conf_password = self.cleaned_data.get('new_password_confirmation', None)
|
||||||
|
if not new_password == conf_password:
|
||||||
|
raise ValidationError("The password confirmation is different than the new password")
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,13 @@ from urlparse import urlsplit
|
||||||
from pyquery import PyQuery
|
from pyquery import PyQuery
|
||||||
from unittest import skipIf
|
from unittest import skipIf
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse as urlreverse
|
|
||||||
import django.contrib.auth.views
|
import django.contrib.auth.views
|
||||||
|
from django.core.urlresolvers import reverse as urlreverse
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
import debug # pyflakes:ignore
|
||||||
|
|
||||||
from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent
|
from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent
|
||||||
from ietf.utils.test_data import make_test_data, make_review_data
|
from ietf.utils.test_data import make_test_data, make_review_data
|
||||||
from ietf.utils.mail import outbox, empty_outbox
|
from ietf.utils.mail import outbox, empty_outbox
|
||||||
|
@ -399,3 +401,51 @@ class IetfAuthTests(TestCase):
|
||||||
update_htpasswd_file("foo", "passwd")
|
update_htpasswd_file("foo", "passwd")
|
||||||
self.assertTrue(self.username_in_htpasswd_file("foo"))
|
self.assertTrue(self.username_in_htpasswd_file("foo"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_password(self):
|
||||||
|
|
||||||
|
chpw_url = urlreverse(ietf.ietfauth.views.change_password)
|
||||||
|
prof_url = urlreverse(ietf.ietfauth.views.profile)
|
||||||
|
login_url = urlreverse(django.contrib.auth.views.login)
|
||||||
|
redir_url = '%s?next=%s' % (login_url, chpw_url)
|
||||||
|
|
||||||
|
# get without logging in
|
||||||
|
r = self.client.get(chpw_url)
|
||||||
|
self.assertRedirects(r, redir_url)
|
||||||
|
|
||||||
|
user = User.objects.create(username="someone@example.com", email="someone@example.com")
|
||||||
|
user.set_password("password")
|
||||||
|
user.save()
|
||||||
|
p = Person.objects.create(name="Some One", ascii="Some One", user=user)
|
||||||
|
Email.objects.create(address=user.username, person=p)
|
||||||
|
|
||||||
|
# log in
|
||||||
|
r = self.client.post(redir_url, {"username":user.username, "password":"password"})
|
||||||
|
self.assertRedirects(r, chpw_url)
|
||||||
|
|
||||||
|
# wrong current password
|
||||||
|
r = self.client.post(chpw_url, {"current_password": "fiddlesticks",
|
||||||
|
"new_password": "foobar",
|
||||||
|
"new_password_confirmation": "foobar",
|
||||||
|
})
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertFormError(r, 'form', 'current_password', 'Invalid password')
|
||||||
|
|
||||||
|
# mismatching new passwords
|
||||||
|
r = self.client.post(chpw_url, {"current_password": "password",
|
||||||
|
"new_password": "foobar",
|
||||||
|
"new_password_confirmation": "barfoo",
|
||||||
|
})
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertFormError(r, 'form', None, "The password confirmation is different than the new password")
|
||||||
|
|
||||||
|
# correct password change
|
||||||
|
r = self.client.post(chpw_url, {"current_password": "password",
|
||||||
|
"new_password": "foobar",
|
||||||
|
"new_password_confirmation": "foobar",
|
||||||
|
})
|
||||||
|
self.assertRedirects(r, prof_url)
|
||||||
|
# refresh user object
|
||||||
|
user = User.objects.get(username="someone@example.com")
|
||||||
|
self.assertTrue(user.check_password(u'foobar'))
|
||||||
|
|
||||||
|
|
|
@ -3,23 +3,20 @@
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.contrib.auth.views import login, logout
|
from django.contrib.auth.views import login, logout
|
||||||
|
|
||||||
from ietf.ietfauth.views import add_account_whitelist
|
from ietf.ietfauth import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', 'ietf.ietfauth.views.index'),
|
url(r'^$', views.index),
|
||||||
# url(r'^login/$', 'ietf.ietfauth.views.ietf_login'),
|
url(r'^confirmnewemail/(?P<auth>[^/]+)/$', views.confirm_new_email),
|
||||||
|
url(r'^create/$', views.create_account),
|
||||||
|
url(r'^create/confirm/(?P<auth>[^/]+)/$', views.confirm_account),
|
||||||
url(r'^login/$', login),
|
url(r'^login/$', login),
|
||||||
url(r'^logout/$', logout),
|
url(r'^logout/$', logout),
|
||||||
# url(r'^loggedin/$', 'ietf.ietfauth.views.ietf_loggedin'),
|
url(r'^password/$', views.change_password),
|
||||||
# url(r'^loggedout/$', 'ietf.ietfauth.views.logged_out'),
|
url(r'^profile/$', views.profile),
|
||||||
url(r'^profile/$', 'ietf.ietfauth.views.profile'),
|
url(r'^reset/$', views.password_reset),
|
||||||
# (r'^login/(?P<user>[a-z0-9.@]+)/(?P<passwd>.+)$', 'ietf.ietfauth.views.url_login'),
|
url(r'^reset/confirm/(?P<auth>[^/]+)/$', views.confirm_password_reset),
|
||||||
url(r'^testemail/$', 'ietf.ietfauth.views.test_email'),
|
url(r'^review/$', views.review_overview),
|
||||||
url(r'^create/$', 'ietf.ietfauth.views.create_account'),
|
url(r'^testemail/$', views.test_email),
|
||||||
url(r'^create/confirm/(?P<auth>[^/]+)/$', 'ietf.ietfauth.views.confirm_account'),
|
url(r'whitelist/add/?$', views.add_account_whitelist),
|
||||||
url(r'^reset/$', 'ietf.ietfauth.views.password_reset'),
|
|
||||||
url(r'^reset/confirm/(?P<auth>[^/]+)/$', 'ietf.ietfauth.views.confirm_password_reset'),
|
|
||||||
url(r'^confirmnewemail/(?P<auth>[^/]+)/$', 'ietf.ietfauth.views.confirm_new_email'),
|
|
||||||
url(r'whitelist/add/?$', add_account_whitelist),
|
|
||||||
url(r'^review/$', 'ietf.ietfauth.views.review_overview'),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -32,24 +32,27 @@
|
||||||
|
|
||||||
# Copyright The IETF Trust 2007, All Rights Reserved
|
# Copyright The IETF Trust 2007, All Rights Reserved
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
|
||||||
from datetime import datetime as DateTime, timedelta as TimeDelta, date as Date
|
from datetime import datetime as DateTime, timedelta as TimeDelta, date as Date
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.http import Http404 #, HttpResponse, HttpResponseRedirect
|
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
|
||||||
#from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login
|
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
#from django.utils.http import urlquote
|
|
||||||
import django.core.signing
|
import django.core.signing
|
||||||
from django.contrib.sites.models import Site
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import update_session_auth_hash
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.sites.models import Site
|
||||||
|
from django.core.urlresolvers import reverse as urlreverse
|
||||||
|
from django.http import Http404, HttpResponseRedirect #, HttpResponse,
|
||||||
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
|
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
|
||||||
from ietf.group.models import Role, Group
|
from ietf.group.models import Role, Group
|
||||||
from ietf.ietfauth.forms import RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm, WhitelistForm
|
from ietf.ietfauth.forms import RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm, WhitelistForm, ChangePasswordForm
|
||||||
from ietf.ietfauth.forms import get_person_form, RoleEmailForm, NewEmailForm
|
from ietf.ietfauth.forms import get_person_form, RoleEmailForm, NewEmailForm
|
||||||
from ietf.ietfauth.htpasswd import update_htpasswd_file
|
from ietf.ietfauth.htpasswd import update_htpasswd_file
|
||||||
from ietf.ietfauth.utils import role_required
|
from ietf.ietfauth.utils import role_required
|
||||||
|
@ -340,10 +343,14 @@ def confirm_password_reset(request, auth):
|
||||||
else:
|
else:
|
||||||
form = PasswordForm()
|
form = PasswordForm()
|
||||||
|
|
||||||
|
hlibname, hashername = settings.PASSWORD_HASHERS[0].rsplit('.',1)
|
||||||
|
hlib = importlib.import_module(hlibname)
|
||||||
|
hasher = getattr(hlib, hashername)
|
||||||
return render(request, 'registration/change_password.html', {
|
return render(request, 'registration/change_password.html', {
|
||||||
'form': form,
|
'form': form,
|
||||||
'username': username,
|
'user': user,
|
||||||
'success': success,
|
'success': success,
|
||||||
|
'hasher': hasher,
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_email(request):
|
def test_email(request):
|
||||||
|
@ -465,3 +472,48 @@ def review_overview(request):
|
||||||
'review_wishes': review_wishes,
|
'review_wishes': review_wishes,
|
||||||
'review_wish_form': review_wish_form,
|
'review_wish_form': review_wish_form,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def change_password(request):
|
||||||
|
success = False
|
||||||
|
person = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
person = request.user.person
|
||||||
|
except Person.DoesNotExist:
|
||||||
|
return render(request, 'registration/missing_person.html')
|
||||||
|
|
||||||
|
emails = [ e.address for e in Email.objects.filter(person=person, active=True).order_by('-primary','-time') ]
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = ChangePasswordForm(user, request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
new_password = form.cleaned_data["new_password"]
|
||||||
|
|
||||||
|
user.set_password(new_password)
|
||||||
|
user.save()
|
||||||
|
# password is also stored in htpasswd file
|
||||||
|
update_htpasswd_file(user.username, new_password)
|
||||||
|
# keep the session
|
||||||
|
update_session_auth_hash(request, user)
|
||||||
|
|
||||||
|
send_mail(request, emails, None, "Datatracker password change notification", "registration/password_change_email.txt", {})
|
||||||
|
|
||||||
|
messages.success(request, "Your password was successfully changed")
|
||||||
|
return HttpResponseRedirect(urlreverse('ietf.ietfauth.views.profile'))
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = ChangePasswordForm(request.user)
|
||||||
|
|
||||||
|
hlibname, hashername = settings.PASSWORD_HASHERS[0].rsplit('.',1)
|
||||||
|
hlib = importlib.import_module(hlibname)
|
||||||
|
hasher = getattr(hlib, hashername)
|
||||||
|
return render(request, 'registration/change_password.html', {
|
||||||
|
'form': form,
|
||||||
|
'user': user,
|
||||||
|
'success': success,
|
||||||
|
'hasher': hasher,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -704,7 +704,6 @@ def get_details_tabs(ipr, selected):
|
||||||
('History', urlreverse('ipr_history', kwargs={ 'id': ipr.pk }))
|
('History', urlreverse('ipr_history', kwargs={ 'id': ipr.pk }))
|
||||||
]]
|
]]
|
||||||
|
|
||||||
@debug.trace
|
|
||||||
def show(request, id):
|
def show(request, id):
|
||||||
"""View of individual declaration"""
|
"""View of individual declaration"""
|
||||||
ipr = get_object_or_404(IprDisclosureBase, id=id).get_child()
|
ipr = get_object_or_404(IprDisclosureBase, id=id).get_child()
|
||||||
|
|
130
ietf/static/ietf/js/password_strength.js
Normal file
130
ietf/static/ietf/js/password_strength.js
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
// Taken from django-password-strength, with changes to use the bower-managed zxcvbn.js The
|
||||||
|
// bower-managed zxcvbn.js is kept up-to-date to a larger extent than the copy packaged with
|
||||||
|
// the django-password-strength component.
|
||||||
|
(function($, window, document, undefined){
|
||||||
|
window.djangoPasswordStrength = {
|
||||||
|
config: {
|
||||||
|
passwordClass: 'password_strength',
|
||||||
|
confirmationClass: 'password_confirmation'
|
||||||
|
},
|
||||||
|
|
||||||
|
init: function (config) {
|
||||||
|
var self = this;
|
||||||
|
// Setup configuration
|
||||||
|
if ($.isPlainObject(config)) {
|
||||||
|
$.extend(self.config, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.initListeners();
|
||||||
|
},
|
||||||
|
|
||||||
|
initListeners: function() {
|
||||||
|
var self = this;
|
||||||
|
var body = $('body');
|
||||||
|
|
||||||
|
$('.' + self.config.passwordClass).on('keyup', function() {
|
||||||
|
var password_strength_bar = $(this).parent().find('.password_strength_bar');
|
||||||
|
var password_strength_info = $(this).parent().find('.password_strength_info');
|
||||||
|
|
||||||
|
if( $(this).val() ) {
|
||||||
|
var result = zxcvbn( $(this).val() );
|
||||||
|
|
||||||
|
if( result.score < 3 ) {
|
||||||
|
password_strength_bar.removeClass('progress-bar-success').addClass('progress-bar-warning');
|
||||||
|
password_strength_info.find('.label').removeClass('hidden');
|
||||||
|
} else {
|
||||||
|
password_strength_bar.removeClass('progress-bar-warning').addClass('progress-bar-success');
|
||||||
|
password_strength_info.find('.label').addClass('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
password_strength_bar.width( ((result.score+1)/5)*100 + '%' ).attr('aria-valuenow', result.score + 1);
|
||||||
|
// henrik@levkowetz.com -- this is the only changed line:
|
||||||
|
password_strength_info.find('.password_strength_time').html(result.crack_times_display.online_no_throttling_10_per_second);
|
||||||
|
password_strength_info.removeClass('hidden');
|
||||||
|
} else {
|
||||||
|
password_strength_bar.removeClass('progress-bar-success').addClass('progress-bar-warning');
|
||||||
|
password_strength_bar.width( '0%' ).attr('aria-valuenow', 0);
|
||||||
|
password_strength_info.addClass('hidden');
|
||||||
|
}
|
||||||
|
self.match_passwords($(this));
|
||||||
|
});
|
||||||
|
|
||||||
|
var timer = null;
|
||||||
|
$('.' + self.config.confirmationClass).on('keyup', function() {
|
||||||
|
var password_field;
|
||||||
|
var confirm_with = $(this).data('confirm-with');
|
||||||
|
|
||||||
|
if( confirm_with ) {
|
||||||
|
password_field = $('#' + confirm_with);
|
||||||
|
} else {
|
||||||
|
password_field = $('.' + self.config.passwordClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timer !== null) clearTimeout(timer);
|
||||||
|
|
||||||
|
timer = setTimeout(function(){
|
||||||
|
self.match_passwords(password_field);
|
||||||
|
}, 400);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
display_time: function(seconds) {
|
||||||
|
var minute = 60;
|
||||||
|
var hour = minute * 60;
|
||||||
|
var day = hour * 24;
|
||||||
|
var month = day * 31;
|
||||||
|
var year = month * 12;
|
||||||
|
var century = year * 100;
|
||||||
|
|
||||||
|
// Provide fake gettext for when it is not available
|
||||||
|
if( typeof gettext !== 'function' ) { gettext = function(text) { return text; }; };
|
||||||
|
|
||||||
|
if( seconds < minute ) return gettext('only an instant');
|
||||||
|
if( seconds < hour) return (1 + Math.ceil(seconds / minute)) + ' ' + gettext('minutes');
|
||||||
|
if( seconds < day) return (1 + Math.ceil(seconds / hour)) + ' ' + gettext('hours');
|
||||||
|
if( seconds < month) return (1 + Math.ceil(seconds / day)) + ' ' + gettext('days');
|
||||||
|
if( seconds < year) return (1 + Math.ceil(seconds / month)) + ' ' + gettext('months');
|
||||||
|
if( seconds < century) return (1 + Math.ceil(seconds / year)) + ' ' + gettext('years');
|
||||||
|
|
||||||
|
return gettext('centuries');
|
||||||
|
},
|
||||||
|
|
||||||
|
match_passwords: function(password_field, confirmation_fields) {
|
||||||
|
var self = this;
|
||||||
|
// Optional parameter: if no specific confirmation field is given, check all
|
||||||
|
if( confirmation_fields === undefined ) { confirmation_fields = $('.' + self.config.confirmationClass) }
|
||||||
|
if( confirmation_fields === undefined ) { return; }
|
||||||
|
|
||||||
|
var password = password_field.val();
|
||||||
|
|
||||||
|
confirmation_fields.each(function(index, confirm_field) {
|
||||||
|
var confirm_value = $(confirm_field).val();
|
||||||
|
var confirm_with = $(confirm_field).data('confirm-with');
|
||||||
|
|
||||||
|
if( confirm_with && confirm_with == password_field.attr('id')) {
|
||||||
|
if( confirm_value && password ) {
|
||||||
|
if (confirm_value === password) {
|
||||||
|
$(confirm_field).parent().find('.password_strength_info').addClass('hidden');
|
||||||
|
} else {
|
||||||
|
$(confirm_field).parent().find('.password_strength_info').removeClass('hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$(confirm_field).parent().find('.password_strength_info').addClass('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If a password field other than our own has been used, add the listener here
|
||||||
|
if( !password_field.hasClass(self.config.passwordClass) && !password_field.data('password-listener') ) {
|
||||||
|
password_field.on('keyup', function() {
|
||||||
|
self.match_passwords($(this));
|
||||||
|
});
|
||||||
|
password_field.data('password-listener', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the init for backwards compatibility
|
||||||
|
djangoPasswordStrength.init();
|
||||||
|
|
||||||
|
})(jQuery, window, document);
|
|
@ -16,14 +16,17 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<li><a rel="nofollow" href="/accounts/logout/" >Sign out</a></li>
|
<li><a rel="nofollow" href="/accounts/logout/" >Sign out</a></li>
|
||||||
<li><a rel="nofollow" href="/accounts/profile/">Edit profile</a></li>
|
<li><a rel="nofollow" href="/accounts/profile/">Account info</a></li>
|
||||||
|
<li><a rel="nofollow" href="/accounts/password/">Change password</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a rel="nofollow" href="/accounts/login/?next={{request.get_full_path|urlencode}}">Sign in</a></li>
|
<li><a rel="nofollow" href="/accounts/login/?next={{request.get_full_path|urlencode}}">Sign in</a></li>
|
||||||
<li><a rel="nofollow" href="/accounts/reset/">Password reset</a></li>
|
<li><a rel="nofollow" href="/accounts/reset/">Password reset</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<li><a href="{% url "ietf.ietfauth.views.create_account" %}">{% if request.user.is_authenticated %}Manage account{% else %}New account{% endif %}</a></li>
|
{% if not request.user.is_authenticated %}
|
||||||
|
<li><a href="{% url "ietf.ietfauth.views.create_account" %}">New account</a></li>
|
||||||
|
{% endif %}
|
||||||
<li><a href="{%url "ietf.cookies.views.preferences" %}" rel="nofollow">Preferences</a></li>
|
<li><a href="{%url "ietf.cookies.views.preferences" %}" rel="nofollow">Preferences</a></li>
|
||||||
|
|
||||||
{% if user|has_role:"Reviewer" %}
|
{% if user|has_role:"Reviewer" %}
|
||||||
|
|
|
@ -3,30 +3,58 @@
|
||||||
{% load origin %}
|
{% load origin %}
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
{% block title %}Change password{% endblock %}
|
{% block title %}Change password{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script type="text/javascript" src="{% static 'zxcvbn/zxcvbn.js' %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static 'ietf/js/password_strength.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% origin %}
|
{% origin %}
|
||||||
|
|
||||||
{% if success %}
|
{% if success %}
|
||||||
<h1>Password change successful</h1>
|
<h1>Your password was successfully changed.</h1>
|
||||||
|
{% if not user.is_authenticated %}
|
||||||
<p>Your password has been updated.</p>
|
<a type="a" class="btn btn-primary" href="/accounts/login/" rel="nofollow">Sign in</a>
|
||||||
<a type="a" class="btn btn-primary" href="/accounts/login/" rel="nofollow">Sign in</a>
|
{% endif %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<h1>Change password</h1>
|
<div class="row">
|
||||||
|
<div class="col-md-2 col-sm-0"></div>
|
||||||
|
<div class="col-md-8 col-sm-12">
|
||||||
|
<h1>Change password</h1>
|
||||||
|
|
||||||
<p>You can change the password below for your user {{ username }} below.</p>
|
<form method="post">
|
||||||
<form method="post">
|
{% csrf_token %}
|
||||||
{% csrf_token %}
|
{% bootstrap_form form %}
|
||||||
{% bootstrap_form form %}
|
|
||||||
|
|
||||||
{% buttons %}
|
{% buttons %}
|
||||||
<button type="submit" class="btn btn-primary">Change password</button>
|
<button type="submit" class="btn btn-primary">Change password</button>
|
||||||
{% endbuttons %}
|
{% endbuttons %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="help-block">
|
||||||
|
This password form uses the
|
||||||
|
<a href="https://blogs.dropbox.com/tech/2012/04/zxcvbn-realistic-password-strength-estimation/">zxcvbn</a>
|
||||||
|
password strength estimator to give an indication of password strength.
|
||||||
|
The crack times given assume online attack without rate limiting,
|
||||||
|
at a rate of 10 attempts per second.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="help-block">
|
||||||
|
The datatracker currently uses a <b>{{ hasher.algorithm }}</b>-based
|
||||||
|
password hasher with
|
||||||
|
<b>{% if hasher.iterations %}{{ hasher.iterations }} iterations{% else %}{{ hasher.rounds }} rounds{% endif %}</b>.
|
||||||
|
Calculating offline attack time if password hashes would leak is left
|
||||||
|
as an excercise for the reader.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 col-sm-0"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||||
{% load origin %}
|
{% load origin %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
{% block title %}Complete account creation{% endblock %}
|
{% block title %}Complete account creation{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script type="text/javascript" src="{% static 'zxcvbn/zxcvbn.js' %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static 'ietf/js/password_strength.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% origin %}
|
{% origin %}
|
||||||
|
|
||||||
|
|
10
ietf/templates/registration/password_change_email.txt
Normal file
10
ietf/templates/registration/password_change_email.txt
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% autoescape off %}
|
||||||
|
Hello,
|
||||||
|
|
||||||
|
{% filter wordwrap:73 %}The password for your datatracker account was just changed using the password change form. If this was not done by you, please contact the secretariat at ietf-action@ietf.org for assistance.{% endfilter %}
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
|
||||||
|
The datatracker account manager service
|
||||||
|
(for the IETF Secretariat)
|
||||||
|
{% endautoescape %}
|
Loading…
Reference in a new issue