Browse Source

close #501

Auth backend finalized, but frontend needs update before it will work
Rafał Pitoń 10 years ago
parent
commit
6bcfb8005f

+ 0 - 90
misago/users/api/activation.py

@@ -1,90 +0,0 @@
-from django.contrib.auth import get_user_model
-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.csrf import csrf_protect
-
-from rest_framework import status
-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.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):
-    @api_view(['POST'])
-    @permission_classes((UnbannedAnonOnly,))
-    @csrf_protect
-    def decorator(request, *args, **kwargs):
-        if 'user_id' in kwargs:
-            User = get_user_model()
-            user = get_object_or_404(User.objects, pk=kwargs.pop('user_id'))
-            kwargs['user'] = user
-
-            if not is_activation_token_valid(user, kwargs['token']):
-                message = _("Your link is invalid. Please try again.")
-                return Response({'detail': message},
-                                status=status.HTTP_404_NOT_FOUND)
-
-            form = ResendActivationForm()
-            try:
-                form.confirm_user_not_banned(user)
-            except ValidationError:
-                message = _("Your link has expired. Please request new one.")
-                return Response({'detail': message},
-                                status=status.HTTP_404_NOT_FOUND)
-
-            try:
-                form.confirm_can_be_activated(user)
-            except ValidationError as e:
-                return Response({'detail': e.messages[0]},
-                                status=status.HTTP_404_NOT_FOUND)
-
-        return f(request, *args, **kwargs)
-    return decorator
-
-
-@activation_api_view
-def send_link(request):
-    form = ResendActivationForm(request.DATA)
-    if form.is_valid():
-        requesting_user = form.user_cache
-
-        mail_subject = _("Change %(user)s password "
-                         "on %(forum_title)s forums")
-        subject_formats = {'user': requesting_user.username,
-                           'forum_title': settings.forum_name}
-        mail_subject = mail_subject % subject_formats
-
-        confirmation_token = make_activation_token(requesting_user)
-
-        mail_user(request, requesting_user, mail_subject,
-                  'misago/emails/change_password_form_link',
-                  {'confirmation_token': confirmation_token})
-
-        return Response({
-            'username': form.user_cache.username,
-            'email': form.user_cache.email
-        })
-    else:
-        return Response(form.get_errors_dict(),
-                        status=status.HTTP_400_BAD_REQUEST)
-
-
-@activation_api_view
-def validate_token(request, user, token):
-    user.requires_activation = False
-    user.save(update_fields=['requires_activation'])
-
-    message = _("%(user)s, your account has been activated")
-    return Response({
-        'detail': message % {'user': user.username}
-    })

+ 198 - 1
misago/users/api/auth.py

@@ -1,15 +1,37 @@
 from django.contrib import auth
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext as _
 from django.views.decorators.csrf import csrf_protect
 
 from rest_framework import status
 from rest_framework.decorators import api_view, permission_classes
 from rest_framework.response import Response
 
-from misago.users.forms.auth import AuthenticationForm
+from misago.conf import settings
+from misago.core.mail import mail_user
+
+from misago.users.forms.auth import (AuthenticationForm, ResendActivationForm,
+                                     ResetPasswordForm)
 from misago.users.rest_permissions import UnbannedAnonOnly
 from misago.users.serializers import AuthenticatedUserSerializer
+from misago.users.tokens import (make_activation_token,
+                                 is_activation_token_valid,
+                                 make_password_change_token,
+                                 is_password_change_token_valid)
+from misago.users.validators import validate_password
+
+
+def gateway(request):
+    if request.method == 'POST':
+        return login(request)
+    else:
+        return session_user(request)
 
 
+"""
+POST /auth/ with CSRF, username and password
+will attempt to authenticate new user
+"""
 @api_view(['POST'])
 @permission_classes((UnbannedAnonOnly,))
 @csrf_protect
@@ -21,3 +43,178 @@ def login(request):
     else:
         return Response(form.get_errors_dict(),
                         status=status.HTTP_400_BAD_REQUEST)
