# along with this program. If not, see <http://www.gnu.org/licenses/>.
import wtforms
+import re
from mediagoblin.util import fake_ugettext_passthrough as _
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()])
+
# 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)
Args:
- user: a user object
- - request: the request
+ - request: the request
"""
rendered_email = render_template(
request, 'mediagoblin/auth/verification_email.txt',
[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)
+
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')]
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import uuid
+import datetime
from webob import exc
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):
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')
{'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)
'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']
--- /dev/null
+{#
+# 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 %}
+
--- /dev/null
+{#
+# 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 %}
+
--- /dev/null
+{#
+# 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 %}
+
--- /dev/null
+{#
+# 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 %}
+
--- /dev/null
+{#
+# 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!
+
<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>