Browse Source

#362: Activate account and Reset password logic

Rafał Pitoń 11 years ago
parent
commit
a47556bc15

+ 5 - 1
misago/static/misago/css/misago/forms.less

@@ -26,7 +26,7 @@
   .form-header {
   .form-header {
     padding: @form-panel-padding;
     padding: @form-panel-padding;
 
 
-    h1, h2 {
+    h1, h2, h3 {
       margin: 0px;
       margin: 0px;
     }
     }
 
 
@@ -38,6 +38,10 @@
       font-size: @font-size-large * 1.2;
       font-size: @font-size-large * 1.2;
     }
     }
 
 
+    h3 {
+      font-size: @font-size-large;
+    }
+
     p {
     p {
       margin: (@line-height-computed / 2) 0px 0px 0px;
       margin: (@line-height-computed / 2) 0px 0px 0px;
     }
     }

+ 1 - 20
misago/templates/misago/activation/request.html

@@ -19,26 +19,7 @@
             <h2>{% trans "Request activation e-mail" %}</h2>
             <h2>{% trans "Request activation e-mail" %}</h2>
           </div>
           </div>
 
 
-          {% for error in form.non_field_errors %}
-          <div class="form-errors-block">
-            <ul class="list-unstyled">
-              <li>
-                {{ error }}
-              </li>
-              {% if form.user_ban and form.user_ban.user_message %}
-              <li>
-                <small>
-                  {{ form.user_ban.user_message|escape|linebreaksbr|urlize }}
-                </small>
-              </li>
-              {% elif form.user_cache.activation_by_user %}
-              <li>
-                <a href="#">{% trans "Click here to activate your account." %}</a>
-              </li>
-              {% endif %}
-            </ul>
-          </div>
-          {% endfor %}
+          {% include "misago/auth_form_errors.html" %}
 
 
           <div class="form-body no-fieldsets">
           <div class="form-body no-fieldsets">
 
 

+ 1 - 1
misago/templates/misago/emails/activation/by_user.html

@@ -4,7 +4,7 @@
 
 
 {% block content %}
 {% block content %}
 {% blocktrans trimmed with username=recipient.username %}
 {% blocktrans trimmed with username=recipient.username %}
-{{ username }}, to activate your account click link below:
+{{ username }}, to activate your account click the below link:
 {% endblocktrans %}
 {% endblocktrans %}
 <br>
 <br>
 <br>
 <br>

+ 2 - 1
misago/templates/misago/emails/activation/by_user.txt

@@ -1,9 +1,10 @@
 {% extends "misago/emails/base.txt" %}
 {% extends "misago/emails/base.txt" %}
 {% load i18n %}
 {% load i18n %}
 
 
+
 {% block content %}
 {% block content %}
 {% blocktrans trimmed with username=recipient.username %}
 {% blocktrans trimmed with username=recipient.username %}
-{{ username }}, to activate your account click link below:
+{{ username }}, to activate your account click the below link:
 {% endblocktrans %}
 {% endblocktrans %}
 {{ SITE_ADDRESS }}{% url 'misago:activate_by_token' user_id=recipient.pk token=activation_token %}"
 {{ SITE_ADDRESS }}{% url 'misago:activate_by_token' user_id=recipient.pk token=activation_token %}"
 {% endblock content %}
 {% endblock content %}

+ 23 - 0
misago/templates/misago/emails/forgottenpassword/confirm.html

@@ -0,0 +1,23 @@
+{% extends "misago/emails/base.html" %}
+{% load i18n misago_capture %}
+
+
+{% block content %}
+{% blocktrans trimmed with username=recipient.username %}
+{{ username }}, you are receiving this message because you have started forgotten password change procedure for your forum account.
+{% endblocktrans %}
+<br>
+<br>
+{% blocktrans trimmed %}
+To replace your account password with new one click the link below:
+{% endblocktrans %}
+<br>
+<br>
+<a href="{{ SITE_ADDRESS }}{% url 'misago:reset_password_confirm' user_id=recipient.pk token=confirmation_token %}">{% trans "Change my password" %}</a>
+<br>
+<br>
+{% blocktrans trimmed %}
+New password will be set on your account and sent to you in next e-mail.
+{% endblocktrans %}
+<br>
+{% endblock content %}

+ 18 - 0
misago/templates/misago/emails/forgottenpassword/confirm.txt