+
+
+"""
+GET /auth/ will return current auth user, either User or AnonymousUser
+"""
+@api_view()
+def session_user(request):
+    if request.user.is_authenticated():
+        serialized_user = AuthenticatedUserSerializer(request.user).data
+    else:
+        serialized_user = {
+            'id': None,
+            'acl': {'is_implemented': False}
+        }
+
+    return Response(serialized_user)
+
+
+"""
+POST /auth/send-activation/ with CSRF token and email
+will mail account activation link to requester
+"""
+@api_view(['POST'])
+@permission_classes((UnbannedAnonOnly,))
+@csrf_protect
+def send_activation(request):
+    form = ResendActivationForm(request.data)
+    if form.is_valid():
+        requesting_user = form.user_cache
+
+        mail_subject = _("Activate %(user)s account "
+                         "on %(forum_title)s forums")
+        subject_formats = {'user': requesting_user.username,
+                           'forum_title': settings.forum_name}
+        mail_subject = mail_subject % subject_formats
+
+        mail_user(request, requesting_user, mail_subject,
+                  'misago/emails/activation/by_user',
+                  {'activation_token': make_activation_token(requesting_user)})
+
+        return Response({
+                'username': form.user_cache.username,
+                'email': form.user_cache.email
+            })
+    else:
+        return Response(form.get_errors_dict(),
+                        status=status.HTTP_400_BAD_REQUEST)
+
+
+"""
+POST /auth/activate-account/ with CSRF token, user ID and activation token
+will activate account
+"""
+@api_view(['POST'])
+@permission_classes((UnbannedAnonOnly,))
+@csrf_protect
+def activate_account(request, user_id, token):
+    User = auth.get_user_model()
+
+    try:
+        user = User.objects.get(pk=user_id)
+    except User.DoesNotExist:
+        message = _("Activation link is invalid. Please try again.")
+        return Response({'detail': message}, status=status.HTTP_400_BAD_REQUEST)
+
+    if not is_activation_token_valid(user, token):
+        message = _("Activation link is invalid. Please try again.")
+        return Response({'detail': message},
+                        status=status.HTTP_400_BAD_REQUEST)
+
+    form = ResendActivationForm()
+    try:
+        form.confirm_user_not_banned(user)
+    except ValidationError:
+        message = _("Activation link has expired. Please request new one.")
+        return Response({'detail': message},
+                        status=status.HTTP_400_BAD_REQUEST)
+
+    try:
+        form.confirm_can_be_activated(user)
+    except ValidationError as e:
+        return Response({'detail': e.messages[0]},
+                        status=status.HTTP_400_BAD_REQUEST)
+
+    user.requires_activation = False
+    user.save(update_fields=['requires_activation'])
+
+    message = _("%(user)s, your account has been activated.")
+    return Response({
+            'detail': message % {'user': user.username}
+        })
+
+
+"""
+POST /auth/send-password-form/ with CSRF token and email
+will mail change password form link to requester
+"""
+@api_view(['POST'])
+@permission_classes((UnbannedAnonOnly,))
+@csrf_protect
+def send_password_form(request):
+    form = ResetPasswordForm(request.data)
+    if form.is_valid():
+        requesting_user = form.user_cache
+
+        mail_subject = _("Change %(user)s password "
+                         "on %(forum_title)s forums")
+        subject_formats = {'user': requesting_user.username,
+                           'forum_title': settings.forum_name}
+        mail_subject = mail_subject % subject_formats
+
+        confirmation_token = make_password_change_token(requesting_user)
+
+        mail_user(request, requesting_user, mail_subject,
+                  'misago/emails/change_password_form_link',
+                  {'confirmation_token': confirmation_token})
+
+        return Response({
+                'username': form.user_cache.username,
+                'email': form.user_cache.email
+            })
+    else:
+        return Response(form.get_errors_dict(),
+                        status=status.HTTP_400_BAD_REQUEST)
+
+
+"""
+GET /auth/change-password/user/token/ will validate change password link
+POST /auth/change-password/user/token/ with CSRF and new password
+will change forgotten password
+"""
+@api_view(['GET', 'POST'])
+@permission_classes((UnbannedAnonOnly,))
+@csrf_protect
+def change_forgotten_password(request, user_id, token):
+    User = auth.get_user_model()
+
+    try:
+        user = User.objects.get(pk=user_id)
+    except User.DoesNotExist:
+        message = _("Form link is invalid. Please try again.")
+        return Response({'detail': message}, status=status.HTTP_400_BAD_REQUEST)
+
+    if not is_password_change_token_valid(user, token):
+        message = _("Form link is invalid. Please try again.")
+        return Response({'detail': message},
+                        status=status.HTTP_400_BAD_REQUEST)
+
+    try:
+        form = ResetPasswordForm()
+        form.confirm_allowed(user)
+    except ValidationError:
+        message = _("Your link has expired. Please request new one.")
+        return Response({'detail': message},
+                        status=status.HTTP_400_BAD_REQUEST)
+
+    if request.method == 'POST':
+        return process_forgotten_password_form(request, user)
+    else:
+        return Response({
+                'username': user.username,
+                'email': user.email
+            })
+
+
+def process_forgotten_password_form(request, user):
+    new_password = request.data.get('password', '').strip()
+    try:
+        validate_password(new_password)
+        user.set_password(new_password)
+        user.save()
+    except ValidationError as e:
+        return Response({'detail': e.messages[0]},
+                        status=status.HTTP_400_BAD_REQUEST)
+    return Response({'detail': 'ok'})

