From: Alejandro Villanueva Date: Thu, 21 Jul 2011 16:55:41 +0000 (-0500) Subject: Adding fotgot password functionality X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=25ba955e20e9262f2599a21d234511b724569717;p=mediagoblin.git Adding fotgot password functionality --- diff --git a/mediagoblin/auth/forms.py b/mediagoblin/auth/forms.py index 917909c5..1be74aa6 100644 --- a/mediagoblin/auth/forms.py +++ b/mediagoblin/auth/forms.py @@ -15,6 +15,7 @@ # along with this program. If not, see . import wtforms +import re from mediagoblin.util import fake_ugettext_passthrough as _ @@ -49,3 +50,34 @@ class LoginForm(wtforms.Form): password = wtforms.PasswordField( _('Password'), [wtforms.validators.Required()]) + + +class ForgotPassForm(wtforms.Form): + username = wtforms.TextField( + 'Username or email', + [wtforms.validators.Required()]) + + def validate_username(form,field): + if not (re.match(r'^\w+$',field.data) or + re.match(r'^.+@[^.].*\.[a-z]{2,10}$',field.data, re.IGNORECASE)): + raise wtforms.ValidationError(u'Incorrect input') + + +class ChangePassForm(wtforms.Form): + password = wtforms.PasswordField( + 'Password', + [wtforms.validators.Required(), + wtforms.validators.Length(min=6, max=30), + wtforms.validators.EqualTo( + 'confirm_password', + 'Passwords must match.')]) + confirm_password = wtforms.PasswordField( + 'Confirm password', + [wtforms.validators.Required()]) + userid = wtforms.HiddenField( + '', + [wtforms.validators.Required()]) + token = wtforms.HiddenField( + '', + [wtforms.validators.Required()]) + diff --git a/mediagoblin/auth/lib.py b/mediagoblin/auth/lib.py index 6d1aec49..df93b666 100644 --- a/mediagoblin/auth/lib.py +++ b/mediagoblin/auth/lib.py @@ -47,7 +47,7 @@ def bcrypt_check_password(raw_pass, stored_hash, extra_salt=None): # number (thx to zooko on this advice, which I hopefully # incorporated right.) # - # See also: + # See also: rand_salt = bcrypt.gensalt(5) randplus_stored_hash = bcrypt.hashpw(stored_hash, rand_salt) randplus_hashed_pass = bcrypt.hashpw(hashed_pass, rand_salt) @@ -99,7 +99,7 @@ def send_verification_email(user, request): Args: - user: a user object - - request: the request + - request: the request """ rendered_email = render_template( request, 'mediagoblin/auth/verification_email.txt', @@ -116,8 +116,38 @@ def send_verification_email(user, request): [user['email']], # TODO # Due to the distributed nature of GNU MediaGoblin, we should - # find a way to send some additional information about the - # specific GNU MediaGoblin instance in the subject line. For - # example "GNU MediaGoblin @ Wandborg - [...]". + # find a way to send some additional information about the + # specific GNU MediaGoblin instance in the subject line. For + # example "GNU MediaGoblin @ Wandborg - [...]". 'GNU MediaGoblin - Verify your email!', rendered_email) + + +EMAIL_FP_VERIFICATION_TEMPLATE = ( + u"http://{host}{uri}?" + u"userid={userid}&token={fp_verification_key}") + +def send_fp_verification_email(user,request): + """ + Send the verification email to users to change their password. + + Args: + - user: a user object + - request: the request + """ + rendered_email = render_template( + request, 'mediagoblin/auth/fp_verification_email.txt', + {'username': user['username'], + 'verification_url': EMAIL_FP_VERIFICATION_TEMPLATE.format( + host=request.host, + uri=request.urlgen('mediagoblin.auth.verify_forgot_password'), + userid=unicode(user['_id']), + fp_verification_key=user['fp_verification_key'])}) + + # TODO: There is no error handling in place + send_email( + mg_globals.email_sender_address, + [user['email']], + 'GNU MediaGoblin - Change forgotten password!', + rendered_email) + diff --git a/mediagoblin/auth/routing.py b/mediagoblin/auth/routing.py index 9547b3ea..14e87133 100644 --- a/mediagoblin/auth/routing.py +++ b/mediagoblin/auth/routing.py @@ -30,4 +30,16 @@ auth_routes = [ Route('mediagoblin.auth.resend_verification_success', '/resend_verification_success/', template='mediagoblin/auth/resent_verification_email.html', + controller='mediagoblin.views:simple_template_render'), + Route('mediagoblin.auth.forgot_password', '/forgotpass/', + controller='mediagoblin.auth.views:forgot_password'), + Route('mediagoblin.auth.verify_forgot_password', '/verifyforgotpass/', + controller='mediagoblin.auth.views:verify_forgot_password'), + Route('mediagoblin.auth.fp_changed_success', + '/fp_changed_success/', + template='mediagoblin/auth/fp_changed_success.html', + controller='mediagoblin.views:simple_template_render'), + Route('mediagoblin.auth.fp_email_sent', + '/fp_email_sent/', + template='mediagoblin/auth/fp_email_sent.html', controller='mediagoblin.views:simple_template_render')] diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py index 4c4a34fd..50276442 100644 --- a/mediagoblin/auth/views.py +++ b/mediagoblin/auth/views.py @@ -15,6 +15,7 @@ # along with this program. If not, see . import uuid +import datetime from webob import exc @@ -22,10 +23,11 @@ from mediagoblin import messages from mediagoblin import mg_globals from mediagoblin.util import render_to_response, redirect, render_404 from mediagoblin.util import pass_to_ugettext as _ -from mediagoblin.db.util import ObjectId +from mediagoblin.db.util import ObjectId, InvalidId from mediagoblin.auth import lib as auth_lib from mediagoblin.auth import forms as auth_forms -from mediagoblin.auth.lib import send_verification_email +from mediagoblin.auth.lib import send_verification_email, \ + send_fp_verification_email def register(request): @@ -187,3 +189,93 @@ def resend_activation(request): return redirect( request, 'mediagoblin.user_pages.user_home', user=request.user['username']) + + +def forgot_password(request): + """ + Forgot password view + + Sends an email whit an url to renew forgoten password + """ + fp_form = auth_forms.ForgotPassForm(request.POST) + + if request.method == 'POST' and fp_form.validate(): + user = request.db.User.one( + {'$or': [{'username': request.POST['username']}, + {'email': request.POST['username']}]}) + + if not user: + fp_form.username.errors.append( + u"Sorry, the username doesn't exists") + else: + user['fp_verification_key'] = unicode(uuid.uuid4()) + user['fp_token_expire'] = datetime.datetime.now() + \ + datetime.timedelta(days=10) + user.save() + + send_fp_verification_email(user, request) + + return redirect(request, 'mediagoblin.auth.fp_email_sent') + + return render_to_response( + request, + 'mediagoblin/auth/forgot_password.html', + {'fp_form': fp_form}) + + +def verify_forgot_password(request): + if request.method == 'GET': + # If we don't have userid and token parameters, we can't do anything;404 + if (not request.GET.has_key('userid') or + not request.GET.has_key('token')): + return exc.HTTPNotFound('You must provide userid and token') + + # check if it's a valid Id + try: + user = request.db.User.find_one( + {'_id': ObjectId(unicode(request.GET['userid']))}) + except InvalidId: + return exc.HTTPNotFound('Invalid id') + + # check if we have a real user and correct token + if (user and + user['fp_verification_key'] == unicode(request.GET['token'])): + cp_form = auth_forms.ChangePassForm(request.GET) + + return render_to_response( + request, + 'mediagoblin/auth/change_fp.html', + {'cp_form': cp_form}) + # in case there is a valid id but no user whit that id in the db + else: + return exc.HTTPNotFound('User not found') + if request.method == 'POST': + # verification doing here to prevent POST values modification + try: + user = request.db.User.find_one( + {'_id': ObjectId(unicode(request.POST['userid']))}) + except InvalidId: + return exc.HTTPNotFound('Invalid id') + + cp_form = auth_forms.ChangePassForm(request.POST) + + # verification doing here to prevent POST values modification + # if token and id are correct they are able to change their password + if (user and + user['fp_verification_key'] == unicode(request.POST['token'])): + + if cp_form.validate(): + user['pw_hash'] = auth_lib.bcrypt_gen_password_hash( + request.POST['password']) + user['fp_verification_key'] = None + user.save() + + return redirect(request, + 'mediagoblin.auth.fp_changed_success') + else: + return render_to_response( + request, + 'mediagoblin/auth/change_fp.html', + {'cp_form': cp_form}) + else: + return exc.HTTPNotFound('User not found') diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py index 5456b248..b0cb6965 100644 --- a/mediagoblin/db/migrations.py +++ b/mediagoblin/db/migrations.py @@ -92,3 +92,18 @@ def mediaentry_add_fail_error_and_metadata(database): {'fail_metadata': {'$exists': False}}, {'$set': {'fail_metadata': {}}}, multi=True) + + +@RegisterMigration(6) +def user_add_forgot_password_token_and_expires(database): + """ + Add token and expiration fields to help recover forgotten passwords + """ + database['users'].update( + {'fp_token': {'$exists': False}}, + {'$set': {'fp_token': ''}}, + multi=True) + database['users'].update( + {'fp_token_expire': {'$exists': False}}, + {'$set': {'fp_token_expire': ''}}, + multi=True) diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index b6e52441..a626937d 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -78,6 +78,8 @@ class User(Document): 'url' : unicode, 'bio' : unicode, # May contain markdown 'bio_html': unicode, # May contain plaintext, or HTML + 'fp_token': unicode, # forgotten password verification key + 'fp_token_expire': datetime.datetime } required_fields = ['username', 'created', 'pw_hash', 'email'] diff --git a/mediagoblin/templates/mediagoblin/auth/change_fp.html b/mediagoblin/templates/mediagoblin/auth/change_fp.html new file mode 100644 index 00000000..0a3c76f6 --- /dev/null +++ b/mediagoblin/templates/mediagoblin/auth/change_fp.html @@ -0,0 +1,37 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +#} +{% extends "mediagoblin/base.html" %} + +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block mediagoblin_content %} + +
+ +
+{% endblock %} + diff --git a/mediagoblin/templates/mediagoblin/auth/forgot_password.html b/mediagoblin/templates/mediagoblin/auth/forgot_password.html new file mode 100644 index 00000000..1708874f --- /dev/null +++ b/mediagoblin/templates/mediagoblin/auth/forgot_password.html @@ -0,0 +1,37 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +#} +{% extends "mediagoblin/base.html" %} + +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block mediagoblin_content %} + +
+ +
+{% endblock %} + diff --git a/mediagoblin/templates/mediagoblin/auth/fp_changed_success.html b/mediagoblin/templates/mediagoblin/auth/fp_changed_success.html new file mode 100644 index 00000000..dfce1423 --- /dev/null +++ b/mediagoblin/templates/mediagoblin/auth/fp_changed_success.html @@ -0,0 +1,25 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +#} +{% extends "mediagoblin/base.html" %} + +{% block mediagoblin_content %} +