@@ -0,0 +1,18 @@
+{% extends "misago/emails/base.txt" %}
+{% load i18n %}
+
+
+{% block content %}
+{% blocktrans trimmed with username=recipient.username %}
+{{ username }}, you are receiving this message because you have started forgotten password change procedure for your forum account.
+{% endblocktrans %}
+
+{% blocktrans trimmed %}
+To replace your account password with new one click the link below:
+{% endblocktrans %}
+{{ SITE_ADDRESS }}{% url 'misago:reset_password_confirm' user_id=recipient.pk token=confirmation_token %}
+
+{% blocktrans trimmed %}
+New password will be set on your account and sent to you in next e-mail.
+{% endblocktrans %}
+{% endblock content %}

+ 26 - 0
misago/templates/misago/emails/forgottenpassword/new.html

@@ -0,0 +1,26 @@
+{% extends "misago/emails/base.html" %}
+{% load i18n misago_capture %}
+
+
+{% block content %}
+{% blocktrans trimmed with username=recipient.username %}
+{{ username }}, your new password is:
+{% endblocktrans %}
+<br>
+<br>
+<div style="background-color: #eee; padding: 8px 0px; font-size: 18px; text-align: center; font-family: Menlo, Monaco, Consolas, monospace">{{ new_password }}</div>
+<br>
+<br>
+{% capture trimmed as login_link %}
+<a href="{{ SITE_ADDRESS }}{% url LOGIN_URL %}">{% trans "this form" %}</a>
+{% endcapture %}
+{% blocktrans trimmed with login_form=login_link|safe %}
+You can now sign in to your account with new password using {{ login_form }}.
+{% endblocktrans %}
+<br>
+<br>
+{% blocktrans trimmed %}
+Please change your password to custom one as soon as you sign in to your account.
+{% endblocktrans %}
+<br>
+{% endblock content %}

+ 20 - 0
misago/templates/misago/emails/forgottenpassword/new.txt

@@ -0,0 +1,20 @@
+{% extends "misago/emails/base.txt" %}
+{% load i18n %}
+
+
+{% block content %}
+{% blocktrans trimmed with username=recipient.username %}
+{{ username }}, your new password is:
+{% endblocktrans %}
+
+{{ new_password }}
+
+{% blocktrans trimmed %}
+You can now sign in to your account with new password using the form below:
+{% endblocktrans %}
+{{ SITE_ADDRESS }}{% url LOGIN_URL %}
+
+{% blocktrans trimmed %}
+Please change your password to custom one as soon as you sign in to your account.
+{% endblocktrans %}
+{% endblock content %}

+ 1 - 0
misago/templates/misago/emails/register/complete.txt

@@ -1,6 +1,7 @@
 {% extends "misago/emails/base.txt" %}
 {% extends "misago/emails/base.txt" %}
 {% load i18n %}
 {% load i18n %}
 
 
+
 {% block content %}
 {% block content %}
 {% blocktrans trimmed with username=recipient.username %}
 {% blocktrans trimmed with username=recipient.username %}
 {{ username }}, thank you for joining us!
 {{ username }}, thank you for joining us!

+ 1 - 0
misago/templates/misago/emails/register/inactive.txt

@@ -1,6 +1,7 @@
 {% extends "misago/emails/base.txt" %}
 {% extends "misago/emails/base.txt" %}
 {% load i18n %}
 {% load i18n %}
 
 
+
 {% block content %}
 {% block content %}
 {% blocktrans trimmed with username=recipient.username %}
 {% blocktrans trimmed with username=recipient.username %}
 {{ username }}, thank you for joining us!
 {{ username }}, thank you for joining us!

+ 29 - 0
misago/templates/misago/forgottenpassword/confirmation_sent.html

@@ -0,0 +1,29 @@
+{% extends "misago/base.html" %}
+{% load i18n %}
+
+
+{% block title %}{% trans "Change password confirmation sent" %} | {{ block.super }}{% endblock %}
+
+
+{% block content %}
+<div class="page-header">
+  <div class="container">
+    <h1>
+      <span class="fa fa-key">
+      {% trans "Change password confirmation sent" %}
+    </h1>
+  </div>
+</div>
+
+<div class="container">
+  <div class="misago-markup">
+
+    <p class="lead">
+      {% blocktrans trimmed with email=requesting_user.email %}
+      We have sent an e-mail to {{ email }} with a link that you have to click to confirm your request for new password.
+      {% endblocktrans %}
+    </p>
+
+  </div>
+</div>
+{% endblock content %}