+ 0 - 97
misago/users/api/changepassword.py

@@ -1,97 +0,0 @@
-from django.contrib.auth import get_user_model
-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.csrf import csrf_protect
-
-from rest_framework import status
-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.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):
-    @api_view(['POST'])
-    @permission_classes((UnbannedAnonOnly,))
-    @csrf_protect
-    def decorator(request, *args, **kwargs):
-        if 'user_id' in kwargs:
-            User = get_user_model()
-            user = get_object_or_404(User.objects, pk=kwargs.pop('user_id'))
-            kwargs['user'] = user
-
-            if not is_password_change_token_valid(user, kwargs['token']):
-                message = _("Your link is invalid. Please try again.")
-                return Response({'detail': message},
-                                status=status.HTTP_404_NOT_FOUND)
-
-            try:
-                form = ResetPasswordForm()
-                form.confirm_allowed(user)
-            except ValidationError:
-                message = _("Your link has expired. Please request new one.")
-                return Response({'detail': message},
-                                status=status.HTTP_404_NOT_FOUND)
-
-        return f(request, *args, **kwargs)
-    return decorator
-
-
-@password_api_view
-def send_link(request):
-    form = ResetPasswordForm(request.DATA)
-    if form.is_valid():
-        requesting_user = form.user_cache
-
-        mail_subject = _("Change %(user)s password "
-                         "on %(forum_title)s forums")
-        subject_formats = {'user': requesting_user.username,
-                           'forum_title': settings.forum_name}
-        mail_subject = mail_subject % subject_formats
-
-        confirmation_token = make_password_change_token(requesting_user)
-
-        mail_user(request, requesting_user, mail_subject,
-                  'misago/emails/change_password_form_link',
-                  {'confirmation_token': confirmation_token})
-
-        return Response({
-            'username': form.user_cache.username,
-            'email': form.user_cache.email
-        })
-    else:
-        return Response(form.get_errors_dict(),
-                        status=status.HTTP_400_BAD_REQUEST)
-
-
-@password_api_view
-def validate_token(request, user, token):
-    return Response({
-        'user_id': user.id,
-        'token': token,
-        'username': user.username
-    })
-
-
-@password_api_view
-def change_password(request, user, token):
-    new_password = request.DATA.get('password', '').strip()
-
-    try:
-        validate_password(new_password)
-        user.set_password(new_password)
-        user.save()
-    except ValidationError as e:
-        return Response({'detail': e.messages[0]},
-                        status=status.HTTP_400_BAD_REQUEST)
-
-    return Response({'detail': 'ok'})

+ 1 - 1
misago/users/forms/auth.py

@@ -151,7 +151,7 @@ class ResendActivationForm(GetUserForm):
         username_format = {'user': user.username}
 
         if not user.requires_activation:
-            message = _("%(user)s, your account is already activated.")
+            message = _("%(user)s, your account is already active.")
             raise forms.ValidationError(message % username_format,
                                         code='already_active')
 

+ 0 - 155
misago/users/tests/test_activation_api.py