+ Your password have been changed. Now you can Login +

+{% endblock %} + diff --git a/mediagoblin/templates/mediagoblin/auth/fp_email_sent.html b/mediagoblin/templates/mediagoblin/auth/fp_email_sent.html new file mode 100644 index 00000000..d7fad722 --- /dev/null +++ b/mediagoblin/templates/mediagoblin/auth/fp_email_sent.html @@ -0,0 +1,26 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +#} +{% extends "mediagoblin/base.html" %} + +{% block mediagoblin_content %} +

+ Please check your email. We send an email whit an url to change your password. +

+ +{% endblock %} + diff --git a/mediagoblin/templates/mediagoblin/auth/fp_verification_email.txt b/mediagoblin/templates/mediagoblin/auth/fp_verification_email.txt new file mode 100644 index 00000000..1b2dbe2f --- /dev/null +++ b/mediagoblin/templates/mediagoblin/auth/fp_verification_email.txt @@ -0,0 +1,25 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +#} +Hi {{ username }}, + +to change your GNU MediaGoblin password, open the following URL in your web browser + +{{ verification_url|safe }} + +If you think this is an error, just ignore this email and continue being a happy goblin! + diff --git a/mediagoblin/templates/mediagoblin/auth/login.html b/mediagoblin/templates/mediagoblin/auth/login.html index afbecf20..75e6eed1 100644 --- a/mediagoblin/templates/mediagoblin/auth/login.html +++ b/mediagoblin/templates/mediagoblin/auth/login.html @@ -44,6 +44,12 @@ {%- trans %}Create one here!{% endtrans %}

+

+ {% trans %}Forgot your password?{% endtrans %} +
+ + {%- trans %}Send a reminder!{% endtrans %} +

{% endif %}