Adding fotgot password functionality

This commit is contained in:
Alejandro Villanueva 2011-07-21 11:55:41 -05:00 committed by Caleb Forbes Davis V
parent ad56a4826b
commit 25ba955e20
12 changed files with 346 additions and 7 deletions

View File

@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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()])

View File

@ -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)

View File

@ -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')]

View File

@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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')

View File

@ -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)

View File

@ -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']

View File

@ -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 <http://www.gnu.org/licenses/>.
#}
{% extends "mediagoblin/base.html" %}
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block mediagoblin_content %}
<form action="{{ request.urlgen('mediagoblin.auth.verify_forgot_password') }}"
method="POST" enctype="multipart/form-data">
<div class="login_box form_box">
<h1>Enter your new password</h1>
{{ wtforms_util.render_divs(cp_form) }}
<div class="form_submit_buttons">
<input type="submit" value="submit" class="button"/>
</div>
</div>
</form>
{% endblock %}

View File

@ -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 <http://www.gnu.org/licenses/>.
#}
{% extends "mediagoblin/base.html" %}
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block mediagoblin_content %}
<form action="{{ request.urlgen('mediagoblin.auth.forgot_password') }}"
method="POST" enctype="multipart/form-data">
<div class="login_box form_box">
<h1>Enter your username or email</h1>
{{ wtforms_util.render_divs(fp_form) }}
<div class="form_submit_buttons">
<input type="submit" value="submit" class="button"/>
</div>
</div>
</form>
{% endblock %}

View File

@ -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 <http://www.gnu.org/licenses/>.
#}
{% extends "mediagoblin/base.html" %}
{% block mediagoblin_content %}
<p>
Your password have been changed. Now you can <a href="{{ request.urlgen('mediagoblin.auth.login') }}">Login</a>
</p>
{% endblock %}

View File

@ -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 <http://www.gnu.org/licenses/>.
#}
{% extends "mediagoblin/base.html" %}
{% block mediagoblin_content %}
<p>
Please check your email. We send an email whit an url to change your password.
</p>
{% endblock %}

View File

@ -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 <http://www.gnu.org/licenses/>.
#}
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!

View File

@ -44,6 +44,12 @@
<a href="{{ request.urlgen('mediagoblin.auth.register') }}">
{%- trans %}Create one here!{% endtrans %}</a>
</p>
<p>
{% trans %}Forgot your password?{% endtrans %}
<br />
<a href="{{ request.urlgen('mediagoblin.auth.forgot_password') }}">
{%- trans %}Send a reminder!{% endtrans %}</a>
</p>
{% endif %}
</div>
</form>