@@ -1,155 +0,0 @@
-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_activation_token
-
-
-class SendLinkAPITests(TestCase):
-    def setUp(self):
-        User = get_user_model()
-        self.user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
-        self.user.requires_activation = 1
-        self.user.save()
-
-        self.link = reverse('misago:api:activation_send_link')
-
-    def test_submit_valid(self):
-        """request activation link api sends reset link mail"""
-        response = self.client.post(self.link, data={'email': self.user.email})
-        self.assertEqual(response.status_code, 200)
-
-        self.assertIn('Change Bob password', mail.outbox[0].subject)
-
-    def test_submit_invalid(self):
-        """request activation link api errors for invalid email"""
-        response = self.client.post(self.link, data={'email': 'fake@mail.com'})
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('not_found', response.content)
-
-        self.assertTrue(not mail.outbox)
-
-    def test_submit_banned(self):
-        """request activation link api errors for banned users"""
-        Ban.objects.create(check_type=BAN_USERNAME,
-                           banned_value=self.user.username,
-                           user_message='Nope!')
-
-        response = self.client.post(self.link, data={'email': self.user.email})
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('Nope!', response.content)
-
-        self.assertTrue(not mail.outbox)
-
-    def test_view_submit_active_user(self):
-        """request activation link api errors for active user"""
-        self.user.requires_activation = 0
-        self.user.save()
-
-        response = self.client.post(self.link, data={'email': self.user.email})
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('Bob, your account is already activated.',
-                      response.content)
-
-    def test_view_submit_inactive_user(self):
-        """request activation link api errors for admin-activated users"""
-        self.user.requires_activation = 2
-        self.user.save()
-
-        response = self.client.post(self.link, data={'email': self.user.email})
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('inactive_admin', response.content)
-
-        self.assertTrue(not mail.outbox)
-
-        # but succeed for user-activated
-        self.user.requires_activation = 1
-        self.user.save()
-
-        response = self.client.post(self.link, data={'email': self.user.email})
-        self.assertEqual(response.status_code, 200)
-
-        self.assertTrue(mail.outbox)
-
-
-class ValidateTokenAPITests(TestCase):
-    def setUp(self):
-        User = get_user_model()
-        self.user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
-        self.user.requires_activation = 1
-        self.user.save()
-
-        self.link = reverse(
-            'misago:api:activation_validate_token',
-            kwargs={
-                'user_id': self.user.id,
-                'token': make_activation_token(self.user)
-            })
-
-    def test_submit_valid(self):
-        """validate link api returns success and activates user"""
-        response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(self.user.username, response.content)
-
-        user = get_user_model().objects.get(id=self.user.id)
-        self.assertFalse(user.requires_activation)
-
-    def test_submit_invalid_token(self):
-        """validate link api errors for invalid token"""
-        response = self.client.post(reverse(
-            'misago:api:activation_validate_token',
-            kwargs={
-                'user_id': self.user.id,
-                'token': 'sadsadsadsdsassdsa'
-            }))
-        self.assertEqual(response.status_code, 404)
-        self.assertIn('Your link is invalid.', response.content)
-
-    def test_submit_invalid_user(self):
-        """validate link api errors for invalid user"""
-        response = self.client.post(reverse(
-            'misago:api:activation_validate_token',
-            kwargs={
-                'user_id': 123,
-                'token': 'sadsadsadsdsassdsa'
-            }))
-        self.assertEqual(response.status_code, 404)
-
-    def test_submit_banned(self):
-        """validate link api errors for banned user"""
-        Ban.objects.create(check_type=BAN_USERNAME,
-                           banned_value=self.user.username,
-                           user_message='Nope!')
-
-        response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 404)
-        self.assertIn('Your link has expired.', response.content)
-
-    def test_view_submit_active_user(self):
-        """validate link api errors for active user"""
-        self.user.requires_activation = 0
-        self.user.save()
-
-        response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 404)
-        self.assertIn('Bob, your account is already activated.',
-                      response.content)
-
-    def test_view_submit_inactive_user(self):
-        """validate link api errors for inactive user"""
-        self.user.requires_activation = 1
-        self.user.save()
-
-        response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 200)
-
-        self.user.requires_activation = 2
-        self.user.save()
-
-        response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 404)
-        self.assertIn('Bob, only administrator may activate your account.',
-                      response.content)

+ 356 - 13
misago/users/tests/test_auth_api.py

@@ -1,47 +1,55 @@
 import json
 
 from django.contrib.auth import get_user_model
-from django.core.urlresolvers import reverse
+from django.core import mail
 from django.test import TestCase
 
 from misago.users.models import Ban, BAN_USERNAME
+from misago.users.tokens import (make_activation_token,
+                                 make_password_change_token)
 
 
-class AuthenticationAPITests(TestCase):
+class GatewayTests(TestCase):
     def test_api_invalid_credentials(self):
         """login api returns 400 on invalid POST"""
         response = self.client.post(
-            reverse('misago:api:login'),
+            '/api/auth/',
             data={'username': 'nope', 'password': 'nope'})
 
         self.assertEqual(response.status_code, 400)
         self.assertIn("Login or password is incorrect.", response.content)
 
-        response = self.client.get(reverse('misago:api:auth_user'))
+        response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
         user_json = json.loads(response.content)
         self.assertIsNone(user_json['id'])
 
-    def test_api_login(self):
+    def test_login(self):
         """api signs user in"""
         User = get_user_model()
         user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
 
         response = self.client.post(
-            reverse('misago:api:login'),
+            '/api/auth/',
             data={'username': 'Bob', 'password': 'Pass.123'})
 
         self.assertEqual(response.status_code, 200)
 
-        response = self.client.get(reverse('misago:api:auth_user'))
+        response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
         user_json = json.loads(response.content)
         self.assertEqual(user_json['id'], user.id)
         self.assertEqual(user_json['username'], user.username)
 
-    def test_api_login_banned(self):
+    def test_submit_empty(self):
+        """login api errors for no body"""
+        response = self.client.post('/api/auth/')
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('empty_data', response.content)
+
+    def test_login_banned(self):
         """login api fails to sign banned user in"""
         User = get_user_model()
         User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
