Adding fotgot password functionality
authorAlejandro Villanueva <admin@ialex.org>
Thu, 21 Jul 2011 16:55:41 +0000 (11:55 -0500)
committerCaleb Forbes Davis V <caldavis@gmail.com>
Mon, 29 Aug 2011 01:08:14 +0000 (20:08 -0500)
12 files changed:
mediagoblin/auth/forms.py
mediagoblin/auth/lib.py
mediagoblin/auth/routing.py
mediagoblin/auth/views.py
mediagoblin/db/migrations.py
mediagoblin/db/models.py
mediagoblin/templates/mediagoblin/auth/change_fp.html [new file with mode: 0644]
mediagoblin/templates/mediagoblin/auth/forgot_password.html [new file with mode: 0644]
mediagoblin/templates/mediagoblin/auth/fp_changed_success.html [new file with mode: 0644]
mediagoblin/templates/mediagoblin/auth/fp_email_sent.html [new file with mode: 0644]
mediagoblin/templates/mediagoblin/auth/fp_verification_email.txt [new file with mode: 0644]
mediagoblin/templates/mediagoblin/auth/login.html

index 917909c5995266eab555102e271856b9cd4588bf..1be74aa672396575043acd19bf4b71cc30b97160 100644 (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()])
+
index 6d1aec49cf13c94cea48d6703e3f4a6e6afdd174..df93b666c319df14e7203e33a43660e8b72e9858 100644 (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)
+
index 9547b3ea58b8a750b1f9d96115353e2df157eb05..14e87133d3a0b31115f07625a6ea022c3240ae3a 100644 (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')]
index 4c4a34fdcc07c78e718fd177fc267670e56d593b..50276442a55f6fae522bb89e8d59ad1260c68502 100644 (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')
index 5456b248ed663151615171c31e1488a77172b21d..b0cb6965eb8e5807aed15ede6ff5760c1f5aea18 100644 (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)
index b6e52441e644188380ca95a2ed2405b541c1c075..a626937d0aefa4a97585a1c0a239ae7c07d0c47a 100644 (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']
diff --git a/mediagoblin/templates/mediagoblin/auth/change_fp.html b/mediagoblin/templates/mediagoblin/auth/change_fp.html
new file mode 100644 (file)
index 0000000..0a3c76f
--- /dev/null
@@ -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 %}
+
diff --git a/mediagoblin/templates/mediagoblin/auth/forgot_password.html b/mediagoblin/templates/mediagoblin/auth/forgot_password.html
new file mode 100644 (file)
index 0000000..1708874
--- /dev/null
@@ -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 %}
+
diff --git a/mediagoblin/templates/mediagoblin/auth/fp_changed_success.html b/mediagoblin/templates/mediagoblin/auth/fp_changed_success.html
new file mode 100644 (file)
index 0000000..dfce142
--- /dev/null
@@ -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 %}
+
diff --git a/mediagoblin/templates/mediagoblin/auth/fp_email_sent.html b/mediagoblin/templates/mediagoblin/auth/fp_email_sent.html
new file mode 100644 (file)
index 0000000..d7fad72
--- /dev/null
@@ -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 %}
+
diff --git a/mediagoblin/templates/mediagoblin/auth/fp_verification_email.txt b/mediagoblin/templates/mediagoblin/auth/fp_verification_email.txt
new file mode 100644 (file)
index 0000000..1b2dbe2
--- /dev/null
@@ -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!
+
index afbecf2061d0d329428248a944f2bc99e4b63263..75e6eed1d43f561712dfb212be5489b0dfff57b6 100644 (file)
           <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>