diff --git a/api/admin.py b/api/admin.py index 931af15..c70b58e 100644 --- a/api/admin.py +++ b/api/admin.py @@ -15,6 +15,7 @@ class UserAdmin(BaseUserAdmin): "is_staff", "mobile", "address", + "wallet_balance", "acc_no", "id_card", "dob", @@ -35,6 +36,7 @@ class UserAdmin(BaseUserAdmin): "email", "mobile", "address", + "wallet_balance", "acc_no", "id_card", "dob", diff --git a/api/migrations/0005_alter_atoll_name_alter_island_name.py b/api/migrations/0005_alter_atoll_name_alter_island_name.py new file mode 100644 index 0000000..3c42f22 --- /dev/null +++ b/api/migrations/0005_alter_atoll_name_alter_island_name.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.2 on 2025-01-20 14:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0004_alter_atoll_id_alter_island_id"), + ] + + operations = [ + migrations.AlterField( + model_name="atoll", + name="name", + field=models.CharField(max_length=255, unique=True), + ), + migrations.AlterField( + model_name="island", + name="name", + field=models.CharField(max_length=255, unique=True), + ), + ] diff --git a/api/models.py b/api/models.py index 2beb9f4..5da0fdc 100644 --- a/api/models.py +++ b/api/models.py @@ -7,6 +7,7 @@ from django.db import models from .managers import CustomUserManager from django.utils import timezone + class User(AbstractUser): email = models.EmailField(unique=True, blank=True, null=True) address = models.CharField(max_length=255, blank=True) @@ -20,8 +21,13 @@ class User(AbstractUser): policy_accepted = models.BooleanField(default=False) wallet_balance = models.FloatField(default=0.0) ninja_user_id = models.CharField(max_length=255, blank=True) - atoll = models.ForeignKey('Atoll', on_delete=models.SET_NULL, null=True, blank=True, related_name='users') - island = models.ForeignKey('Island', on_delete=models.SET_NULL, null=True, blank=True, related_name='users') + atoll = models.ForeignKey( + "Atoll", on_delete=models.SET_NULL, null=True, blank=True, related_name="users" + ) + island = models.ForeignKey( + "Island", on_delete=models.SET_NULL, null=True, blank=True, related_name="users" + ) + def get_all_fields(self, instance): return [field.name for field in instance.get_fields()] @@ -29,16 +35,17 @@ class User(AbstractUser): class Atoll(models.Model): - name = models.CharField(max_length=255) + name = models.CharField(max_length=255, unique=True) created_at = models.DateTimeField(default=timezone.now) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return self.name + class Island(models.Model): - atoll = models.ForeignKey(Atoll, on_delete=models.CASCADE, related_name='islands') - name = models.CharField(max_length=255) + atoll = models.ForeignKey(Atoll, on_delete=models.CASCADE, related_name="islands") + name = models.CharField(max_length=255, unique=True) created_at = models.DateTimeField(default=timezone.now) updated_at = models.DateTimeField(auto_now=True) diff --git a/api/serializers.py b/api/serializers.py index 37d5129..e8f5fa0 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,6 +1,6 @@ from knox.models import AuthToken from django.contrib.auth import authenticate -from api.models import User +from api.models import User, Atoll, Island from rest_framework import serializers @@ -46,7 +46,6 @@ class CustomReadOnlyUserSerializer(serializers.ModelSerializer): "username", "mobile", "address", - ) @@ -92,3 +91,15 @@ class KnoxTokenSerializer(serializers.ModelSerializer): class Meta: # type: ignore model = AuthToken fields = "__all__" + + +class AtollSerializer(serializers.ModelSerializer): + class Meta: # type: ignore + model = Atoll + fields = "__all__" + + +class IslandSerializer(serializers.ModelSerializer): + class Meta: # type: ignore + model = Island + fields = "__all__" diff --git a/api/signals.py b/api/signals.py index 9db169f..7024a7e 100644 --- a/api/signals.py +++ b/api/signals.py @@ -7,17 +7,23 @@ from django.db.models.signals import post_save from api.models import User from django.contrib.auth.models import Permission + @receiver(post_save, sender=User) def assign_device_permissions(sender, instance, created, **kwargs): if created: # Assign all permissions for devices and read permission for atoll and island - device_permissions = Permission.objects.filter(content_type__model='device') - atoll_read_permission = Permission.objects.get(codename='view_atoll') - island_read_permission = Permission.objects.get(codename='view_island') + device_permissions = Permission.objects.filter(content_type__model="device") + atoll_read_permission = Permission.objects.get(codename="view_atoll") + island_read_permission = Permission.objects.get(codename="view_island") + payment_permissions = Permission.objects.filter( + content_type__model="payment" + ).exclude(codename="delete_payment") for permission in device_permissions: instance.user_permissions.add(permission) instance.user_permissions.add(atoll_read_permission, island_read_permission) + for permission in payment_permissions: + instance.user_permissions.add(permission) @receiver(reset_password_token_created) diff --git a/api/urls.py b/api/urls.py index fbc41c9..8e93185 100644 --- a/api/urls.py +++ b/api/urls.py @@ -11,6 +11,10 @@ from .views import ( UserDetailAPIView, healthcheck, test_email, + ListCreateAtollView, + RetrieveUpdateDestroyAtollView, + ListCreateIslandView, + RetrieveUpdateDestroyIslandView, ) @@ -26,5 +30,16 @@ urlpatterns = [ path("users//", UserDetailAPIView.as_view(), name="user-detail"), path("healthcheck/", healthcheck, name="healthcheck"), path("test/", test_email, name="testemail"), - + path("atolls/", ListCreateAtollView.as_view(), name="atolls"), + path( + "atolls//", + RetrieveUpdateDestroyAtollView.as_view(), + name="atoll-detail", + ), + path("islands/", ListCreateIslandView.as_view(), name="islands"), + path( + "islands//", + RetrieveUpdateDestroyIslandView.as_view(), + name="island-detail", + ), ] diff --git a/api/views.py b/api/views.py index cc60189..71c9a32 100644 --- a/api/views.py +++ b/api/views.py @@ -11,6 +11,7 @@ from rest_framework.response import Response from rest_framework import status from rest_framework.exceptions import ValidationError from rest_framework.decorators import api_view, permission_classes +from api.serializers import AtollSerializer, IslandSerializer # knox imports from knox.views import LoginView as KnoxLoginView @@ -85,13 +86,19 @@ class CreateUserView(generics.CreateAPIView): return Response({"message": "Policy acceptance is required."}, status=400) if not re.match(r"^[A-Z]{1,2}[0-9]{6,7}$", id_card): - return Response({"message": "Please enter a valid ID card number."}, status=400) + return Response( + {"message": "Please enter a valid ID card number."}, status=400 + ) if not re.match(r"^[7|9][0-9]{6}$", mobile): - return Response({"message": "Please enter a valid mobile number."}, status=400) + return Response( + {"message": "Please enter a valid mobile number."}, status=400 + ) if not re.match(r"^(7\d{12}|9\d{16})$", acc_no): - return Response({"message": "Please enter a valid account number."}, status=400) + return Response( + {"message": "Please enter a valid account number."}, status=400 + ) # Fetch Atoll and Island instances try: @@ -109,7 +116,7 @@ class CreateUserView(generics.CreateAPIView): username=username, password=password, address=address, - mobile=str("+960") + str(mobile), + mobile=mobile, acc_no=acc_no, id_card=id_card, dob=dob, @@ -219,3 +226,64 @@ def test_email(request): fail_silently=False, ) return Response({"status": "ok"}, status=status.HTTP_200_OK) + + +class ListCreateAtollView(StaffEditorPermissionMixin, generics.ListCreateAPIView): + serializer_class = AtollSerializer + queryset = Atoll.objects.all() + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + name = serializer.validated_data.get("name") + print(name) + if Atoll.objects.filter(name=name).exists(): + return Response({"message": "Atoll name already exists."}, status=400) + return super().create(request, *args, **kwargs) + + +class RetrieveUpdateDestroyAtollView( + StaffEditorPermissionMixin, generics.RetrieveUpdateDestroyAPIView +): + serializer_class = AtollSerializer + queryset = Atoll.objects.all() + lookup_field = "pk" + + def update(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + name = serializer.validated_data.get("name") + if name and Atoll.objects.filter(name=name).exclude(pk=instance.pk).exists(): + return Response({"message": "Atoll name already exists."}, status=400) + return super().update(request, *args, **kwargs) + + +class ListCreateIslandView(StaffEditorPermissionMixin, generics.ListCreateAPIView): + serializer_class = IslandSerializer + queryset = Island.objects.all() + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + name = serializer.validated_data.get("name") + if Island.objects.filter(name=name).exists(): + return Response({"message": "Island name already exists."}, status=400) + return super().create(request, *args, **kwargs) + + +class RetrieveUpdateDestroyIslandView( + StaffEditorPermissionMixin, generics.RetrieveUpdateDestroyAPIView +): + serializer_class = IslandSerializer + queryset = Island.objects.all() + lookup_field = "pk" + + def update(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + name = serializer.validated_data.get("name") + if name and Island.objects.filter(name=name).exclude(pk=instance.pk).exists(): + return Response({"message": "Island name already exists."}, status=400) + return super().update(request, *args, **kwargs) diff --git a/apibase/urls.py b/apibase/urls.py index 70138f9..7f1763a 100644 --- a/apibase/urls.py +++ b/apibase/urls.py @@ -36,7 +36,8 @@ urlpatterns = [ path("api/auth/", include("api.urls")), # Devices path("api/devices/", include("devices.urls")), - + # Billing + path("api/billing/", include("billing.urls")), ] if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/billing/admin.py b/billing/admin.py index 8c38f3f..acce970 100644 --- a/billing/admin.py +++ b/billing/admin.py @@ -1,3 +1,13 @@ from django.contrib import admin +from .models import Payment, BillFormula, Topup # Register your models here. + + +class PaymentAdmin(admin.ModelAdmin): + list_display = ("id", "user", "amount", "paid", "paid_at", "method") + + +admin.site.register(Payment, PaymentAdmin) +admin.site.register(BillFormula) +admin.site.register(Topup) diff --git a/billing/migrations/0003_alter_billformula_id_alter_payment_id.py b/billing/migrations/0003_alter_billformula_id_alter_payment_id.py new file mode 100644 index 0000000..159433b --- /dev/null +++ b/billing/migrations/0003_alter_billformula_id_alter_payment_id.py @@ -0,0 +1,27 @@ +# Generated by Django 5.1.2 on 2025-01-20 14:58 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("billing", "0002_billformula_payment_topup_delete_device"), + ] + + operations = [ + migrations.AlterField( + model_name="billformula", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="payment", + name="id", + field=models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), + ] diff --git a/billing/migrations/0004_alter_topup_id.py b/billing/migrations/0004_alter_topup_id.py new file mode 100644 index 0000000..2634b4b --- /dev/null +++ b/billing/migrations/0004_alter_topup_id.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.2 on 2025-01-20 15:00 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("billing", "0003_alter_billformula_id_alter_payment_id"), + ] + + operations = [ + migrations.AlterField( + model_name="topup", + name="id", + field=models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), + ] diff --git a/billing/models.py b/billing/models.py index 22e4b37..e4b44a9 100644 --- a/billing/models.py +++ b/billing/models.py @@ -1,35 +1,37 @@ from django.db import models from django.utils import timezone from api.models import User +import uuid # Create your models here. from devices.models import Device + # Create your models here. + class Payment(models.Model): PAYMENT_TYPES = [ - ('WALLET', 'Wallet'), - ('TRANSFER', 'Transfer'), + ("WALLET", "Wallet"), + ("TRANSFER", "Transfer"), ] - - id = models.CharField(primary_key=True, max_length=255) + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) number_of_months = models.IntegerField() amount = models.FloatField() paid = models.BooleanField(default=False) - user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='payments') + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="payments") paid_at = models.DateTimeField(null=True, blank=True) - method = models.CharField(max_length=255, choices=PAYMENT_TYPES, default='TRANSFER') + method = models.CharField(max_length=255, choices=PAYMENT_TYPES, default="TRANSFER") expires_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(default=timezone.now) updated_at = models.DateTimeField(auto_now=True) - devices = models.ManyToManyField(Device, related_name='payments') + devices = models.ManyToManyField(Device, related_name="payments") def __str__(self): return f"Payment by {self.user}" + class BillFormula(models.Model): - id = models.CharField(primary_key=True, max_length=255) formula = models.CharField(max_length=255) base_amount = models.FloatField() discount_percentage = models.FloatField() @@ -39,10 +41,11 @@ class BillFormula(models.Model): def __str__(self): return self.formula + class Topup(models.Model): - id = models.CharField(primary_key=True, max_length=255) + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) amount = models.FloatField() - user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='topups') + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="topups") paid = models.BooleanField(default=False) created_at = models.DateTimeField(default=timezone.now) updated_at = models.DateTimeField(auto_now=True) diff --git a/billing/serializers.py b/billing/serializers.py new file mode 100644 index 0000000..efb5aa9 --- /dev/null +++ b/billing/serializers.py @@ -0,0 +1,11 @@ +from rest_framework import serializers +from .models import Payment +from devices.serializers import DeviceSerializer + + +class PaymentSerializer(serializers.ModelSerializer): + devices = DeviceSerializer(many=True, read_only=True) + + class Meta: + model = Payment + fields = "__all__" diff --git a/billing/urls.py b/billing/urls.py new file mode 100644 index 0000000..492e8cf --- /dev/null +++ b/billing/urls.py @@ -0,0 +1,8 @@ +# billing/urls.py +from django.urls import path +from .views import CreatePaymentView, VerifyPaymentView + +urlpatterns = [ + path("create-payment/", CreatePaymentView.as_view(), name="create-payment"), + path("verify-payment/", VerifyPaymentView.as_view(), name="verify-payment"), +] diff --git a/billing/views.py b/billing/views.py index b8e4ee0..6e7ef2e 100644 --- a/billing/views.py +++ b/billing/views.py @@ -1,2 +1,141 @@ - # Create your views here. +# billing/views.py +from .models import Payment, Device +from datetime import datetime, timedelta +import requests +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from decouple import config +from .serializers import PaymentSerializer +from rest_framework.permissions import AllowAny +from api.mixins import StaffEditorPermissionMixin +from rest_framework import generics + + +class InsufficientFundsError(Exception): + pass + + +class CreatePaymentView(StaffEditorPermissionMixin, generics.CreateAPIView): + serializer_class = PaymentSerializer + queryset = Payment.objects.all() + + def create(self, request): + data = request.data + user = request.user + amount = data.get("amount") + number_of_months = data.get("number_of_months") + device_ids = data.get("device_ids", []) + print(amount, number_of_months, device_ids) + + if not amount or not number_of_months: + return Response( + {"message": "amount and number_of_months are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + if not device_ids: + return Response( + {"message": "device_ids are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + # Create payment + payment = Payment.objects.create( + amount=amount, + number_of_months=number_of_months, + paid=data.get("paid", False), + user=user, + ) + + # Connect devices to payment + devices = Device.objects.filter(id__in=device_ids, user=user) + payment.devices.set(devices) + + serializer = PaymentSerializer(payment) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + +class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView): + serializer_class = PaymentSerializer + queryset = Payment.objects.all() + + def update(self, request, *args, **kwargs): + data = request.data + user = request.user + print(user) + method = data.get("method") + payment_id = data.get("payment_id") + abs_amount = data.get("abs_amount") + if not method: + return Response( + {"message": "method is required. 'WALLET' or 'TRANSFER'"}, + status=status.HTTP_400_BAD_REQUEST, + ) + if not payment_id: + return Response( + {"message": "payment_id is required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + if not abs_amount: + return Response( + {"message": "abs_amount is required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + try: + payment = Payment.objects.get(id=payment_id) + devices = payment.devices.all() + + if data["type"] == "WALLET": + print("processing WALLET payment") + self.process_wallet_payment(user, payment, float(data["abs_amount"])) + elif data["type"] == "TRANSFER": + self.verify_external_payment(data, payment) + + # Update devices + expiry_date = datetime.now() + timedelta(days=30 * payment.number_of_months) + devices.update(is_active=True, expiry_date=expiry_date) + + return Response({"message": "Payment verified successfully."}) + + except Payment.DoesNotExist: + return Response( + {"message": "Payment not found."}, status=status.HTTP_404_NOT_FOUND + ) + except InsufficientFundsError: + return Response( + {"message": "Insufficient funds in wallet."}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + return Response( + {"message": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + def process_wallet_payment(self, user, payment, amount): + print("processing wallet payment") + print(user, amount) + if user.wallet_balance < amount: + return Response( + {"message": "Insufficient funds in wallet."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + payment.paid = True + payment.paid_at = datetime.now() + payment.method = "WALLET" + payment.save() + + user.wallet_balance -= amount + user.save() + + def verify_external_payment(self, data, payment): + response = requests.post( + f"{config('PAYMENT_VERIFY_BASE_URL')}/verify-payment", + json=data, + headers={"Content-Type": "application/json"}, + ) + response.raise_for_status() + print(response.json()) + if not response.json().get("success"): + raise Exception("Payment verification failed.") diff --git a/devices/views.py b/devices/views.py index 828135b..0bdeeb1 100644 --- a/devices/views.py +++ b/devices/views.py @@ -2,7 +2,11 @@ from rest_framework import generics, status from rest_framework.response import Response from django_filters.rest_framework import DjangoFilterBackend from .models import Device -from .serializers import CreateDeviceSerializer, DeviceSerializer, ReadOnlyDeviceSerializer +from .serializers import ( + CreateDeviceSerializer, + DeviceSerializer, + ReadOnlyDeviceSerializer, +) from api.mixins import StaffEditorPermissionMixin from .filters import DeviceFilter import re @@ -27,16 +31,17 @@ class DeviceListCreateAPIView( def create(self, request, *args, **kwargs): mac = request.data.get("mac", None) if not re.match(r"^([0-9A-Fa-f]{2}([.:-]?)){5}[0-9A-Fa-f]{2}$", mac): - return Response({"error": "Invalid mac address"}, status=400) + return Response({"message": "Invalid mac address."}, status=400) if Device.objects.filter(mac=mac).exists(): - return Response({"error": "Device with this mac already exists"}, status=400) + return Response( + {"message": "Device with this mac address already exists."}, status=400 + ) return super().create(request, *args, **kwargs) def perform_create(self, serializer): serializer.save(user=self.request.user) - class DeviceDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView): queryset = Device.objects.select_related("user").all() serializer_class = ReadOnlyDeviceSerializer @@ -52,6 +57,9 @@ class DeviceUpdateAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView): # Pass 'partial=True' to allow partial updates partial = kwargs.pop("partial", True) instance = self.get_object() + mac = request.data.get("mac", None) + if not re.match(r"^([0-9A-Fa-f]{2}([.:-]?)){5}[0-9A-Fa-f]{2}$", mac): + return Response({"message": "Invalid mac address"}, status=400) serializer = self.get_serializer(instance, data=request.data, partial=partial) serializer.is_valid(raise_exception=True) self.perform_update(serializer) @@ -73,4 +81,3 @@ class DeviceDestroyAPIView(StaffEditorPermissionMixin, generics.DestroyAPIView): {"message": f"Device '{device_name}' deleted."}, status=status.HTTP_200_OK, ) - diff --git a/djangopasswordlessknox/templates/passwordless_default_token_email.html b/djangopasswordlessknox/templates/passwordless_default_token_email.html index 5ad6942..29d6a06 100644 --- a/djangopasswordlessknox/templates/passwordless_default_token_email.html +++ b/djangopasswordlessknox/templates/passwordless_default_token_email.html @@ -1,5 +1,9 @@ + + + + Your Login Token @@ -62,6 +66,7 @@ } +
@@ -85,4 +90,5 @@
- + + \ No newline at end of file diff --git a/djangopasswordlessknox/templates/passwordless_default_verification_token_email.html b/djangopasswordlessknox/templates/passwordless_default_verification_token_email.html index 9e00c13..819450f 100644 --- a/djangopasswordlessknox/templates/passwordless_default_verification_token_email.html +++ b/djangopasswordlessknox/templates/passwordless_default_verification_token_email.html @@ -1,10 +1,16 @@ + + + + Your Verification Token +

