Просмотр исходного кода

Auth progress

* added DRF permission policies that raise exceptions on fail
* return bans from ban_smth functions
* adnotate client IP on request instead of user (so its reachable via
DRF’s request object)
* register user account via registration modal (needs Ember tests)
* ban_xyz utils return ban models
Rafał Pitoń 10 лет назад
Родитель
Сommit
46836fce88

+ 7 - 1
misago/conf/defaults.py

@@ -355,7 +355,13 @@ MISAGO_EMBER_CLI_ORIGIN = 'http://localhost:4200'
 
 # Rest Framework Configuration
 REST_FRAMEWORK = {
-    'EXCEPTION_HANDLER': 'misago.core.exceptionhandler.handle_api_exception'
+    'UNAUTHENTICATED_USER': 'misago.users.models.AnonymousUser',
+
+    'EXCEPTION_HANDLER': 'misago.core.exceptionhandler.handle_api_exception',
+
+    'DEFAULT_PERMISSION_CLASSES': (
+        'misago.users.rest_permissions.IsAuthenticatedOrReadOnly',
+    )
 }
 
 

+ 0 - 1
misago/core/exceptionhandler.py

@@ -84,7 +84,6 @@ def handle_misago_exception(request, exception):
 
 def handle_api_exception(exception):
     response = rest_exception_handler(exception)
-
     if response:
         if isinstance(exception, PermissionDenied):
             try:

+ 9 - 0
misago/core/utils.py

@@ -21,6 +21,15 @@ def format_plaintext_for_html(string):
 
 
 """
+Mark request as having sensitive parameters
+We can't use decorator because of DRF uses custom HttpRequest
+that is incompatibile with Django's decorator
+"""
+def hide_post_parameters(request):
+    request.sensitive_post_parameters = '__ALL__'
+
+
+"""
 Return path utility
 """
 def clean_return_path(request):

+ 25 - 3
misago/emberapp/app/components/forms/register-form.js