+ 29 - 0
misago/templates/misago/forgottenpassword/password_sent.html

@@ -0,0 +1,29 @@
+{% extends "misago/base.html" %}
+{% load i18n %}
+
+
+{% block title %}{% trans "Password changed" %} | {{ block.super }}{% endblock %}
+
+
+{% block content %}
+<div class="page-header">
+  <div class="container">
+    <h1>
+      <span class="fa fa-key">
+      {% trans "Password changed" %}
+    </h1>
+  </div>
+</div>
+
+<div class="container">
+  <div class="misago-markup">
+
+    <p class="lead">
+      {% blocktrans trimmed with email=requesting_user.email %}
+      New password has been set on your account and was send to {{ email }}.
+      {% endblocktrans %}
+    </p>
+
+  </div>
+</div>
+{% endblock content %}

+ 62 - 0
misago/templates/misago/forgottenpassword/request.html

@@ -0,0 +1,62 @@
+{% extends "misago/base.html" %}
+{% load i18n %}
+
+
+{% block title %}{% trans "Change forgotten password" %} | {{ block.super }}{% endblock %}
+
+
+{% block content %}
+<div class="page-header">
+  <div class="container">
+    <h1>
+      <span class="fa fa-key">
+      {% trans "Change forgotten password" %}
+    </h1>
+  </div>
+</div>
+<div class="container">
+
+  <div class="row">
+    <div class="col-md-8">
+
+      <h2>{% trans "Recovering password" %}</h2>
+
+      <p>{% trans "Because user passwords are processed in an irreversible way before being saved to database, it is not possible for us to simply send you your password. Instead your current password can be replaced with new randomly generated password." %}</p>
+
+      <p>{% trans "In order to protect our users from fraudulent password change, you need to first confirm your wish to change password by clicking the confirmation link that will be generated and sent to e-mail address associated with your account." %}</p>
+
+    </div>
+    <div class="col-md-4">
+
+      <div class="form-panel">
+        <form method="POST" role="form">
+          {% csrf_token %}
+
+          <div class="form-header">
+            <h3>{% trans "Request confirmation link" %}</h3>
+          </div>
+
+          {% include "misago/auth_form_errors.html" %}
+
+          <div class="form-body no-fieldsets">
+
+            <div class="form-group">
+              <div class="control-input">
+                <input type="text" name="username" class="form-control input-lg" placeholder="{% trans "Username or e-mail" %}" {% if form.username.value %}value="{{ form.username.value }}"{% endif %}>
+              </div>
+            </div>
+
+          </div>
+
+          <div class="form-footer">
+            <button class="btn btn-primary btn-block">{% trans "Send confirmation link" %}</button>
+          </div>
+
+        </form>
+      </div>
+
+    </div>
+  </div>
+
+</div>
+{% endblock content %}

+ 2 - 2
misago/templates/misago/login.html

@@ -56,8 +56,8 @@
             <span class="fa fa-fw fa-key"></span>
             <span class="fa fa-fw fa-key"></span>
             {% trans "I don't remember my password." %}
             {% trans "I don't remember my password." %}
           </h5>
           </h5>
-          <a href="#">
-            {% trans "Reset password" %}
+          <a href="{% url 'misago:request_password_reset' %}">
+            {% trans "Change forgotten password" %}
           </a>
           </a>
         </li>
         </li>
         <li>
         <li>

+ 24 - 10
misago/users/forms/auth.py

@@ -10,6 +10,15 @@ from misago.users.bans import get_user_ban
 
 
 
 
 class MisagoAuthMixin(object):
 class MisagoAuthMixin(object):
