From bf5d684c05a26787de0de80b0894b9d2d031c6ad Mon Sep 17 00:00:00 2001 From: Marian Sigler Date: Wed, 26 Sep 2012 03:47:57 +0200 Subject: Implement password recovery functionality. --- app.py | 52 ++++++++++++++++++++++++++++++++++- forms.py | 25 +++++++++++++---- templates/index.html | 7 +++-- templates/lost_password.html | 16 +++++++++++ templates/lost_password_complete.html | 19 +++++++++++++ templates/mail/lost_password.txt | 11 ++++++++ templates/register.html | 2 +- templates/register_complete.html | 2 +- templates/settings.html | 2 +- utils.py | 1 - 10 files changed, 124 insertions(+), 13 deletions(-) create mode 100644 templates/lost_password.html create mode 100644 templates/lost_password_complete.html create mode 100644 templates/mail/lost_password.txt diff --git a/app.py b/app.py index 8eb8ece..518be05 100644 --- a/app.py +++ b/app.py @@ -104,6 +104,55 @@ def register_complete(token): } +@app.route('/lost_password', methods=['GET', 'POST']) +@templated('lost_password.html') +@logout_required +def lost_password(): + form = LostPasswordForm(request.form) + if request.method == 'POST' and form.validate(): + #TODO: make the link only usable once (e.g include a hash of the old pw) + # atm the only thing we do is make the link valid for only little time + confirm_token = make_confirmation('lost_password', (form.username.data,)) + confirm_link = url_for('lost_password_complete', token=confirm_token, _external=True) + + body = render_template('mail/lost_password.txt', username=form.username.data, + link=confirm_link) + + send_mail(form.user.mail, u'Passwort vergessen', body, + sender=app.config.get('MAIL_CONFIRM_SENDER')) + + flash(u'Wir haben dir eine E-Mail mit einem Link zum Passwort ändern ' + u'geschickt. Bitte folge den Anweisungen in der E-Mail.', 'success') + + return redirect(url_for('index')) + + return {'form': form} + + +@app.route('/lost_password/', methods=['GET', 'POST']) +@templated('lost_password_complete.html') +@logout_required +def lost_password_complete(token): + username, = http_verify_confirmation('lost_password', token.encode('ascii'), timeout=4*60*60) + + form = RegisterCompleteForm(request.form) + if request.method == 'POST' and form.validate(): + user = g.ldap.get_by_uid(username) + user.change_password(form.password.data) + g.ldap.update(user, as_admin=True) + + session['username'] = username + session['password'] = encrypt_password(form.password.data) + flash(u'Passwort geändert.', 'success') + + return redirect(url_for('settings')) + + return { + 'form': form, + 'token': token, + 'username': username, + } + @app.route('/settings', methods=['GET', 'POST']) @templated('settings.html') @@ -186,7 +235,8 @@ def debug(): # we need the app to exist before initializing the forms -from forms import RegisterForm, RegisterCompleteForm, LoginForm, SettingsForm +from forms import RegisterForm, RegisterCompleteForm, LoginForm, SettingsForm,\ + LostPasswordForm if __name__ == '__main__': diff --git a/forms.py b/forms.py index c5728d5..ff54449 100644 --- a/forms.py +++ b/forms.py @@ -1,14 +1,17 @@ # -*- coding: utf-8 -*- -from account import SERVICES -from flask.ext.wtf import Form, validators, TextField, PasswordField +from account import SERVICES, NoSuchUserError +from flask import g +from flask.ext.wtf import Form, validators, TextField, PasswordField,\ + ValidationError +from functools import partial from utils import _username_re -username = TextField('Benutzername', [validators.Regexp(_username_re, message=u'Benutzername darf nur aus a-z bestehen (2-16 Zeichen)')]) +username = partial(TextField, 'Benutzername', [validators.Regexp(_username_re, message=u'Benutzername darf nur aus a-z bestehen (2-16 Zeichen)')]) class RegisterForm(Form): - username = username + username = username() mail = TextField('E-Mail-Adresse', [validators.Email(), validators.Length(min=6, max=50)]) @@ -16,13 +19,24 @@ class RegisterCompleteForm(Form): password = PasswordField('Passwort', [validators.Required(), validators.EqualTo('password_confirm', message=u'Passwörter stimmen nicht überein')]) password_confirm = PasswordField(u'Passwort bestätigen') + # n.b. this form is also used in lost_password_complete class LoginForm(Form): - username = username + username = username() password = PasswordField('Passwort', [validators.Required()]) +class LostPasswordForm(Form): + username = username() + + def validate_username(form, field): + try: + form.user = g.ldap.get_by_uid(field.data) + except NoSuchUserError: + raise ValidationError(u'Es gibt keinen Benutzer mit diesem Namen.') + + class SettingsForm(Form): old_password = PasswordField('Bisheriges Passwort', [validators.Required(u'Bitte gib dein (altes) Passwort an, um deine Daten zu ändern.')]) @@ -47,4 +61,3 @@ for service in SERVICES: ])) setattr(SettingsForm, 'password_confirm_%s' % service.id, PasswordField(u'Passwort für %s (Bestätigung)' % service.name)) - diff --git a/templates/index.html b/templates/index.html index abcbb1d..cbbaa44 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,9 +5,12 @@ {%- if session.username %}