@@ -14,7 +14,18 @@ export default Ember.Component.extend({
   email: '',
   password: '',
 
-  validation: Ember.Object.create({}),
+  clearForm: function() {
+    this.setProperties({
+      username: '',
+      email: '',
+      password: ''
+    });
+  }.on('willDestroyElement'),
+
+  validation: null,
+  setValidation: function() {
+    this.set('validation', Ember.Object.create({}));
+  }.on('init'),
 
   passwordScore: function() {
     return this.get('zxcvbn').scorePassword(this.get('password'), [
@@ -193,12 +204,18 @@ export default Ember.Component.extend({
     return false;
   },
 
-  success: function(response) {
-    console.log(response);
+  success: function(responseJSON) {
     this.get('captcha').reset();
+
+    var model = Ember.Object.create(responseJSON);
+    model.set('isActive', model.get('activation') === 'active');
+    model.set('needsAdminActivation', model.get('activation') === 'activation_by_admin');
+
+    this.modal.render('register.done', model);
   },
 
   error: function(jqXHR) {
+    var rejection = jqXHR.responseJSON;
     if (jqXHR.status === 400) {
       this.toast.error(gettext("Form contains errors."));
       this.get('validation').setProperties(jqXHR.responseJSON);
@@ -208,6 +225,11 @@ export default Ember.Component.extend({
       } else {
         this.get('captcha').refresh();
       }
+    } else if (jqXHR.status === 403 && typeof rejection.ban !== 'undefined') {
+      this.get('router').intermediateTransitionTo('error-banned', rejection.ban);
+      this.modal.hide();
+    } else {
+      this.toast.apiError(jqXHR);
     }
   },
 

+ 2 - 2
misago/emberapp/app/components/recaptcha-field.js

@@ -2,11 +2,11 @@
 import FormRow from 'misago/components/form-row';
 
 export default FormRow.extend({
-  classNames: ['form-re-captcha'],
+  classNames: ['form-recaptcha'],
 
   renderWidget: function() {
     grecaptcha.render('g-captcha', {
-      'sitekey': settings.recaptcha_site_key
+      'sitekey': this.get('settings.recaptcha_site_key')
     });
   }.on('didInsertElement')
 });

+ 12 - 0
misago/emberapp/app/components/refresh-button.js

@@ -0,0 +1,12 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+  tagName: 'button',
+
+  attributeBindings: ['type'],
+  type: 'button',
+
+  click: function() {
+    document.location.reload();
+  }
+});

+ 2 - 0
misago/emberapp/app/components/register-button.js

@@ -2,6 +2,8 @@ import Ember from 'ember';
 
 export default Ember.Component.extend({
   tagName: 'button',
+
+  attributeBindings: ['type'],
   type: 'button',
 
   modalState: Ember.inject.service('registration-modal'),

+ 0 - 1
misago/emberapp/app/services/qacaptcha.js

@@ -1,4 +1,3 @@
-import Ember from 'ember';
 import NoCaptcha from 'misago/services/nocaptcha';
 
 export default NoCaptcha.extend({

+ 37 - 3
misago/emberapp/app/templates/modals/register/done.hbs

@@ -2,14 +2,48 @@
   <div class="modal-content">
     <div class="modal-header">
       <button type="button" class="close" data-dismiss="modal" aria-label="{{unbound gettext "Close"}}"><span aria-hidden="true">&times;</span></button>
-      <h4 class="modal-title" id="loginModalLabel">{{gettext "Register account"}}</h4>
+      <h4 class="modal-title" id="loginModalLabel">{{gettext "Registation complete"}}</h4>
     </div>
 
     <div class="modal-body modal-message">
+      {{#if model.isActive}}
       <p class="lead">
-        <span class="fa fa-info-circle fa-lg"></span>
-        {{gettext "Your account has been registered successfully."}}
+        {{gettext "%(username)s, your account has been registered successfully." username=model.username}}
       </p>
+      <p>
+        {{gettext "You have been signed in. Refresh page before continuing."}}
+      </p>
+      {{else if model.needsAdminActivation}}
+      <p class="lead">
+        {{gettext "%(username)s, your account has been registered successfully, but site administrator has to activate it." username=model.username}}
+      </p>
+      <p>
+        {{gettext "Notification will be sent to %(email)s once it happens." email=model.email}}
+      </p>
+      {{else}}
+      <p class="lead">
+        {{gettext "%(username)s, your account has been registered successfully, but you have to activate it." username=model.username}}
+      </p>
+      <p>
+        {{gettext "E-mail with activation link was sent to %(email)s." email=model.email}}
+      </p>
+      {{/if}}
+    </div>
+
+    <div class="modal-footer">
+      <div class="text-center">
+        {{#if model.isActive}}
+          {{#refresh-button class="btn btn-default"}}
+            <span class="fa fa-refresh"></span>
+            {{gettext "Refresh page"}}
+          {{/refresh-button}}
+        {{else}}
+          <button type="button" class="btn btn-default" data-dismiss="modal">
+            <span class="fa fa-ok"></span>
+            {{gettext "Continue"}}
+          </button>
+        {{/if}}
+      </div>
     </div>
 
   </div>

+ 1 - 1
misago/project_template/requirements.txt

@@ -1,4 +1,4 @@
-django==1.7.2
+django==1.7.6
 djangorestframework==3.0.2
 beautifulsoup4==4.3.2
 bleach==1.4.1

+ 1 - 1
misago/threads/posting/reply.py

@@ -77,5 +77,5 @@ class ReplyFormMiddleware(PostingMiddleware):
         self.post.thread = self.thread
         self.post.poster = self.user
         self.post.poster_name = self.user.username
-        self.post.poster_ip = self.request._misago_real_ip
+        self.post.poster_ip = self.request.user_ip
         self.post.posted_on = self.datetime

+ 1 - 1
misago/threads/reports.py

@@ -45,7 +45,7 @@ def report_post(request, post, message):
         reported_by=request.user,
         reported_by_name=request.user.username,
         reported_by_slug=request.user.slug,
-        reported_by_ip=request.user.ip,
+        reported_by_ip=request.user_ip,
         message=message,
         checksum=''
     )

+ 1 - 2
misago/threads/tests/test_forumthreads_view.py

@@ -37,6 +37,7 @@ class MockRequest(object):
     def __init__(self, user, method='GET', POST=None):
         self.POST = POST or {}
         self.user = user
+        self.user_ip = '127.0.0.1'
         self.session = {}
         self.path = '/forum/fake-forum-1/'
 
@@ -44,8 +45,6 @@ class MockRequest(object):
 class ActionsTests(ForumViewHelperTestCase):
     def setUp(self):
         super(ActionsTests, self).setUp()
-
-        self.user._misago_real_ip = '127.0.0.1'
         Label.objects.clear_cache()
 
     def tearDown(self):

+ 3 - 8
misago/users/api/activation.py

@@ -3,31 +3,26 @@ from django.core.exceptions import ValidationError
 from django.core.urlresolvers import reverse
 from django.shortcuts import get_object_or_404
 from django.utils.translation import ugettext as _
-from django.views.decorators.cache import never_cache
 from django.views.decorators.csrf import csrf_protect
-from django.views.decorators.debug import sensitive_post_parameters
 
 from rest_framework import status
-from rest_framework.decorators import api_view
+from rest_framework.decorators import api_view, permission_classes
 from rest_framework.response import Response
 
 from misago.conf import settings
 from misago.core.mail import mail_user
 
-from misago.users.decorators import deny_authenticated, deny_banned_ips
 from misago.users.forms.auth import ResendActivationForm
+from misago.users.rest_permissions import UnbannedAnonOnly
 from misago.users.tokens import (make_activation_token,
                                  is_activation_token_valid)
 from misago.users.validators import validate_password
 
 
 def activation_api_view(f):
-    @sensitive_post_parameters()
     @api_view(['POST'])
-    @never_cache
-    @deny_authenticated
+    @permission_classes((UnbannedAnonOnly,))
     @csrf_protect
-    @deny_banned_ips
     def decorator(request, *args, **kwargs):
         if 'user_id' in kwargs:
             User = get_user_model()

+ 3 - 8
misago/users/api/auth.py

@@ -1,23 +1,18 @@
 from django.contrib import auth
-from django.views.decorators.cache import never_cache
 from django.views.decorators.csrf import csrf_protect
-from django.views.decorators.debug import sensitive_post_parameters
 
 from rest_framework import status
-from rest_framework.decorators import api_view
+from rest_framework.decorators import api_view, permission_classes
 from rest_framework.response import Response
 
-from misago.users.decorators import deny_authenticated, deny_banned_ips
 from misago.users.forms.auth import AuthenticationForm
+from misago.users.rest_permissions import UnbannedAnonOnly
 from misago.users.serializers import AuthenticatedUserSerializer
 
 
-@sensitive_post_parameters()
 @api_view(['POST'])
-@never_cache
-@deny_authenticated
+@permission_classes((UnbannedAnonOnly,))
 @csrf_protect
-@deny_banned_ips
 def login(request):
     form = AuthenticationForm(request, data=request.data)
     if form.is_valid():

+ 3 - 8
misago/users/api/changepassword.py

@@ -3,31 +3,26 @@ from django.core.exceptions import ValidationError
 from django.core.urlresolvers import reverse
 from django.shortcuts import get_object_or_404
 from django.utils.translation import ugettext as _
-from django.views.decorators.cache import never_cache
 from django.views.decorators.csrf import csrf_protect
-from django.views.decorators.debug import sensitive_post_parameters
 
 from rest_framework import status
-from rest_framework.decorators import api_view
+from rest_framework.decorators import api_view, permission_classes
 from rest_framework.response import Response
 
 from misago.conf import settings
 from misago.core.mail import mail_user
 
-from misago.users.decorators import deny_authenticated, deny_banned_ips
 from misago.users.forms.auth import ResetPasswordForm
+from misago.users.rest_permissions import UnbannedAnonOnly
 from misago.users.tokens import (make_password_change_token,
                                  is_password_change_token_valid)
 from misago.users.validators import validate_password
 
 
 def password_api_view(f):
-    @sensitive_post_parameters()
     @api_view(['POST'])
-    @never_cache
-    @deny_authenticated
+    @permission_classes((UnbannedAnonOnly,))
     @csrf_protect
-    @deny_banned_ips
     def decorator(request, *args, **kwargs):
         if 'user_id' in kwargs:
             User = get_user_model()

+ 126 - 19
misago/users/api/users.py

@@ -1,37 +1,144 @@
-from django.contrib.auth import get_user_model
+from django.contrib.auth import authenticate, get_user_model, login
+from django.core.exceptions import PermissionDenied
+from django.utils.translation import ugettext as _
+from django.views.decorators.csrf import csrf_protect
 
 from rest_framework import status, viewsets
 from rest_framework.response import Response
 
+from misago.conf import settings
 from misago.core import forms
+from misago.core.mail import mail_user
 
 from misago.users import captcha
+from misago.users.bans import ban_ip
 from misago.users.forms.register import RegisterForm
+from misago.users.models import (ACTIVATION_REQUIRED_USER,
+                                 ACTIVATION_REQUIRED_ADMIN)
+from misago.users.rest_permissions import (BasePermission,
+    IsAuthenticatedOrReadOnly, UnbannedAnonOnly)
+from misago.users.serializers import AuthenticatedUserSerializer
+from misago.users.tokens import make_activation_token
+from misago.users.validators import validate_new_registration
+
+
+class UserViewSetPermission(BasePermission):
+    def has_permission(self, request, view):
+        if view.action == 'create':
+            policy = UnbannedAnonOnly()
+        else:
+            policy = IsAuthenticatedOrReadOnly()
+        return policy.has_permission(request, view)
 
 
 class UserViewSet(viewsets.ViewSet):
-    """
-    API endpoint for users manipulation
-    """
+    permission_classes = (UserViewSetPermission,)
     queryset = get_user_model().objects.all()
 
     def list(self, request):
         pass
 
     def create(self, request):
-        """
-        POST to /api/users is treated as new user registration
-        """
-        form = RegisterForm(request.data)
-
-        try:
-            captcha.test_request(request)
-        except forms.ValidationError as e:
-            form.add_error('captcha', e)
-
-        if form.is_valid():
-            captcha.reset_session(request.session)
-            return Response({'detail': 'Wolololo!'})
+        return _create_user(request)
+
+
+@csrf_protect
+def _create_user(request):
+    if settings.account_activation == 'disabled':
+        raise PermissionDenied(
+            _("New users registrations are currently disabled."))
+
+    form = RegisterForm(request.data)
+
+    try:
+        captcha.test_request(request)
+    except forms.ValidationError as e:
+        form.add_error('captcha', e)
+
+    if not form.is_valid():
+        return Response(form.errors,
+                        status=status.HTTP_400_BAD_REQUEST)
+
+    captcha.reset_session(request.session)
+
+    try:
+        validate_new_registration(
+            request.user_ip,
+            form.cleaned_data['username'],
+            form.cleaned_data['email'])
+    except PermissionDenied:
+        staff_message = _("This ban was automatically imposed on "
+                          "%(date)s due to denied register attempt.")
+
+        message_formats = {'date': date_format(timezone.now())}
+        staff_message = staff_message % message_formats
+        validation_ban = ban_ip(
+            request.user_ip,
+            staff_message=staff_message,
+            length={'days': 1}
+        )
+
+        raise PermissionDenied(
+            _("Your IP address is banned from performing this action."),
+            {'ban': validation_ban.get_serialized_message()})
+
+    activation_kwargs = {}
+    if settings.account_activation == 'user':
+        activation_kwargs = {
+            'requires_activation': ACTIVATION_REQUIRED_USER
+        }
+    elif settings.account_activation == 'admin':
+        activation_kwargs = {
+            'requires_activation': ACTIVATION_REQUIRED_ADMIN
+        }
+
+    User = get_user_model()
+    new_user = User.objects.create_user(form.cleaned_data['username'],
+                                        form.cleaned_data['email'],
+                                        form.cleaned_data['password'],
+                                        joined_from_ip=request.user_ip,
+                                        set_default_avatar=True,
+                                        **activation_kwargs)
+
+    mail_subject = _("Welcome on %(forum_title)s forums!")
+    mail_subject = mail_subject % {'forum_title': settings.forum_name}
+
+    if settings.account_activation == 'none':
+        authenticated_user = authenticate(
+            username=new_user.email,
+            password=form.cleaned_data['password'])
+        login(request, authenticated_user)
+
+        mail_user(request, new_user, mail_subject,
+                  'misago/emails/register/complete')
+
+        return Response({
+            'activation': 'active',
+            'username': new_user.username,
+            'email': new_user.email
+        })
+    else:
+        activation_token = make_activation_token(new_user)
+
+        activation_by_admin = new_user.requires_activation_by_admin
+        activation_by_user = new_user.requires_activation_by_user
+
+        mail_user(
+            request, new_user, mail_subject,
+            'misago/emails/register/inactive',
+            {
+                'activation_token': activation_token,
+                'activation_by_admin': activation_by_admin,
+                'activation_by_user': activation_by_user,
+            })
+
+        if activation_by_admin:
+            activation_method = 'activation_by_admin'
         else:
-            return Response(form.errors,
-                            status=status.HTTP_400_BAD_REQUEST)
+            activation_method = 'activation_by_user'
+
+        return Response({
+            'activation': activation_method,
+            'username': new_user.username,
+            'email': new_user.email
+        })

+ 11 - 15
misago/users/bans.py

@@ -96,11 +96,11 @@ def get_request_ip_ban(request):
         else:
             return False
 
-    found_ban = get_ip_ban(request._misago_real_ip)
+    found_ban = get_ip_ban(request.user_ip)
 
     ban_cache = request.session[BAN_CACHE_SESSION_KEY] = {
         'version': cachebuster.get_version(BAN_VERSION_KEY),
-        'ip': request._misago_real_ip,
+        'ip': request.user_ip,
     }
 
     if found_ban:
@@ -125,7 +125,7 @@ def _get_session_bancache(request):
     try:
         ban_cache = request.session[BAN_CACHE_SESSION_KEY]
         ban_cache = _hydrate_session_cache(ban_cache)
-        if ban_cache['ip'] != request._misago_real_ip:
+        if ban_cache['ip'] != request.user_ip:
             return None
         if not cachebuster.is_valid(BAN_VERSION_KEY, ban_cache['version']):
             return None
@@ -157,30 +157,25 @@ Utilities for front-end based bans
 """
 def ban_user(user, user_message=None, staff_message=None, length=None,
              expires_on=None):
-    if not expires_on:
-        if length:
-            expires_on = timezone.now() + timedelta(**length)
-        else:
-            expires_on = None
+    if not expires_on and length:
+        expires_on = timezone.now() + timedelta(**length)
 
-    Ban.objects.create(
+    ban = Ban.objects.create(
         banned_value=user.username.lower(),
         user_message=user_message,
         staff_message=staff_message,
         expires_on=expires_on
     )
     Ban.objects.invalidate_cache()
+    return ban
 
 
 def ban_ip(ip, user_message=None, staff_message=None, length=None,
            expires_on=None):
-    if not expires_on:
-        if length:
-            expires_on = timezone.now() + timedelta(**length)
-        else:
-            expires_on = None
+    if not expires_on and length:
+        expires_on = timezone.now() + timedelta(**length)
 
-    Ban.objects.create(
+    ban = Ban.objects.create(
         check_type=BAN_IP,
         banned_value=ip,
         user_message=user_message,
@@ -188,3 +183,4 @@ def ban_ip(ip, user_message=None, staff_message=None, length=None,
         expires_on=expires_on
     )
     Ban.objects.invalidate_cache()
+    return ban

+ 1 - 1
misago/users/captcha.py

@@ -29,7 +29,7 @@ def recaptcha_test(request):
     r = requests.post('https://www.google.com/recaptcha/api/siteverify', data={
         'secret': settings.recaptcha_secret_key,
         'response': request.data.get('captcha'),
-        'remoteip': request._misago_real_ip
+        'remoteip': request.user_ip
     })
 
     if r.status_code == 200:

+ 2 - 3
misago/users/middleware.py

@@ -16,9 +16,9 @@ class RealIPMiddleware(object):
     def process_request(self, request):
         x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
         if x_forwarded_for:
-            request._misago_real_ip = x_forwarded_for.split(',')[0]
+            request.user_ip = x_forwarded_for.split(',')[0]
         else:
-            request._misago_real_ip = request.META.get('REMOTE_ADDR')
+            request.user_ip = request.META.get('REMOTE_ADDR')
 
 
 class AvatarServerMiddleware(object):
@@ -36,7 +36,6 @@ class UserMiddleware(object):
         elif not request.user.is_superuser:
             if get_request_ip_ban(request) or get_user_ban(request.user):
                 logout(request)
-        request.user.ip = request._misago_real_ip
 
 
 class TimezoneMiddleware(object):

+ 2 - 2
misago/users/online/tracker.py

@@ -11,7 +11,7 @@ def mute_tracker(request):
 def start_tracking(request, user):
     online_tracker = Online.objects.create(
         user=user,
-        current_ip=request._misago_real_ip,
+        current_ip=request.user_ip,
         is_visible_on_index=user.rank.is_on_index
     )
 
@@ -23,7 +23,7 @@ def start_tracking(request, user):
 
 
 def update_tracker(request, tracker):
-    tracker.current_ip = request._misago_real_ip
+    tracker.current_ip = request.user_ip
     tracker.last_click = timezone.now()
 
     rank_visible_on_index = request.user.rank.is_on_index

+ 37 - 0
misago/users/rest_permissions.py

@@ -0,0 +1,37 @@
+from rest_framework.permissions import BasePermission, AllowAny, SAFE_METHODS
+
+from django.core.exceptions import PermissionDenied
+from django.utils.translation import ugettext as _
+
+from misago.users.bans import get_request_ip_ban
+
+
+__all__ = [
+    'AllowAny',
+    'IsAuthenticatedOrReadOnly',
+    'UnbannedAnonOnly'
+]
+
+
+class IsAuthenticatedOrReadOnly(BasePermission):
+    def has_permission(self, request, view):
+        if request.user.is_anonymous() and request.method not in SAFE_METHODS:
+            raise PermissionDenied(
+                _("This action is not available to guests."))
+        else:
+            return True
+
+
+class UnbannedAnonOnly(BasePermission):
+    def has_permission(self, request, view):
+        if request.user.is_authenticated():
+            raise PermissionDenied(
+                _("This action is not available to signed in users."))
+
+        ban = get_request_ip_ban(request)
+        if ban:
+            raise PermissionDenied(
+                _("Your IP address is banned from performing this action."),
+                {'ban': ban.get_serialized_message()})
+
+        return True

+ 102 - 3
misago/users/tests/test_bans.py

@@ -4,8 +4,82 @@ from django.contrib.auth import get_user_model
 from django.test import TestCase
 from django.utils import timezone
 
-from misago.users.bans import get_user_ban, get_request_ip_ban
-from misago.users.models import Ban, BAN_IP
+from misago.users.bans import (get_username_ban, get_email_ban, get_ip_ban,
+                               get_user_ban, get_request_ip_ban,
+                               ban_user, ban_ip)
+from misago.users.models import Ban, BAN_USERNAME, BAN_EMAIL, BAN_IP
+
+
+class GetBanTests(TestCase):
+    def test_get_username_ban(self):
+        """get_username_ban returns valid ban"""
+        nonexistent_ban = get_username_ban('nonexistent')
+        self.assertIsNone(nonexistent_ban)
+
+        Ban.objects.create(banned_value='expired',
+                           expires_on=timezone.now() - timedelta(days=7))
+
+        expired_ban = get_username_ban('expired')
+        self.assertIsNone(expired_ban)
+
+        Ban.objects.create(banned_value='wrongtype',
+                           check_type=BAN_EMAIL)
+
+        wrong_type_ban = get_username_ban('wrongtype')
+        self.assertIsNone(wrong_type_ban)
+
+        valid_ban = Ban.objects.create(
+            banned_value='admi*',
+            expires_on=timezone.now() + timedelta(days=7))
+        self.assertEqual(get_username_ban('admiral').pk, valid_ban.pk)
+
+    def test_get_email_ban(self):
+        """get_email_ban returns valid ban"""
+        nonexistent_ban = get_email_ban('non@existent.com')
+        self.assertIsNone(nonexistent_ban)
+
+        Ban.objects.create(banned_value='ex@pired.com',
+                           check_type=BAN_EMAIL,
+                           expires_on=timezone.now() - timedelta(days=7))
+
+        expired_ban = get_email_ban('ex@pired.com')
+        self.assertIsNone(expired_ban)
+
+        Ban.objects.create(banned_value='wrong@type.com',
+                           check_type=BAN_IP)
+
+        wrong_type_ban = get_email_ban('wrong@type.com')
+        self.assertIsNone(wrong_type_ban)
+
+        valid_ban = Ban.objects.create(
+            banned_value='*.ru',
+            check_type=BAN_EMAIL,
+            expires_on=timezone.now() + timedelta(days=7))
+        self.assertEqual(get_email_ban('banned@mail.ru').pk, valid_ban.pk)
+
+    def test_get_ip_ban(self):
+        """get_ip_ban returns valid ban"""
+        nonexistent_ban = get_ip_ban('123.0.0.1')
+        self.assertIsNone(nonexistent_ban)
+
+        Ban.objects.create(banned_value='124.0.0.1',
+                           check_type=BAN_IP,
+                           expires_on=timezone.now() - timedelta(days=7))
+
+        expired_ban = get_ip_ban('124.0.0.1')
+        self.assertIsNone(expired_ban)
+
+        Ban.objects.create(banned_value='wrongtype',
+                           check_type=BAN_EMAIL)
+
+        wrong_type_ban = get_ip_ban('wrongtype')
+        self.assertIsNone(wrong_type_ban)
+
+        valid_ban = Ban.objects.create(
+            banned_value='125.0.0.*',
+            check_type=BAN_IP,
+            expires_on=timezone.now() + timedelta(days=7))
+        self.assertEqual(get_ip_ban('125.0.0.1').pk, valid_ban.pk)
 
 
 class UserBansTests(TestCase):
@@ -56,7 +130,7 @@ class UserBansTests(TestCase):
 
 class FakeRequest(object):
     def __init__(self):
-        self._misago_real_ip = '127.0.0.1'
+        self.user_ip = '127.0.0.1'
         self.session = {}
 
 
@@ -107,3 +181,28 @@ class RequestIPBansTests(TestCase):
 
         # repeated call uses cache
         get_request_ip_ban(FakeRequest())
+
+
+class BanUserTests(TestCase):
+    def test_ban_user(self):
+        """ban_user bans user"""
+        User = get_user_model()
+        user = User.objects.create_user('Bob', 'bob@boberson.com', 'pass123')
+
+        ban = ban_user(user, 'User reason', 'Staff reason')
+        self.assertEqual(ban.user_message, 'User reason')
+        self.assertEqual(ban.staff_message, 'Staff reason')
+
+        db_ban = get_user_ban(user)
+        self.assertEqual(ban.pk, db_ban.ban_id)
+
+
+class BanIpTests(TestCase):
+    def test_ban_ip(self):
+        """ban_ip bans IP address"""
+        ban = ban_ip('127.0.0.1', 'User reason', 'Staff reason')
+        self.assertEqual(ban.user_message, 'User reason')
+        self.assertEqual(ban.staff_message, 'Staff reason')
+
+        db_ban = get_ip_ban('127.0.0.1')
+        self.assertEqual(ban.pk, db_ban.pk)

+ 2 - 2
misago/users/tests/test_realip_middleware.py

@@ -16,12 +16,12 @@ class RealIPMiddlewareTests(TestCase):
         request = MockRequest('83.42.13.77')
         RealIPMiddleware().process_request(request)
 
-        self.assertEqual(request._misago_real_ip, request.META['REMOTE_ADDR'])
+        self.assertEqual(request.user_ip, request.META['REMOTE_ADDR'])
 
     def test_middleware_sets_ip_from_forwarded_for(self):
         """Middleware sets ip from forwarded_for header"""
         request = MockRequest('127.0.0.1', '83.42.13.77')
         RealIPMiddleware().process_request(request)
 
-        self.assertEqual(request._misago_real_ip,
+        self.assertEqual(request.user_ip,
                          request.META['HTTP_X_FORWARDED_FOR'])

+ 39 - 38
misago/users/tests/test_registration_views.py → misago/users/tests/test_users_api.py

@@ -1,49 +1,48 @@
 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.conf import settings
 
+from misago.users.testutils import UserTestCase
 
-class RegisterDecoratorTests(TestCase):
-    def tearDown(self):
-        settings.reset_settings()
 
-    def test_register_decorator_calls_valid_view_200(self):
-        """register decorator calls valid view"""
-        settings.override_setting('account_activation', 'disabled')
-
-        response = self.client.get(reverse('misago:register'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('new registrations are not currently accepted',
-                      response.content)
+class CreateTests(UserTestCase):
+    """
+    tests for new user registration (POST to /api/users/)
+    """
+    def test_empty_request(self):
+        """empty request errors with code 400"""
+        response = self.client.post('/api/users/')
+        self.assertEqual(response.status_code, 400)
 
-        settings.override_setting('account_activation', 'none')
-        response = self.client.get(reverse('misago:register'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Register new account',
-                      response.content)
+    def test_authenticated_request(self):
+        """authentiated user request errors with code 403"""
+        self.login_user(self.get_authenticated_user())
+        response = self.client.post('/api/users/')
+        self.assertEqual(response.status_code, 403)
 
+    def test_registration_off_request(self):
+        """registrations off request errors with code 403"""
+        settings.override_setting('account_activation', 'disabled')
 
-class RegisterViewTests(TestCase):
-    def test_register_view_get_returns_200(self):
-        """register view returns 200 on GET"""
-        response = self.client.get(reverse('misago:register'))
-        self.assertEqual(response.status_code, 200)
+        response = self.client.post('/api/users/')
+        self.assertEqual(response.status_code, 403)
+        self.assertIn('disabled', response.content)
 
-    def test_register_view_post_creates_active_user(self):
-        """register view creates active and signed in user on POST"""
+    def test_registration_creates_active_user(self):
+        """api creates active and signed in user on POST"""
         settings.override_setting('account_activation', 'none')
 
-        response = self.client.post(reverse('misago:register'),
+        response = self.client.post('/api/users/',
                                     data={'username': 'Bob',
                                           'email': 'bob@bob.com',
                                           'password': 'pass123'})
-        self.assertEqual(response.status_code, 302)
 
-        response = self.client.get(reverse('misago:index'))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('active', response.content)
         self.assertIn('Bob', response.content)
+        self.assertIn('bob@bob.com', response.content)
 
         User = get_user_model()
         User.objects.get_by_username('Bob')
@@ -54,17 +53,18 @@ class RegisterViewTests(TestCase):
 
         self.assertIn('Welcome', mail.outbox[0].subject)
 
-    def test_register_view_post_creates_inactive_user(self):
-        """register view creates inactive user on POST"""
+    def test_registration_creates_inactive_user(self):
+        """api creates inactive user on POST"""
         settings.override_setting('account_activation', 'user')
 
-        response = self.client.post(reverse('misago:register'),
+        response = self.client.post('/api/users/',
                                     data={'username': 'Bob',
                                           'email': 'bob@bob.com',
                                           'password': 'pass123'})
-        self.assertEqual(response.status_code, 302)
 
-        response = self.client.get(reverse('misago:register_completed'))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('activation_by_user', response.content)
+        self.assertIn('Bob', response.content)
         self.assertIn('bob@bob.com', response.content)
 
         User = get_user_model()
@@ -73,18 +73,19 @@ class RegisterViewTests(TestCase):
 
         self.assertIn('Welcome', mail.outbox[0].subject)
 
-    def test_register_view_post_creates_admin_activated_user(self):
-        """register view creates admin activated user on POST"""
+    def test_registration_creates_admin_activated_user(self):
+        """api creates admin activated user on POST"""
         settings.override_setting('account_activation', 'admin')
 
-        response = self.client.post(reverse('misago:register'),
+        response = self.client.post('/api/users/',
                                     data={'username': 'Bob',
                                           'email': 'bob@bob.com',
                                           'password': 'pass123'})
-        self.assertEqual(response.status_code, 302)
 
-        response = self.client.get(reverse('misago:register_completed'))
-        self.assertIn('administrator', response.content)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('activation_by_admin', response.content)
+        self.assertIn('Bob', response.content)
+        self.assertIn('bob@bob.com', response.content)
 
         User = get_user_model()
         User.objects.get_by_username('Bob')

+ 0 - 6
misago/users/urls/__init__.py

@@ -7,12 +7,6 @@ urlpatterns = patterns('misago.users.views.auth',
 )
 
 
-urlpatterns += patterns('misago.users.views.register',
-    url(r'^register/$', 'register', name='register'),
-    url(r'^register/completed/$', 'register_completed', name='register_completed'),
-)
-
-
 urlpatterns += patterns('misago.users.views.activation',
     url(r'^activation/$', 'activation_noscript', name="request_activation"),
     url(r'^activation/(?P<user_id>\d+)/(?P<token>[a-zA-Z0-9]+)/$', 'activation_noscript', name="activate_by_token"),

+ 1 - 1
misago/users/views/admin/users.py

@@ -237,7 +237,7 @@ class NewUser(UserAdmin, generic.ModelFormView):
             form.cleaned_data['new_password'],
             title=form.cleaned_data['title'],
             rank=form.cleaned_data.get('rank'),
-            joined_from_ip=request._misago_real_ip,
+            joined_from_ip=request.user_ip,
             set_default_avatar=True)
 
         if form.cleaned_data.get('staff_level'):

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

@@ -1,144 +0,0 @@
-from django.contrib import messages
-from django.contrib.auth import authenticate, get_user_model, login
-from django.core.exceptions import PermissionDenied
-from django.http import Http404
-from django.shortcuts import get_object_or_404, redirect, render
-from django.utils import timezone
-from django.utils.formats import date_format
-from django.utils.translation import ugettext as _
-from django.views.decorators.cache import never_cache
-from django.views.decorators.debug import sensitive_post_parameters
-
-from misago.conf import settings
-from misago.core.mail import mail_user
-
-from misago.users.bans import ban_ip
-from misago.users.decorators import deny_authenticated, deny_banned_ips
-from misago.users.forms.register import RegisterForm
-from misago.users.models import (ACTIVATION_REQUIRED_USER,
-                                 ACTIVATION_REQUIRED_ADMIN)
-from misago.users.tokens import make_activation_token
-from misago.users.validators import validate_new_registration
-
-
-def register_decorator(f):
-    def decorator(request):
-        if settings.account_activation == 'disabled':
-            return register_disabled(request)
-        else:
-            return f(request)
-    return decorator
-
-
-@sensitive_post_parameters("email", "password")
-@never_cache
-@deny_authenticated
-@deny_banned_ips
-@register_decorator
-def register(request):
-    SecuredForm = add_captcha_to_form(RegisterForm, request)
-
-    form = SecuredForm()
-    if request.method == 'POST':
-        form = SecuredForm(request.POST)
-        if form.is_valid():
-            try:
-                validate_new_registration(
-                    request.user.ip,
-                    form.cleaned_data['username'],
-                    form.cleaned_data['email'])
-            except PermissionDenied as e:
-                staff_message = _("This ban was automatically imposed on "
-                                  "%(date)s due to denied register attempt.")
-
-                message_formats = {'date': date_format(timezone.now())}
-                staff_message = staff_message % message_formats
-                ban_ip(request.user.ip,
-                       staff_message=staff_message,
-                       length={'days': 1})
-                raise e
-
-            activation_kwargs = {}
-            if settings.account_activation == 'user':
-                activation_kwargs = {
-                    'requires_activation': ACTIVATION_REQUIRED_USER
-                }
-            elif settings.account_activation == 'admin':
-                activation_kwargs = {
-                    'requires_activation': ACTIVATION_REQUIRED_ADMIN
-                }
-
-            User = get_user_model()
-            new_user = User.objects.create_user(form.cleaned_data['username'],
-                                                form.cleaned_data['email'],
-                                                form.cleaned_data['password'],
-                                                set_default_avatar=True,
-                                                **activation_kwargs)
-
-            mail_subject = _("Welcome on %(forum_title)s forums!")
-            mail_subject = mail_subject % {'forum_title': settings.forum_name}
-
-            if settings.account_activation == 'none':
-                authenticated_user = authenticate(
-                    username=new_user.email,
-                    password=form.cleaned_data['password'])
-                login(request, authenticated_user)
-
-                welcome_message = _("Welcome aboard, %(user)s!")
-                welcome_message = welcome_message % {'user': new_user.username}
-                messages.success(request, welcome_message)
-
-                mail_user(request, new_user, mail_subject,
-                          'misago/emails/register/complete')
-
-                return redirect(settings.LOGIN_REDIRECT_URL)
-            else:
-                activation_token = make_activation_token(new_user)
-
-                activation_by_admin = new_user.requires_activation_by_admin
-                activation_by_user = new_user.requires_activation_by_user
-
-                mail_user(
-                    request, new_user, mail_subject,
-                    'misago/emails/register/inactive',
-                    {
-                        'activation_token': activation_token,
-                        'activation_by_admin': activation_by_admin,
-                        'activation_by_user': activation_by_user,
-                    })
-
-                request.session['registered_user'] = new_user.pk
-                return redirect('misago:register_completed')
-
-    return render(request, 'misago/register/form.html', {'form': form})
-
-
-def register_disabled(request):
-    return render(request, 'misago/register/disabled.html')
-
-
-def register_completed(request):
-    """
-    If user needs to activate his account, we display him page with message
-    """
-    registered_user_pk = request.session.get('registered_user')
-    if not registered_user_pk:
-        raise Http404()
-
-    registered_user = get_object_or_404(get_user_model().objects,
-                                        pk=registered_user_pk)
-
-    if not registered_user.requires_activation:
-        return redirect('misago:index')
-
-    activation_by_admin = registered_user.requires_activation_by_admin
-    activation_by_user = registered_user.requires_activation_by_user
-
-    return render(
-        request,
-        'misago/register/completed.html',
-        {
-            'activation_by_admin': activation_by_admin,
-            'activation_by_user': activation_by_user,
-            'registered_user': registered_user,
-        })