+    error_messages = {
+        'empty_data': _("You have to fill out both fields."),
+        'invalid_login': _("Your login or password is incorrect."),
+        'inactive_user': _("You have to activate your account before "
+                           "you will be able to sign in."),
+        'inactive_admin': _("Administrator has to activate your account "
+                            "before you will be able to sign in."),
+    }
+
     def confirm_user_active(self, user):
     def confirm_user_active(self, user):
         if user.requires_activation_by_admin:
         if user.requires_activation_by_admin:
             raise ValidationError(
             raise ValidationError(
@@ -58,15 +67,6 @@ class AuthenticationForm(MisagoAuthMixin, forms.Form, BaseAuthenticationForm):
     password = forms.CharField(label=_("Password"), required=False,
     password = forms.CharField(label=_("Password"), required=False,
                                widget=forms.PasswordInput)
                                widget=forms.PasswordInput)
 
 
-    error_messages = {
-        'empty_data': _("You have to fill out both fields."),
-        'invalid_login': _("Your login or password is incorrect."),
-        'inactive_user': _("You have to activate your account before "
-                           "you will be able to sign in."),
-        'inactive_admin': _("Administrator has to activate your account "
-                            "before you will be able to sign in."),
-    }
-
     def clean(self):
     def clean(self):
         username = self.cleaned_data.get('username')
         username = self.cleaned_data.get('username')
         password = self.cleaned_data.get('password')
         password = self.cleaned_data.get('password')
@@ -127,7 +127,7 @@ class GetUserForm(MisagoAuthMixin, forms.Form):
             user =  User.objects.get_by_username_or_email(data['username'])
             user =  User.objects.get_by_username_or_email(data['username'])
             self.user_cache = user
             self.user_cache = user
         except User.DoesNotExist:
         except User.DoesNotExist:
-            raise forms.ValidationError(_("User could not be found."))
+            raise forms.ValidationError(_("Invalid username or e-mail."))
 
 
         self.confirm_allowed(user)
         self.confirm_allowed(user)
 
 
@@ -152,3 +152,17 @@ class ResendActivationForm(GetUserForm):
             message = _("%(username)s, only administrator may activate "
             message = _("%(username)s, only administrator may activate "
                         "your account.")
                         "your account.")
             raise forms.ValidationError(message % username_format)
             raise forms.ValidationError(message % username_format)
+
+
+class ResetPasswordForm(GetUserForm):
+    error_messages = {
+        'inactive_user': _("You have to activate your account before "
+                           "you will be able to request new password."),
+        'inactive_admin': _("Administrator has to activate your account "
+                            "before you will be able to request "
+                            "new password."),
+    }
+
+    def confirm_allowed(self, user):
+        self.confirm_user_not_banned(user)
+        self.confirm_user_active(user)

+ 54 - 1
misago/users/tests/test_activation_views.py

@@ -4,9 +4,10 @@ from django.core.urlresolvers import reverse
 from django.test import TestCase
 from django.test import TestCase
 
 
 from misago.users.models import Ban, BAN_USERNAME
 from misago.users.models import Ban, BAN_USERNAME
+from misago.users.tokens import make_activation_token
 
 
 
 
-class ActivationViewTests(TestCase):
+class ActivationViewsTests(TestCase):
     def test_view_get_returns_200(self):
     def test_view_get_returns_200(self):
         """request activation view returns 200 on GET"""
         """request activation view returns 200 on GET"""
         response = self.client.get(reverse('misago:request_activation'))
         response = self.client.get(reverse('misago:request_activation'))
@@ -54,3 +55,55 @@ class ActivationViewTests(TestCase):
         self.assertIn('already active', response.content)
         self.assertIn('already active', response.content)
 
 
         self.assertTrue(not mail.outbox)
         self.assertTrue(not mail.outbox)
+
+    def test_view_activate_banned(self):
+        """activate banned user shows error"""
+        User = get_user_model()
+        test_user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123',
+                                             requires_activation=1)
+        Ban.objects.create(test=BAN_USERNAME, banned_value='bob',
+                           user_message='Nope!')
+
+        activation_token = make_activation_token(test_user)
+
+        response = self.client.get(
+            reverse('misago:activate_by_token',
+                    kwargs={'user_id': test_user.pk,
+                            'token': activation_token}))
+        self.assertEqual(response.status_code, 302)
+
+        test_user = User.objects.get(pk=test_user.pk)
+        self.assertEqual(test_user.requires_activation, 1)
+
+    def test_view_activate_active(self):
+        """activate active user shows error"""
+        User = get_user_model()
+        test_user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
+
+        activation_token = make_activation_token(test_user)
+
+        response = self.client.get(
+            reverse('misago:activate_by_token',
+                    kwargs={'user_id': test_user.pk,
+                            'token': activation_token}))
+        self.assertEqual(response.status_code, 302)
+
+        test_user = User.objects.get(pk=test_user.pk)
+        self.assertEqual(test_user.requires_activation, 0)
+
+    def test_view_activate_inactive(self):
+        """activate inactive user passess"""
+        User = get_user_model()
+        test_user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123',
+                                             requires_activation=1)
+
+        activation_token = make_activation_token(test_user)
+
+        response = self.client.get(
+            reverse('misago:activate_by_token',
+                    kwargs={'user_id': test_user.pk,
+                            'token': activation_token}))
+        self.assertEqual(response.status_code, 302)
+
+        test_user = User.objects.get(pk=test_user.pk)
+        self.assertEqual(test_user.requires_activation, 0)

