diff options
author | Nathan Yergler <nathan@yergler.net> | 2011-10-01 12:08:58 -0700 |
---|---|---|
committer | Nathan Yergler <nathan@yergler.net> | 2011-10-01 12:08:58 -0700 |
commit | 6bfbe0242653678c09258b7a642514d706153eac (patch) | |
tree | d3e0c7f94f566886a6b80ce98d8a003465f4368f /mediagoblin/auth | |
parent | 0a8a3fc1571100aba3bd3a3dec98f5e9e252780b (diff) | |
parent | 573aba86b58c2ab064d0d57ed0bbae6bdf9a2819 (diff) | |
download | mediagoblin-6bfbe0242653678c09258b7a642514d706153eac.tar.lz mediagoblin-6bfbe0242653678c09258b7a642514d706153eac.tar.xz mediagoblin-6bfbe0242653678c09258b7a642514d706153eac.zip |
Merge remote-tracking branch 'refs/remotes/upstream/master' into 569-application-middleware
Conflicts:
mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html
Diffstat (limited to 'mediagoblin/auth')
-rw-r--r-- | mediagoblin/auth/forms.py | 44 | ||||
-rw-r--r-- | mediagoblin/auth/lib.py | 40 | ||||
-rw-r--r-- | mediagoblin/auth/routing.py | 12 | ||||
-rw-r--r-- | mediagoblin/auth/views.py | 133 |
4 files changed, 210 insertions, 19 deletions
diff --git a/mediagoblin/auth/forms.py b/mediagoblin/auth/forms.py index daf7b993..6339b4a3 100644 --- a/mediagoblin/auth/forms.py +++ b/mediagoblin/auth/forms.py @@ -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 _ @@ -24,18 +25,14 @@ class RegistrationForm(wtforms.Form): _('Username'), [wtforms.validators.Required(), wtforms.validators.Length(min=3, max=30), - wtforms.validators.Regexp(r'^\w+$')], - description=_( - u"This is the name other users will identify you with.")) + wtforms.validators.Regexp(r'^\w+$')]) password = wtforms.PasswordField( _('Password'), [wtforms.validators.Required(), wtforms.validators.Length(min=6, max=30), wtforms.validators.EqualTo( 'confirm_password', - _('Passwords must match.'))], - description=_( - u"Try to use a strong password!")) + _('Passwords must match.'))]) confirm_password = wtforms.PasswordField( _('Confirm password'), [wtforms.validators.Required()], @@ -44,9 +41,7 @@ class RegistrationForm(wtforms.Form): email = wtforms.TextField( _('Email address'), [wtforms.validators.Required(), - wtforms.validators.Email()], - description=_( - u"Your email will never be published.")) + wtforms.validators.Email()]) class LoginForm(wtforms.Form): @@ -57,3 +52,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 89cfb6ff..d7d351a5 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.app_config['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 76c7ceed..912d89fa 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', '/forgot_password/', + controller='mediagoblin.auth.views:forgot_password'), + Route('mediagoblin.auth.verify_forgot_password', '/forgot_password/verify/', + controller='mediagoblin.auth.views:verify_forgot_password'), + Route('mediagoblin.auth.fp_changed_success', + '/forgot_password/changed_success/', + template='mediagoblin/auth/fp_changed_success.html', + controller='mediagoblin.views:simple_template_render'), + Route('mediagoblin.auth.fp_email_sent', + '/forgot_password/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 1b942280..f67f0588 100644 --- a/mediagoblin/auth/views.py +++ b/mediagoblin/auth/views.py @@ -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): @@ -151,9 +153,12 @@ def verify_email(request): {'_id': ObjectId(unicode(request.GET['userid']))}) if user and user['verification_key'] == unicode(request.GET['token']): - user['status'] = u'active' - user['email_verified'] = True + user[u'status'] = u'active' + user[u'email_verified'] = True + user[u'verification_key'] = None + user.save() + messages.add_message( request, messages.SUCCESS, @@ -176,7 +181,7 @@ def resend_activation(request): Resend the activation email. """ - request.user['verification_key'] = unicode(uuid.uuid4()) + request.user[u'verification_key'] = unicode(uuid.uuid4()) request.user.save() send_verification_email(request.user, request) @@ -188,3 +193,121 @@ 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(): + # '$or' not available till mongodb 1.5.3 + user = request.db.User.find_one( + {'username': request.POST['username']}) + if not user: + user = request.db.User.find_one( + {'email': request.POST['username']}) + + if user: + if user['email_verified'] and user['status'] == 'active': + user[u'fp_verification_key'] = unicode(uuid.uuid4()) + user[u'fp_token_expire'] = datetime.datetime.now() + \ + datetime.timedelta(days=10) + user.save() + + send_fp_verification_email(user, request) + else: + # special case... we can't send the email because the + # username is inactive / hasn't verified their email + messages.add_message( + request, + messages.WARNING, + _("Could not send password recovery email as " + "your username is inactive or your account's " + "email address has not been verified.")) + + return redirect( + request, 'mediagoblin.user_pages.user_home', + user=user['username']) + + + # do not reveal whether or not there is a matching user, just move along + 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): + """ + Check the forgot-password verification and possibly let the user + change their password because of it. + """ + # get form data variables, and specifically check for presence of token + formdata = _process_for_token(request) + if not formdata['has_userid_and_token']: + return render_404(request) + + formdata_token = formdata['vars']['token'] + formdata_userid = formdata['vars']['userid'] + formdata_vars = formdata['vars'] + + # check if it's a valid Id + try: + user = request.db.User.find_one( + {'_id': ObjectId(unicode(formdata_userid))}) + except InvalidId: + return render_404(request) + + # check if we have a real user and correct token + if ((user and user['fp_verification_key'] and + user['fp_verification_key'] == unicode(formdata_token) and + datetime.datetime.now() < user['fp_token_expire'] + and user['email_verified'] and user['status'] == 'active')): + + cp_form = auth_forms.ChangePassForm(formdata_vars) + + if request.method == 'POST' and cp_form.validate(): + user[u'pw_hash'] = auth_lib.bcrypt_gen_password_hash( + request.POST['password']) + user[u'fp_verification_key'] = None + user[u'fp_token_expire'] = 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}) + + # in case there is a valid id but no user whit that id in the db + # or the token expired + else: + return render_404(request) + + +def _process_for_token(request): + """ + Checks for tokens in formdata without prior knowledge of request method + + For now, returns whether the userid and token formdata variables exist, and + the formdata variables in a hash. Perhaps an object is warranted? + """ + # retrieve the formdata variables + if request.method == 'GET': + formdata_vars = request.GET + else: + formdata_vars = request.POST + + formdata = { + 'vars': formdata_vars, + 'has_userid_and_token': + formdata_vars.has_key('userid') and formdata_vars.has_key('token')} + + return formdata |