Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c5ed1e89d | ||
|
|
976a119fcc | ||
|
|
d64a2675e4 | ||
|
|
eee314af46 | ||
|
|
ff065fa4a9 | ||
|
|
72c2ea1ecc | ||
|
|
596ce510c7 | ||
|
|
436a8b7d7a | ||
|
|
82ae1e6cea | ||
|
|
56ab79bd8c | ||
|
|
64c2189209 | ||
|
|
dacf821bad | ||
|
|
3f0a5f0f03 | ||
|
|
db53874ff4 | ||
|
|
d557bb879f |
@@ -6,6 +6,8 @@ class UserFilter(django_filters.FilterSet):
|
||||
last_name = django_filters.CharFilter(lookup_expr="icontains")
|
||||
first_name = django_filters.CharFilter(lookup_expr="icontains")
|
||||
email = django_filters.CharFilter(lookup_expr="icontains")
|
||||
id_card = django_filters.CharFilter(lookup_expr="icontains")
|
||||
mobile = django_filters.CharFilter(lookup_expr="icontains")
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Generated by Django 5.2 on 2025-07-15 20:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0016_user_is_admin"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="temporaryuser",
|
||||
name="t_id_card",
|
||||
field=models.CharField(
|
||||
blank=True, db_index=True, max_length=255, null=True, unique=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="temporaryuser",
|
||||
name="t_mobile",
|
||||
field=models.CharField(
|
||||
blank=True, db_index=True, max_length=255, null=True, unique=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="id_card",
|
||||
field=models.CharField(
|
||||
blank=True, db_index=True, max_length=255, null=True, unique=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="mobile",
|
||||
field=models.CharField(
|
||||
blank=True, db_index=True, max_length=255, null=True, unique=True
|
||||
),
|
||||
),
|
||||
]
|
||||
+12
-4
@@ -13,10 +13,14 @@ import pyotp
|
||||
class User(AbstractUser):
|
||||
address = models.CharField(max_length=255, blank=True)
|
||||
email = models.EmailField(blank=True, null=True, unique=True)
|
||||
mobile = models.CharField(max_length=255, blank=True, unique=True, null=True)
|
||||
mobile = models.CharField(
|
||||
max_length=255, blank=True, unique=True, null=True, db_index=True
|
||||
)
|
||||
designation = models.CharField(max_length=255, blank=True)
|
||||
acc_no = models.CharField(max_length=255, blank=True)
|
||||
id_card = models.CharField(max_length=255, blank=True, unique=True, null=True)
|
||||
id_card = models.CharField(
|
||||
max_length=255, blank=True, unique=True, null=True, db_index=True
|
||||
)
|
||||
verified = models.BooleanField(default=False)
|
||||
is_admin = models.BooleanField(default=False)
|
||||
dob = models.DateField(blank=True, null=True)
|
||||
@@ -46,10 +50,14 @@ class TemporaryUser(models.Model):
|
||||
t_last_name = models.CharField(max_length=255, blank=True)
|
||||
t_address = models.CharField(max_length=255, blank=True)
|
||||
t_email = models.EmailField(blank=True, null=True, unique=True)
|
||||
t_mobile = models.CharField(max_length=255, blank=True, unique=True, null=True)
|
||||
t_mobile = models.CharField(
|
||||
max_length=255, blank=True, unique=True, null=True, db_index=True
|
||||
)
|
||||
t_designation = models.CharField(max_length=255, blank=True)
|
||||
t_acc_no = models.CharField(max_length=255, blank=True)
|
||||
t_id_card = models.CharField(max_length=255, blank=True, unique=True, null=True)
|
||||
t_id_card = models.CharField(
|
||||
max_length=255, blank=True, unique=True, null=True, db_index=True
|
||||
)
|
||||
t_verified = models.BooleanField(default=False)
|
||||
t_dob = models.DateField(blank=True, null=True)
|
||||
t_terms_accepted = models.BooleanField(default=False)
|
||||
|
||||
+29
-1
@@ -1,9 +1,12 @@
|
||||
from knox.models import AuthToken
|
||||
from django.contrib.auth import authenticate
|
||||
from api.models import User, Atoll, Island, TemporaryUser
|
||||
from api.models import Atoll, Island, TemporaryUser
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
from rest_framework import serializers
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class IslandSerializer(serializers.ModelSerializer):
|
||||
@@ -21,6 +24,30 @@ class AtollSerializer(serializers.ModelSerializer):
|
||||
depth = 2
|
||||
|
||||
|
||||
class UserProfileUpdateSerializer(serializers.ModelSerializer):
|
||||
class Meta: # type: ignore
|
||||
model = User
|
||||
fields = (
|
||||
"email",
|
||||
"mobile",
|
||||
) # Only allow these fields
|
||||
|
||||
|
||||
class UserUpdateSerializer(serializers.ModelSerializer):
|
||||
class Meta: # type: ignore
|
||||
model = User
|
||||
fields = (
|
||||
"id_card",
|
||||
"mobile",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"address",
|
||||
"dob",
|
||||
"atoll",
|
||||
"island",
|
||||
)
|
||||
|
||||
|
||||
class CustomUserSerializer(serializers.ModelSerializer):
|
||||
"""serializer for the user object"""
|
||||
|
||||
@@ -80,6 +107,7 @@ class CustomReadOnlyUserSerializer(serializers.ModelSerializer):
|
||||
"username",
|
||||
"mobile",
|
||||
"address",
|
||||
"acc_no",
|
||||
"id_card",
|
||||
)
|
||||
depth = 1
|
||||
|
||||
+6
-3
@@ -12,12 +12,15 @@ from api.tasks import verify_user_with_person_api_task
|
||||
@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")
|
||||
payment_permissions = Permission.objects.filter(content_type__model="payment")
|
||||
topup_permissions = Permission.objects.filter(content_type__model="topup")
|
||||
payment_permissions = Permission.objects.filter(
|
||||
content_type__model="payment"
|
||||
).exclude(codename__startswith="delete_")
|
||||
topup_permissions = Permission.objects.filter(
|
||||
content_type__model="topup"
|
||||
).exclude(codename__startswith="delete_")
|
||||
|
||||
for permission in topup_permissions:
|
||||
instance.user_permissions.add(permission)
|
||||
|
||||
+8
-3
@@ -5,7 +5,7 @@ from knox import views as knox_views
|
||||
from .views import (
|
||||
LoginView,
|
||||
CreateTemporaryUserView,
|
||||
ManageUserView,
|
||||
UserprofileAPIView,
|
||||
KnoxTokenListApiView,
|
||||
ListUserView,
|
||||
UserDetailAPIView,
|
||||
@@ -21,13 +21,15 @@ from .views import (
|
||||
UpdateUserWalletView,
|
||||
VerifyOTPView,
|
||||
UserVerifyAPIView,
|
||||
UserUpdateAPIView,
|
||||
UserRejectAPIView,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("register/", CreateTemporaryUserView.as_view(), name="register"),
|
||||
path("register/verify/", VerifyOTPView.as_view(), name="verify-otp"),
|
||||
path("profile/", ManageUserView.as_view(), name="profile"),
|
||||
path("profile/", UserprofileAPIView.as_view(), name="profile"),
|
||||
path("login/", LoginView.as_view(), name="knox_login"),
|
||||
path("logout/", knox_views.LogoutView.as_view(), name="knox_logout"),
|
||||
path("logoutall/", knox_views.LogoutAllView.as_view(), name="knox_logoutall"),
|
||||
@@ -38,9 +40,12 @@ urlpatterns = [
|
||||
"update-wallet/<int:pk>/", UpdateUserWalletView.as_view(), name="update-wallet"
|
||||
),
|
||||
path("users/<int:pk>/", UserDetailAPIView.as_view(), name="user-detail"),
|
||||
path("users/<int:pk>/verify/", UserVerifyAPIView.as_view(), name="user-verify"),
|
||||
path("users/<int:pk>/update/", UserUpdateAPIView.as_view(), name="user-update"),
|
||||
path("users/filter/", filter_user, name="filter-users"),
|
||||
path("users/temp/filter/", filter_temporary_user, name="filter-temporary-users"),
|
||||
# User verification flow
|
||||
path("users/<int:pk>/verify/", UserVerifyAPIView.as_view(), name="user-verify"),
|
||||
path("users/<int:pk>/reject/", UserRejectAPIView.as_view(), name="user-reject"),
|
||||
path("healthcheck/", healthcheck, name="healthcheck"),
|
||||
path("test/", test_email, name="testemail"),
|
||||
path("atolls/", ListAtollView.as_view(), name="atolls"),
|
||||
|
||||
+27
-18
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from typing import List, TypedDict
|
||||
from typing import List, Optional, TypedDict
|
||||
import requests
|
||||
from decouple import config
|
||||
from api.models import User
|
||||
@@ -40,7 +40,9 @@ def reverse_dhivehi_string(input_str):
|
||||
|
||||
class MismatchResult(TypedDict):
|
||||
ok: bool
|
||||
mismatch_fields: List[str]
|
||||
mismatch_fields: Optional[List[str]]
|
||||
error: Optional[str]
|
||||
detail: Optional[str]
|
||||
|
||||
|
||||
def check_person_api_verification(
|
||||
@@ -63,20 +65,22 @@ def check_person_api_verification(
|
||||
raise ValueError(
|
||||
"PERSON_VERIFY_BASE_URL is not set in the environment variables."
|
||||
)
|
||||
print(id_card)
|
||||
response = requests.get(f"{PERSON_VERIFY_BASE_URL}/api/person/{id_card}")
|
||||
api_reponse = response.json()
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(
|
||||
f"Failed to fetch data from Person API for ID Card '{id_card}'. "
|
||||
f"Status Code: {response.status_code}, Response: {response.text}"
|
||||
)
|
||||
return {"ok": False, "mismatch_fields": ["api_error"]}
|
||||
api_data = response.json()
|
||||
if not api_data:
|
||||
logger.error(
|
||||
f"No data found in Person API for ID Card '{id_card}'. Response: {response.text}"
|
||||
)
|
||||
return {"ok": False, "mismatch_fields": ["no_data"]}
|
||||
return {
|
||||
"ok": False,
|
||||
"mismatch_fields": None,
|
||||
"error": response.json()["error"] if "error" in response.json() else None,
|
||||
"detail": response.json()["detail"]
|
||||
if "detail" in response.json()
|
||||
else None,
|
||||
}
|
||||
|
||||
# Initialize a list to hold fields that do not match
|
||||
mismatch_fields = []
|
||||
@@ -86,12 +90,12 @@ def check_person_api_verification(
|
||||
user_dob_iso = user_data.dob.isoformat() if user_data.dob else None
|
||||
|
||||
# Prepare API data for comparison
|
||||
api_nic = api_data.get("nic")
|
||||
api_name = api_data.get("name_en")
|
||||
api_house_name = api_data.get("house_name_en")
|
||||
api_dob = api_data.get("dob")
|
||||
api_atoll = api_data.get("atoll_en")
|
||||
api_island_name = api_data.get("island_name_en")
|
||||
api_nic = api_reponse.get("nic")
|
||||
api_name = api_reponse.get("name_en")
|
||||
api_house_name = api_reponse.get("house_name_en")
|
||||
api_dob = api_reponse.get("dob")
|
||||
api_atoll = api_reponse.get("atoll_en")
|
||||
api_island_name = api_reponse.get("island_name_en")
|
||||
|
||||
# Perform comparisons and identify mismatches
|
||||
if user_data.id_card != api_nic:
|
||||
@@ -134,6 +138,11 @@ def check_person_api_verification(
|
||||
)
|
||||
|
||||
if mismatch_fields:
|
||||
return {"ok": False, "mismatch_fields": mismatch_fields}
|
||||
return {
|
||||
"ok": False,
|
||||
"mismatch_fields": mismatch_fields,
|
||||
"error": None,
|
||||
"detail": None,
|
||||
}
|
||||
else:
|
||||
return {"ok": True, "mismatch_fields": []}
|
||||
return {"ok": True, "mismatch_fields": [], "error": None, "detail": None}
|
||||
|
||||
+114
-22
@@ -7,6 +7,7 @@ from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||
from api.filters import UserFilter
|
||||
from api.mixins import StaffEditorPermissionMixin
|
||||
from api.models import User, Atoll, Island, TemporaryUser
|
||||
from api.notifications import send_sms
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
@@ -17,6 +18,7 @@ from api.serializers import (
|
||||
CustomUserByWalletBalanceSerializer,
|
||||
OTPVerificationSerializer,
|
||||
TemporaryUserSerializer,
|
||||
UserUpdateSerializer,
|
||||
)
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
@@ -40,6 +42,7 @@ from .serializers import (
|
||||
CustomUserSerializer,
|
||||
CustomReadOnlyUserSerializer,
|
||||
CustomReadOnlyUserByIDCardSerializer,
|
||||
UserProfileUpdateSerializer,
|
||||
)
|
||||
|
||||
ID_CARD_PATTERN = r"^[A-Z]{1,2}[0-9]{6,7}$"
|
||||
@@ -305,17 +308,56 @@ class LoginView(KnoxLoginView):
|
||||
return Response({"message": message}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class ManageUserView(generics.RetrieveUpdateAPIView):
|
||||
"""Manage the authenticated user"""
|
||||
class UserprofileAPIView(generics.RetrieveUpdateAPIView):
|
||||
"""Retrieve user api view"""
|
||||
|
||||
serializer_class = CustomUserSerializer
|
||||
queryset = User.objects.all()
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""Return the serializer class based on the request method"""
|
||||
if self.request.method == "GET":
|
||||
return CustomReadOnlyUserSerializer
|
||||
elif self.request.method == "PUT" or self.request.method == "PATCH":
|
||||
return UserProfileUpdateSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def get_object(self):
|
||||
"""Retrieve and return authenticated user"""
|
||||
return self.request.user
|
||||
|
||||
|
||||
class UserUpdateAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||
serializer_class = UserUpdateSerializer
|
||||
queryset = User.objects.all()
|
||||
lookup_field = "pk"
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
user_id = kwargs.get("pk")
|
||||
user = get_object_or_404(User, pk=user_id)
|
||||
if user.is_superuser:
|
||||
return Response(
|
||||
{"message": "You cannot update a superuser."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
if request.user != user and (
|
||||
not request.user.is_authenticated
|
||||
or not getattr(request.user, "is_admin", False)
|
||||
):
|
||||
return Response(
|
||||
{"message": "You are not authorized to update this user."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
serializer = self.get_serializer(
|
||||
user,
|
||||
data=request.data,
|
||||
partial=True,
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user.save()
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
|
||||
class KnoxTokenListApiView(
|
||||
StaffEditorPermissionMixin,
|
||||
generics.ListAPIView,
|
||||
@@ -343,9 +385,9 @@ class ListUserView(StaffEditorPermissionMixin, generics.ListAPIView):
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
if user.is_authenticated and user.is_staff:
|
||||
return User.objects.all()
|
||||
return User.objects.filter(is_staff=False)
|
||||
if user.is_authenticated and getattr(user, "is_admin"):
|
||||
return User.objects.filter(is_superuser=False)
|
||||
return User.objects.none()
|
||||
|
||||
|
||||
class UserVerifyAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||
@@ -364,30 +406,70 @@ class UserVerifyAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||
{"message": "You are not authorized to update this user."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
serializer = self.get_serializer(user, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
verified_person = check_person_api_verification(
|
||||
user_data=user, id_card=user.id_card
|
||||
)
|
||||
if not verified_person["ok"]:
|
||||
if user.verified:
|
||||
return Response(
|
||||
{
|
||||
"message": "User verification failed. Please check sarlink user details.",
|
||||
"mismatch_fields": verified_person["mismatch_fields"],
|
||||
},
|
||||
{"message": "User is already verified."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
if verified_person["mismatch_fields"]:
|
||||
serializer = self.get_serializer(user, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
result = check_person_api_verification(user_data=user, id_card=user.id_card)
|
||||
if not result["ok"]:
|
||||
return Response(
|
||||
{
|
||||
"message": "User verification failed due to mismatched fields.",
|
||||
"mismatch_fields": verified_person["mismatch_fields"],
|
||||
},
|
||||
result,
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
if result["mismatch_fields"]:
|
||||
return Response(
|
||||
result,
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
user.verified = True
|
||||
user.save()
|
||||
return Response({"message": "User verification status updated."})
|
||||
return Response({"message": "User successfully verified."})
|
||||
|
||||
|
||||
class UserRejectAPIView(StaffEditorPermissionMixin, generics.DestroyAPIView):
|
||||
serializer_class = CustomUserSerializer
|
||||
queryset = User.objects.all()
|
||||
lookup_field = "pk"
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
rejection_details = request.data.get("rejection_details", "")
|
||||
if not rejection_details:
|
||||
return Response(
|
||||
{"message": "Rejection details are required."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
user_id = kwargs.get("pk")
|
||||
user = get_object_or_404(User, pk=user_id)
|
||||
mobile_number = user.mobile
|
||||
if not mobile_number:
|
||||
return Response(
|
||||
{"message": "User does not have a mobile number."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
if user.is_superuser:
|
||||
return Response(
|
||||
{"message": "You cannot remove a superuser."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
if request.user != user and (
|
||||
not request.user.is_authenticated
|
||||
or not getattr(request.user, "is_admin", False)
|
||||
):
|
||||
return Response(
|
||||
{"message": "You are not authorized to reject this user."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
user.delete()
|
||||
t_user = get_object_or_404(TemporaryUser, t_mobile=user.mobile)
|
||||
t_user.delete()
|
||||
send_sms(message=rejection_details, mobile=mobile_number)
|
||||
return Response(
|
||||
{"message": "User successfully rejected."},
|
||||
status=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
@@ -459,6 +541,16 @@ class UserDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
user = request.user
|
||||
if (
|
||||
user != instance
|
||||
and not getattr(user, "is_admin", False)
|
||||
and not user.is_superuser
|
||||
):
|
||||
return Response(
|
||||
{"message": "You are not authorized to view this user's details."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
serializer = self.get_serializer(instance)
|
||||
data = serializer.data
|
||||
|
||||
|
||||
@@ -105,6 +105,30 @@ class ListCreatePaymentView(StaffEditorPermissionMixin, generics.ListCreateAPIVi
|
||||
serializer = PaymentSerializer(payment)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
all_payments = request.query_params.get("all_payments", "false").lower() in [
|
||||
"true",
|
||||
"1",
|
||||
"yes",
|
||||
]
|
||||
if (
|
||||
request.user.is_authenticated
|
||||
and getattr(request.user, "is_admin")
|
||||
and bool(all_payments)
|
||||
):
|
||||
pass
|
||||
else:
|
||||
queryset = queryset.filter(user=request.user)
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class PaymentDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
||||
queryset = Payment.objects.select_related("user").all()
|
||||
|
||||
Reference in New Issue
Block a user