+ 117 - 0
misago/users/tests/test_forgottenpassword_views.py

@@ -0,0 +1,117 @@
+from django.contrib.auth import get_user_model
+from django.core import mail
+from django.core.urlresolvers import reverse
+from django.test import TestCase
+
+from misago.users.models import Ban, BAN_USERNAME
+from misago.users.tokens import make_password_reset_token
+
+
+class ForgottenPasswordViewsTests(TestCase):
+    def test_view_get_returns_200(self):
+        """request new password view returns 200 on GET"""
+        response = self.client.get(reverse('misago:request_password_reset'))
+        self.assertEqual(response.status_code, 200)
+
+    def test_view_submit(self):
+        """request new password view sends confirmation mail"""
+        User = get_user_model()
+        User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
+
+        response = self.client.post(
+            reverse('misago:request_password_reset'),
+            data={'username': 'Bob'})
+
+        self.assertEqual(response.status_code, 302)
+
+        self.assertIn('password change', mail.outbox[0].subject)
+
+    def test_view_submit_banned(self):
+        """request new password view errors for banned users"""
+        User = get_user_model()
+        User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
+        Ban.objects.create(test=BAN_USERNAME, banned_value='bob',
+                           user_message='Nope!')
+
+        response = self.client.post(
+            reverse('misago:request_password_reset'),
+            data={'username': 'Bob'})
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('Nope!', response.content)
+
+        self.assertTrue(not mail.outbox)
+
+    def test_view_submit_inactive(self):
+        """request new password view errors for inactive users"""
+        User = get_user_model()
+        User.objects.create_user('Bob', 'bob@test.com', 'Pass.123',
+                                 requires_activation=1)
+
+        response = self.client.post(
+            reverse('misago:request_password_reset'),
+            data={'username': 'Bob'})
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('activate', response.content)
+
+        self.assertTrue(not mail.outbox)
+
+    def test_change_password_on_banned(self):
+        """change banned user password errors"""
+        User = get_user_model()
+        test_user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
+        old_password = test_user.password
+
+        Ban.objects.create(test=BAN_USERNAME, banned_value='bob',
+                           user_message='Nope!')
+
+        password_token = make_password_reset_token(test_user)
+
+        response = self.client.get(
+            reverse('misago:reset_password_confirm',
+                    kwargs={'user_id': test_user.pk,
+                            'token': password_token}))
+        self.assertEqual(response.status_code, 302)
+
+        test_user = User.objects.get(pk=test_user.pk)
+        self.assertEqual(test_user.password, old_password)
+
+        self.assertTrue(not mail.outbox)
+
+    def test_change_password_on_inactive(self):
+        """change inactive user password errors"""
+        User = get_user_model()
+        test_user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123',
+                                             requires_activation=1)
+        old_password = test_user.password
+
+        password_token = make_password_reset_token(test_user)
+
+        response = self.client.get(
+            reverse('misago:reset_password_confirm',
+                    kwargs={'user_id': test_user.pk,
+                            'token': password_token}))
+        self.assertEqual(response.status_code, 302)
+
+        test_user = User.objects.get(pk=test_user.pk)
+        self.assertEqual(test_user.password, old_password)
+
+        self.assertTrue(not mail.outbox)
+
+    def test_successful_change(self):
+        """change allright user password"""
+        User = get_user_model()
+        test_user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
+        old_password = test_user.password
+
+        password_token = make_password_reset_token(test_user)
+
+        response = self.client.get(
+            reverse('misago:reset_password_confirm',
+                    kwargs={'user_id': test_user.pk,
+                            'token': password_token}))
+        self.assertEqual(response.status_code, 302)
+
+        test_user = User.objects.get(pk=test_user.pk)
+        self.assertNotEqual(test_user.password, old_password)
+
+        self.assertIn('New password', mail.outbox[0].subject)

