diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -294,6 +294,10 @@ def make_map(config): rmap.connect('reset_password', '%s/password_reset' % ADMIN_PREFIX, controller='login', action='password_reset') + rmap.connect('reset_password_confirmation', + '%s/password_reset_confirmation' % ADMIN_PREFIX, + controller='login', action='password_reset_confirmation') + #FEEDS rmap.connect('rss_feed_home', '/{repo_name:.*}/feed/rss', controller='feed', action='rss', diff --git a/rhodecode/controllers/login.py b/rhodecode/controllers/login.py --- a/rhodecode/controllers/login.py +++ b/rhodecode/controllers/login.py @@ -129,8 +129,8 @@ class LoginController(BaseController): password_reset_form = PasswordResetForm()() try: form_result = password_reset_form.to_python(dict(request.POST)) - user_model.reset_password(form_result) - h.flash(_('Your new password was sent'), + user_model.reset_password_link(form_result) + h.flash(_('Your password reset link was sent'), category='success') return redirect(url('login_home')) @@ -144,6 +144,23 @@ class LoginController(BaseController): return render('/password_reset.html') + def password_reset_confirmation(self): + + if request.GET and request.GET.get('key'): + try: + user_model = UserModel() + user = User.get_by_api_key(request.GET.get('key')) + data = dict(email=user.email) + user_model.reset_password(data) + h.flash(_('Your password reset was successful, ' + 'new password has been sent to your email'), + category='success') + except Exception, e: + log.error(e) + return redirect(url('reset_password')) + + return redirect(url('login_home')) + def logout(self): del session['rhodecode_user'] session.save() diff --git a/rhodecode/lib/celerylib/tasks.py b/rhodecode/lib/celerylib/tasks.py --- a/rhodecode/lib/celerylib/tasks.py +++ b/rhodecode/lib/celerylib/tasks.py @@ -34,7 +34,7 @@ from time import mktime from operator import itemgetter from string import lower -from pylons import config +from pylons import config, url from pylons.i18n.translation import _ from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP, safe_str @@ -249,6 +249,45 @@ def get_commits_stats(repo_name, ts_min_ log.info('LockHeld') return 'Task with key %s already running' % lockkey +@task(ignore_result=True) +def send_password_link(user_email): + try: + log = reset_user_password.get_logger() + except: + log = logging.getLogger(__name__) + + from rhodecode.lib import auth + from rhodecode.model.db import User + + try: + sa = get_session() + user = sa.query(User).filter(User.email == user_email).scalar() + + if user: + link = url('reset_password_confirmation', key=user.api_key, + qualified=True) + tmpl = """ +Hello %s + +We received a request to create a new password for your account. + +You can generate it by clicking following URL: + +%s + +If you didn't request new password please ignore this email. + """ + run_task(send_email, user_email, + "RhodeCode password reset link", + tmpl % (user.short_contact, link)) + log.info('send new password mail to %s', user_email) + + except: + log.error('Failed to update user password') + log.error(traceback.format_exc()) + return False + + return True @task(ignore_result=True) def reset_user_password(user_email): @@ -280,8 +319,8 @@ def reset_user_password(user_email): sa.rollback() run_task(send_email, user_email, - "Your new rhodecode password", - 'Your new rhodecode password:%s' % (new_passwd)) + "Your new RhodeCode password", + 'Your new RhodeCode password:%s' % (new_passwd)) log.info('send new password mail to %s', user_email) except: diff --git a/rhodecode/lib/smtp_mailer.py b/rhodecode/lib/smtp_mailer.py --- a/rhodecode/lib/smtp_mailer.py +++ b/rhodecode/lib/smtp_mailer.py @@ -74,13 +74,19 @@ class SmtpMailer(object): date_ = formatdate(localtime=True) msg = MIMEMultipart() + msg.set_type('multipart/alternative') + msg.preamble = 'You will not see this in a MIME-aware mail reader.\n' + + text_msg = MIMEText(body) + text_msg.set_type('text/plain') + text_msg.set_param('charset', 'UTF-8') + msg['From'] = self.mail_from msg['To'] = ','.join(recipients) msg['Date'] = date_ msg['Subject'] = subject - msg.preamble = 'You will not see this in a MIME-aware mail reader.\n' - msg.attach(MIMEText(body)) + msg.attach(text_msg) if attachment_files: self.__atach_files(msg, attachment_files) diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -243,6 +243,11 @@ class User(Base, BaseModel): else: return Session.query(cls).filter(cls.username == username).one() + @classmethod + def get_by_api_key(cls, api_key): + return Session.query(cls).filter(cls.api_key == api_key).one() + + def update_lastlogin(self): """Update user lastlogin""" diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -213,6 +213,10 @@ class UserModel(BaseModel): self.sa.rollback() raise + def reset_password_link(self, data): + from rhodecode.lib.celerylib import tasks, run_task + run_task(tasks.send_password_link, data['email']) + def reset_password(self, data): from rhodecode.lib.celerylib import tasks, run_task run_task(tasks.reset_user_password, data['email']) diff --git a/rhodecode/public/css/style.css b/rhodecode/public/css/style.css --- a/rhodecode/public/css/style.css +++ b/rhodecode/public/css/style.css @@ -198,7 +198,8 @@ margin-bottom:5px !important; -moz-border-radius: 0px 0px 8px 8px; border-radius: 0px 0px 8px 8px; height:37px; -background:url("../images/header_inner.png") repeat-x scroll 0 0 #003367 +background:url("../images/header_inner.png") repeat-x scroll 0 0 #003367; +box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6); } #header ul#logged-user li { @@ -1383,6 +1384,13 @@ position: absolute; margin-left: -16px; width: 281px; border-radius: 0 0 8px 8px; +box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6); +} + +#quick_login .password_forgoten{ +padding-right:10px; +padding-top:10px; +float:left; } #quick_login div.form div.fields{ diff --git a/rhodecode/templates/base/base.html b/rhodecode/templates/base/base.html --- a/rhodecode/templates/base/base.html +++ b/rhodecode/templates/base/base.html @@ -30,7 +30,7 @@
- ${h.submit('sign_in','Sign In',class_="ui-button")} +
${h.link_to(_('Forgot password ?'),h.url('reset_password'))}
${h.submit('sign_in','Sign In',class_="ui-button")}
diff --git a/rhodecode/templates/password_reset.html b/rhodecode/templates/password_reset.html --- a/rhodecode/templates/password_reset.html +++ b/rhodecode/templates/password_reset.html @@ -28,7 +28,7 @@
${h.submit('send','Reset my password',class_="ui-button")} -
${_('Your new password will be send to matching email address')}
+
${_('Password reset link will be send to matching email address')}
diff --git a/rhodecode/templates/password_reset_confirmation.html b/rhodecode/templates/password_reset_confirmation.html new file mode 100644 diff --git a/rhodecode/tests/functional/test_login.py b/rhodecode/tests/functional/test_login.py --- a/rhodecode/tests/functional/test_login.py +++ b/rhodecode/tests/functional/test_login.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from rhodecode.tests import * from rhodecode.model.db import User +from rhodecode.lib import generate_api_key from rhodecode.lib.auth import check_password @@ -8,39 +9,42 @@ class TestLoginController(TestController def test_index(self): response = self.app.get(url(controller='login', action='index')) - assert response.status == '200 OK', 'Wrong response from login page got %s' % response.status + self.assertEqual(response.status, '200 OK') # Test response... def test_login_admin_ok(self): response = self.app.post(url(controller='login', action='index'), {'username':'test_admin', 'password':'test12'}) - assert response.status == '302 Found', 'Wrong response code from login got %s' % response.status - assert response.session['rhodecode_user'].username == 'test_admin', 'wrong logged in user' + self.assertEqual(response.status, '302 Found') + self.assertEqual(response.session['rhodecode_user'].username , + 'test_admin') response = response.follow() - assert '%s repository' % HG_REPO in response.body + self.assertTrue('%s repository' % HG_REPO in response.body) def test_login_regular_ok(self): response = self.app.post(url(controller='login', action='index'), {'username':'test_regular', 'password':'test12'}) - print response - assert response.status == '302 Found', 'Wrong response code from login got %s' % response.status - assert response.session['rhodecode_user'].username == 'test_regular', 'wrong logged in user' + + self.assertEqual(response.status, '302 Found') + self.assertEqual(response.session['rhodecode_user'].username , + 'test_regular') response = response.follow() - assert '%s repository' % HG_REPO in response.body - assert '' not in response.body + self.assertTrue('%s repository' % HG_REPO in response.body) + self.assertTrue('' not in response.body) def test_login_ok_came_from(self): test_came_from = '/_admin/users' - response = self.app.post(url(controller='login', action='index', came_from=test_came_from), + response = self.app.post(url(controller='login', action='index', + came_from=test_came_from), {'username':'test_admin', 'password':'test12'}) - assert response.status == '302 Found', 'Wrong response code from came from redirection' + self.assertEqual(response.status, '302 Found') response = response.follow() - assert response.status == '200 OK', 'Wrong response from login page got %s' % response.status - assert 'Users administration' in response.body, 'No proper title in response' + self.assertEqual(response.status, '200 OK') + self.assertTrue('Users administration' in response.body) def test_login_short_password(self): @@ -48,24 +52,24 @@ class TestLoginController(TestController {'username':'test_admin', 'password':'as'}) self.assertEqual(response.status, '200 OK') - print response.body + self.assertTrue('Enter 3 characters or more' in response.body) def test_login_wrong_username_password(self): response = self.app.post(url(controller='login', action='index'), {'username':'error', 'password':'test12'}) - assert response.status == '200 OK', 'Wrong response from login page' + self.assertEqual(response.status , '200 OK') - assert 'invalid user name' in response.body, 'No error username message in response' - assert 'invalid password' in response.body, 'No error password message in response' + self.assertTrue('invalid user name' in response.body) + self.assertTrue('invalid password' in response.body) #========================================================================== # REGISTRATIONS #========================================================================== def test_register(self): response = self.app.get(url(controller='login', action='register')) - assert 'Sign Up to RhodeCode' in response.body, 'wrong page for user registration' + self.assertTrue('Sign Up to RhodeCode' in response.body) def test_register_err_same_username(self): response = self.app.post(url(controller='login', action='register'), @@ -76,8 +80,8 @@ class TestLoginController(TestController 'name':'test', 'lastname':'test'}) - assert response.status == '200 OK', 'Wrong response from register page got %s' % response.status - assert 'This username already exists' in response.body + self.assertEqual(response.status , '200 OK') + self.assertTrue('This username already exists' in response.body) def test_register_err_same_email(self): response = self.app.post(url(controller='login', action='register'), @@ -88,7 +92,7 @@ class TestLoginController(TestController 'name':'test', 'lastname':'test'}) - assert response.status == '200 OK', 'Wrong response from register page got %s' % response.status + self.assertEqual(response.status , '200 OK') assert 'This e-mail address is already taken' in response.body def test_register_err_same_email_case_sensitive(self): @@ -99,7 +103,7 @@ class TestLoginController(TestController 'email':'TesT_Admin@mail.COM', 'name':'test', 'lastname':'test'}) - assert response.status == '200 OK', 'Wrong response from register page got %s' % response.status + self.assertEqual(response.status , '200 OK') assert 'This e-mail address is already taken' in response.body def test_register_err_wrong_data(self): @@ -110,7 +114,7 @@ class TestLoginController(TestController 'email':'goodmailm', 'name':'test', 'lastname':'test'}) - assert response.status == '200 OK', 'Wrong response from register page got %s' % response.status + self.assertEqual(response.status , '200 OK') assert 'An email address must contain a single @' in response.body assert 'Enter a value 6 characters long or more' in response.body @@ -124,8 +128,7 @@ class TestLoginController(TestController 'name':'test', 'lastname':'test'}) - print response.body - assert response.status == '200 OK', 'Wrong response from register page got %s' % response.status + self.assertEqual(response.status , '200 OK') assert 'An email address must contain a single @' in response.body assert ('Username may only contain ' 'alphanumeric characters underscores, ' @@ -141,7 +144,7 @@ class TestLoginController(TestController 'name':'test', 'lastname':'test'}) - assert response.status == '200 OK', 'Wrong response from register page got %s' % response.status + self.assertEqual(response.status , '200 OK') assert 'An email address must contain a single @' in response.body assert 'This username already exists' in response.body @@ -156,8 +159,7 @@ class TestLoginController(TestController 'name':'test', 'lastname':'test'}) - print response.body - assert response.status == '200 OK', 'Wrong response from register page got %s' % response.status + self.assertEqual(response.status , '200 OK') assert 'Invalid characters in password' in response.body @@ -170,8 +172,7 @@ class TestLoginController(TestController 'name':'test', 'lastname':'test'}) - assert response.status == '200 OK', 'Wrong response from register page got %s' % response.status - print response.body + self.assertEqual(response.status , '200 OK') assert 'Password do not match' in response.body def test_register_ok(self): @@ -188,7 +189,7 @@ class TestLoginController(TestController 'email':email, 'name':name, 'lastname':lastname}) - assert response.status == '302 Found', 'Wrong response from register page got %s' % response.status + self.assertEqual(response.status , '302 Found') assert 'You have successfully registered into rhodecode' in response.session['flash'][0], 'No flash message about user registration' ret = self.sa.query(User).filter(User.username == 'test_regular4').one() @@ -206,8 +207,9 @@ class TestLoginController(TestController assert "This e-mail address doesn't exist" in response.body, 'Missing error message about wrong email' def test_forgot_password(self): - response = self.app.get(url(controller='login', action='password_reset')) - assert response.status == '200 OK', 'Wrong response from login page got %s' % response.status + response = self.app.get(url(controller='login', + action='password_reset')) + self.assertEqual(response.status , '200 OK') username = 'test_password_reset_1' password = 'qweqwe' @@ -215,16 +217,45 @@ class TestLoginController(TestController name = 'passwd' lastname = 'reset' - response = self.app.post(url(controller='login', action='register'), - {'username':username, - 'password':password, - 'password_confirmation':password, - 'email':email, - 'name':name, - 'lastname':lastname}) - #register new user for email test - response = self.app.post(url(controller='login', action='password_reset'), - {'email':email, }) - print response.session['flash'] - assert 'You have successfully registered into rhodecode' in response.session['flash'][0], 'No flash message about user registration' - assert 'Your new password was sent' in response.session['flash'][1], 'No flash message about password reset' + new = User() + new.username = username + new.password = password + new.email = email + new.name = name + new.lastname = lastname + new.api_key = generate_api_key(username) + self.sa.add(new) + self.sa.commit() + + response = self.app.post(url(controller='login', + action='password_reset'), + {'email':email, }) + + self.checkSessionFlash(response, 'Your password reset link was sent') + + response = response.follow() + + # BAD KEY + + key = "bad" + response = self.app.get(url(controller='login', + action='password_reset_confirmation', + key=key)) + self.assertEqual(response.status, '302 Found') + self.assertTrue(response.location.endswith(url('reset_password'))) + + # GOOD KEY + + key = User.by_username(username).api_key + + response = self.app.get(url(controller='login', + action='password_reset_confirmation', + key=key)) + self.assertEqual(response.status, '302 Found') + self.assertTrue(response.location.endswith(url('login_home'))) + + self.checkSessionFlash(response, + ('Your password reset was successful, ' + 'new password has been sent to your email')) + + response = response.follow() diff --git a/setup.cfg b/setup.cfg --- a/setup.cfg +++ b/setup.cfg @@ -6,10 +6,10 @@ tag_svn_revision = true find_links = http://www.pylonshq.com/download/ [nosetests] -verbose=False +verbose=True verbosity=2 with-pylons=test.ini -detailed-errors=0 +detailed-errors=1 nologcapture=1 # Babel configuration