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