+ 16 - 2
misago/users/tokens.py

@@ -60,11 +60,11 @@ def is_valid(user, token_type, token):
         return False
         return False
 
 
     creation_day = int(unobfuscated[8:])
     creation_day = int(unobfuscated[8:])
-    return creation_day + 14 >= days_since_epoch()
+    return creation_day + 5 >= days_since_epoch()
 
 
 
 
 """
 """
-Shortcuts for activation token
+Convenience functions for activation token
 """
 """
 ACTIVATION_TOKEN = 'activation'
 ACTIVATION_TOKEN = 'activation'
 
 
@@ -75,3 +75,17 @@ def make_activation_token(user):
 
 
 def is_activation_token_valid(user, token):
 def is_activation_token_valid(user, token):
     return is_valid(user, ACTIVATION_TOKEN, token)
     return is_valid(user, ACTIVATION_TOKEN, token)
+
+
+"""
+Convenience functions for password reset token
+"""
+PASSWORD_RESET_TOKEN = 'reset_password'
+
+
+def make_password_reset_token(user):
+    return make(user, PASSWORD_RESET_TOKEN)
+
+
+def is_password_reset_token_valid(user, token):
+    return is_valid(user, PASSWORD_RESET_TOKEN, token)

+ 8 - 0
misago/users/urls.py

@@ -20,6 +20,14 @@ urlpatterns += patterns('misago.users.views.activation',
 )
 )
 
 
 
 
+urlpatterns += patterns('misago.users.views.forgottenpassword',
+    url(r'^forgotten-password/$', 'request_reset', name='request_password_reset'),
+    url(r'^forgotten-password/confirmation-sent/$', 'confirmation_sent', name='reset_password_confirmation_sent'),
+    url(r'^forgotten-password/(?P<user_id>\d+)/(?P<token>[a-zA-Z0-9]+)/$', 'reset_password', name='reset_password_confirm'),
+    url(r'^forgotten-password/password-sent/$', 'new_password_sent', name='request_password_new_sent'),
+)
+
+
 urlpatterns += patterns('misago.users.views.api',
 urlpatterns += patterns('misago.users.views.api',
     url(r'^api/validate/username/$', 'validate_username', name='api_validate_username'),
     url(r'^api/validate/username/$', 'validate_username', name='api_validate_username'),
     url(r'^api/validate/username/(?P<user_id>\d+)/$', 'validate_username', name='api_validate_username'),
     url(r'^api/validate/username/(?P<user_id>\d+)/$', 'validate_username', name='api_validate_username'),

+ 22 - 17
misago/users/views/activation.py

@@ -7,6 +7,7 @@ from django.utils.translation import ugettext as _
 from misago.conf import settings
 from misago.conf import settings
 from misago.core.mail import mail_user
 from misago.core.mail import mail_user
 
 
+from misago.users.bans import get_user_ban
 from misago.users.decorators import deny_authenticated, deny_banned_ips
 from misago.users.decorators import deny_authenticated, deny_banned_ips
 from misago.users.forms.auth import ResendActivationForm
 from misago.users.forms.auth import ResendActivationForm
 from misago.users.models import ACTIVATION_REQUIRED_NONE
 from misago.users.models import ACTIVATION_REQUIRED_NONE
@@ -14,8 +15,15 @@ from misago.users.tokens import (make_activation_token,
                                  is_activation_token_valid)
                                  is_activation_token_valid)
 
 
 
 
-@deny_authenticated
-@deny_banned_ips
+def activation_view(f):
+    @deny_authenticated
+    @deny_banned_ips
+    def decorator(*args, **kwargs):
+        return f(*args, **kwargs)
+    return decorator
+
+
+@activation_view
 def request_activation(request):
 def request_activation(request):
     form = ResendActivationForm()
     form = ResendActivationForm()
 
 
@@ -25,20 +33,15 @@ def request_activation(request):
             requesting_user = form.user_cache
             requesting_user = form.user_cache
             request.session['activation_sent_to'] = requesting_user.pk
             request.session['activation_sent_to'] = requesting_user.pk
 
 
-            activation_token = make_activation_token(requesting_user)
-
-            activation_by_admin = requesting_user.requires_activation_by_admin
-            activation_by_user = requesting_user.requires_activation_by_user
-
             mail_subject = _("Account activation on %(forum_title)s forums")
             mail_subject = _("Account activation on %(forum_title)s forums")
             mail_subject = mail_subject % {'forum_title': settings.forum_name}
             mail_subject = mail_subject % {'forum_title': settings.forum_name}
 
 
+            activation_token = make_activation_token(requesting_user)
+
             mail_user(
             mail_user(
                 request, requesting_user, mail_subject,
                 request, requesting_user, mail_subject,
                 'misago/emails/activation/by_user',
                 'misago/emails/activation/by_user',
-                {
-                    'activation_token': activation_token,
-                })
+                {'activation_token': activation_token})
 
 
             return redirect('misago:activation_sent')
             return redirect('misago:activation_sent')
 
 
@@ -46,9 +49,7 @@ def request_activation(request):
                   {'form': form})
                   {'form': form})
 
 
 
 
