Refactor and enhance device management and authentication features
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 4m12s

- Updated the `reverse_dhivehi_string` function to correct the range for combining characters.
- Added new device handling in the health check view and integrated the `add_new_devices_to_omada` task.
- Improved date handling in `CreateTemporaryUserView` to ensure proper string conversion.
- Enhanced OTP sending by converting mobile numbers to strings.
- Implemented MAC address validation in the `Device` model using a custom validator.
- Removed unnecessary fields from the `CreateDeviceSerializer`.
- Normalized MAC address format in the `DeviceListCreateAPIView`.
- Updated the `djangopasswordlessknox` package to improve code consistency and readability.
- Added migration to enforce MAC address validation in the database.
This commit is contained in:
i701 2025-04-25 14:37:27 +05:00
parent 0f19f0c15c
commit 83db42cc60
Signed by: i701
GPG Key ID: 54A0DA1E26D8E587
24 changed files with 475 additions and 209 deletions

View File

@ -47,3 +47,8 @@ curl -X POST http://localhost:4000/api/auth/login/ \
``` ```
expected response: `{"message":"Unable to log in with provided credentials."}` expected response: `{"message":"Unable to log in with provided credentials."}`
5. For Celery to work run the worker and the beat
```
celery -A apibase worker --loglevel=info
celery -A apibase beat --loglevel=info
```

View File

@ -27,6 +27,8 @@ class IsStaffEditorPermission(permissions.DjangoModelPermissions):
# Check permissions based on the request method # Check permissions based on the request method
perms = self.perms_map.get(request.method, []) perms = self.perms_map.get(request.method, [])
perms = [perm % {'app_label': app_label, 'model_name': model_name} for perm in perms] perms = [
perm % {"app_label": app_label, "model_name": model_name} for perm in perms
]
return request.user.has_perms(perms) return request.user.has_perms(perms)

View File

@ -13,7 +13,7 @@ if not api_url or not api_key:
) )
def send_otp(mobile: str, otp: int, message: str): def send_otp(mobile: str, otp: str, message: str):
if not api_url or not api_key: if not api_url or not api_key:
logger.debug("Failed to send SMS. Missing SMS_API_URL or SMS_API_KEY.") logger.debug("Failed to send SMS. Missing SMS_API_URL or SMS_API_KEY.")
return False return False

View File

@ -14,6 +14,27 @@ logger = logging.getLogger(__name__)
env.read_env(os.path.join(BASE_DIR, ".env")) env.read_env(os.path.join(BASE_DIR, ".env"))
PERSON_VERIFY_BASE_URL = env.str("PERSON_VERIFY_BASE_URL") PERSON_VERIFY_BASE_URL = env.str("PERSON_VERIFY_BASE_URL")
OMADA_PROXY_API_KEY = env.str("OMADA_PROXY_API_KEY")
OMADA_PROXY_URL = env("OMADA_PROXY_URL")
OMADA_SITE_ID = env(
"OMADA_SITE_ID",
)
OMADA_GROUP_ID = env(
"OMADA_GROUP_ID",
)
if not OMADA_SITE_ID:
raise ValueError(
"OMADA_SITE_ID is not set. Please set it in your environment variables."
)
if not OMADA_GROUP_ID:
raise ValueError(
"OMADA_GROUP_ID is not set. Please set it in your environment variables."
)
if not OMADA_PROXY_URL:
raise ValueError(
"OMADA_PROXY_URL is not set. Please set it in your environment variables."
)
@shared_task @shared_task
@ -54,6 +75,81 @@ def deactivate_expired_devices():
} }
def get_existing_omada_devices():
"""
Get existing Omada devices from the database.
:return: List of existing device names.
"""
try:
response = requests.get(
f"{OMADA_PROXY_URL}/9fd0cffa3475a74ae4e4d37de0d12414/api/v2/sites/66dcddb804aa0d2978cf145f/setting/profiles/groups",
headers={"X-API-Key": str(OMADA_PROXY_API_KEY)},
)
print("Response: ", response.status_code)
data = response.json()
existing_devices = []
if "result" in data and len(data["result"]["data"]) > 0:
last_entry = data["result"]["data"][-1]
print("Last Entry: ", last_entry)
if "macAddressList" in last_entry:
existing_devices = last_entry["macAddressList"]
print(existing_devices)
return existing_devices
except requests.RequestException as e:
print(f"Error fetching existing devices: {e}")
return []
@shared_task
def add_new_devices_to_omada(new_devices: list[dict]):
"""
Add new devices to Omada.
:param new_devices: List of new device names to add.
"""
try:
PAYLOAD = {
"name": "REGISTERED_DEVICES",
"type": 2,
"resource": 0,
"ipList": None,
"ipv6List": None,
"macAddressList": None,
"portList": None,
"countryList": None,
"portType": None,
"portMaskList": None,
"domainNamePort": None,
}
existing_devices = get_existing_omada_devices()
PAYLOAD["macAddressList"] = existing_devices
print("Payload with existing devices: ", PAYLOAD)
for device in new_devices:
print("Device in loop: ", device)
PAYLOAD["macAddressList"].append(
{
"macAddress": device["mac"],
"name": device["name"],
}
)
print("New Payload: ", PAYLOAD)
print(
f"{OMADA_PROXY_URL}/9fd0cffa3475a74ae4e4d37de0d12414/api/v2/sites/{OMADA_SITE_ID}/setting/profiles/groups/2/{OMADA_GROUP_ID}"
)
response = requests.patch(
f"{OMADA_PROXY_URL}/9fd0cffa3475a74ae4e4d37de0d12414/api/v2/sites/{OMADA_SITE_ID}/setting/profiles/groups/2/{OMADA_GROUP_ID}",
headers={"X-API-Key": str(OMADA_PROXY_API_KEY)},
json=PAYLOAD,
)
print("Response: ", response.status_code)
if response.status_code == 200:
print("Devices successfully added.")
print(response.json())
else:
print(f"Failed to add devices: {response.text}")
except requests.RequestException as e:
print(f"Error adding devices: {e}")
def verify_user_with_person_api_task(user_id: int): def verify_user_with_person_api_task(user_id: int):
""" """
Verify the user with the Person API. Verify the user with the Person API.

View File

@ -18,7 +18,7 @@ def reverse_dhivehi_string(input_str):
i = 0 i = 0
while i < len(reversed_str): while i < len(reversed_str):
# Check if current character is a combining character # Check if current character is a combining character
if i + 1 < len(reversed_str) and "\u0300" <= reversed_str[i + 1] <= "\u036F": if i + 1 < len(reversed_str) and "\u0300" <= reversed_str[i + 1] <= "\u036f":
# If next character is a combining mark, add it before the base character # If next character is a combining mark, add it before the base character
corrected_chars.append(reversed_str[i + 1] + reversed_str[i]) corrected_chars.append(reversed_str[i + 1] + reversed_str[i])
i += 2 i += 2