Hallo {{ session.username }}. Einstellungen

{%- else %} -

Account erstellen

+

+ Account erstellen | + Passwort vergessen +

- {{ render_field(form.username) }} + {{ render_field(form.username, autofocus="autofocus") }} {{ render_field(form.password) }} {{ form.csrf_token }}
diff --git a/templates/lost_password.html b/templates/lost_password.html new file mode 100644 index 0000000..391af0d --- /dev/null +++ b/templates/lost_password.html @@ -0,0 +1,16 @@ +{%- extends 'base.html' %} +{%- from '_macros.html' import render_field %} +{%- set title = 'Passwort vergessen' %} +{%- set no_login_message = true %} +{%- block content %} + +

+ Du hast dein Passwort vergessen? Kein Problem. + Gib einfach unten deinen Benutzernamen ein, und wir schicken dir einen Link, + mit dem du dir ein neues setzen kannst. +

+ {{ render_field(form.username, autofocus="autofocus") }} + {{ form.csrf_token }} +
+
+{%- endblock %} diff --git a/templates/lost_password_complete.html b/templates/lost_password_complete.html new file mode 100644 index 0000000..828bd6d --- /dev/null +++ b/templates/lost_password_complete.html @@ -0,0 +1,19 @@ +{%- extends 'base.html' %} +{%- from '_macros.html' import render_field %} +{%- set title = 'Passwort vergessen' %} +{%- set no_login_message = true %} +{%- block content %} +
+

+ Hier kannst du jetzt ein neues Passwort setzen. +

+
+
Benutzername
+
+
+ {{ render_field(form.password, autofocus="autofocus") }} + {{ render_field(form.password_confirm) }} + {{ form.csrf_token }} +
+
+{%- endblock %} diff --git a/templates/mail/lost_password.txt b/templates/mail/lost_password.txt new file mode 100644 index 0000000..af51ae4 --- /dev/null +++ b/templates/mail/lost_password.txt @@ -0,0 +1,11 @@ +Hallo {{ username }}, + +Jemand, vermutlich du, hat auf spline accounts einen Link zum Ändern +deines Passworts angefordert. + +Hier kannst du dein Passwort ändern: + <{{ link }}> + + +Wenn du diese Mail nicht angefordert hast, brauchst du nichts +weiter zu tun. Dein altes Passwort bleibt weiter gültig. diff --git a/templates/register.html b/templates/register.html index ab785ea..d8ef800 100644 --- a/templates/register.html +++ b/templates/register.html @@ -4,7 +4,7 @@ {%- set no_login_message = true %} {%- block content %}
- {{ render_field(form.username) }} + {{ render_field(form.username, autofocus="autofocus") }} {{ render_field(form.mail) }} {{ form.csrf_token }}
diff --git a/templates/register_complete.html b/templates/register_complete.html index 9320995..629f9c9 100644 --- a/templates/register_complete.html +++ b/templates/register_complete.html @@ -17,7 +17,7 @@
E-Mail-Adresse
- {{ render_field(form.password) }} + {{ render_field(form.password, autofocus="autofocus") }} {{ render_field(form.password_confirm) }} {{ form.csrf_token }}
diff --git a/templates/settings.html b/templates/settings.html index 1d112d8..c672493 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -3,7 +3,7 @@ {%- set title = 'Einstellungen' %} {%- block content %} - {{ render_field(form.old_password) }} + {{ render_field(form.old_password, autofocus="autofocus") }}

Globale Einstellungen ändern

{{ render_field(form.mail) }} {{ render_field(form.password) }} diff --git a/utils.py b/utils.py index 6ab7ed4..42b3bf5 100644 --- a/utils.py +++ b/utils.py @@ -133,7 +133,6 @@ def verify_confirmation(realm, token, timeout=None): if mac != hmac.new(key, token[20:], sha1).digest(): raise ConfirmationInvalid('MAC does not match') - print '%d+%d=%d <> %d' % (tokentime, timeout, tokentime+timeout, time()) if timeout is not None and time() > tokentime + timeout: raise ConfirmationTimeout('Token is too old') -- cgit v1.2.3-1-g7c22