@@ -51,7 +59,7 @@ class AuthenticationAPITests(TestCase):
                                  user_message='You are tragically banned.')
 
         response = self.client.post(
-            reverse('misago:api:login'),
+            '/api/auth/',
             data={'username': 'Bob', 'password': 'Pass.123'})
         self.assertEqual(response.status_code, 400)
 
@@ -62,30 +70,365 @@ class AuthenticationAPITests(TestCase):
         self.assertEqual(response_json['detail']['message']['html'],
                          '<p>%s</p>' % ban.user_message)
 
-    def test_api_login_inactive_admin(self):
+        response = self.client.get('/api/auth/')
+        self.assertEqual(response.status_code, 200)
+
+        user_json = json.loads(response.content)
+        self.assertIsNone(user_json['id'])
+
+    def test_login_inactive_admin(self):
         """login api fails to sign admin-activated user in"""
         User = get_user_model()
         User.objects.create_user('Bob', 'bob@test.com', 'Pass.123',
                                  requires_activation=1)
 
         response = self.client.post(
-            reverse('misago:api:login'),
+            '/api/auth/',
             data={'username': 'Bob', 'password': 'Pass.123'})
         self.assertEqual(response.status_code, 400)
 
         response_json = json.loads(response.content)
         self.assertEqual(response_json['code'], 'inactive_user')
 
-    def test_api_login_inactive_user(self):
+        response = self.client.get('/api/auth/')
+        self.assertEqual(response.status_code, 200)
+
+        user_json = json.loads(response.content)
+        self.assertIsNone(user_json['id'])
+
+    def test_login_inactive_user(self):
         """login api fails to sign user-activated user in"""
         User = get_user_model()
         User.objects.create_user('Bob', 'bob@test.com', 'Pass.123',
                                  requires_activation=2)
 
         response = self.client.post(
-            reverse('misago:api:login'),
+            '/api/auth/',
             data={'username': 'Bob', 'password': 'Pass.123'})
         self.assertEqual(response.status_code, 400)
 
         response_json = json.loads(response.content)
         self.assertEqual(response_json['code'], 'inactive_admin')
