mirror of
https://github.com/i701/sarlink-portal-api.git
synced 2025-04-30 13:55:41 +00:00
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
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:
parent
0f19f0c15c
commit
83db42cc60
@ -47,3 +47,8 @@ curl -X POST http://localhost:4000/api/auth/login/ \
|
||||
```
|
||||
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
|
||||
```
|
@ -27,6 +27,8 @@ class IsStaffEditorPermission(permissions.DjangoModelPermissions):
|
||||
|
||||
# Check permissions based on the 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)
|
||||
|
@ -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:
|
||||
logger.debug("Failed to send SMS. Missing SMS_API_URL or SMS_API_KEY.")
|
||||
return False
|
||||
|
96
api/tasks.py
96
api/tasks.py
@ -14,6 +14,27 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
env.read_env(os.path.join(BASE_DIR, ".env"))
|
||||
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
|
||||
@ -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):
|
||||
"""
|
||||
Verify the user with the Person API.
|
||||
|
@ -18,7 +18,7 @@ def reverse_dhivehi_string(input_str):
|
||||
i = 0
|
||||
while i < len(reversed_str):
|
||||
# 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
|
||||
corrected_chars.append(reversed_str[i + 1] + reversed_str[i])
|
||||
i += 2
|
||||
|
13
api/views.py
13
api/views.py
@ -1,4 +1,5 @@
|
||||
# django imports
|
||||
import pprint
|
||||
from django.contrib.auth import login
|
||||
|
||||
# rest_framework imports
|
||||
@ -31,7 +32,8 @@ from typing import cast, Dict, Any
|
||||
from django.core.mail import send_mail
|
||||
from django.db.models import Q
|
||||
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
|
||||
from .serializers import (
|
||||
@ -60,7 +62,8 @@ class ErrorMessages:
|
||||
@api_view(["GET"])
|
||||
def healthcheck(request):
|
||||
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)
|
||||
|
||||
|
||||
@ -117,7 +120,7 @@ class CreateTemporaryUserView(generics.CreateAPIView):
|
||||
|
||||
current_date = timezone.now()
|
||||
try:
|
||||
dob = timezone.datetime.strptime(dob, "%Y-%m-%d").date()
|
||||
dob = timezone.datetime.strptime(str(dob), "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
return Response(
|
||||
{"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")
|
||||
otp = temp_user.generate_otp()
|
||||
send_otp(
|
||||
temp_user.t_mobile,
|
||||
str(temp_user.t_mobile),
|
||||
otp,
|
||||
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(
|
||||
first_name=temp_user.t_first_name,
|
||||
last_name=temp_user.t_last_name,
|
||||
username=temp_user.t_username,
|
||||
username=str(temp_user.t_username),
|
||||
password="",
|
||||
address=temp_user.t_address,
|
||||
mobile=temp_user.t_mobile,
|
||||
|
@ -1,32 +1,25 @@
|
||||
# Create your views here.
|
||||
# billing/views.py
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
import requests
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.timezone import localtime
|
||||
from rest_framework import generics, status
|
||||
from rest_framework.response import Response
|
||||
|
||||
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 .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"))
|
||||
|
||||
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:
|
||||
raise ValueError(
|
||||
"PAYMENT_BASE_URL is not set. Please set it in your environment variables."
|
||||
@ -180,10 +173,19 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||
registered=True,
|
||||
)
|
||||
# Need to add to omada if its a new device and not an existing device
|
||||
device_list = []
|
||||
for device in devices:
|
||||
device_list.append(
|
||||
{
|
||||
"mac": device.mac,
|
||||
"name": device.name,
|
||||
}
|
||||
)
|
||||
if not device.registered:
|
||||
# Add to omada
|
||||
pass
|
||||
add_new_devices_to_omada.delay(new_devices=device_list)
|
||||
device.registered = True
|
||||
device.save()
|
||||
|
||||
return Response(
|
||||
{"message": f"Payment verified successfully using [{method}]."}
|
||||
@ -242,12 +244,3 @@ class DeletePaymentView(StaffEditorPermissionMixin, generics.DestroyAPIView):
|
||||
devices = instance.devices.all()
|
||||
devices.update(is_active=False, expiry_date=None, has_a_pending_payment=False)
|
||||
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()
|
||||
|
@ -5,7 +5,9 @@ from .models import Device
|
||||
class DeviceFilter(django_filters.FilterSet):
|
||||
name = 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:
|
||||
model = Device
|
||||
|
20
devices/migrations/0006_alter_device_mac.py
Normal file
20
devices/migrations/0006_alter_device_mac.py
Normal 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]
|
||||
),
|
||||
),
|
||||
]
|
@ -1,11 +1,26 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
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):
|
||||
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)
|
||||
reason_for_blocking = models.CharField(max_length=255, null=True, blank=True)
|
||||
is_active = models.BooleanField(default=False)
|
||||
|
@ -7,14 +7,12 @@ from billing.models import Payment # Import the Payment model
|
||||
class CreateDeviceSerializer(serializers.ModelSerializer):
|
||||
name = serializers.CharField(required=True)
|
||||
mac = serializers.CharField(required=True)
|
||||
registered = serializers.BooleanField(required=True)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = [
|
||||
"name",
|
||||
"mac",
|
||||
"registered",
|
||||
"blocked_by",
|
||||
]
|
||||
depth = 2
|
||||
|
@ -52,6 +52,11 @@ class DeviceListCreateAPIView(
|
||||
return Response(
|
||||
{"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)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
|
@ -1,12 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__title__ = 'djangopasswordlessknox'
|
||||
__version__ = '1.4.0'
|
||||
__author__ = 'Lijo'
|
||||
__license__ = 'MIT'
|
||||
__copyright__ = 'Copyright 2019 lijo'
|
||||
__title__ = "djangopasswordlessknox"
|
||||
__version__ = "1.4.0"
|
||||
__author__ = "Lijo"
|
||||
__license__ = "MIT"
|
||||
__copyright__ = "Copyright 2019 lijo"
|
||||
|
||||
# Version synonym
|
||||
VERSION = __version__
|
||||
|
||||
default_app_config = 'djangopasswordlessknox.apps.DrfpasswordlessConfig'
|
||||
default_app_config = "djangopasswordlessknox.apps.DrfpasswordlessConfig"
|
||||
|
@ -1,3 +1,3 @@
|
||||
VERSION = (1, 4, 0)
|
||||
|
||||
__version__ = '.'.join(map(str, VERSION))
|
||||
__version__ = ".".join(map(str, VERSION))
|
||||
|
@ -7,20 +7,22 @@ class UserLinkMixin(object):
|
||||
"""
|
||||
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):
|
||||
link = reverse('admin:users_user_change', args=[obj.user.id])
|
||||
return u'<a href={}>{}</a>'.format(link, obj.user.username)
|
||||
link = reverse("admin:users_user_change", args=[obj.user.id])
|
||||
return "<a href={}>{}</a>".format(link, obj.user.username)
|
||||
|
||||
link_to_user.allow_tags = True
|
||||
link_to_user.short_description = 'User'
|
||||
link_to_user.short_description = "User"
|
||||
|
||||
|
||||
class AbstractCallbackTokenInline(admin.StackedInline):
|
||||
max_num = 0
|
||||
extra = 0
|
||||
readonly_fields = ('created_at', 'key', 'is_active')
|
||||
fields = ('created_at', 'user', 'key', 'is_active')
|
||||
readonly_fields = ("created_at", "key", "is_active")
|
||||
fields = ("created_at", "user", "key", "is_active")
|
||||
|
||||
|
||||
class CallbackInline(AbstractCallbackTokenInline):
|
||||
@ -28,7 +30,7 @@ class CallbackInline(AbstractCallbackTokenInline):
|
||||
|
||||
|
||||
class AbstractCallbackTokenAdmin(UserLinkMixin, admin.ModelAdmin):
|
||||
readonly_fields = ('created_at', 'user', 'key')
|
||||
list_display = ('created_at', UserLinkMixin.LINK_TO_USER_FIELD, 'key', 'is_active')
|
||||
fields = ('created_at', 'user', 'key', 'is_active')
|
||||
readonly_fields = ("created_at", "user", "key")
|
||||
list_display = ("created_at", UserLinkMixin.LINK_TO_USER_FIELD, "key", "is_active")
|
||||
fields = ("created_at", "user", "key", "is_active")
|
||||
extra = 0
|
||||
|
@ -1,8 +1,9 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class DrfpasswordlessConfig(AppConfig):
|
||||
name = 'djangopasswordlessknox'
|
||||
name = "djangopasswordlessknox"
|
||||
verbose = _("DRF Passwordless")
|
||||
|
||||
def ready(self):
|
||||
|
@ -10,7 +10,6 @@ import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
@ -19,25 +18,47 @@ class Migration(migrations.Migration):
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CallbackToken',
|
||||
name="CallbackToken",
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, 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)),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
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={
|
||||
'verbose_name': 'Callback Token',
|
||||
'abstract': False,
|
||||
'ordering': ['-id'],
|
||||
'get_latest_by': 'created_at',
|
||||
"verbose_name": "Callback Token",
|
||||
"abstract": False,
|
||||
"ordering": ["-id"],
|
||||
"get_latest_by": "created_at",
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='callbacktoken',
|
||||
unique_together=set([('key', 'is_active')]),
|
||||
name="callbacktoken",
|
||||
unique_together=set([("key", "is_active")]),
|
||||
),
|
||||
]
|
||||
|
@ -33,9 +33,14 @@ class AbstractBaseCallbackToken(models.Model):
|
||||
When a new token is created, older ones of the same type are invalidated
|
||||
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)
|
||||
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)
|
||||
to_alias = models.CharField(blank=True, max_length=40)
|
||||
to_alias_type = models.CharField(blank=True, max_length=20)
|
||||
@ -44,9 +49,9 @@ class AbstractBaseCallbackToken(models.Model):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
get_latest_by = 'created_at'
|
||||
ordering = ['-id']
|
||||
unique_together = (('key', 'is_active'),)
|
||||
get_latest_by = "created_at"
|
||||
ordering = ["-id"]
|
||||
unique_together = (("key", "is_active"),)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.key)
|
||||
@ -56,7 +61,8 @@ class CallbackToken(AbstractBaseCallbackToken):
|
||||
"""
|
||||
Generates a random six digit number to be returned.
|
||||
"""
|
||||
|
||||
key = models.CharField(default=generate_numeric_token, max_length=6, unique=True)
|
||||
|
||||
class Meta(AbstractBaseCallbackToken.Meta):
|
||||
verbose_name = 'Callback Token'
|
||||
verbose_name = "Callback Token"
|
||||
|
@ -6,7 +6,11 @@ from django.core.validators import RegexValidator
|
||||
from rest_framework import serializers
|
||||
from djangopasswordlessknox.models import CallbackToken
|
||||
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__)
|
||||
User = get_user_model()
|
||||
@ -14,11 +18,11 @@ User = get_user_model()
|
||||
|
||||
class TokenField(serializers.CharField):
|
||||
default_error_messages = {
|
||||
'required': _('Invalid Token'),
|
||||
'invalid': _('Invalid Token'),
|
||||
'blank': _('Invalid Token'),
|
||||
'max_length': _('Tokens are {max_length} digits long.'),
|
||||
'min_length': _('Tokens are {min_length} digits long.')
|
||||
"required": _("Invalid Token"),
|
||||
"invalid": _("Invalid Token"),
|
||||
"blank": _("Invalid Token"),
|
||||
"max_length": _("Tokens are {max_length} digits long."),
|
||||
"min_length": _("Tokens are {min_length} digits long."),
|
||||
}
|
||||
|
||||
|
||||
@ -48,10 +52,10 @@ class AbstractBaseAliasAuthenticationSerializer(serializers.Serializer):
|
||||
except User.DoesNotExist:
|
||||
# If no user is found, raise an error
|
||||
msg = ""
|
||||
if self.alias_type == 'email':
|
||||
msg = _('No user found with this email.')
|
||||
elif self.alias_type == 'mobile':
|
||||
msg = _('No user found with this mobile number.')
|
||||
if self.alias_type == "email":
|
||||
msg = _("No user found with this email.")
|
||||
elif self.alias_type == "mobile":
|
||||
msg = _("No user found with this mobile number.")
|
||||
raise serializers.ValidationError(msg)
|
||||
else:
|
||||
# If new aliases should not register new users.
|
||||
@ -63,23 +67,23 @@ class AbstractBaseAliasAuthenticationSerializer(serializers.Serializer):
|
||||
if user:
|
||||
if not user.is_active:
|
||||
# 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)
|
||||
else:
|
||||
msg = _('No account is associated with this alias.')
|
||||
msg = _("No account is associated with this alias.")
|
||||
raise serializers.ValidationError(msg)
|
||||
else:
|
||||
msg = _('Missing %s.') % self.alias_type
|
||||
msg = _("Missing %s.") % self.alias_type
|
||||
raise serializers.ValidationError(msg)
|
||||
|
||||
attrs['user'] = user
|
||||
attrs["user"] = user
|
||||
return attrs
|
||||
|
||||
|
||||
class EmailAuthSerializer(AbstractBaseAliasAuthenticationSerializer):
|
||||
@property
|
||||
def alias_type(self):
|
||||
return 'email'
|
||||
return "email"
|
||||
|
||||
email = serializers.EmailField()
|
||||
|
||||
@ -87,11 +91,13 @@ class EmailAuthSerializer(AbstractBaseAliasAuthenticationSerializer):
|
||||
class MobileAuthSerializer(AbstractBaseAliasAuthenticationSerializer):
|
||||
@property
|
||||
def alias_type(self):
|
||||
return 'mobile'
|
||||
return "mobile"
|
||||
|
||||
phone_regex = RegexValidator(regex=r'^[7|9][0-9]{6}$',
|
||||
phone_regex = RegexValidator(
|
||||
regex=r"^[7|9][0-9]{6}$",
|
||||
message="Mobile number must be entered in the format:"
|
||||
" '7xxxxxx' or '9xxxxxx'.")
|
||||
" '7xxxxxx' or '9xxxxxx'.",
|
||||
)
|
||||
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
|
||||
Returns a token if valid, None or a message if not.
|
||||
"""
|
||||
|
||||
@property
|
||||
def alias_type(self):
|
||||
# The alias type, either email or mobile
|
||||
raise NotImplementedError
|
||||
|
||||
def validate(self, attrs):
|
||||
|
||||
msg = _('There was a problem with your request.')
|
||||
msg = _("There was a problem with your request.")
|
||||
|
||||
if self.alias_type:
|
||||
# Get request.user
|
||||
@ -125,31 +131,31 @@ class AbstractBaseAliasVerificationSerializer(serializers.Serializer):
|
||||
if user:
|
||||
if not user.is_active:
|
||||
# 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:
|
||||
if hasattr(user, self.alias_type):
|
||||
# Has the appropriate alias type
|
||||
attrs['user'] = user
|
||||
attrs["user"] = user
|
||||
return attrs
|
||||
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)
|
||||
else:
|
||||
msg = _('Missing %s.') % self.alias_type
|
||||
msg = _("Missing %s.") % self.alias_type
|
||||
raise serializers.ValidationError(msg)
|
||||
|
||||
|
||||
class EmailVerificationSerializer(AbstractBaseAliasVerificationSerializer):
|
||||
@property
|
||||
def alias_type(self):
|
||||
return 'email'
|
||||
return "email"
|
||||
|
||||
|
||||
class MobileVerificationSerializer(AbstractBaseAliasVerificationSerializer):
|
||||
@property
|
||||
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.
|
||||
Returns a user if valid, None or a message if not.
|
||||
"""
|
||||
|
||||
token = TokenField(min_length=6, max_length=6, validators=[token_age_validator])
|
||||
|
||||
|
||||
class CallbackTokenAuthSerializer(AbstractBaseCallbackTokenSerializer):
|
||||
|
||||
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)
|
||||
|
||||
@ -189,27 +195,29 @@ class CallbackTokenAuthSerializer(AbstractBaseCallbackTokenSerializer):
|
||||
user = authenticate_by_token(token)
|
||||
if user:
|
||||
if not user.is_active:
|
||||
msg = _('User account is disabled.')
|
||||
msg = _("User account is disabled.")
|
||||
raise serializers.ValidationError(msg)
|
||||
|
||||
if api_settings.PASSWORDLESS_USER_MARK_EMAIL_VERIFIED \
|
||||
or api_settings.PASSWORDLESS_USER_MARK_MOBILE_VERIFIED:
|
||||
if (
|
||||
api_settings.PASSWORDLESS_USER_MARK_EMAIL_VERIFIED
|
||||
or api_settings.PASSWORDLESS_USER_MARK_MOBILE_VERIFIED
|
||||
):
|
||||
# Mark this alias as verified
|
||||
user = User.objects.get(pk=token.user.pk)
|
||||
success = verify_user_alias(user, token)
|
||||
|
||||
if success is False:
|
||||
msg = _('Error validating user alias.')
|
||||
msg = _("Error validating user alias.")
|
||||
raise serializers.ValidationError(msg)
|
||||
|
||||
attrs['user'] = user
|
||||
attrs["user"] = user
|
||||
return attrs
|
||||
|
||||
else:
|
||||
msg = _('Invalid Token')
|
||||
msg = _("Invalid Token")
|
||||
raise serializers.ValidationError(msg)
|
||||
else:
|
||||
msg = _('Missing authentication token.')
|
||||
msg = _("Missing authentication token.")
|
||||
raise serializers.ValidationError(msg)
|
||||
|
||||
|
||||
@ -222,7 +230,7 @@ class CallbackTokenVerificationSerializer(AbstractBaseCallbackTokenSerializer):
|
||||
def validate(self, attrs):
|
||||
try:
|
||||
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)
|
||||
user = User.objects.get(pk=user_id)
|
||||
@ -235,23 +243,31 @@ class CallbackTokenVerificationSerializer(AbstractBaseCallbackTokenSerializer):
|
||||
if success is False:
|
||||
logger.debug("djangopasswordlessknox: Error verifying alias.")
|
||||
|
||||
attrs['user'] = user
|
||||
attrs["user"] = user
|
||||
return attrs
|
||||
else:
|
||||
msg = _('This token is invalid. Try again later.')
|
||||
logger.debug("djangopasswordlessknox: User token mismatch when verifying alias.")
|
||||
msg = _("This token is invalid. Try again later.")
|
||||
logger.debug(
|
||||
"djangopasswordlessknox: User token mismatch when verifying alias."
|
||||
)
|
||||
|
||||
except CallbackToken.DoesNotExist:
|
||||
msg = _('Missing authentication token.')
|
||||
logger.debug("djangopasswordlessknox: Tried to validate alias with bad token.")
|
||||
msg = _("Missing authentication token.")
|
||||
logger.debug(
|
||||
"djangopasswordlessknox: Tried to validate alias with bad token."
|
||||
)
|
||||
pass
|
||||
except User.DoesNotExist:
|
||||
msg = _('Missing user.')
|
||||
logger.debug("djangopasswordlessknox: Tried to validate alias with bad user.")
|
||||
msg = _("Missing user.")
|
||||
logger.debug(
|
||||
"djangopasswordlessknox: Tried to validate alias with bad user."
|
||||
)
|
||||
pass
|
||||
except PermissionDenied:
|
||||
msg = _('Insufficient permissions.')
|
||||
logger.debug("djangopasswordlessknox: Permission denied while validating alias.")
|
||||
msg = _("Insufficient permissions.")
|
||||
logger.debug(
|
||||
"djangopasswordlessknox: Permission denied while validating alias."
|
||||
)
|
||||
pass
|
||||
|
||||
raise serializers.ValidationError(msg)
|
||||
|
@ -1,7 +1,7 @@
|
||||
from djangopasswordlessknox.utils import (
|
||||
create_callback_token_for_user,
|
||||
send_email_with_callback_token,
|
||||
send_sms_with_callback_token
|
||||
send_sms_with_callback_token,
|
||||
)
|
||||
|
||||
|
||||
@ -10,9 +10,9 @@ class TokenService(object):
|
||||
def send_token(user, alias_type, **message_payload):
|
||||
token = create_callback_token_for_user(user, alias_type)
|
||||
send_action = None
|
||||
if alias_type == 'email':
|
||||
if alias_type == "email":
|
||||
send_action = send_email_with_callback_token
|
||||
elif alias_type == 'mobile':
|
||||
elif alias_type == "mobile":
|
||||
send_action = send_sms_with_callback_token
|
||||
|
||||
if send_action is None:
|
||||
|
@ -17,7 +17,11 @@ def invalidate_previous_tokens(sender, instance, **kwargs):
|
||||
"""
|
||||
active_tokens = None
|
||||
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
|
||||
if active_tokens:
|
||||
@ -46,15 +50,15 @@ def update_alias_verification(sender, instance, **kwargs):
|
||||
Optionally sends a verification token to the new endpoint.
|
||||
"""
|
||||
if isinstance(instance, User):
|
||||
|
||||
if instance.id:
|
||||
|
||||
if api_settings.PASSWORDLESS_USER_MARK_EMAIL_VERIFIED is True:
|
||||
"""
|
||||
For marking email aliases as not verified when a user changes it.
|
||||
"""
|
||||
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.
|
||||
try:
|
||||
@ -62,24 +66,41 @@ def update_alias_verification(sender, instance, **kwargs):
|
||||
instance_email = getattr(instance, email_field) # Incoming 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
|
||||
setattr(instance, email_verified_field, False)
|
||||
if api_settings.PASSWORDLESS_AUTO_SEND_VERIFICATION_TOKEN is True:
|
||||
email_subject = api_settings.PASSWORDLESS_EMAIL_VERIFICATION_SUBJECT
|
||||
if (
|
||||
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_html = api_settings.PASSWORDLESS_EMAIL_VERIFICATION_TOKEN_HTML_TEMPLATE_NAME
|
||||
message_payload = {'email_subject': email_subject,
|
||||
'email_plaintext': email_plaintext,
|
||||
'email_html': email_html}
|
||||
success = TokenService.send_token(instance, 'email', **message_payload)
|
||||
message_payload = {
|
||||
"email_subject": email_subject,
|
||||
"email_plaintext": email_plaintext,
|
||||
"email_html": email_html,
|
||||
}
|
||||
success = TokenService.send_token(
|
||||
instance, "email", **message_payload
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info('djangopasswordlessknox: Successfully sent email on updated address: %s'
|
||||
% instance_email)
|
||||
logger.info(
|
||||
"djangopasswordlessknox: Successfully sent email on updated address: %s"
|
||||
% instance_email
|
||||
)
|
||||
else:
|
||||
logger.info('djangopasswordlessknox: Failed to send email to updated address: %s'
|
||||
% instance_email)
|
||||
logger.info(
|
||||
"djangopasswordlessknox: Failed to send email to updated address: %s"
|
||||
% instance_email
|
||||
)
|
||||
|
||||
except User.DoesNotExist:
|
||||
# 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.
|
||||
"""
|
||||
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.
|
||||
try:
|
||||
user_old = User.objects.get(id=instance.id) # Pre-save object
|
||||
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
|
||||
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
|
||||
message_payload = {'mobile_message': mobile_message}
|
||||
success = TokenService.send_token(instance, 'mobile', **message_payload)
|
||||
message_payload = {"mobile_message": mobile_message}
|
||||
success = TokenService.send_token(
|
||||
instance, "mobile", **message_payload
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info('djangopasswordlessknox: Successfully sent SMS on updated mobile: %s'
|
||||
% instance_mobile)
|
||||
logger.info(
|
||||
"djangopasswordlessknox: Successfully sent SMS on updated mobile: %s"
|
||||
% instance_mobile
|
||||
)
|
||||
else:
|
||||
logger.info('djangopasswordlessknox: Failed to send SMS to updated mobile: %s'
|
||||
% instance_mobile)
|
||||
logger.info(
|
||||
"djangopasswordlessknox: Failed to send SMS to updated mobile: %s"
|
||||
% instance_mobile
|
||||
)
|
||||
|
||||
except User.DoesNotExist:
|
||||
# User probably is just initially being created
|
||||
|
@ -9,10 +9,26 @@ from djangopasswordlessknox.views import (
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('callback/auth/', ObtainAuthTokenFromCallbackToken.as_view(), name='auth_callback'),
|
||||
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'),
|
||||
path(
|
||||
"callback/auth/",
|
||||
ObtainAuthTokenFromCallbackToken.as_view(),
|
||||
name="auth_callback",
|
||||
),
|
||||
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",
|
||||
),
|
||||
]
|
||||
|
@ -29,6 +29,7 @@ class AbstractBaseObtainCallbackToken(APIView):
|
||||
"""
|
||||
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."
|
||||
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.
|
||||
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):
|
||||
# Validate -
|
||||
user = serializer.validated_data['user']
|
||||
user = serializer.validated_data["user"]
|
||||
# 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
|
||||
if success:
|
||||
@ -63,67 +68,79 @@ class AbstractBaseObtainCallbackToken(APIView):
|
||||
else:
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
response_detail = self.failure_response
|
||||
return Response({'detail': response_detail}, status=status_code)
|
||||
return Response({"detail": response_detail}, status=status_code)
|
||||
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,)
|
||||
serializer_class = EmailAuthSerializer
|
||||
success_response = "A login token has been sent to your email."
|
||||
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_plaintext = api_settings.PASSWORDLESS_EMAIL_PLAINTEXT_MESSAGE
|
||||
email_html = api_settings.PASSWORDLESS_EMAIL_TOKEN_HTML_TEMPLATE_NAME
|
||||
message_payload = {'email_subject': email_subject,
|
||||
'email_plaintext': email_plaintext,
|
||||
'email_html': email_html}
|
||||
message_payload = {
|
||||
"email_subject": email_subject,
|
||||
"email_plaintext": email_plaintext,
|
||||
"email_html": email_html,
|
||||
}
|
||||
|
||||
|
||||
class ObtainMobileCallbackToken(AbstractBaseObtainCallbackToken, generics.GenericAPIView):
|
||||
class ObtainMobileCallbackToken(
|
||||
AbstractBaseObtainCallbackToken, generics.GenericAPIView
|
||||
):
|
||||
permission_classes = (AllowAny,)
|
||||
serializer_class = MobileAuthSerializer
|
||||
success_response = "We texted you a login code."
|
||||
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
|
||||
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,)
|
||||
serializer_class = EmailVerificationSerializer
|
||||
success_response = "A verification token has been sent to your email."
|
||||
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_plaintext = api_settings.PASSWORDLESS_EMAIL_VERIFICATION_PLAINTEXT_MESSAGE
|
||||
email_html = api_settings.PASSWORDLESS_EMAIL_VERIFICATION_TOKEN_HTML_TEMPLATE_NAME
|
||||
message_payload = {
|
||||
'email_subject': email_subject,
|
||||
'email_plaintext': email_plaintext,
|
||||
'email_html': email_html
|
||||
"email_subject": email_subject,
|
||||
"email_plaintext": email_plaintext,
|
||||
"email_html": email_html,
|
||||
}
|
||||
|
||||
|
||||
class ObtainMobileVerificationCallbackToken(AbstractBaseObtainCallbackToken, generics.GenericAPIView):
|
||||
class ObtainMobileVerificationCallbackToken(
|
||||
AbstractBaseObtainCallbackToken, generics.GenericAPIView
|
||||
):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
serializer_class = MobileVerificationSerializer
|
||||
success_response = "We texted you a verification code."
|
||||
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
|
||||
message_payload = {'mobile_message': mobile_message}
|
||||
message_payload = {"mobile_message": mobile_message}
|
||||
|
||||
|
||||
class AbstractBaseObtainAuthToken(APIView):
|
||||
@ -131,10 +148,11 @@ 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 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):
|
||||
return knox_settings.TOKEN_TTL
|
||||
@ -155,43 +173,41 @@ class AbstractBaseObtainAuthToken(APIView):
|
||||
def get_post_response_data(self, user, token, instance):
|
||||
UserSerializer = self.get_user_serializer_class()
|
||||
|
||||
data = {
|
||||
'expiry': self.format_expiry_datetime(instance.expiry),
|
||||
'token': token
|
||||
}
|
||||
data = {"expiry": self.format_expiry_datetime(instance.expiry), "token": token}
|
||||
if UserSerializer is not None:
|
||||
data["user"] = UserSerializer(
|
||||
user,
|
||||
context=self.get_context()
|
||||
).data
|
||||
data["user"] = UserSerializer(user, context=self.get_context()).data
|
||||
return data
|
||||
|
||||
def post(self, request, format=None):
|
||||
token_limit_per_user = self.get_token_limit_per_user()
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
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:
|
||||
now = timezone.now()
|
||||
token = user.auth_token_set.filter(expiry__gt=now)
|
||||
if token.count() >= token_limit_per_user:
|
||||
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()
|
||||
instance, token = AuthToken.objects.create(user, token_ttl)
|
||||
user_logged_in.send(sender=user.__class__,
|
||||
request=request, user=user)
|
||||
user_logged_in.send(sender=user.__class__, request=request, user=user)
|
||||
data = self.get_post_response_data(user, token, instance)
|
||||
return Response(data)
|
||||
|
||||
|
||||
class ObtainAuthTokenFromCallbackToken(AbstractBaseObtainAuthToken, generics.GenericAPIView):
|
||||
class ObtainAuthTokenFromCallbackToken(
|
||||
AbstractBaseObtainAuthToken, generics.GenericAPIView
|
||||
):
|
||||
"""
|
||||
This is a duplicate of rest_framework's own ObtainAuthToken method.
|
||||
Instead, this returns an Auth Token based on our callback token and source.
|
||||
"""
|
||||
|
||||
permission_classes = (AllowAny,)
|
||||
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.
|
||||
Should be refactored at some point.
|
||||
"""
|
||||
|
||||
serializer_class = CallbackTokenVerificationSerializer
|
||||
|
||||
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):
|
||||
return Response({'detail': 'Alias verified.'}, status=status.HTTP_200_OK)
|
||||
return Response({"detail": "Alias verified."}, status=status.HTTP_200_OK)
|
||||
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,
|
||||
)
|
||||
|
@ -6,6 +6,7 @@
|
||||
"typeCheckingMode": "standard",
|
||||
"reportArgumentType": "warning",
|
||||
"reportUnusedVariable": "warning",
|
||||
"reportFunctionMemberAccess": "none",
|
||||
"exclude": [
|
||||
"council-api/**/migrations",
|
||||
"**/__pycache__",
|
||||
|
Loading…
x
Reference in New Issue
Block a user