-
-@deny_authenticated
-@deny_banned_ips
+@activation_view
 def activation_sent(request):
 def activation_sent(request):
     requesting_user_pk = request.session.get('activation_sent_to')
     requesting_user_pk = request.session.get('activation_sent_to')
     if not requesting_user_pk:
     if not requesting_user_pk:
@@ -69,8 +70,7 @@ class ActivationError(Exception):
     pass
     pass
 
 
 
 
-@deny_authenticated
-@deny_banned_ips
+@activation_view
 def activate_by_token(request, user_id, token):
 def activate_by_token(request, user_id, token):
     User = get_user_model()
     User = get_user_model()
     inactive_user = get_object_or_404(User.objects, pk=user_id)
     inactive_user = get_object_or_404(User.objects, pk=user_id)
@@ -82,9 +82,14 @@ def activate_by_token(request, user_id, token):
             raise ActivationStopped(message)
             raise ActivationStopped(message)
         if inactive_user.requires_activation_by_admin:
         if inactive_user.requires_activation_by_admin:
             message = _("%(username)s, your account can be activated "
             message = _("%(username)s, your account can be activated "
-                        "only by one ofthe  administrators.")
+                        "only by one of the administrators.")
             message = message % {'username': inactive_user.username}
             message = message % {'username': inactive_user.username}
             raise ActivationStopped(message)
             raise ActivationStopped(message)
+        if get_user_ban(inactive_user):
+            message = _("%(username)s, your account is banned "
+                        "and can't be activated.")
+            message = message % {'username': inactive_user.username}
+            raise ActivationError(message)
         if not is_activation_token_valid(inactive_user, token):
         if not is_activation_token_valid(inactive_user, token):
             message = _("%(username)s, your activation link is invalid. "
             message = _("%(username)s, your activation link is invalid. "
                         "Try again or request new activation message.")
                         "Try again or request new activation message.")
@@ -95,7 +100,7 @@ def activate_by_token(request, user_id, token):
         return redirect('misago:index')
         return redirect('misago:index')
     except ActivationError as e:
     except ActivationError as e:
         messages.error(request, e.args[0])
         messages.error(request, e.args[0])
-        return redirect('misago:index')
+        return redirect('misago:request_activation')
 
 
     inactive_user.requires_activation = ACTIVATION_REQUIRED_NONE
     inactive_user.requires_activation = ACTIVATION_REQUIRED_NONE
     inactive_user.save(update_fields=['requires_activation'])
     inactive_user.save(update_fields=['requires_activation'])

+ 144 - 0
misago/users/views/forgottenpassword.py