+
+        response = self.client.get('/api/auth/')
+        self.assertEqual(response.status_code, 200)
+
+        user_json = json.loads(response.content)
+        self.assertIsNone(user_json['id'])
+
+
+class SendActivationAPITests(TestCase):
+    def setUp(self):
+        User = get_user_model()
+        self.user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
+        self.user.requires_activation = 1
+        self.user.save()
+
+        self.link = '/api/auth/send-activation/'
+
+    def test_submit_valid(self):
+        """request activation link api sends reset link mail"""
+        response = self.client.post(self.link, data={'email': self.user.email})
+        self.assertEqual(response.status_code, 200)
+
+        self.assertIn('Activate Bob', mail.outbox[0].subject)
+
+    def test_submit_empty(self):
+        """request activation link api errors for no body"""
+        response = self.client.post(self.link)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('empty_email', response.content)
+
+        self.assertTrue(not mail.outbox)
+
+    def test_submit_invalid(self):
+        """request activation link api errors for invalid email"""
+        response = self.client.post(self.link, data={'email': 'fake@mail.com'})
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('not_found', response.content)
+
+        self.assertTrue(not mail.outbox)
+
+    def test_submit_banned(self):
+        """request activation link api errors for banned users"""
+        Ban.objects.create(check_type=BAN_USERNAME,
+                           banned_value=self.user.username,
+                           user_message='Nope!')
+
+        response = self.client.post(self.link, data={'email': self.user.email})
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('Nope!', response.content)
+
+        self.assertTrue(not mail.outbox)
+
+    def test_submit_active_user(self):
+        """request activation link api errors for active user"""
+        self.user.requires_activation = 0
+        self.user.save()
+
+        response = self.client.post(self.link, data={'email': self.user.email})
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('Bob, your account is already active.',
+                      response.content)
+
+    def test_submit_inactive_user(self):
+        """request activation link api errors for admin-activated users"""
+        self.user.requires_activation = 2
+        self.user.save()
+
+        response = self.client.post(self.link, data={'email': self.user.email})
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('inactive_admin', response.content)
+
+        self.assertTrue(not mail.outbox)
+
+        # but succeed for user-activated
+        self.user.requires_activation = 1
+        self.user.save()
+
+        response = self.client.post(self.link, data={'email': self.user.email})
+        self.assertEqual(response.status_code, 200)
+
+        self.assertTrue(mail.outbox)
+
+
+class ActivateAccountAPITests(TestCase):
+    def setUp(self):
+        User = get_user_model()
+        self.user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
+        self.user.requires_activation = 1
+        self.user.save()
+
+        self.link = '/api/auth/activate-account/%s/%s/'
+
+    def test_submit_valid(self):
+        """activate user api returns success and activates user"""
+        response = self.client.post(self.link % (
+                self.user.id,
+                make_activation_token(self.user)
+            ))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(self.user.username, response.content)
+
+        user = get_user_model().objects.get(id=self.user.id)
+        self.assertFalse(user.requires_activation)
+
+    def test_submit_invalid_token(self):
+        """activate user api errors for invalid token"""
+        response = self.client.post(self.link % (
+                self.user.id,
+                'sadsadsadsdsassdsa'
+            ))
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('Activation link is invalid.', response.content)
+
+    def test_submit_invalid_user(self):
+        """activate user api errors for invalid user"""
+        response = self.client.post(self.link % (
+                123,
+                'sadsadsadsdsassdsa'
+            ))
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('Activation link is invalid.', response.content)
+
+    def test_submit_banned(self):
+        """activate user api errors for banned user"""
+        Ban.objects.create(check_type=BAN_USERNAME,
+                           banned_value=self.user.username,
+                           user_message='Nope!')
+
+        response = self.client.post(self.link % (
+                self.user.id,
+                make_activation_token(self.user)
+            ))
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('Activation link has expired.', response.content)
+
+    def test_submit_active_user(self):
+        """validate link api errors for active user"""
+        self.user.requires_activation = 0
+        self.user.save()
+
+        response = self.client.post(self.link % (
+                self.user.id,
+                make_activation_token(self.user)
+            ))
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('Bob, your account is already active.',
+                      response.content)
+
+    def test_submit_inactive_user(self):
+        """validate link api errors for inactive user"""
+        self.user.requires_activation = 1
+        self.user.save()
+
+        response = self.client.post(self.link % (
+                self.user.id,
+                make_activation_token(self.user)
+            ))
+        self.assertEqual(response.status_code, 200)
+
+        self.user.requires_activation = 2
+        self.user.save()
+
+        response = self.client.post(self.link % (
+                self.user.id,
+                make_activation_token(self.user)
+            ))
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('Bob, only administrator may activate your account.',
+                      response.content)
+
+
+class SendPasswordFormAPITests(TestCase):
+    def setUp(self):
+        User = get_user_model()
+        self.user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
+
+        self.link = '/api/auth/send-password-form/'
+
+    def test_submit_valid(self):
+        """request change password form link api sends reset link mail"""
+        response = self.client.post(self.link, data={'email': self.user.email})
+        self.assertEqual(response.status_code, 200)
+
+        self.assertIn('Change Bob password', mail.outbox[0].subject)
+
+    def test_submit_empty(self):
+        """request change password form link api errors for no body"""
+        response = self.client.post(self.link)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('empty_email', response.content)
+
+        self.assertTrue(not mail.outbox)
+
+    def test_submit_invalid(self):
+        """request change password form link api errors for invalid email"""
+        response = self.client.post(self.link, data={'email': 'fake@mail.com'})
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('not_found', response.content)
+
+        self.assertTrue(not mail.outbox)
+
+    def test_submit_banned(self):
+        """request change password form link api errors for banned users"""
+        Ban.objects.create(check_type=BAN_USERNAME,
+                           banned_value=self.user.username,
+                           user_message='Nope!')
+
+        response = self.client.post(self.link, data={'email': self.user.email})
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('Nope!', response.content)
+
+        self.assertTrue(not mail.outbox)
+
+    def test_submit_inactive_user(self):
+        """request change password form link api errors for inactive users"""
+        self.user.requires_activation = 1
+        self.user.save()
+
+        response = self.client.post(self.link, data={'email': self.user.email})
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('inactive_user', response.content)
+
+        self.user.requires_activation = 2
+        self.user.save()
+
+        response = self.client.post(self.link, data={'email': self.user.email})
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('inactive_admin', response.content)
+
+        self.assertTrue(not mail.outbox)
+
+
+class ChangePasswordAPITests(TestCase):
+    def setUp(self):
+        User = get_user_model()
+        self.user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
+
+        self.link = '/api/auth/change-password/%s/%s/'
+
+    def test_valid_link(self):
+        """get validates link"""
+        response = self.client.get(self.link % (
+                self.user.id,
+                make_password_change_token(self.user)
+            ))
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(self.user.username, response.content)
+
+    def test_invalid_user_id_link(self):
+        """get errors on invalid user id link"""
+        response = self.client.get(self.link % (
+                123,
+                make_password_change_token(self.user)
+            ))
+
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('Form link is invalid.', response.content)
+
+    def test_invalid_token_link(self):
+        """get errors on invalid user id link"""
+        response = self.client.get(self.link % (
+                self.user.id,
+                'asda7ad89sa7d9s789as'
+            ))
+
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('Form link is invalid.', response.content)
+
+    def test_banned_user_link(self):
+        """get errors because user is banned"""
+        Ban.objects.create(check_type=BAN_USERNAME,
+                           banned_value=self.user.username,
+                           user_message='Nope!')
+
+        response = self.client.get(self.link % (
+                self.user.id,
+                make_password_change_token(self.user)
+            ))
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('Your link has expired.', response.content)
+
+    def test_inactive_user(self):
+        """request change password form link api errors for inactive users"""
+        self.user.requires_activation = 1
+        self.user.save()
+
+        response = self.client.get(self.link % (
+                self.user.id,
+                make_password_change_token(self.user)
+            ))
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('Your link has expired.', response.content)
+
+        self.user.requires_activation = 2
+        self.user.save()
+
+        response = self.client.get(self.link % (
+                self.user.id,
+                make_password_change_token(self.user)
+            ))
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('Your link has expired.', response.content)
+
+    def test_submit_empty(self):
+        """submit change password form api errors for empty body"""
+        response = self.client.post(self.link % (
+                self.user.id,
+                make_password_change_token(self.user)
+            ))
+        self.assertEqual(response.status_code, 400)
+        self.assertIn('Valid password must', response.content)
+
+    def test_submit_valid(self):
+        """submit change password form api errors for empty body"""
+        response = self.client.post(self.link % (
+                self.user.id,
+                make_password_change_token(self.user)
+            ), data={'password': 'n3wp4ss!'})
+        self.assertEqual(response.status_code, 200)
+
+        user = get_user_model().objects.get(id=self.user.id)
+        self.assertTrue(user.check_password('n3wp4ss!'))

