From 3957ca0ea4a040020628c5221e8f8dfbbc70c3b0 Mon Sep 17 00:00:00 2001 From: i701 Date: Tue, 10 Jun 2025 16:42:13 +0500 Subject: [PATCH] Add user verification endpoint and logic; implement check against Person API --- api/urls.py | 2 + api/utils.py | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++ api/views.py | 49 +++++++++++++++++++++++ 3 files changed, 161 insertions(+) diff --git a/api/urls.py b/api/urls.py index 393a139..828d5e3 100644 --- a/api/urls.py +++ b/api/urls.py @@ -20,6 +20,7 @@ from .views import ( filter_temporary_user, UpdateUserWalletView, VerifyOTPView, + UserVerifyAPIView, ) @@ -37,6 +38,7 @@ urlpatterns = [ "update-wallet//", UpdateUserWalletView.as_view(), name="update-wallet" ), path("users//", UserDetailAPIView.as_view(), name="user-detail"), + path("users//verify/", UserVerifyAPIView.as_view(), name="user-verify"), path("users/filter/", filter_user, name="filter-users"), path("users/temp/filter/", filter_temporary_user, name="filter-temporary-users"), path("healthcheck/", healthcheck, name="healthcheck"), diff --git a/api/utils.py b/api/utils.py index 6242a6d..6e2e92e 100644 --- a/api/utils.py +++ b/api/utils.py @@ -1,3 +1,12 @@ +import logging +from typing import List, TypedDict +import requests +from decouple import config +from api.models import User + +logger = logging.getLogger(__name__) + + def reverse_dhivehi_string(input_str): """ Reverses a Dhivehi string while preserving character composition. @@ -27,3 +36,104 @@ def reverse_dhivehi_string(input_str): i += 1 return "".join(corrected_chars) + + +class MismatchResult(TypedDict): + ok: bool + mismatch_fields: List[str] + + +def check_person_api_verification( + user_data: User, + id_card: str | None, +) -> MismatchResult: + """ + Compares user data with data from the Person API and returns a verification result. + + :param user_data: A dictionary containing user information. Expected keys: + 'id_card', 'first_name', 'last_name', 'address', 'dob', + 'atoll_name', 'island_name'. + :param api_data: A dictionary containing data from the Person API. Expected keys: + 'nic', 'name_en', 'house_name_en', 'dob', 'atoll_en', + 'island_name_en'. + :return: A dictionary with 'ok' (boolean) and 'mismatch_fields' (list of strings). + """ + PERSON_VERIFY_BASE_URL = config("PERSON_VERIFY_BASE_URL", default="") # type: ignore + if not PERSON_VERIFY_BASE_URL: + 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}") + 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"]} + + # Initialize a list to hold fields that do not match + mismatch_fields = [] + + # Prepare user data for comparison + user_full_name = f"{user_data.first_name} {user_data.last_name}".strip() + 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") + + # Perform comparisons and identify mismatches + if user_data.id_card != api_nic: + mismatch_fields.append("id_card") + logger.debug(f"ID Card mismatch: User '{user_data.id_card}' vs API '{api_nic}'") + + if user_full_name != api_name: + mismatch_fields.append("name") + logger.debug(f"Name mismatch: User '{user_full_name}' vs API '{api_name}'") + + if user_data.address != api_house_name: + mismatch_fields.append("address") + logger.debug( + f"Address mismatch: User '{user_data.address}' vs API '{api_house_name}'" + ) + + # API DOB might include time component, so split it + api_dob_date_only = api_dob.split("T")[0] if api_dob else None + if user_dob_iso != api_dob_date_only: + mismatch_fields.append("dob") + logger.debug( + f"DOB mismatch: User '{user_dob_iso}' vs API '{api_dob_date_only}'" + ) + + # Use .strip() for atoll and island names due to potential whitespace + user_atoll_name = user_data.atoll.name if user_data.atoll is not None else None + api_atoll_stripped = api_atoll.strip() if api_atoll else None + if user_atoll_name != api_atoll_stripped: + mismatch_fields.append("atoll") + logger.debug( + f"Atoll mismatch: User '{user_atoll_name}' vs API '{api_atoll_stripped}'" + ) + + user_island_name = user_data.island.name if user_data.island is not None else None + api_island_name_stripped = api_island_name.strip() if api_island_name else None + if user_island_name != api_island_name_stripped: + mismatch_fields.append("island_name") + logger.debug( + f"Island Name mismatch: User '{user_island_name}' vs API '{api_island_name_stripped}'" + ) + + if mismatch_fields: + return {"ok": False, "mismatch_fields": mismatch_fields} + else: + return {"ok": True, "mismatch_fields": []} diff --git a/api/views.py b/api/views.py index c981710..073f178 100644 --- a/api/views.py +++ b/api/views.py @@ -32,6 +32,7 @@ from django.core.mail import send_mail from django.db.models import Q from api.notifications import send_otp from .tasks import add +from .utils import check_person_api_verification # local apps import from .serializers import ( @@ -344,6 +345,54 @@ class ListUserView(StaffEditorPermissionMixin, generics.ListAPIView): filterset_class = UserFilter queryset = User.objects.all() + 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) + + +class UserVerifyAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView): + serializer_class = CustomUserSerializer + 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 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) + verified_person = check_person_api_verification( + user_data=user, id_card=user.id_card + ) + if not verified_person["ok"]: + return Response( + { + "message": "User verification failed. Please check sarlink user details.", + "mismatch_fields": verified_person["mismatch_fields"], + }, + status=status.HTTP_400_BAD_REQUEST, + ) + if verified_person["mismatch_fields"]: + return Response( + { + "message": "User verification failed due to mismatched fields.", + "mismatch_fields": verified_person["mismatch_fields"], + }, + status=status.HTTP_400_BAD_REQUEST, + ) + user.verified = True + user.save() + return Response({"message": "User verification status updated."}) + @api_view(["GET"]) def filter_user(request):