diff --git a/README.md b/README.md index d6ceaba..69b1848 100644 --- a/README.md +++ b/README.md @@ -321,8 +321,10 @@ DEFAULTS = { 'PASSWORDLESS_SMS_CALLBACK': 'drfpasswordless.utils.send_sms_with_callback_token', # Token Generation Retry Count - 'PASSWORDLESS_TOKEN_GENERATION_ATTEMPTS': 3 + 'PASSWORDLESS_TOKEN_GENERATION_ATTEMPTS': 3, + # The length of the token to send in email or sms, maximum 6 + 'PASSWORDLESS_TOKEN_LENGTH': 6 } ``` diff --git a/drfpasswordless/__version__.py b/drfpasswordless/__version__.py index 8efe3d1..0a630a2 100644 --- a/drfpasswordless/__version__.py +++ b/drfpasswordless/__version__.py @@ -1,3 +1,3 @@ -VERSION = (1, 5, 8) +VERSION = (1, 6, 4) __version__ = '.'.join(map(str, VERSION)) diff --git a/drfpasswordless/auth.py b/drfpasswordless/auth.py new file mode 100644 index 0000000..ace62fd --- /dev/null +++ b/drfpasswordless/auth.py @@ -0,0 +1,6 @@ +from rest_framework.authentication import TokenAuthentication +from drfpasswordless.authtoken.models import Token + + +class TokenAuthentication(TokenAuthentication): + model = Token \ No newline at end of file diff --git a/drfpasswordless/authtoken/__init__.py b/drfpasswordless/authtoken/__init__.py new file mode 100644 index 0000000..7c33df6 --- /dev/null +++ b/drfpasswordless/authtoken/__init__.py @@ -0,0 +1,4 @@ +import django + +if django.VERSION < (3, 2): + default_app_config = 'drfpasswordless.authtoken.apps.AuthTokenConfig' diff --git a/drfpasswordless/authtoken/admin.py b/drfpasswordless/authtoken/admin.py new file mode 100644 index 0000000..0f055a6 --- /dev/null +++ b/drfpasswordless/authtoken/admin.py @@ -0,0 +1,53 @@ +from django.contrib import admin +from django.contrib.admin.utils import quote +from django.contrib.admin.views.main import ChangeList +from django.contrib.auth import get_user_model +from django.core.exceptions import ValidationError +from django.urls import reverse + +from drfpasswordless.authtoken.models import Token, TokenProxy + +User = get_user_model() + + +class TokenChangeList(ChangeList): + """Map to matching User id""" + def url_for_result(self, result): + pk = result.user.pk + return reverse('admin:%s_%s_change' % (self.opts.app_label, + self.opts.model_name), + args=(quote(pk),), + current_app=self.model_admin.admin_site.name) + + +class TokenAdmin(admin.ModelAdmin): + list_display = ('key', 'user', 'device_type', 'created') + fields = ('user', 'device_id', 'device_type') + ordering = ('-created',) + actions = None # Actions not compatible with mapped IDs. + readonly_fields = ("user", "device_id", "device_type") + list_filter = ("device_type",) + + def get_changelist(self, request, **kwargs): + return TokenChangeList + + def get_object(self, request, object_id, from_field=None): + """ + Map from User ID to matching Token. + """ + queryset = self.get_queryset(request) + field = User._meta.pk + try: + object_id = field.to_python(object_id) + user = User.objects.filter(**{field.name: object_id}).first() + return queryset.filter(user=user).first() + except (queryset.model.DoesNotExist, User.DoesNotExist, ValidationError, ValueError): + return None + + def delete_model(self, request, obj): + # Map back to actual Token, since delete() uses pk. + token = Token.objects.get(key=obj.key) + return super().delete_model(request, token) + + +admin.site.register(TokenProxy, TokenAdmin) diff --git a/drfpasswordless/authtoken/apps.py b/drfpasswordless/authtoken/apps.py new file mode 100644 index 0000000..d521c6a --- /dev/null +++ b/drfpasswordless/authtoken/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class AuthTokenConfig(AppConfig): + name = 'drfpasswordless.authtoken' + verbose_name = _("Auth Token") diff --git a/drfpasswordless/authtoken/management/__init__.py b/drfpasswordless/authtoken/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/drfpasswordless/authtoken/management/commands/__init__.py b/drfpasswordless/authtoken/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/drfpasswordless/authtoken/management/commands/drf_create_token.py b/drfpasswordless/authtoken/management/commands/drf_create_token.py new file mode 100644 index 0000000..4705c8a --- /dev/null +++ b/drfpasswordless/authtoken/management/commands/drf_create_token.py @@ -0,0 +1,45 @@ +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand, CommandError + +from drfpasswordless.authtoken.models import Token + +UserModel = get_user_model() + + +class Command(BaseCommand): + help = 'Create DRF Token for a given user' + + def create_user_token(self, username, reset_token): + user = UserModel._default_manager.get_by_natural_key(username) + + if reset_token: + Token.objects.filter(user=user).delete() + + token = Token.objects.get_or_create(user=user) + return token[0] + + def add_arguments(self, parser): + parser.add_argument('username', type=str) + + parser.add_argument( + '-r', + '--reset', + action='store_true', + dest='reset_token', + default=False, + help='Reset existing User token and create a new one', + ) + + def handle(self, *args, **options): + username = options['username'] + reset_token = options['reset_token'] + + try: + token = self.create_user_token(username, reset_token) + except UserModel.DoesNotExist: + raise CommandError( + 'Cannot create the Token: user {} does not exist'.format( + username) + ) + self.stdout.write( + 'Generated token {} for user {}'.format(token.key, username)) diff --git a/drfpasswordless/authtoken/migrations/0001_initial.py b/drfpasswordless/authtoken/migrations/0001_initial.py new file mode 100644 index 0000000..6a46ccf --- /dev/null +++ b/drfpasswordless/authtoken/migrations/0001_initial.py @@ -0,0 +1,23 @@ +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Token', + fields=[ + ('key', models.CharField(primary_key=True, serialize=False, max_length=40)), + ('created', models.DateTimeField(auto_now_add=True)), + ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, related_name='auth_token', on_delete=models.CASCADE)), + ], + options={ + }, + bases=(models.Model,), + ), + ] diff --git a/drfpasswordless/authtoken/migrations/0002_auto_20160226_1747.py b/drfpasswordless/authtoken/migrations/0002_auto_20160226_1747.py new file mode 100644 index 0000000..4311909 --- /dev/null +++ b/drfpasswordless/authtoken/migrations/0002_auto_20160226_1747.py @@ -0,0 +1,31 @@ +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authtoken', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='token', + options={'verbose_name_plural': 'Tokens', 'verbose_name': 'Token'}, + ), + migrations.AlterField( + model_name='token', + name='created', + field=models.DateTimeField(verbose_name='Created', auto_now_add=True), + ), + migrations.AlterField( + model_name='token', + name='key', + field=models.CharField(verbose_name='Key', max_length=40, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='token', + name='user', + field=models.OneToOneField(to=settings.AUTH_USER_MODEL, verbose_name='User', related_name='auth_token', on_delete=models.CASCADE), + ), + ] diff --git a/drfpasswordless/authtoken/migrations/0003_tokenproxy.py b/drfpasswordless/authtoken/migrations/0003_tokenproxy.py new file mode 100644 index 0000000..79405a7 --- /dev/null +++ b/drfpasswordless/authtoken/migrations/0003_tokenproxy.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1.1 on 2020-09-28 09:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('authtoken', '0002_auto_20160226_1747'), + ] + + operations = [ + migrations.CreateModel( + name='TokenProxy', + fields=[ + ], + options={ + 'verbose_name': 'token', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('authtoken.token',), + ), + ] diff --git a/drfpasswordless/authtoken/migrations/0004_auto_20240927_1224.py b/drfpasswordless/authtoken/migrations/0004_auto_20240927_1224.py new file mode 100644 index 0000000..abd63cc --- /dev/null +++ b/drfpasswordless/authtoken/migrations/0004_auto_20240927_1224.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.18 on 2024-10-04 08:26 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('authtoken', '0003_tokenproxy'), + ] + + operations = [ + migrations.AddField( + model_name='token', + name='device_id', + field=models.CharField(blank=True, max_length=64, null=True), + ), + migrations.AddField( + model_name='token', + name='device_type', + field=models.CharField(blank=True, choices=[('WEB', 'WEB'), ('SPECTRO_TV', 'SPECTRO_TV'), ('LOG_SHEET', 'LOG_SHEET'), ('MELTING_REPORT', 'MELTING_REPORT'), ('IOT', 'IOT')], max_length=32, null=True), + ), + migrations.AlterField( + model_name='token', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='auth_token', to=settings.AUTH_USER_MODEL, verbose_name='User'), + ), + migrations.AddConstraint( + model_name='token', + constraint=models.UniqueConstraint(condition=models.Q(('device_id__isnull', False), models.Q(('device_id', ''), _negated=True)), fields=('device_id',), name='unique_device_id_not_null_blank'), + ), + ] diff --git a/drfpasswordless/authtoken/migrations/0005_auto_20241017_1246.py b/drfpasswordless/authtoken/migrations/0005_auto_20241017_1246.py new file mode 100644 index 0000000..8d36a5a --- /dev/null +++ b/drfpasswordless/authtoken/migrations/0005_auto_20241017_1246.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.18 on 2024-10-17 07:16 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('authtoken', '0004_auto_20240927_1224'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='token', + name='unique_device_id_not_null_blank', + ), + migrations.AlterUniqueTogether( + name='token', + unique_together={('user', 'device_id')}, + ), + ] + diff --git a/drfpasswordless/authtoken/migrations/__init__.py b/drfpasswordless/authtoken/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/drfpasswordless/authtoken/models.py b/drfpasswordless/authtoken/models.py new file mode 100644 index 0000000..1df2aa9 --- /dev/null +++ b/drfpasswordless/authtoken/models.py @@ -0,0 +1,71 @@ +import binascii +import os +from django.db.models import Q +from django.conf import settings +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class Token(models.Model): + """ + The default authorization token model. + """ + WEB = "WEB" + SPECTRO_TV = "SPECTRO_TV" + LOG_SHEET = "LOG_SHEET" + MELTING_REPORT = "MELTING_REPORT" + IOT = "IOT" + DEVICE_TYPES = ( + (WEB, "WEB"), + (SPECTRO_TV, "SPECTRO_TV"), + (LOG_SHEET, "LOG_SHEET"), + (MELTING_REPORT, "MELTING_REPORT"), + (IOT, "IOT"), + ) + key = models.CharField(_("Key"), max_length=40, primary_key=True) + device_id = models.CharField(max_length=64, blank=True, null=True) + device_type = models.CharField( + choices=DEVICE_TYPES, max_length=32, blank=True, null=True + ) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, related_name='auth_token', + on_delete=models.CASCADE, verbose_name=_("User") + ) + created = models.DateTimeField(_("Created"), auto_now_add=True) + + class Meta: + # Work around for a bug in Django: + # https://code.djangoproject.com/ticket/19422 + # + # Also see corresponding ticket: + # https://github.com/encode/django-rest-framework/issues/705 + abstract = 'drfpasswordless.authtoken' not in settings.INSTALLED_APPS + verbose_name = _("Token") + verbose_name_plural = _("Tokens") + unique_together = (('user', 'device_id'),) + + def save(self, *args, **kwargs): + if not self.key: + self.key = self.generate_key() + return super().save(*args, **kwargs) + + @classmethod + def generate_key(cls): + return binascii.hexlify(os.urandom(20)).decode() + + def __str__(self): + return self.key + + +class TokenProxy(Token): + """ + Proxy mapping pk to user pk for use in admin. + """ + @property + def pk(self): + return self.user_id + + class Meta: + proxy = 'drfpasswordless.authtoken' in settings.INSTALLED_APPS + abstract = 'drfpasswordless.authtoken' not in settings.INSTALLED_APPS + verbose_name = "token" diff --git a/drfpasswordless/authtoken/serializers.py b/drfpasswordless/authtoken/serializers.py new file mode 100644 index 0000000..63e64d6 --- /dev/null +++ b/drfpasswordless/authtoken/serializers.py @@ -0,0 +1,42 @@ +from django.contrib.auth import authenticate +from django.utils.translation import gettext_lazy as _ + +from rest_framework import serializers + + +class AuthTokenSerializer(serializers.Serializer): + username = serializers.CharField( + label=_("Username"), + write_only=True + ) + password = serializers.CharField( + label=_("Password"), + style={'input_type': 'password'}, + trim_whitespace=False, + write_only=True + ) + token = serializers.CharField( + label=_("Token"), + read_only=True + ) + + def validate(self, attrs): + username = attrs.get('username') + password = attrs.get('password') + + if username and password: + user = authenticate(request=self.context.get('request'), + username=username, password=password) + + # The authenticate call simply returns None for is_active=False + # users. (Assuming the default ModelBackend authentication + # backend.) + if not user: + msg = _('Unable to log in with provided credentials.') + raise serializers.ValidationError(msg, code='authorization') + else: + msg = _('Must include "username" and "password".') + raise serializers.ValidationError(msg, code='authorization') + + attrs['user'] = user + return attrs diff --git a/drfpasswordless/authtoken/views.py b/drfpasswordless/authtoken/views.py new file mode 100644 index 0000000..0baf739 --- /dev/null +++ b/drfpasswordless/authtoken/views.py @@ -0,0 +1,62 @@ +from rest_framework import parsers, renderers +from drfpasswordless.authtoken.models import Token +from rest_framework.authtoken.serializers import AuthTokenSerializer +from rest_framework.compat import coreapi, coreschema +from rest_framework.response import Response +from rest_framework.schemas import ManualSchema +from rest_framework.schemas import coreapi as coreapi_schema +from rest_framework.views import APIView + + +class ObtainAuthToken(APIView): + throttle_classes = () + permission_classes = () + parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,) + renderer_classes = (renderers.JSONRenderer,) + serializer_class = AuthTokenSerializer + + if coreapi_schema.is_enabled(): + schema = ManualSchema( + fields=[ + coreapi.Field( + name="username", + required=True, + location='form', + schema=coreschema.String( + title="Username", + description="Valid username for authentication", + ), + ), + coreapi.Field( + name="password", + required=True, + location='form', + schema=coreschema.String( + title="Password", + description="Valid password for authentication", + ), + ), + ], + encoding="application/json", + ) + + def get_serializer_context(self): + return { + 'request': self.request, + 'format': self.format_kwarg, + 'view': self + } + + def get_serializer(self, *args, **kwargs): + kwargs['context'] = self.get_serializer_context() + return self.serializer_class(*args, **kwargs) + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.validated_data['user'] + token, created = Token.objects.get_or_create(user=user) + return Response({'token': token.key}) + + +obtain_auth_token = ObtainAuthToken.as_view() diff --git a/drfpasswordless/migrations/0006_callbacktoken_attempts.py b/drfpasswordless/migrations/0006_callbacktoken_attempts.py new file mode 100644 index 0000000..53ec959 --- /dev/null +++ b/drfpasswordless/migrations/0006_callbacktoken_attempts.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2025-03-20 05:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('drfpasswordless', '0005_auto_20201117_0410'), + ] + + operations = [ + migrations.AddField( + model_name='callbacktoken', + name='attempts', + field=models.IntegerField(default=0), + ), + ] diff --git a/drfpasswordless/models.py b/drfpasswordless/models.py index d09f3fc..081473d 100644 --- a/drfpasswordless/models.py +++ b/drfpasswordless/models.py @@ -3,6 +3,7 @@ from django.conf import settings import string from django.utils.crypto import get_random_string +from drfpasswordless.settings import api_settings def generate_hex_token(): return uuid.uuid1().hex @@ -13,7 +14,10 @@ def generate_numeric_token(): Generate a random 6 digit string of numbers. We use this formatting to allow leading 0s. """ - return get_random_string(length=6, allowed_chars=string.digits) + return get_random_string( + length=api_settings.PASSWORDLESS_TOKEN_LENGTH, + allowed_chars=string.digits + ) class CallbackTokenManger(models.Manager): @@ -62,6 +66,16 @@ class CallbackToken(AbstractBaseCallbackToken): key = models.CharField(default=generate_numeric_token, max_length=6) type = models.CharField(max_length=20, choices=TOKEN_TYPES) + attempts = models.IntegerField(default=0) class Meta(AbstractBaseCallbackToken.Meta): verbose_name = 'Callback Token' + + def increment_attempts(self): + """ + Increment the number of attempts and deactivate if max attempts reached + """ + self.attempts += 1 + if self.attempts > 3: # Max 3 attempts + self.is_active = False + self.save() diff --git a/drfpasswordless/serializers.py b/drfpasswordless/serializers.py index 597f4d4..f56ee08 100644 --- a/drfpasswordless/serializers.py +++ b/drfpasswordless/serializers.py @@ -8,7 +8,7 @@ from drfpasswordless.models import CallbackToken from drfpasswordless.settings import api_settings from drfpasswordless.utils import verify_user_alias, validate_token_age - +from drfpasswordless.authtoken.models import Token logger = logging.getLogger(__name__) User = get_user_model() @@ -175,7 +175,10 @@ class AbstractBaseCallbackTokenSerializer(serializers.Serializer): email = serializers.EmailField(required=False) # Needs to be required=false to require both. mobile = serializers.CharField(required=False, validators=[phone_regex], max_length=17) - token = TokenField(min_length=6, max_length=6, validators=[token_age_validator]) + token = TokenField( + min_length=api_settings.PASSWORDLESS_TOKEN_LENGTH, + max_length=api_settings.PASSWORDLESS_TOKEN_LENGTH, + ) def validate_alias(self, attrs): email = attrs.get('email', None) @@ -196,6 +199,8 @@ def validate_alias(self, attrs): class CallbackTokenAuthSerializer(AbstractBaseCallbackTokenSerializer): + device_id = serializers.CharField(required=False, max_length=64) + device_type = serializers.ChoiceField(choices=Token.DEVICE_TYPES, required=False) def validate(self, attrs): # Check Aliases @@ -203,6 +208,18 @@ def validate(self, attrs): alias_type, alias = self.validate_alias(attrs) callback_token = attrs.get('token', None) user = User.objects.get(**{alias_type+'__iexact': alias}) + + + #increment the attempt + token = CallbackToken.objects.filter(**{'user': user, + 'type': CallbackToken.TOKEN_TYPE_AUTH, + 'is_active': True}).first() + + if token: + token.increment_attempts() + + validate_token_age(callback_token) + token = CallbackToken.objects.get(**{'user': user, 'key': callback_token, 'type': CallbackToken.TOKEN_TYPE_AUTH, @@ -232,7 +249,7 @@ def validate(self, attrs): msg = _('Invalid Token') raise serializers.ValidationError(msg) except CallbackToken.DoesNotExist: - msg = _('Invalid alias parameters provided.') + msg = _('Invalid OTP or OTP may be expired') raise serializers.ValidationError(msg) except User.DoesNotExist: msg = _('Invalid user alias parameters provided.') @@ -241,6 +258,13 @@ def validate(self, attrs): msg = _('Invalid alias parameters provided.') raise serializers.ValidationError(msg) + def to_internal_value(self, data): + device_type = data.get('device_type', None) + if device_type and device_type not in dict(Token.DEVICE_TYPES).keys(): + del data['device_type'] + + return super().to_internal_value(data) + class CallbackTokenVerificationSerializer(AbstractBaseCallbackTokenSerializer): """ diff --git a/drfpasswordless/settings.py b/drfpasswordless/settings.py index 5b93197..c9bd842 100644 --- a/drfpasswordless/settings.py +++ b/drfpasswordless/settings.py @@ -89,7 +89,10 @@ 'PASSWORDLESS_SMS_CALLBACK': 'drfpasswordless.utils.send_sms_with_callback_token', # Token Generation Retry Count - 'PASSWORDLESS_TOKEN_GENERATION_ATTEMPTS': 3 + 'PASSWORDLESS_TOKEN_GENERATION_ATTEMPTS': 3, + + # The length of the token to send in email or sms, maximum 6 + 'PASSWORDLESS_TOKEN_LENGTH': 6 } # List of settings that may be in string import notation. diff --git a/drfpasswordless/utils.py b/drfpasswordless/utils.py index ba02deb..6a92b8d 100644 --- a/drfpasswordless/utils.py +++ b/drfpasswordless/utils.py @@ -5,7 +5,7 @@ from django.core.mail import send_mail from django.template import loader from django.utils import timezone -from rest_framework.authtoken.models import Token +from drfpasswordless.authtoken.models import Token from drfpasswordless.models import CallbackToken from drfpasswordless.settings import api_settings @@ -208,6 +208,7 @@ def send_sms_with_callback_token(user, mobile_token, **kwargs): return False -def create_authentication_token(user): - """ Default way to create an authentication token""" - return Token.objects.get_or_create(user=user) +def create_authentication_token(user, device_id="", device_type=""): + return Token.objects.get_or_create( + user=user, device_id=device_id, defaults={"device_type": device_type} + ) \ No newline at end of file diff --git a/drfpasswordless/views.py b/drfpasswordless/views.py index 6692ac8..9e6ffbd 100644 --- a/drfpasswordless/views.py +++ b/drfpasswordless/views.py @@ -134,14 +134,19 @@ class AbstractBaseObtainAuthToken(APIView): This is a duplicate of rest_framework's own ObtainAuthToken method. Instead, this returns an Auth Token based on our 6 digit callback token and source. """ + serializer_class = None def post(self, request, *args, **kwargs): serializer = self.serializer_class(data=request.data) if serializer.is_valid(raise_exception=True): - user = serializer.validated_data['user'] + user = serializer.validated_data["user"] token_creator = import_string(api_settings.PASSWORDLESS_AUTH_TOKEN_CREATOR) - (token, _) = token_creator(user) + (token, _) = token_creator( + user=user, + device_id=serializer.validated_data.get("device_id", None), + device_type=serializer.validated_data.get("device_type", None), + ) if token: TokenSerializer = import_string(api_settings.PASSWORDLESS_AUTH_TOKEN_SERIALIZER) @@ -150,8 +155,15 @@ def post(self, request, *args, **kwargs): # Return our key for consumption. return Response(token_serializer.data, status=status.HTTP_200_OK) else: - logger.error("Couldn't log in unknown user. Errors on serializer: {}".format(serializer.error_messages)) - return Response({'detail': 'Couldn\'t log you in. Try again later.'}, status=status.HTTP_400_BAD_REQUEST) + logger.error( + "Couldn't log in unknown user. Errors on serializer: {}".format( + serializer.error_messages + ) + ) + return Response( + {"detail": "Couldn't log you in. Try again later."}, + status=status.HTTP_400_BAD_REQUEST, + ) class ObtainAuthTokenFromCallbackToken(AbstractBaseObtainAuthToken): diff --git a/tests/test_settings.py b/tests/test_settings.py index 7fe709d..cbc2ba6 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,5 +1,5 @@ from rest_framework import status -from rest_framework.authtoken.models import Token +from drfpasswordless.authtoken.models import Token from rest_framework.test import APITestCase from django.contrib.auth import get_user_model diff --git a/tests/test_verification.py b/tests/test_verification.py index 16a0464..c519663 100644 --- a/tests/test_verification.py +++ b/tests/test_verification.py @@ -1,5 +1,5 @@ from rest_framework import status -from rest_framework.authtoken.models import Token +from drfpasswordless.authtoken.models import Token from django.utils.translation import gettext_lazy as _ from rest_framework.test import APITestCase from django.contrib.auth import get_user_model