View File

@ -1,4 +1,5 @@
# django imports # django imports
import pprint
from django.contrib.auth import login from django.contrib.auth import login
# rest_framework imports # rest_framework imports
@ -31,7 +32,8 @@ from typing import cast, Dict, Any
from django.core.mail import send_mail from django.core.mail import send_mail
from django.db.models import Q from django.db.models import Q
from api.sms import send_otp from api.sms import send_otp
from .tasks import add, deactivate_expired_devices from .tasks import add, add_new_devices_to_omada
from devices.models import Device
# local apps import # local apps import
from .serializers import ( from .serializers import (
@ -60,7 +62,8 @@ class ErrorMessages:
@api_view(["GET"]) @api_view(["GET"])
def healthcheck(request): def healthcheck(request):
add.delay(1, 2) add.delay(1, 2)
deactivate_expired_devices.delay() # devices = Device.objects.filter(is_active=False).values()
# add_new_devices_to_omada.delay(new_devices=list(devices))
return Response({"status": "Good"}, status=status.HTTP_200_OK) return Response({"status": "Good"}, status=status.HTTP_200_OK)
@ -117,7 +120,7 @@ class CreateTemporaryUserView(generics.CreateAPIView):
current_date = timezone.now() current_date = timezone.now()
try: try:
dob = timezone.datetime.strptime(dob, "%Y-%m-%d").date() dob = timezone.datetime.strptime(str(dob), "%Y-%m-%d").date()
except ValueError: except ValueError:
return Response( return Response(
{"message": "Invalid date format for DOB. Use YYYY-MM-DD."}, status=400 {"message": "Invalid date format for DOB. Use YYYY-MM-DD."}, status=400
@ -193,7 +196,7 @@ class CreateTemporaryUserView(generics.CreateAPIView):
formatted_time = otp_expiry.strftime("%d/%m/%Y %H:%M:%S") formatted_time = otp_expiry.strftime("%d/%m/%Y %H:%M:%S")
otp = temp_user.generate_otp() otp = temp_user.generate_otp()
send_otp( send_otp(
temp_user.t_mobile, str(temp_user.t_mobile),
otp, otp,
f"Your Registration SARLink OTP: {otp}. \nExpires at {formatted_time}. \n\n- SAR Link", f"Your Registration SARLink OTP: {otp}. \nExpires at {formatted_time}. \n\n- SAR Link",
) )
@ -263,7 +266,7 @@ class VerifyOTPView(generics.GenericAPIView):
User.objects.create_user( User.objects.create_user(
first_name=temp_user.t_first_name, first_name=temp_user.t_first_name,
last_name=temp_user.t_last_name, last_name=temp_user.t_last_name,
username=temp_user.t_username, username=str(temp_user.t_username),
password="", password="",
address=temp_user.t_address, address=temp_user.t_address,
mobile=temp_user.t_mobile, mobile=temp_user.t_mobile,

View File

@ -1,32 +1,25 @@
# Create your views here. # Create your views here.
# billing/views.py # billing/views.py
import os
from datetime import timedelta from datetime import timedelta
from django.utils import timezone
import requests import requests
from django.utils import timezone
from django.utils.timezone import localtime
from rest_framework import generics, status from rest_framework import generics, status
from rest_framework.response import Response from rest_framework.response import Response
from api.mixins import StaffEditorPermissionMixin from api.mixins import StaffEditorPermissionMixin
from api.tasks import add_new_devices_to_omada
from apibase.env import BASE_DIR, env
from .models import Device, Payment from .models import Device, Payment
from .serializers import PaymentSerializer, UpdatePaymentSerializer from .serializers import PaymentSerializer, UpdatePaymentSerializer
from apibase.env import env, BASE_DIR
from django.utils.timezone import localtime
import os
env.read_env(os.path.join(BASE_DIR, ".env")) env.read_env(os.path.join(BASE_DIR, ".env"))
PAYMENT_BASE_URL = env("PAYMENT_BASE_URL", default=None) PAYMENT_BASE_URL = env("PAYMENT_BASE_URL", default=None)
OMADA_PROXY_URL = env("OMADA_PROXY_URL", default=None)
if not OMADA_PROXY_URL:
raise ValueError(
"OMADA_PROXY_URL is not set. Please set it in your environment variables."
)
if not PAYMENT_BASE_URL: if not PAYMENT_BASE_URL:
raise ValueError( raise ValueError(
"PAYMENT_BASE_URL is not set. Please set it in your environment variables." "PAYMENT_BASE_URL is not set. Please set it in your environment variables."
@ -180,10 +173,19 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
registered=True, registered=True,
) )
# Need to add to omada if its a new device and not an existing device # Need to add to omada if its a new device and not an existing device
device_list = []
for device in devices: for device in devices:
device_list.append(
{
"mac": device.mac,
"name": device.name,
}
)
if not device.registered: if not device.registered:
# Add to omada # Add to omada
pass add_new_devices_to_omada.delay(new_devices=device_list)
device.registered = True
device.save()
return Response( return Response(
{"message": f"Payment verified successfully using [{method}]."} {"message": f"Payment verified successfully using [{method}]."}
@ -242,12 +244,3 @@ class DeletePaymentView(StaffEditorPermissionMixin, generics.DestroyAPIView):
devices = instance.devices.all() devices = instance.devices.all()
devices.update(is_active=False, expiry_date=None, has_a_pending_payment=False) devices.update(is_active=False, expiry_date=None, has_a_pending_payment=False)
return super().delete(request, *args, **kwargs) return super().delete(request, *args, **kwargs)
def get_existing_devices(OMADA_SITE_ID: str, OMADA_GROUP_ID: str):
# Get existing devices from omada
response = requests.get(
f"{OMADA_PROXY_URL}/{OMADA_GROUP_ID}/api/v2/sites/{OMADA_SITE_ID}/setting/profiles/groups",
)
response.raise_for_status()
return response.json()

View File

@ -5,8 +5,10 @@ from .models import Device
class DeviceFilter(django_filters.FilterSet): class DeviceFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr="icontains") name = django_filters.CharFilter(lookup_expr="icontains")
mac = django_filters.CharFilter(lookup_expr="icontains") mac = django_filters.CharFilter(lookup_expr="icontains")
user = django_filters.CharFilter(field_name='user__last_name', lookup_expr="icontains") user = django_filters.CharFilter(
field_name="user__last_name", lookup_expr="icontains"
)
class Meta: class Meta:
model = Device model = Device
fields = "__all__" fields = "__all__"

View File

@ -0,0 +1,20 @@
# Generated by Django 5.2 on 2025-04-25 08:42
import devices.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("devices", "0005_device_has_a_pending_payment"),
]
operations = [
migrations.AlterField(
model_name="device",
name="mac",
field=models.CharField(
max_length=255, validators=[devices.models.validate_mac_address]
),
),
]

View File

@ -1,11 +1,26 @@
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from api.models import User from api.models import User
import regex
from django.core.exceptions import ValidationError
def validate_mac_address(value):
if not regex.match(r"^([0-9A-Fa-f]{2}-){5}[0-9A-Fa-f]{2}$", value):
raise ValidationError(
"This field accepts a valid MAC address in the format XX-XX-XX-XX-XX-XX using '-' as the separator."
)
return value
class Device(models.Model): class Device(models.Model):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
mac = models.CharField(max_length=255) mac = models.CharField(
max_length=255,
validators=[
validate_mac_address,
],
)
has_a_pending_payment = models.BooleanField(default=False) has_a_pending_payment = models.BooleanField(default=False)
reason_for_blocking = models.CharField(max_length=255, null=True, blank=True) reason_for_blocking = models.CharField(max_length=255, null=True, blank=True)
is_active = models.BooleanField(default=False) is_active = models.BooleanField(default=False)

View File

@ -7,14 +7,12 @@ from billing.models import Payment # Import the Payment model
class CreateDeviceSerializer(serializers.ModelSerializer): class CreateDeviceSerializer(serializers.ModelSerializer):
name = serializers.CharField(required=True) name = serializers.CharField(required=True)
mac = serializers.CharField(required=True) mac = serializers.CharField(required=True)
registered = serializers.BooleanField(required=True)
class Meta: class Meta:
model = Device model = Device
fields = [ fields = [
"name", "name",
"mac", "mac",
"registered",
"blocked_by", "blocked_by",
] ]
depth = 2 depth = 2

View File

@ -52,6 +52,11 @@ class DeviceListCreateAPIView(
return Response( return Response(
{"message": "Device with this mac address already exists."}, status=400 {"message": "Device with this mac address already exists."}, status=400
) )
# Normalize MAC address to use "-" as separators
mac = re.sub(r"[^0-9A-Fa-f]", "-", mac).upper()
request.data["mac"] = mac
return super().create(request, *args, **kwargs) return super().create(request, *args, **kwargs)
def perform_create(self, serializer): def perform_create(self, serializer):

View File

@ -1,12 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
__title__ = 'djangopasswordlessknox' __title__ = "djangopasswordlessknox"
__version__ = '1.4.0' __version__ = "1.4.0"
__author__ = 'Lijo' __author__ = "Lijo"
__license__ = 'MIT' __license__ = "MIT"
__copyright__ = 'Copyright 2019 lijo' __copyright__ = "Copyright 2019 lijo"
# Version synonym # Version synonym
VERSION = __version__ VERSION = __version__
default_app_config = 'djangopasswordlessknox.apps.DrfpasswordlessConfig' default_app_config = "djangopasswordlessknox.apps.DrfpasswordlessConfig"

View File

@ -1,3 +1,3 @@
VERSION = (1, 4, 0) VERSION = (1, 4, 0)
__version__ = '.'.join(map(str, VERSION)) __version__ = ".".join(map(str, VERSION))

View File

@ -7,20 +7,22 @@ class UserLinkMixin(object):
""" """
A mixin to add a linkable list_display user field. A mixin to add a linkable list_display user field.
""" """
LINK_TO_USER_FIELD = 'link_to_user'
LINK_TO_USER_FIELD = "link_to_user"
def link_to_user(self, obj): def link_to_user(self, obj):
link = reverse('admin:users_user_change', args=[obj.user.id]) link = reverse("admin:users_user_change", args=[obj.user.id])
return u'<a href={}>{}</a>'.format(link, obj.user.username) return "<a href={}>{}</a>".format(link, obj.user.username)
link_to_user.allow_tags = True link_to_user.allow_tags = True
link_to_user.short_description = 'User' link_to_user.short_description = "User"
class AbstractCallbackTokenInline(admin.StackedInline): class AbstractCallbackTokenInline(admin.StackedInline):
max_num = 0 max_num = 0
extra = 0 extra = 0
readonly_fields = ('created_at', 'key', 'is_active') readonly_fields = ("created_at", "key", "is_active")
fields = ('created_at', 'user', 'key', 'is_active') fields = ("created_at", "user", "key", "is_active")
class CallbackInline(AbstractCallbackTokenInline): class CallbackInline(AbstractCallbackTokenInline):
@ -28,7 +30,7 @@ class CallbackInline(AbstractCallbackTokenInline):
class AbstractCallbackTokenAdmin(UserLinkMixin, admin.ModelAdmin): class AbstractCallbackTokenAdmin(UserLinkMixin, admin.ModelAdmin):
readonly_fields = ('created_at', 'user', 'key') readonly_fields = ("created_at", "user", "key")
list_display = ('created_at', UserLinkMixin.LINK_TO_USER_FIELD, 'key', 'is_active') list_display = ("created_at", UserLinkMixin.LINK_TO_USER_FIELD, "key", "is_active")
fields = ('created_at', 'user', 'key', 'is_active') fields = ("created_at", "user", "key", "is_active")
extra = 0 extra = 0

View File

@ -1,8 +1,9 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
class DrfpasswordlessConfig(AppConfig): class DrfpasswordlessConfig(AppConfig):
name = 'djangopasswordlessknox' name = "djangopasswordlessknox"
verbose = _("DRF Passwordless") verbose = _("DRF Passwordless")
def ready(self): def ready(self):

View File

@ -10,7 +10,6 @@ import uuid
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
@ -19,25 +18,47 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='CallbackToken', name="CallbackToken",
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), (
('created_at', models.DateTimeField(auto_now_add=True)), "id",
('is_active', models.BooleanField(default=True)), models.UUIDField(
('to_alias', models.CharField(blank=True, max_length=40)), default=uuid.uuid4,
('to_alias_type', models.CharField(blank=True, max_length=20)), editable=False,
('key', models.CharField(default=djangopasswordlessknox.models.generate_numeric_token, max_length=6, unique=True)), primary_key=True,
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), serialize=False,
unique=True,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("is_active", models.BooleanField(default=True)),
("to_alias", models.CharField(blank=True, max_length=40)),
("to_alias_type", models.CharField(blank=True, max_length=20)),
(
"key",
models.CharField(
default=djangopasswordlessknox.models.generate_numeric_token,
max_length=6,
unique=True,
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
], ],
options={ options={
'verbose_name': 'Callback Token', "verbose_name": "Callback Token",
'abstract': False, "abstract": False,
'ordering': ['-id'], "ordering": ["-id"],
'get_latest_by': 'created_at', "get_latest_by": "created_at",
}, },
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='callbacktoken', name="callbacktoken",
unique_together=set([('key', 'is_active')]), unique_together=set([("key", "is_active")]),
), ),
] ]

View File

@ -33,9 +33,14 @@ class AbstractBaseCallbackToken(models.Model):
When a new token is created, older ones of the same type are invalidated When a new token is created, older ones of the same type are invalidated
via the pre_save signal in signals.py. via the pre_save signal in signals.py.
""" """
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
id = models.UUIDField(
primary_key=True, default=uuid.uuid4, editable=False, unique=True
)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name=None, on_delete=models.CASCADE) user = models.ForeignKey(
settings.AUTH_USER_MODEL, related_name=None, on_delete=models.CASCADE
)
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
to_alias = models.CharField(blank=True, max_length=40) to_alias = models.CharField(blank=True, max_length=40)
to_alias_type = models.CharField(blank=True, max_length=20) to_alias_type = models.CharField(blank=True, max_length=20)
@ -44,9 +49,9 @@ class AbstractBaseCallbackToken(models.Model):
class Meta: class Meta:
abstract = True abstract = True
get_latest_by = 'created_at' get_latest_by = "created_at"
ordering = ['-id'] ordering = ["-id"]
unique_together = (('key', 'is_active'),) unique_together = (("key", "is_active"),)
def __str__(self): def __str__(self):
return str(self.key) return str(self.key)
@ -56,7 +61,8 @@ class CallbackToken(AbstractBaseCallbackToken):
""" """
Generates a random six digit number to be returned. Generates a random six digit number to be returned.
""" """
key = models.CharField(default=generate_numeric_token, max_length=6, unique=True) key = models.CharField(default=generate_numeric_token, max_length=6, unique=True)
class Meta(AbstractBaseCallbackToken.Meta): class Meta(AbstractBaseCallbackToken.Meta):
verbose_name = 'Callback Token' verbose_name = "Callback Token"

View File

@ -6,7 +6,11 @@ from django.core.validators import RegexValidator
from rest_framework import serializers from rest_framework import serializers
from djangopasswordlessknox.models import CallbackToken from djangopasswordlessknox.models import CallbackToken
from djangopasswordlessknox.settings import api_settings from djangopasswordlessknox.settings import api_settings
from djangopasswordlessknox.utils import authenticate_by_token, verify_user_alias, validate_token_age from djangopasswordlessknox.utils import (
authenticate_by_token,
verify_user_alias,
validate_token_age,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
User = get_user_model() User = get_user_model()
@ -14,11 +18,11 @@ User = get_user_model()
class TokenField(serializers.CharField): class TokenField(serializers.CharField):
default_error_messages = { default_error_messages = {
'required': _('Invalid Token'), "required": _("Invalid Token"),
'invalid': _('Invalid Token'), "invalid": _("Invalid Token"),
'blank': _('Invalid Token'), "blank": _("Invalid Token"),
'max_length': _('Tokens are {max_length} digits long.'), "max_length": _("Tokens are {max_length} digits long."),
'min_length': _('Tokens are {min_length} digits long.') "min_length": _("Tokens are {min_length} digits long."),
} }
@ -48,10 +52,10 @@ class AbstractBaseAliasAuthenticationSerializer(serializers.Serializer):
except User.DoesNotExist: except User.DoesNotExist:
# If no user is found, raise an error # If no user is found, raise an error
msg = "" msg = ""
if self.alias_type == 'email': if self.alias_type == "email":
msg = _('No user found with this email.') msg = _("No user found with this email.")
elif self.alias_type == 'mobile': elif self.alias_type == "mobile":
msg = _('No user found with this mobile number.') msg = _("No user found with this mobile number.")
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
else: else:
# If new aliases should not register new users. # If new aliases should not register new users.
@ -63,23 +67,23 @@ class AbstractBaseAliasAuthenticationSerializer(serializers.Serializer):
if user: if user:
if not user.is_active: if not user.is_active:
# If valid, return attrs so we can create a token in our logic controller # If valid, return attrs so we can create a token in our logic controller
msg = _('User account is disabled.') msg = _("User account is disabled.")
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
else: else:
msg = _('No account is associated with this alias.') msg = _("No account is associated with this alias.")
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
else: else:
msg = _('Missing %s.') % self.alias_type msg = _("Missing %s.") % self.alias_type
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
attrs['user'] = user attrs["user"] = user
return attrs return attrs
class EmailAuthSerializer(AbstractBaseAliasAuthenticationSerializer): class EmailAuthSerializer(AbstractBaseAliasAuthenticationSerializer):
@property @property
def alias_type(self): def alias_type(self):
return 'email' return "email"
email = serializers.EmailField() email = serializers.EmailField()
@ -87,11 +91,13 @@ class EmailAuthSerializer(AbstractBaseAliasAuthenticationSerializer):
class MobileAuthSerializer(AbstractBaseAliasAuthenticationSerializer): class MobileAuthSerializer(AbstractBaseAliasAuthenticationSerializer):
@property @property
def alias_type(self): def alias_type(self):
return 'mobile' return "mobile"
phone_regex = RegexValidator(regex=r'^[7|9][0-9]{6}$', phone_regex = RegexValidator(
message="Mobile number must be entered in the format:" regex=r"^[7|9][0-9]{6}$",
" '7xxxxxx' or '9xxxxxx'.") message="Mobile number must be entered in the format:"
" '7xxxxxx' or '9xxxxxx'.",
)
mobile = serializers.CharField(validators=[phone_regex], max_length=15) mobile = serializers.CharField(validators=[phone_regex], max_length=15)
@ -105,14 +111,14 @@ class AbstractBaseAliasVerificationSerializer(serializers.Serializer):
Abstract class that returns a callback token based on the field given Abstract class that returns a callback token based on the field given
Returns a token if valid, None or a message if not. Returns a token if valid, None or a message if not.
""" """
@property @property
def alias_type(self): def alias_type(self):
# The alias type, either email or mobile # The alias type, either email or mobile
raise NotImplementedError raise NotImplementedError
def validate(self, attrs): def validate(self, attrs):
msg = _("There was a problem with your request.")
msg = _('There was a problem with your request.')
if self.alias_type: if self.alias_type:
# Get request.user # Get request.user
@ -125,31 +131,31 @@ class AbstractBaseAliasVerificationSerializer(serializers.Serializer):
if user: if user:
if not user.is_active: if not user.is_active:
# If valid, return attrs so we can create a token in our logic controller # If valid, return attrs so we can create a token in our logic controller
msg = _('User account is disabled.') msg = _("User account is disabled.")
else: else:
if hasattr(user, self.alias_type): if hasattr(user, self.alias_type):
# Has the appropriate alias type # Has the appropriate alias type
attrs['user'] = user attrs["user"] = user
return attrs return attrs
else: else:
msg = _('This user doesn\'t have an %s.' % self.alias_type) msg = _("This user doesn't have an %s." % self.alias_type)
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
else: else:
msg = _('Missing %s.') % self.alias_type msg = _("Missing %s.") % self.alias_type
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
class EmailVerificationSerializer(AbstractBaseAliasVerificationSerializer): class EmailVerificationSerializer(AbstractBaseAliasVerificationSerializer):
@property @property
def alias_type(self): def alias_type(self):
return 'email' return "email"
class MobileVerificationSerializer(AbstractBaseAliasVerificationSerializer): class MobileVerificationSerializer(AbstractBaseAliasVerificationSerializer):
@property @property
def alias_type(self): def alias_type(self):
return 'mobile' return "mobile"
""" """
@ -173,13 +179,13 @@ class AbstractBaseCallbackTokenSerializer(serializers.Serializer):
Abstract class inspired by DRF's own token serializer. Abstract class inspired by DRF's own token serializer.
Returns a user if valid, None or a message if not. Returns a user if valid, None or a message if not.
""" """
token = TokenField(min_length=6, max_length=6, validators=[token_age_validator]) token = TokenField(min_length=6, max_length=6, validators=[token_age_validator])
class CallbackTokenAuthSerializer(AbstractBaseCallbackTokenSerializer): class CallbackTokenAuthSerializer(AbstractBaseCallbackTokenSerializer):
def validate(self, attrs): def validate(self, attrs):
callback_token = attrs.get('token', None) callback_token = attrs.get("token", None)
token = CallbackToken.objects.get(key=callback_token, is_active=True) token = CallbackToken.objects.get(key=callback_token, is_active=True)
@ -189,27 +195,29 @@ class CallbackTokenAuthSerializer(AbstractBaseCallbackTokenSerializer):
user = authenticate_by_token(token) user = authenticate_by_token(token)
if user: if user:
if not user.is_active: if not user.is_active:
msg = _('User account is disabled.') msg = _("User account is disabled.")
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
if api_settings.PASSWORDLESS_USER_MARK_EMAIL_VERIFIED \ if (
or api_settings.PASSWORDLESS_USER_MARK_MOBILE_VERIFIED: api_settings.PASSWORDLESS_USER_MARK_EMAIL_VERIFIED
or api_settings.PASSWORDLESS_USER_MARK_MOBILE_VERIFIED
):
# Mark this alias as verified # Mark this alias as verified
user = User.objects.get(pk=token.user.pk) user = User.objects.get(pk=token.user.pk)
success = verify_user_alias(user, token) success = verify_user_alias(user, token)
if success is False: if success is False:
msg = _('Error validating user alias.') msg = _("Error validating user alias.")
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
attrs['user'] = user attrs["user"] = user
return attrs return attrs
else: else:
msg = _('Invalid Token') msg = _("Invalid Token")
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
else: else:
msg = _('Missing authentication token.') msg = _("Missing authentication token.")
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
@ -222,7 +230,7 @@ class CallbackTokenVerificationSerializer(AbstractBaseCallbackTokenSerializer):
def validate(self, attrs): def validate(self, attrs):
try: try:
user_id = self.context.get("user_id") user_id = self.context.get("user_id")
callback_token = attrs.get('token', None) callback_token = attrs.get("token", None)
token = CallbackToken.objects.get(key=callback_token, is_active=True) token = CallbackToken.objects.get(key=callback_token, is_active=True)
user = User.objects.get(pk=user_id) user = User.objects.get(pk=user_id)
@ -235,23 +243,31 @@ class CallbackTokenVerificationSerializer(AbstractBaseCallbackTokenSerializer):
if success is False: if success is False:
logger.debug("djangopasswordlessknox: Error verifying alias.") logger.debug("djangopasswordlessknox: Error verifying alias.")
attrs['user'] = user attrs["user"] = user
return attrs return attrs
else: else:
msg = _('This token is invalid. Try again later.') msg = _("This token is invalid. Try again later.")
logger.debug("djangopasswordlessknox: User token mismatch when verifying alias.") logger.debug(
"djangopasswordlessknox: User token mismatch when verifying alias."
)
except CallbackToken.DoesNotExist: except CallbackToken.DoesNotExist:
msg = _('Missing authentication token.') msg = _("Missing authentication token.")
logger.debug("djangopasswordlessknox: Tried to validate alias with bad token.") logger.debug(
"djangopasswordlessknox: Tried to validate alias with bad token."
)
pass pass
except User.DoesNotExist: except User.DoesNotExist:
msg = _('Missing user.') msg = _("Missing user.")
logger.debug("djangopasswordlessknox: Tried to validate alias with bad user.") logger.debug(
"djangopasswordlessknox: Tried to validate alias with bad user."
)
pass pass
except PermissionDenied: except PermissionDenied:
msg = _('Insufficient permissions.') msg = _("Insufficient permissions.")
logger.debug("djangopasswordlessknox: Permission denied while validating alias.") logger.debug(
"djangopasswordlessknox: Permission denied while validating alias."
)
pass pass
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)

View File

@ -1,23 +1,23 @@
from djangopasswordlessknox.utils import ( from djangopasswordlessknox.utils import (
create_callback_token_for_user, create_callback_token_for_user,
send_email_with_callback_token, send_email_with_callback_token,
send_sms_with_callback_token send_sms_with_callback_token,
) )
class TokenService(object): class TokenService(object):
@staticmethod @staticmethod
def send_token(user, alias_type, **message_payload): def send_token(user, alias_type, **message_payload):
token = create_callback_token_for_user(user, alias_type) token = create_callback_token_for_user(user, alias_type)
send_action = None send_action = None
if alias_type == 'email': if alias_type == "email":
send_action = send_email_with_callback_token send_action = send_email_with_callback_token
elif alias_type == 'mobile': elif alias_type == "mobile":
send_action = send_sms_with_callback_token send_action = send_sms_with_callback_token
if send_action is None: if send_action is None:
raise ValueError(f"Invalid alias_type: {alias_type}") raise ValueError(f"Invalid alias_type: {alias_type}")
# Send to alias # Send to alias
success = send_action(user, token, **message_payload) success = send_action(user, token, **message_payload)
return success return success

View File

@ -17,7 +17,11 @@ def invalidate_previous_tokens(sender, instance, **kwargs):
""" """
active_tokens = None active_tokens = None
if isinstance(instance, CallbackToken): if isinstance(instance, CallbackToken):
active_tokens = CallbackToken.objects.active().filter(user=instance.user).exclude(id=instance.id) active_tokens = (
CallbackToken.objects.active()
.filter(user=instance.user)
.exclude(id=instance.id)
)
# Invalidate tokens # Invalidate tokens
if active_tokens: if active_tokens:
@ -46,15 +50,15 @@ def update_alias_verification(sender, instance, **kwargs):
Optionally sends a verification token to the new endpoint. Optionally sends a verification token to the new endpoint.
""" """
if isinstance(instance, User): if isinstance(instance, User):
if instance.id: if instance.id:
if api_settings.PASSWORDLESS_USER_MARK_EMAIL_VERIFIED is True: if api_settings.PASSWORDLESS_USER_MARK_EMAIL_VERIFIED is True:
""" """
For marking email aliases as not verified when a user changes it. For marking email aliases as not verified when a user changes it.
""" """
email_field = api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME email_field = api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME
email_verified_field = api_settings.PASSWORDLESS_USER_EMAIL_VERIFIED_FIELD_NAME email_verified_field = (
api_settings.PASSWORDLESS_USER_EMAIL_VERIFIED_FIELD_NAME
)
# Verify that this is an existing instance and not a new one. # Verify that this is an existing instance and not a new one.
try: try:
@ -62,24 +66,41 @@ def update_alias_verification(sender, instance, **kwargs):
instance_email = getattr(instance, email_field) # Incoming Email instance_email = getattr(instance, email_field) # Incoming Email
old_email = getattr(user_old, email_field) # Pre-save object email old_email = getattr(user_old, email_field) # Pre-save object email
if instance_email != old_email and instance_email != "" and instance_email is not None: if (
instance_email != old_email
and instance_email != ""
and instance_email is not None
):
# Email changed, verification should be flagged # Email changed, verification should be flagged
setattr(instance, email_verified_field, False) setattr(instance, email_verified_field, False)
if api_settings.PASSWORDLESS_AUTO_SEND_VERIFICATION_TOKEN is True: if (
email_subject = api_settings.PASSWORDLESS_EMAIL_VERIFICATION_SUBJECT api_settings.PASSWORDLESS_AUTO_SEND_VERIFICATION_TOKEN
is True
):
email_subject = (
api_settings.PASSWORDLESS_EMAIL_VERIFICATION_SUBJECT
)
email_plaintext = api_settings.PASSWORDLESS_EMAIL_VERIFICATION_PLAINTEXT_MESSAGE email_plaintext = api_settings.PASSWORDLESS_EMAIL_VERIFICATION_PLAINTEXT_MESSAGE
email_html = api_settings.PASSWORDLESS_EMAIL_VERIFICATION_TOKEN_HTML_TEMPLATE_NAME email_html = api_settings.PASSWORDLESS_EMAIL_VERIFICATION_TOKEN_HTML_TEMPLATE_NAME
message_payload = {'email_subject': email_subject, message_payload = {
'email_plaintext': email_plaintext, "email_subject": email_subject,
'email_html': email_html} "email_plaintext": email_plaintext,
success = TokenService.send_token(instance, 'email', **message_payload) "email_html": email_html,
}
success = TokenService.send_token(
instance, "email", **message_payload
)
if success: if success:
logger.info('djangopasswordlessknox: Successfully sent email on updated address: %s' logger.info(
% instance_email) "djangopasswordlessknox: Successfully sent email on updated address: %s"
% instance_email
)
else: else:
logger.info('djangopasswordlessknox: Failed to send email to updated address: %s' logger.info(
% instance_email) "djangopasswordlessknox: Failed to send email to updated address: %s"
% instance_email
)
except User.DoesNotExist: except User.DoesNotExist:
# User probably is just initially being created # User probably is just initially being created
@ -90,28 +111,45 @@ def update_alias_verification(sender, instance, **kwargs):
For marking mobile aliases as not verified when a user changes it. For marking mobile aliases as not verified when a user changes it.
""" """
mobile_field = api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME mobile_field = api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME
mobile_verified_field = api_settings.PASSWORDLESS_USER_MOBILE_VERIFIED_FIELD_NAME mobile_verified_field = (
api_settings.PASSWORDLESS_USER_MOBILE_VERIFIED_FIELD_NAME
)
# Verify that this is an existing instance and not a new one. # Verify that this is an existing instance and not a new one.
try: try:
user_old = User.objects.get(id=instance.id) # Pre-save object user_old = User.objects.get(id=instance.id) # Pre-save object
instance_mobile = getattr(instance, mobile_field) # Incoming mobile instance_mobile = getattr(instance, mobile_field) # Incoming mobile
old_mobile = getattr(user_old, mobile_field) # Pre-save object mobile old_mobile = getattr(
user_old, mobile_field
) # Pre-save object mobile
if instance_mobile != old_mobile and instance_mobile != "" and instance_mobile is not None: if (
instance_mobile != old_mobile
and instance_mobile != ""
and instance_mobile is not None
):
# Mobile changed, verification should be flagged # Mobile changed, verification should be flagged
setattr(instance, mobile_verified_field, False) setattr(instance, mobile_verified_field, False)
if api_settings.PASSWORDLESS_AUTO_SEND_VERIFICATION_TOKEN is True: if (
api_settings.PASSWORDLESS_AUTO_SEND_VERIFICATION_TOKEN
is True
):
mobile_message = api_settings.PASSWORDLESS_MOBILE_MESSAGE mobile_message = api_settings.PASSWORDLESS_MOBILE_MESSAGE
message_payload = {'mobile_message': mobile_message} message_payload = {"mobile_message": mobile_message}
success = TokenService.send_token(instance, 'mobile', **message_payload) success = TokenService.send_token(
instance, "mobile", **message_payload
)
if success: if success:
logger.info('djangopasswordlessknox: Successfully sent SMS on updated mobile: %s' logger.info(
% instance_mobile) "djangopasswordlessknox: Successfully sent SMS on updated mobile: %s"
% instance_mobile
)
else: else:
logger.info('djangopasswordlessknox: Failed to send SMS to updated mobile: %s' logger.info(
% instance_mobile) "djangopasswordlessknox: Failed to send SMS to updated mobile: %s"
% instance_mobile
)
except User.DoesNotExist: except User.DoesNotExist:
# User probably is just initially being created # User probably is just initially being created

View File

@ -1,18 +1,34 @@
from django.urls import path from django.urls import path
from djangopasswordlessknox.views import ( from djangopasswordlessknox.views import (
ObtainEmailCallbackToken, ObtainEmailCallbackToken,
ObtainMobileCallbackToken, ObtainMobileCallbackToken,
ObtainAuthTokenFromCallbackToken, ObtainAuthTokenFromCallbackToken,
VerifyAliasFromCallbackToken, VerifyAliasFromCallbackToken,
ObtainEmailVerificationCallbackToken, ObtainEmailVerificationCallbackToken,
ObtainMobileVerificationCallbackToken, ObtainMobileVerificationCallbackToken,
) )
urlpatterns = [ urlpatterns = [
path('callback/auth/', ObtainAuthTokenFromCallbackToken.as_view(), name='auth_callback'), path(
path('auth/email/', ObtainEmailCallbackToken.as_view(), name='auth_email'), "callback/auth/",
path('auth/mobile/', ObtainMobileCallbackToken.as_view(), name='auth_mobile'), ObtainAuthTokenFromCallbackToken.as_view(),
path('callback/verify/', VerifyAliasFromCallbackToken.as_view(), name='verify_callback'), name="auth_callback",
path('verify/email/', ObtainEmailVerificationCallbackToken.as_view(), name='verify_email'), ),
path('verify/mobile/', ObtainMobileVerificationCallbackToken.as_view(), name='verify_mobile'), path("auth/email/", ObtainEmailCallbackToken.as_view(), name="auth_email"),
path("auth/mobile/", ObtainMobileCallbackToken.as_view(), name="auth_mobile"),
path(
"callback/verify/",
VerifyAliasFromCallbackToken.as_view(),
name="verify_callback",
),
path(
"verify/email/",
ObtainEmailVerificationCallbackToken.as_view(),
name="verify_email",
),
path(
"verify/mobile/",
ObtainMobileVerificationCallbackToken.as_view(),
name="verify_mobile",
),
] ]

View File

@ -2,7 +2,7 @@ import logging
from rest_framework import parsers, renderers, status from rest_framework import parsers, renderers, status
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework import generics from rest_framework import generics
from djangopasswordlessknox.settings import api_settings from djangopasswordlessknox.settings import api_settings
@ -29,6 +29,7 @@ class AbstractBaseObtainCallbackToken(APIView):
""" """
This returns a 6-digit callback token we can trade for a user's Auth Token. This returns a 6-digit callback token we can trade for a user's Auth Token.
""" """
success_response = "A login token has been sent to you." success_response = "A login token has been sent to you."
failure_response = "Unable to send you a login code. Try again later." failure_response = "Unable to send you a login code. Try again later."
@ -49,12 +50,16 @@ class AbstractBaseObtainCallbackToken(APIView):
# Only allow auth types allowed in settings. # Only allow auth types allowed in settings.
return Response(status=status.HTTP_404_NOT_FOUND) return Response(status=status.HTTP_404_NOT_FOUND)
serializer = self.serializer_class(data=request.data, context={'request': request}) serializer = self.serializer_class(
data=request.data, context={"request": request}
)
if serializer.is_valid(raise_exception=True): if serializer.is_valid(raise_exception=True):
# Validate - # Validate -
user = serializer.validated_data['user'] user = serializer.validated_data["user"]
# Create and send callback token # Create and send callback token
success = TokenService.send_token(user, self.alias_type, **self.message_payload) success = TokenService.send_token(
user, self.alias_type, **self.message_payload
)
# Respond With Success Or Failure of Sent # Respond With Success Or Failure of Sent
if success: if success:
@ -63,67 +68,79 @@ class AbstractBaseObtainCallbackToken(APIView):
else: else:
status_code = status.HTTP_400_BAD_REQUEST status_code = status.HTTP_400_BAD_REQUEST
response_detail = self.failure_response response_detail = self.failure_response
return Response({'detail': response_detail}, status=status_code) return Response({"detail": response_detail}, status=status_code)
else: else:
return Response(serializer.error_messages, status=status.HTTP_400_BAD_REQUEST) return Response(
serializer.error_messages, status=status.HTTP_400_BAD_REQUEST
)
class ObtainEmailCallbackToken(AbstractBaseObtainCallbackToken, generics.GenericAPIView): class ObtainEmailCallbackToken(
AbstractBaseObtainCallbackToken, generics.GenericAPIView
):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
serializer_class = EmailAuthSerializer serializer_class = EmailAuthSerializer
success_response = "A login token has been sent to your email." success_response = "A login token has been sent to your email."
failure_response = "Unable to email you a login code. Try again later." failure_response = "Unable to email you a login code. Try again later."
alias_type = 'email' alias_type = "email"
email_subject = api_settings.PASSWORDLESS_EMAIL_SUBJECT email_subject = api_settings.PASSWORDLESS_EMAIL_SUBJECT
email_plaintext = api_settings.PASSWORDLESS_EMAIL_PLAINTEXT_MESSAGE email_plaintext = api_settings.PASSWORDLESS_EMAIL_PLAINTEXT_MESSAGE
email_html = api_settings.PASSWORDLESS_EMAIL_TOKEN_HTML_TEMPLATE_NAME email_html = api_settings.PASSWORDLESS_EMAIL_TOKEN_HTML_TEMPLATE_NAME
message_payload = {'email_subject': email_subject, message_payload = {
'email_plaintext': email_plaintext, "email_subject": email_subject,
'email_html': email_html} "email_plaintext": email_plaintext,
"email_html": email_html,
}
class ObtainMobileCallbackToken(AbstractBaseObtainCallbackToken, generics.GenericAPIView): class ObtainMobileCallbackToken(
AbstractBaseObtainCallbackToken, generics.GenericAPIView
):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
serializer_class = MobileAuthSerializer serializer_class = MobileAuthSerializer
success_response = "We texted you a login code." success_response = "We texted you a login code."
failure_response = "Unable to send you a login code. Try again later." failure_response = "Unable to send you a login code. Try again later."
alias_type = 'mobile' alias_type = "mobile"
mobile_message = api_settings.PASSWORDLESS_MOBILE_MESSAGE mobile_message = api_settings.PASSWORDLESS_MOBILE_MESSAGE
message_payload = {'mobile_message': mobile_message} message_payload = {"mobile_message": mobile_message}
class ObtainEmailVerificationCallbackToken(AbstractBaseObtainCallbackToken, generics.GenericAPIView): class ObtainEmailVerificationCallbackToken(
AbstractBaseObtainCallbackToken, generics.GenericAPIView
):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
serializer_class = EmailVerificationSerializer serializer_class = EmailVerificationSerializer
success_response = "A verification token has been sent to your email." success_response = "A verification token has been sent to your email."
failure_response = "Unable to email you a verification code. Try again later." failure_response = "Unable to email you a verification code. Try again later."
alias_type = 'email' alias_type = "email"
email_subject = api_settings.PASSWORDLESS_EMAIL_VERIFICATION_SUBJECT email_subject = api_settings.PASSWORDLESS_EMAIL_VERIFICATION_SUBJECT
email_plaintext = api_settings.PASSWORDLESS_EMAIL_VERIFICATION_PLAINTEXT_MESSAGE email_plaintext = api_settings.PASSWORDLESS_EMAIL_VERIFICATION_PLAINTEXT_MESSAGE
email_html = api_settings.PASSWORDLESS_EMAIL_VERIFICATION_TOKEN_HTML_TEMPLATE_NAME email_html = api_settings.PASSWORDLESS_EMAIL_VERIFICATION_TOKEN_HTML_TEMPLATE_NAME
message_payload = { message_payload = {
'email_subject': email_subject, "email_subject": email_subject,
'email_plaintext': email_plaintext, "email_plaintext": email_plaintext,
'email_html': email_html "email_html": email_html,
} }
class ObtainMobileVerificationCallbackToken(AbstractBaseObtainCallbackToken, generics.GenericAPIView): class ObtainMobileVerificationCallbackToken(
AbstractBaseObtainCallbackToken, generics.GenericAPIView
):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
serializer_class = MobileVerificationSerializer serializer_class = MobileVerificationSerializer
success_response = "We texted you a verification code." success_response = "We texted you a verification code."
failure_response = "Unable to send you a verification code. Try again later." failure_response = "Unable to send you a verification code. Try again later."
alias_type = 'mobile' alias_type = "mobile"
mobile_message = api_settings.PASSWORDLESS_MOBILE_MESSAGE mobile_message = api_settings.PASSWORDLESS_MOBILE_MESSAGE
message_payload = {'mobile_message': mobile_message} message_payload = {"mobile_message": mobile_message}
class AbstractBaseObtainAuthToken(APIView): class AbstractBaseObtainAuthToken(APIView):
@ -131,10 +148,11 @@ class AbstractBaseObtainAuthToken(APIView):
This is a duplicate of rest_framework's own ObtainAuthToken method. 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. Instead, this returns an Auth Token based on our 6 digit callback token and source.
""" """
serializer_class = None serializer_class = None
def get_context(self): def get_context(self):
return {'request': self.request, 'format': self.format_kwarg, 'view': self} return {"request": self.request, "format": self.format_kwarg, "view": self}
def get_token_ttl(self): def get_token_ttl(self):
return knox_settings.TOKEN_TTL return knox_settings.TOKEN_TTL
@ -155,43 +173,41 @@ class AbstractBaseObtainAuthToken(APIView):
def get_post_response_data(self, user, token, instance): def get_post_response_data(self, user, token, instance):
UserSerializer = self.get_user_serializer_class() UserSerializer = self.get_user_serializer_class()
data = { data = {"expiry": self.format_expiry_datetime(instance.expiry), "token": token}
'expiry': self.format_expiry_datetime(instance.expiry),
'token': token
}
if UserSerializer is not None: if UserSerializer is not None:
data["user"] = UserSerializer( data["user"] = UserSerializer(user, context=self.get_context()).data
user,
context=self.get_context()
).data
return data return data
def post(self, request, format=None): def post(self, request, format=None):
token_limit_per_user = self.get_token_limit_per_user() token_limit_per_user = self.get_token_limit_per_user()
serializer = self.serializer_class(data=request.data) serializer = self.serializer_class(data=request.data)
if serializer.is_valid(raise_exception=True): if serializer.is_valid(raise_exception=True):
user = serializer.validated_data['user'] user = serializer.validated_data["user"]
if token_limit_per_user is not None: if token_limit_per_user is not None:
now = timezone.now() now = timezone.now()
token = user.auth_token_set.filter(expiry__gt=now) token = user.auth_token_set.filter(expiry__gt=now)
if token.count() >= token_limit_per_user: if token.count() >= token_limit_per_user:
return Response( return Response(
{"error": "Maximum amount of tokens allowed per user exceeded."}, {
status=status.HTTP_403_FORBIDDEN "error": "Maximum amount of tokens allowed per user exceeded."
},
status=status.HTTP_403_FORBIDDEN,
) )
token_ttl = self.get_token_ttl() token_ttl = self.get_token_ttl()
instance, token = AuthToken.objects.create(user, token_ttl) instance, token = AuthToken.objects.create(user, token_ttl)
user_logged_in.send(sender=user.__class__, user_logged_in.send(sender=user.__class__, request=request, user=user)
request=request, user=user)
data = self.get_post_response_data(user, token, instance) data = self.get_post_response_data(user, token, instance)
return Response(data) return Response(data)
class ObtainAuthTokenFromCallbackToken(AbstractBaseObtainAuthToken, generics.GenericAPIView): class ObtainAuthTokenFromCallbackToken(
AbstractBaseObtainAuthToken, generics.GenericAPIView
):
""" """
This is a duplicate of rest_framework's own ObtainAuthToken method. This is a duplicate of rest_framework's own ObtainAuthToken method.
Instead, this returns an Auth Token based on our callback token and source. Instead, this returns an Auth Token based on our callback token and source.
""" """
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
serializer_class = CallbackTokenAuthSerializer serializer_class = CallbackTokenAuthSerializer
@ -201,13 +217,23 @@ class VerifyAliasFromCallbackToken(APIView):
This verifies an alias on correct callback token entry using the same logic as auth. This verifies an alias on correct callback token entry using the same logic as auth.
Should be refactored at some point. Should be refactored at some point.
""" """
serializer_class = CallbackTokenVerificationSerializer serializer_class = CallbackTokenVerificationSerializer
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data, context={'user_id': self.request.user.id}) serializer = self.serializer_class(
data=request.data, context={"user_id": self.request.user.id}
)
if serializer.is_valid(raise_exception=True): if serializer.is_valid(raise_exception=True):
return Response({'detail': 'Alias verified.'}, status=status.HTTP_200_OK) return Response({"detail": "Alias verified."}, status=status.HTTP_200_OK)
else: else:
logger.error("Couldn't verify unknown user. Errors on serializer: {}".format(serializer.error_messages)) logger.error(
"Couldn't verify unknown user. Errors on serializer: {}".format(
serializer.error_messages
)
)
return Response({'detail': 'We couldn\'t verify this alias. Try again later.'}, status.HTTP_400_BAD_REQUEST) return Response(
{"detail": "We couldn't verify this alias. Try again later."},
status.HTTP_400_BAD_REQUEST,
)

View File

@ -6,6 +6,7 @@
"typeCheckingMode": "standard", "typeCheckingMode": "standard",
"reportArgumentType": "warning", "reportArgumentType": "warning",
"reportUnusedVariable": "warning", "reportUnusedVariable": "warning",
"reportFunctionMemberAccess": "none",
"exclude": [ "exclude": [
"council-api/**/migrations", "council-api/**/migrations",
"**/__pycache__", "**/__pycache__",