+ 0 - 197
misago/users/tests/test_changepassword_api.py

@@ -1,197 +0,0 @@
-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_change_token
-
-
-class SendLinkAPITests(TestCase):
-    def setUp(self):
-        User = get_user_model()
-        self.user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
-        self.link = reverse('misago:api:change_password_send_link')
-
-    def test_submit_valid(self):
-        """request change password form link api sends reset link mail"""
-        response = self.client.post(self.link, data={'email': self.user.email})
-        self.assertEqual(response.status_code, 200)
-
-        self.assertIn('Change Bob password', mail.outbox[0].subject)
-
-    def test_submit_invalid(self):
-        """request change password form link api errors for invalid email"""
-        response = self.client.post(self.link, data={'email': 'fake@mail.com'})
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('not_found', response.content)
-
-        self.assertTrue(not mail.outbox)
-
-    def test_submit_banned(self):
-        """request change password form link api errors for banned users"""
-        Ban.objects.create(check_type=BAN_USERNAME,
-                           banned_value=self.user.username,
-                           user_message='Nope!')
-
-        response = self.client.post(self.link, data={'email': self.user.email})
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('Nope!', response.content)
-
-        self.assertTrue(not mail.outbox)
-
-    def test_view_submit_inactive_user(self):
-        """request change password form link api errors for inactive users"""
-        self.user.requires_activation = 1
-        self.user.save()
-
-        response = self.client.post(self.link, data={'email': self.user.email})
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('inactive_user', response.content)
-
-        self.user.requires_activation = 2
-        self.user.save()
-
-        response = self.client.post(self.link, data={'email': self.user.email})
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('inactive_admin', response.content)
-
-        self.assertTrue(not mail.outbox)
-
-
-class ValidateTokenAPITests(TestCase):
-    def setUp(self):
-        User = get_user_model()
-        self.user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
-        self.link = reverse(
-            'misago:api:change_password_validate_token',
-            kwargs={
-                'user_id': self.user.id,
-                'token': make_password_change_token(self.user)
-            })
-
-    def test_submit_valid(self):
-        """validate form link api returns success"""
-        response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(self.user.username, response.content)
-
-    def test_submit_invalid_token(self):
-        """validate form link api errors for invalid token"""
-        response = self.client.post(reverse(
-            'misago:api:change_password_validate_token',
-            kwargs={
-                'user_id': self.user.id,
-                'token': 'sadsadsadsdsassdsa'
-            }))
-        self.assertEqual(response.status_code, 404)
-        self.assertIn('Your link is invalid.', response.content)
-
-    def test_submit_invalid_user(self):
-        """validate form link api errors for invalid user"""
-        response = self.client.post(reverse(
-            'misago:api:change_password_validate_token',
-            kwargs={
-                'user_id': 123,
-                'token': 'sadsadsadsdsassdsa'
-            }))
-        self.assertEqual(response.status_code, 404)
-
-    def test_submit_banned(self):
-        """validate form link api errors for banned user"""
-        Ban.objects.create(check_type=BAN_USERNAME,
-                           banned_value=self.user.username,
-                           user_message='Nope!')
-
-        response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 404)
-        self.assertIn('Your link has expired.', response.content)
-
-    def test_view_submit_inactive_user(self):
-        """validate form link api errors for inactive user"""
-        self.user.requires_activation = 1
-        self.user.save()
-
-        response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 404)
-        self.assertIn('Your link has expired.', response.content)
-
-        self.user.requires_activation = 2
-        self.user.save()
-
-        response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 404)
-        self.assertIn('Your link has expired.', response.content)
-
-
-class ChangePasswordAPITests(TestCase):
-    def setUp(self):
-        User = get_user_model()
-        self.user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
-        self.link = reverse(
-            'misago:api:change_password',
-            kwargs={
-                'user_id': self.user.id,
-                'token': make_password_change_token(self.user)
-            })
-
-    def test_submit_valid(self):
-        """change password api changes user password"""
-        response = self.client.post(self.link, data={'password': 'newpass'})
-        self.assertEqual(response.status_code, 200)
-
-        user = get_user_model().objects.get(id=self.user.id)
-        self.assertTrue(user.check_password('newpass'))
-
-    def test_submit_invalid_empty(self):
-        """change password api errors for unvalid password"""
-        response = self.client.post(self.link, data={'password': ''})
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('Valid password must be', response.content)
-
-    def test_submit_invalid_token(self):
-        """change password api errors for unvalid token"""
-        response = self.client.post(reverse('misago:api:change_password',
-            kwargs={
-                'user_id': self.user.id,
-                'token': 'sadsadsadsdsassdsa'
-            }),
-            data={'password': 'newpass'})
-        self.assertEqual(response.status_code, 404)
-        self.assertIn('Your link is invalid.', response.content)
-
-    def test_submit_invalid_user(self):
-        """validate form link api errors for invalid user"""
-        response = self.client.post(reverse('misago:api:change_password',
-            kwargs={
-                'user_id': 123,
-                'token': 'sadsadsadsdsassdsa'
-            }),
-            data={'password': 'newpass'})
-        self.assertEqual(response.status_code, 404)
-
-    def test_submit_banned(self):
-        """validate form link api errors for banned user"""
-        Ban.objects.create(check_type=BAN_USERNAME,
-                           banned_value=self.user.username,
-                           user_message='Nope!')
-
-        response = self.client.post(self.link, data={'password': 'newpass'})
-        self.assertEqual(response.status_code, 404)
-        self.assertIn('Your link has expired.', response.content)
-
-    def test_view_submit_inactive_user(self):
-        """validate form link api errors for inactive user"""
-        self.user.requires_activation = 1
-        self.user.save()
-
-        response = self.client.post(self.link, data={'password': 'newpass'})
-        self.assertEqual(response.status_code, 404)
-        self.assertIn('Your link has expired.', response.content)
-
-        self.user.requires_activation = 2
-        self.user.save()
-
-        response = self.client.post(self.link, data={'password': 'newpass'})
-        self.assertEqual(response.status_code, 404)
-        self.assertIn('Your link has expired.', response.content)

