mirror of
https://github.com/i701/sarlink-portal-api.git
synced 2025-06-06 17:36:20 +00:00
Refactor device management: add vendor field, enhance admin display, and improve MAC address handling
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 4m18s
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 4m18s
This commit is contained in:
parent
e1c862184e
commit
4db9d7fabd
@ -59,9 +59,13 @@ def send_sms(mobile: str, message: str):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# def escape_markdown_v2(text: str) -> str:
|
def escape_markdown_v2(text: str) -> str:
|
||||||
# special_chars = r"\_*[]()~`>#+-=|{}.!"
|
special_chars = r"\~`>#+-=|{}!"
|
||||||
# return "".join(["\\" + c if c in special_chars else c for c in text])
|
return "".join(["\\" + c if c in special_chars else c for c in text])
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_whitespace(text: str) -> str:
|
||||||
|
return "\n".join(line.strip() for line in text.strip().splitlines())
|
||||||
|
|
||||||
|
|
||||||
def send_telegram_markdown(message: str):
|
def send_telegram_markdown(message: str):
|
||||||
@ -83,3 +87,9 @@ def send_telegram_markdown(message: str):
|
|||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
logger.error(f"Error sending Telegram message: {e}")
|
logger.error(f"Error sending Telegram message: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def send_clean_telegram_markdown(message: str):
|
||||||
|
text = normalize_whitespace(message)
|
||||||
|
escaped = escape_markdown_v2(text)
|
||||||
|
return send_telegram_markdown(escaped)
|
||||||
|
18
api/tasks.py
18
api/tasks.py
@ -8,7 +8,7 @@ import os
|
|||||||
import logging
|
import logging
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from api.notifications import send_telegram_markdown
|
from api.notifications import send_clean_telegram_markdown
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -161,15 +161,15 @@ def verify_user_with_person_api_task(user_id: int):
|
|||||||
|
|
||||||
verification_failed_message = f"""
|
verification_failed_message = f"""
|
||||||
_The following user verification failed_:
|
_The following user verification failed_:
|
||||||
*ID Card:* {user.id_card} \n
|
*ID Card:* {user.id_card}
|
||||||
*Name:* {user.first_name} {user.last_name} \n
|
*Name:* {user.first_name} {user.last_name}
|
||||||
*House Name:* {user.address} \n
|
*House Name:* {user.address}
|
||||||
*Date of Birth:* {user.dob} \n
|
*Date of Birth:* {user.dob}
|
||||||
*Island:* {(user.atoll.name if user.atoll else "N/A")} {(user.island.name if user.island else "N/A")} \n
|
*Island:* {(user.atoll.name if user.atoll else "N/A")} {(user.island.name if user.island else "N/A")}
|
||||||
*Mobile:* {user.mobile} \n
|
*Mobile:* {user.mobile}
|
||||||
|
|
||||||
Visit [SAR Link Portal](https://portal.sarlink.net) to manually verify this user.
|
Visit [SAR Link Portal](https://portal.sarlink.net) to manually verify this user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger.info(verification_failed_message)
|
logger.info(verification_failed_message)
|
||||||
if not PERSON_VERIFY_BASE_URL:
|
if not PERSON_VERIFY_BASE_URL:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@ -245,7 +245,7 @@ def verify_user_with_person_api_task(user_id: int):
|
|||||||
user.mobile,
|
user.mobile,
|
||||||
f"Dear {user.first_name} {user.last_name}, \n\nYour account registration is being processed. \n\nWe will notify you once verification is complete. \n\n - SAR Link",
|
f"Dear {user.first_name} {user.last_name}, \n\nYour account registration is being processed. \n\nWe will notify you once verification is complete. \n\n - SAR Link",
|
||||||
)
|
)
|
||||||
send_telegram_markdown(message=verification_failed_message)
|
send_clean_telegram_markdown(message=verification_failed_message)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
# Handle the error case
|
# Handle the error case
|
||||||
|
@ -9,6 +9,7 @@ class PaymentAdmin(admin.ModelAdmin):
|
|||||||
"id",
|
"id",
|
||||||
"user",
|
"user",
|
||||||
"amount",
|
"amount",
|
||||||
|
"number_of_months",
|
||||||
"paid",
|
"paid",
|
||||||
"paid_at",
|
"paid_at",
|
||||||
"method",
|
"method",
|
||||||
|
@ -3,4 +3,20 @@ from django.contrib import admin
|
|||||||
# Register your models here.
|
# Register your models here.
|
||||||
from .models import Device
|
from .models import Device
|
||||||
|
|
||||||
admin.site.register(Device)
|
|
||||||
|
class DeviceAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"id",
|
||||||
|
"user",
|
||||||
|
"mac",
|
||||||
|
"vendor",
|
||||||
|
"blocked_by",
|
||||||
|
"name",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
)
|
||||||
|
search_fields = ("mac", "name")
|
||||||
|
list_filter = ("user",)
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Device, DeviceAdmin)
|
||||||
|
17
devices/migrations/0007_device_vendor.py
Normal file
17
devices/migrations/0007_device_vendor.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-06-01 13:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("devices", "0006_alter_device_mac"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="device",
|
||||||
|
name="vendor",
|
||||||
|
field=models.CharField(blank=True, default="", max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
23
devices/migrations/0008_alter_device_blocked_by.py
Normal file
23
devices/migrations/0008_alter_device_blocked_by.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-06-01 14:15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("devices", "0007_device_vendor"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="device",
|
||||||
|
name="blocked_by",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[("ADMIN", "Admin"), ("PARENT", "Parent")],
|
||||||
|
default=None,
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -21,6 +21,7 @@ class Device(models.Model):
|
|||||||
validate_mac_address,
|
validate_mac_address,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
vendor = models.CharField(max_length=255, null=True, blank=True, default="")
|
||||||
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)
|
||||||
@ -29,7 +30,9 @@ class Device(models.Model):
|
|||||||
blocked_by = models.CharField(
|
blocked_by = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
choices=[("ADMIN", "Admin"), ("PARENT", "Parent")],
|
choices=[("ADMIN", "Admin"), ("PARENT", "Parent")],
|
||||||
default="PARENT",
|
default=None,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
)
|
)
|
||||||
expiry_date = models.DateTimeField(null=True, blank=True)
|
expiry_date = models.DateTimeField(null=True, blank=True)
|
||||||
created_at = models.DateTimeField(default=timezone.now)
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from attr import dataclass
|
||||||
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 django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
@ -11,6 +12,8 @@ from .serializers import (
|
|||||||
from api.mixins import StaffEditorPermissionMixin
|
from api.mixins import StaffEditorPermissionMixin
|
||||||
from .filters import DeviceFilter
|
from .filters import DeviceFilter
|
||||||
import re
|
import re
|
||||||
|
import requests
|
||||||
|
from decouple import config
|
||||||
|
|
||||||
|
|
||||||
class DeviceListCreateAPIView(
|
class DeviceListCreateAPIView(
|
||||||
@ -26,7 +29,6 @@ class DeviceListCreateAPIView(
|
|||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
|
||||||
# Filter devices by the logged-in user unless the user is a superuser
|
|
||||||
if not request.user.is_superuser:
|
if not request.user.is_superuser:
|
||||||
queryset = queryset.filter(user=request.user)
|
queryset = queryset.filter(user=request.user)
|
||||||
|
|
||||||
@ -43,24 +45,31 @@ class DeviceListCreateAPIView(
|
|||||||
return CreateDeviceSerializer
|
return CreateDeviceSerializer
|
||||||
return DeviceSerializer
|
return DeviceSerializer
|
||||||
|
|
||||||
# @method_decorator(cache_page(10))
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
mac = request.data.get("mac", None)
|
mac = request.data.get("mac", None)
|
||||||
if not re.match(r"^([0-9A-Fa-f]{2}([.:-]?)){5}[0-9A-Fa-f]{2}$", mac):
|
MAC_REGEX = re.compile(r"^([0-9A-Fa-f]{2}([.:-]?)){5}[0-9A-Fa-f]{2}$")
|
||||||
|
NORMALIZE_MAC_REGEX = re.compile(r"[^0-9A-Fa-f]")
|
||||||
|
if not isinstance(mac, str) or not MAC_REGEX.match(mac):
|
||||||
return Response({"message": "Invalid mac address."}, status=400)
|
return Response({"message": "Invalid mac address."}, status=400)
|
||||||
if Device.objects.filter(mac=mac).exists():
|
if Device.objects.filter(mac=mac).exists():
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "Device with this mac address already exists."}, status=400
|
{"message": "Device with this mac address already exists."}, status=400
|
||||||
)
|
)
|
||||||
|
mac_details = get_mac_address_details(mac)
|
||||||
|
if mac_details.vendor == "Unknown":
|
||||||
|
return Response({"message": "MAC address vendor not found."}, status=400)
|
||||||
|
|
||||||
# Normalize MAC address to use "-" as separators
|
mac = re.sub(NORMALIZE_MAC_REGEX, "-", mac).upper()
|
||||||
mac = re.sub(r"[^0-9A-Fa-f]", "-", mac).upper()
|
|
||||||
request.data["mac"] = mac
|
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):
|
||||||
serializer.save(user=self.request.user)
|
mac_details = get_mac_address_details(serializer.validated_data.get("mac"))
|
||||||
|
serializer.save(
|
||||||
|
user=self.request.user,
|
||||||
|
vendor=mac_details.vendor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeviceDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
class DeviceDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
||||||
@ -138,3 +147,26 @@ class DeviceDestroyAPIView(StaffEditorPermissionMixin, generics.DestroyAPIView):
|
|||||||
{"message": f"Device '{device_name}' deleted."},
|
{"message": f"Device '{device_name}' deleted."},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MacResponse:
|
||||||
|
mac_address: str
|
||||||
|
vendor: str
|
||||||
|
detail: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_mac_address_details(mac: str) -> MacResponse:
|
||||||
|
API_URL = config("MACVENDOR_API_URL")
|
||||||
|
if not API_URL:
|
||||||
|
raise ValueError("MACVENDOR API URL Not set. Please set it.")
|
||||||
|
response = requests.get(f"{API_URL}/lookup/{mac}")
|
||||||
|
json_data = response.json()
|
||||||
|
if response.status_code == 200:
|
||||||
|
return MacResponse(
|
||||||
|
mac_address=json_data.get("mac_address", mac),
|
||||||
|
vendor=json_data.get("vendor", ""),
|
||||||
|
detail=json_data.get("detail"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return MacResponse(mac_address=mac, vendor="Unknown", detail=None)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user