Use this verification code: {{ callback_token }}

- + + \ No newline at end of file diff --git a/djangopasswordlessknox/utils.py b/djangopasswordlessknox/utils.py index c078d76..df073a4 100644 --- a/djangopasswordlessknox/utils.py +++ b/djangopasswordlessknox/utils.py @@ -1,4 +1,5 @@ import logging + # import os from django.contrib.auth import get_user_model from django.core.exceptions import PermissionDenied @@ -7,10 +8,12 @@ from django.template import loader from django.utils import timezone from djangopasswordlessknox.models import CallbackToken from djangopasswordlessknox.settings import api_settings + # from twilio.rest import Client from decouple import config import requests import json + logger = logging.getLogger(__name__) User = get_user_model() @@ -27,9 +30,13 @@ def authenticate_by_token(callback_token): return token.user except CallbackToken.DoesNotExist: - logger.debug("djangopasswordlessknox: Challenged with a callback token that doesn't exist.") + logger.debug( + "djangopasswordlessknox: Challenged with a callback token that doesn't exist." + ) except User.DoesNotExist: - logger.debug("djangopasswordlessknox: Authenticated user somehow doesn't exist.") + logger.debug( + "djangopasswordlessknox: Authenticated user somehow doesn't exist." + ) except PermissionDenied: logger.debug("djangopasswordlessknox: Permission denied while authenticating.") @@ -41,15 +48,19 @@ def create_callback_token_for_user(user, token_type): token = None token_type = token_type.upper() - if token_type == 'EMAIL': - token = CallbackToken.objects.create(user=user, - to_alias_type=token_type, - to_alias=getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME)) + if token_type == "EMAIL": + token = CallbackToken.objects.create( + user=user, + to_alias_type=token_type, + to_alias=getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME), + ) - elif token_type == 'MOBILE': - token = CallbackToken.objects.create(user=user, - to_alias_type=token_type, - to_alias=getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME)) + elif token_type == "MOBILE": + token = CallbackToken.objects.create( + user=user, + to_alias_type=token_type, + to_alias=getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME), + ) if token is not None: return token @@ -83,12 +94,20 @@ def verify_user_alias(user, token): """ Marks a user's contact point as verified depending on accepted token type. """ - if token.to_alias_type == 'EMAIL': - if token.to_alias == getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME): - setattr(user, api_settings.PASSWORDLESS_USER_EMAIL_VERIFIED_FIELD_NAME, True) - elif token.to_alias_type == 'MOBILE': - if token.to_alias == getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME): - setattr(user, api_settings.PASSWORDLESS_USER_MOBILE_VERIFIED_FIELD_NAME, True) + if token.to_alias_type == "EMAIL": + if token.to_alias == getattr( + user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME + ): + setattr( + user, api_settings.PASSWORDLESS_USER_EMAIL_VERIFIED_FIELD_NAME, True + ) + elif token.to_alias_type == "MOBILE": + if token.to_alias == getattr( + user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME + ): + setattr( + user, api_settings.PASSWORDLESS_USER_MOBILE_VERIFIED_FIELD_NAME, True + ) else: return False user.save() @@ -116,32 +135,47 @@ def send_email_with_callback_token(user, email_token, **kwargs): # Make sure we have a sending address before sending. # Get email subject and message - email_subject = kwargs.get('email_subject', - api_settings.PASSWORDLESS_EMAIL_SUBJECT) - email_plaintext = kwargs.get('email_plaintext', - api_settings.PASSWORDLESS_EMAIL_PLAINTEXT_MESSAGE) - email_html = kwargs.get('email_html', - api_settings.PASSWORDLESS_EMAIL_TOKEN_HTML_TEMPLATE_NAME) + email_subject = kwargs.get( + "email_subject", api_settings.PASSWORDLESS_EMAIL_SUBJECT + ) + email_plaintext = kwargs.get( + "email_plaintext", api_settings.PASSWORDLESS_EMAIL_PLAINTEXT_MESSAGE + ) + email_html = kwargs.get( + "email_html", api_settings.PASSWORDLESS_EMAIL_TOKEN_HTML_TEMPLATE_NAME + ) # Inject context if user specifies. - context = inject_template_context({'callback_token': email_token.key, }) - html_message = loader.render_to_string(email_html, context,) + context = inject_template_context( + { + "callback_token": email_token.key, + } + ) + html_message = loader.render_to_string( + email_html, + context, + ) send_mail( email_subject, email_plaintext % email_token.key, api_settings.PASSWORDLESS_EMAIL_NOREPLY_ADDRESS, [getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME)], fail_silently=False, - html_message=html_message,) + html_message=html_message, + ) else: - logger.debug("Failed to send token email. Missing PASSWORDLESS_EMAIL_NOREPLY_ADDRESS.") + logger.debug( + "Failed to send token email. Missing PASSWORDLESS_EMAIL_NOREPLY_ADDRESS." + ) return False return True except Exception as e: - logger.debug("Failed to send token email to user: %d." - "Possibly no email on user object. Email entered was %s" % - (user.id, getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME))) + logger.debug( + "Failed to send token email to user: %d." + "Possibly no email on user object. Email entered was %s" + % (user.id, getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME)) + ) logger.debug(e) return False @@ -152,60 +186,62 @@ def send_sms_with_callback_token(user, mobile_token, **kwargs): Passes silently without sending in test environment. """ - base_string = kwargs.get('mobile_message', api_settings.PASSWORDLESS_MOBILE_MESSAGE) + base_string = kwargs.get("mobile_message", api_settings.PASSWORDLESS_MOBILE_MESSAGE) try: - if api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER: - print("Sending SMS") - # We need a sending number to send properly - if api_settings.PASSWORDLESS_TEST_SUPPRESSION is True: - # we assume success to prevent spamming SMS during testing. - return True - to_number = getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME) - if to_number.__class__.__name__ == 'PhoneNumber': - to_number = to_number.__str__() + print("Sending SMS") + # We need a sending number to send properly + if api_settings.PASSWORDLESS_TEST_SUPPRESSION is True: + # we assume success to prevent spamming SMS during testing. + return True + to_number = getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME) + if to_number.__class__.__name__ == "PhoneNumber": + to_number = to_number.__str__() - # user_withh_mobile_exists = User.objects.filter(mobile=to_number).exists() - # if not user_withh_mobile_exists: - # print("User with mobile number does not exist.") - # logger.debug("User with mobile number does not exist.") - # return False + # user_withh_mobile_exists = User.objects.filter(mobile=to_number).exists() + # if not user_withh_mobile_exists: + # print("User with mobile number does not exist.") + # logger.debug("User with mobile number does not exist.") + # return False - - api_url = config("SMS_API_URL") - api_key = config("SMS_API_KEY") - if not api_url or not api_key: - logger.debug("Failed to send SMS. Missing SMS_API_URL or SMS_API_KEY.") - return False - - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {api_key}" - } - data = { - "number": to_number, - "message": base_string % mobile_token.key, - "check_delivery": False - } - - response = requests.post(api_url, headers=headers, data=json.dumps(data)) - if response.status_code == 200: - return True - else: - logger.debug(f"Failed to send SMS. Status code: {response.status_code}") - return False - else: - logger.debug("Failed to send token sms. Missing PASSWORDLESS_MOBILE_NOREPLY_NUMBER.") + api_url = config("SMS_API_URL") + api_key = config("SMS_API_KEY") + if not api_url or not api_key: + logger.debug("Failed to send SMS. Missing SMS_API_URL or SMS_API_KEY.") return False + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}", + } + data = { + "number": to_number, + "message": base_string % mobile_token.key, + "check_delivery": False, + } + + response = requests.post(api_url, headers=headers, data=json.dumps(data)) + if response.status_code == 200: + return True + else: + logger.debug(f"Failed to send SMS. Status code: {response.status_code}") + return False + except ImportError: logger.debug("Couldn't import Twilio client. Is twilio installed?") return False except KeyError: - logger.debug("Couldn't send SMS." - "Did you set your Twilio account tokens and specify a PASSWORDLESS_MOBILE_NOREPLY_NUMBER?") + logger.debug( + "Couldn't send SMS." + "Did you set your Twilio account tokens and specify a PASSWORDLESS_MOBILE_NOREPLY_NUMBER?" + ) except Exception as e: - logger.debug("Failed to send token SMS to user: {}. " - "Possibly no mobile number on user object or the twilio package isn't set up yet. " - "Number entered was {}".format(user.id, getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME))) + logger.debug( + "Failed to send token SMS to user: {}. " + "Possibly no mobile number on user object or the twilio package isn't set up yet. " + "Number entered was {}".format( + user.id, getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME) + ) + ) logger.debug(e) return False