@@ -0,0 +1,144 @@
+from faker import Factory
+from django.contrib import messages
+from django.contrib.auth import get_user_model
+from django.http import Http404
+from django.shortcuts import get_object_or_404, redirect, render
+from django.utils.translation import ugettext as _
+
+from misago.conf import settings
+from misago.core.mail import mail_user
+
+from misago.users.bans import get_user_ban
+from misago.users.decorators import deny_authenticated, deny_banned_ips
+from misago.users.forms.auth import ResetPasswordForm
+from misago.users.models import ACTIVATION_REQUIRED_NONE
+from misago.users.tokens import (make_password_reset_token,
+                                 is_password_reset_token_valid)
+
+
+def reset_view(f):
+    @deny_authenticated
+    @deny_banned_ips
+    def decorator(*args, **kwargs):
+        return f(*args, **kwargs)
+    return decorator
+
+
+@reset_view
+def request_reset(request):
+    form = ResetPasswordForm()
+
+    if request.method == 'POST':
+        form = ResetPasswordForm(request.POST)
+        if form.is_valid():
+            requesting_user = form.user_cache
+            request.session['confirmation_sent_to'] = requesting_user.pk
+
+            mail_subject = _("Confirm %(username)s password change "
+                             "on %(forum_title)s forums")
+            subject_formats = {'username': requesting_user.username,
+                               'forum_title': settings.forum_name}
+            mail_subject = mail_subject % subject_formats
+
+            confirmation_token = make_password_reset_token(requesting_user)
+
+            mail_user(
+                request, requesting_user, mail_subject,
+                'misago/emails/forgottenpassword/confirm',
+                {'confirmation_token': confirmation_token})
+
+            return redirect('misago:reset_password_confirmation_sent')
+
+    return render(request, 'misago/forgottenpassword/request.html',
+                  {'form': form})
+
+
+@reset_view
+def confirmation_sent(request):
+    requesting_user_pk = request.session.get('confirmation_sent_to')
+    if not requesting_user_pk:
+        raise Http404()
+
+    User = get_user_model()
+    requesting_user = get_object_or_404(User.objects, pk=requesting_user_pk)
+
+    return render(request, 'misago/forgottenpassword/confirmation_sent.html',
+                  {'requesting_user': requesting_user})
+
+
+
+class ResetStopped(Exception):
+    pass
+
+
+class ResetError(Exception):
+    pass
+
+
+@reset_view
+def reset_password(request, user_id, token):
+    User = get_user_model()
+    requesting_user = get_object_or_404(User.objects, pk=user_id)
+
+    try:
+        if requesting_user.requires_activation_by_admin:
+            message = _("%(username)s, administrator has to activate your "
+                        "account before you will be able to request "
+                        "new password.")
+            message = message % {'username': requesting_user.username}
+            raise ResetStopped(message)
+        if requesting_user.requires_activation_by_user:
+            message = _("%(username)s, you have to activate your account "
+                        "before you will be able to request new password.")
+            message = message % {'username': requesting_user.username}
+            raise ResetStopped(message)
+        if get_user_ban(requesting_user):
+            message = _("%(username)s, your account is banned "
+                        "and it's password can't be changed.")
+            message = message % {'username': requesting_user.username}
+            raise ResetError(message)
+        if not is_password_reset_token_valid(requesting_user, token):
+            message = _("%(username)s, your confirmation link is invalid. "
+                        "Try again or request new confirmation message.")
+            message = message % {'username': requesting_user.username}
+            raise ResetError(message)
+    except ResetStopped as e:
+        messages.info(request, e.args[0])
+        return redirect('misago:index')
+    except ResetError as e:
+        messages.error(request, e.args[0])
+        return redirect('misago:request_password_reset')
+
+    fake = Factory.create()
+    new_password = ' '.join([fake.word() for x in xrange(4)])
+    while len(new_password) < settings.password_length_min:
+        new_password = '%s %s' % (new_password, fake.word())
+
+    requesting_user.set_password(new_password)
+    requesting_user.save(update_fields=['password'])
+
+    mail_subject = _("New password on %(forum_title)s forums")
+    mail_subject = mail_subject % {'forum_title': settings.forum_name}
+
+    confirmation_token = make_password_reset_token(requesting_user)
+
+    mail_user(
+        request, requesting_user, mail_subject,
+        'misago/emails/forgottenpassword/new',
+        {'new_password': new_password})
+
+    request.session['password_sent_to'] = requesting_user.pk
+    return redirect('misago:request_password_new_sent')
+
+
+@reset_view
+def new_password_sent(request):
+    requesting_user_pk = request.session.get('password_sent_to')
+    if not requesting_user_pk:
+        raise Http404()
+
+    User = get_user_model()
+    requesting_user = get_object_or_404(User.objects, pk=requesting_user_pk)
+
+    return render(request, 'misago/forgottenpassword/password_sent.html',
+                  {'requesting_user': requesting_user})

+ 0 - 0
misago/users/views/resetpasswd.py