+ 5 - 12
misago/users/urls/api.py

@@ -4,18 +4,11 @@ from misago.users.api.users import UserViewSet
 
 
 urlpatterns = patterns('misago.users.api.auth',
-    url(r'^login/$', 'login', name='login'),
-)
-
-urlpatterns += patterns('misago.users.api.activation',
-    url(r'^activation/send-link/$', 'send_link', name="activation_send_link"),
-    url(r'^activation/(?P<user_id>\d+)/(?P<token>[a-zA-Z0-9]+)/validate-token/$', 'validate_token', name="activation_validate_token"),
-)
-
-urlpatterns += patterns('misago.users.api.changepassword',
-    url(r'^change-password/send-link/$', 'send_link', name='change_password_send_link'),
-    url(r'^change-password/(?P<user_id>\d+)/(?P<token>[a-zA-Z0-9]+)/validate-token/$', 'validate_token', name='change_password_validate_token'),
-    url(r'^change-password/(?P<user_id>\d+)/(?P<token>[a-zA-Z0-9]+)/$', 'change_password', name='change_password'),
+    url(r'^auth/$', 'gateway'),
+    url(r'^auth/send-activation/$', 'send_activation'),
+    url(r'^auth/activate-account/(?P<user_id>\d+)/(?P<token>[a-zA-Z0-9]+)/$', 'activate_account'),
+    url(r'^auth/send-password-form/$', 'send_password_form'),
+    url(r'^auth/change-password/(?P<user_id>\d+)/(?P<token>[a-zA-Z0-9]+)/$', 'change_forgotten_password'),
 )
 
 urlpatterns += patterns('misago.users.api.captcha',