# django imports from django.contrib.auth import login # rest_framework imports from django.core.exceptions import ObjectDoesNotExist from rest_framework import generics, permissions from rest_framework.authtoken.serializers import AuthTokenSerializer from api.filters import UserFilter from api.mixins import StaffEditorPermissionMixin from api.permissions import IsAdminOrStaffPermission, user_is_admin from api.models import User, Atoll, Island, TemporaryUser, IdUploadToken from api.notifications import send_sms from rest_framework.response import Response from rest_framework import status from rest_framework.exceptions import ValidationError from rest_framework.parsers import MultiPartParser, FormParser from rest_framework.decorators import api_view, permission_classes from decouple import config import os from api.serializers import ( AtollSerializer, IslandSerializer, OTPVerificationSerializer, TemporaryUserSerializer, UserUpdateSerializer, UserAgreementSerializer, ) from django.shortcuts import get_object_or_404 from django.utils import timezone # knox imports from knox.views import LoginView as KnoxLoginView from knox.models import AuthToken from django_filters.rest_framework import DjangoFilterBackend from typing import cast, Dict, Any from django.db.models import Q from api.notifications import send_otp from .utils import check_person_api_verification import uuid from .helpers import ErrorMessages, validate_required_fields, validate_unique_fields, validate_patterns, calculate_age # local apps import from .serializers import ( KnoxTokenSerializer, AuthSerializer, CustomUserSerializer, CustomReadOnlyUserSerializer, UserProfileUpdateSerializer, ) @api_view(["GET"]) def healthcheck(request): return Response({"status": "Good"}, status=status.HTTP_200_OK) FRONTEND_URL = config("FRONTEND_URL", default="https://portal.sarlink.net") # File types accepted for the ID card / passport upload. ID_PHOTO_CONTENT_TYPES = [ "image/jpeg", "image/jpg", "image/png", "application/pdf", ] # Default editable body of the "please upload your ID" SMS. The greeting # ("Dear ,"), the secure upload link, and the "- SAR Link" signature are # always added by request_id_upload() and are NOT part of the editable body. DEFAULT_ID_UPLOAD_BODY = ( "We're sorry, but your SAR Link account registration could not be " "approved automatically.\n\n" "Please upload a clear photo of your ID card / passport at the " "link below." ) def request_id_upload(user, message_body=""): """ Mark a user as needing an ID/passport photo, mint a fresh magic-link token, and SMS them the upload link. Used both by the automatic auto-verify-fail path and by the admin "Request ID Upload" action. `message_body` is the admin-editable middle of the message; when blank the default body is used. The greeting, the one-time upload link, and the signature are always appended here so the link is never exposed to the admin. """ token = IdUploadToken.issue(user) user.set_status(User.STATUS_ID_REQUIRED) full_name = f"{user.first_name} {user.last_name}".strip() or "Customer" link = f"{FRONTEND_URL}/upload-id?token={token.key}" body = (message_body or "").strip() or DEFAULT_ID_UPLOAD_BODY message = f"Dear {full_name},\n\n{body}\n\n{link}\n\n- SAR Link" if user.mobile: send_sms(mobile=user.mobile, message=message) return token class CreateTemporaryUserView(generics.CreateAPIView): serializer_class = TemporaryUserSerializer permission_classes = (permissions.AllowAny,) queryset = TemporaryUser.objects.all() throttle_classes = [] def post(self, request, *args, **kwargs): # Extract data once data = request.data # Validate required fields required_error = validate_required_fields(data) if required_error: return required_error # Parse DOB dob_str = data.get("dob") try: dob = timezone.datetime.strptime(str(dob_str), "%Y-%m-%d").date() # pyright: ignore[reportAttributeAccessIssue] except ValueError: return Response({"message": "Invalid date format for DOB. Use YYYY-MM-DD."}, status=400) # Check age age = calculate_age(dob) if age < 18: return Response({"message": ErrorMessages.UNDERAGE_ERROR}, status=400) # Validate uniqueness uniqueness_error = validate_unique_fields( username=data.get("username"), mobile=data.get("mobile"), id_card=data.get("id_card"), ) if uniqueness_error: return uniqueness_error # Validate patterns pattern_error = validate_patterns( id_card=data.get("id_card"), mobile=data.get("mobile"), acc_no=data.get("acc_no"), ) if pattern_error: return pattern_error # Fetch related objects atoll_id = data.get("atoll") island_id = data.get("island") try: atoll = Atoll.objects.get(id=atoll_id) island = Island.objects.get(id=island_id) except ObjectDoesNotExist as e: model_name = "Atoll" if isinstance(e, Atoll.DoesNotExist) else "Island" return Response({"message": f"{model_name} not found."}, status=404) # Create user temp_user = TemporaryUser.objects.create( t_first_name=data.get("firstname"), t_last_name=data.get("lastname"), t_username=str(data.get("username")), t_email=None, t_address=data.get("address"), t_mobile=data.get("mobile"), t_acc_no=data.get("acc_no"), t_id_card=data.get("id_card"), t_dob=dob, t_atoll=atoll, t_island=island, t_terms_accepted=data.get("terms_accepted"), t_policy_accepted=data.get("policy_accepted"), ) # Generate and send OTP otp_expiry = timezone.now() + timezone.timedelta(minutes=3) #type: ignore formatted_time = otp_expiry.strftime("%d/%m/%Y %H:%M:%S") otp = temp_user.generate_otp() send_otp( str(temp_user.t_mobile), f"Your Registration SARLink OTP: {otp}. \nExpires at {formatted_time}. \n\n- SAR Link", ) # Return success serializer = self.get_serializer(temp_user) headers = self.get_success_headers(serializer.data) return Response( serializer.data, status=status.HTTP_201_CREATED, headers=headers ) class VerifyOTPView(generics.GenericAPIView): permission_classes = (permissions.AllowAny,) serializer_class = OTPVerificationSerializer throttle_classes = [] def post(self, request): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) data = request.data mobile = data.get("mobile") t_user = get_object_or_404(TemporaryUser, t_mobile=mobile) if not t_user: return Response({"message": "User not found."}, status=404) if User.objects.filter(username=t_user.t_username).exists(): return Response( {"message": "User with this mobile number already registered."}, status=400, ) try: temp_user = TemporaryUser.objects.get(t_mobile=data["mobile"]) except TemporaryUser.DoesNotExist: return Response({"message": "User not found."}, status=404) if temp_user.is_expired(): return Response({"message": "OTP expired."}, status=400) if not temp_user.verify_otp(data["otp"]): return Response({"message": "Invalid OTP."}, status=400) # Create real user user = User.objects.create_user( first_name=temp_user.t_first_name, last_name=temp_user.t_last_name, username=str(temp_user.t_username), password="", address=temp_user.t_address, mobile=temp_user.t_mobile, acc_no=temp_user.t_acc_no, id_card=temp_user.t_id_card, dob=temp_user.t_dob, verified=temp_user.t_verified, atoll=temp_user.t_atoll, island=temp_user.t_island, terms_accepted=temp_user.t_terms_accepted, policy_accepted=temp_user.t_policy_accepted, ) temp_user.otp_verified = True temp_user.save() if temp_user.t_verified: user.set_status(User.STATUS_VERIFIED) send_sms( t_user.t_mobile, f"Dear {temp_user.t_first_name} {temp_user.t_last_name}, \n\nYour account has been successfully verified. \n\nYou can now manage your devices and make payments through our portal at {FRONTEND_URL}. \n\n - SAR Link", ) return Response( { "message": "User created successfully.", "verified": True, "status": user.status, } ) # Auto-verification failed -> ask the user to upload their ID/passport. token = request_id_upload(user) return Response( { "message": "User created successfully.", "verified": False, "status": user.status, "upload_token": token.key, } ) class ResendRegistrationOTPView(generics.GenericAPIView): """Resend the registration OTP for a pending (not-yet-verified) signup. Handles the "registered but closed the browser before entering the OTP" case: logging in again finds the TemporaryUser and resends a fresh code instead of dead-ending at the signup form (which rejects the existing mobile as already taken). """ permission_classes = (permissions.AllowAny,) throttle_classes = [] def post(self, request, *args, **kwargs): mobile = request.data.get("mobile", "") t_user = TemporaryUser.objects.filter(t_mobile=mobile).first() if ( not t_user or t_user.otp_verified or User.objects.filter(mobile=mobile).exists() ): return Response( {"message": "No pending registration for this number."}, status=status.HTTP_400_BAD_REQUEST, ) # Reset the OTP validity window (is_expired() is based on created_at) # and resend a fresh code. t_user.created_at = timezone.now() t_user.save(update_fields=["created_at"]) otp = t_user.generate_otp() otp_expiry = timezone.now() + timezone.timedelta(minutes=3) formatted_time = otp_expiry.strftime("%d/%m/%Y %H:%M:%S") send_otp( str(t_user.t_mobile), f"Your Registration SARLink OTP: {otp}. \nExpires at {formatted_time}. \n\n- SAR Link", ) return Response({"message": "OTP resent.", "t_username": t_user.t_username}) class LoginView(KnoxLoginView): # login view extending KnoxLoginView serializer_class = AuthSerializer permission_classes = (permissions.AllowAny,) throttle_scope = "login" def post(self, request, format=None): try: serializer = AuthTokenSerializer(data=request.data) serializer.is_valid(raise_exception=True) user = cast(Dict[str, Any], serializer.validated_data)["user"] login(request, user) response = super(LoginView, self).post(request, format=None) return response except ValidationError as e: message = "Unable to log in with provided credentials." if ( hasattr(e, "detail") and isinstance(e.detail, list) and len(e.detail) > 0 ): message = e.detail[0] return Response({"message": message}, status=status.HTTP_400_BAD_REQUEST) class UserprofileAPIView(generics.RetrieveUpdateAPIView): """Retrieve user api view""" 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(generics.UpdateAPIView): permission_classes = [IsAdminOrStaffPermission] 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 user_is_admin(request.user): 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 AgreementUpdateAPIView(generics.UpdateAPIView): permission_classes = [IsAdminOrStaffPermission] serializer_class = UserAgreementSerializer 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 user_is_admin(request.user): 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, ) agreement = request.data.get("agreement") if not agreement: return Response( {"message": "Agreement file is required."}, status=status.HTTP_400_BAD_REQUEST, ) if agreement.size > 10 * 1024 * 1024: # 5 MB limit return Response( {"message": "File size exceeds 10 MB limit."}, status=status.HTTP_400_BAD_REQUEST, ) if agreement.content_type not in [ "application/pdf", ]: return Response( {"message": "Invalid file type. Only PDF files are allowed."}, status=status.HTTP_400_BAD_REQUEST, ) # rename the file name to a random UUID followed by user_id agreement.name = f"{uuid.uuid4()}_{user_id}_agreement.pdf" if agreement: user.agreement = agreement serializer.is_valid(raise_exception=True) user.save() return super().update(request, *args, **kwargs) class KnoxTokenListApiView( StaffEditorPermissionMixin, generics.ListAPIView, ): # Create user API view serializer_class = KnoxTokenSerializer permission_classes = (permissions.IsAuthenticated,) queryset = AuthToken.objects.all() def get(self, request, *args, **kwargs): user_id = getattr(request.user, "id", None) if user_id is None: return Response({"error": "User ID not found"}, status=400) queryset = AuthToken.objects.filter(user_id=user_id) data = KnoxTokenSerializer(queryset, many=True).data return Response({"data": data}) class ListUserView(StaffEditorPermissionMixin, generics.ListAPIView): serializer_class = CustomReadOnlyUserSerializer filter_backends = [DjangoFilterBackend] #type: ignore filterset_fields = "__all__" filterset_class = UserFilter queryset = User.objects.all() def get_queryset(self): user = self.request.user if user.is_authenticated and getattr(user, "is_admin"): return User.objects.filter(is_superuser=False) return User.objects.none() class UserVerifyAPIView(generics.UpdateAPIView): permission_classes = [IsAdminOrStaffPermission] 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 user_is_admin(request.user): return Response( {"message": "You are not authorized to update this user."}, status=status.HTTP_403_FORBIDDEN, ) if user.verified: return Response( {"message": "User is already verified."}, status=status.HTTP_400_BAD_REQUEST, ) 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) # The verification system might not have the records of every user hence can be skipped if not found and verify directly. if result.get("error") == "Not Found": user.verified = True user.save() return Response( { "message": "User not found in the verification system. User marked as verified." }, status=status.HTTP_404_NOT_FOUND, ) if not result["ok"]: return Response( 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 successfully verified."}) class UserRejectAPIView(generics.DestroyAPIView): permission_classes = [IsAdminOrStaffPermission] 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 user_is_admin(request.user): return Response( {"message": "You are not authorized to reject this user."}, status=status.HTTP_403_FORBIDDEN, ) full_name = f"{user.first_name} {user.last_name}".strip() or "Customer" rejection_message = ( f"Dear {full_name}, \n\n" "We're sorry, but your SAR Link account registration could not be " "approved at this time. \n\n" f"Reason: {rejection_details} \n\n" "Register again at " f"{FRONTEND_URL}, or contact us for assistance. \n\n" " - SAR Link" ) user.delete() t_user = get_object_or_404(TemporaryUser, t_mobile=user.mobile) t_user.delete() send_sms(message=rejection_message, mobile=mobile_number) return Response( {"message": "User successfully rejected."}, status=status.HTTP_204_NO_CONTENT, ) class RequestIdUploadAPIView(generics.GenericAPIView): """Admin action: ask a user to (re)upload their ID/passport photo.""" permission_classes = [IsAdminOrStaffPermission] queryset = User.objects.all() lookup_field = "pk" def post(self, request, *args, **kwargs): user = get_object_or_404(User, pk=kwargs.get("pk")) if user.is_superuser: return Response( {"message": "You cannot modify a superuser."}, status=status.HTTP_403_FORBIDDEN, ) if not user.mobile: return Response( {"message": "User does not have a mobile number."}, status=status.HTTP_400_BAD_REQUEST, ) message_body = request.data.get("message", "") request_id_upload(user, message_body=message_body) return Response({"message": "ID upload request sent to the user."}) class IdUploadTokenInfoAPIView(generics.GenericAPIView): """Public: validate a magic-link token and return whose upload it is.""" permission_classes = [permissions.AllowAny] def get(self, request, *args, **kwargs): key = request.query_params.get("token", "") token = IdUploadToken.objects.filter(key=key).select_related("user").first() if not token or not token.is_valid(): return Response( {"message": "This upload link is invalid or has expired."}, status=status.HTTP_400_BAD_REQUEST, ) user = token.user return Response( { "first_name": user.first_name, "last_name": user.last_name, "status": user.status, } ) class IdUploadStartAPIView(generics.GenericAPIView): """Public: mint a fresh upload token for a mobile whose account needs an ID. Lets the login flow send an `id_required` user straight to the upload page instead of a "pending verification" dead-end. Only works for accounts actually in the id_required state. """ permission_classes = [permissions.AllowAny] def post(self, request, *args, **kwargs): mobile = request.data.get("mobile", "") user = User.objects.filter(mobile=mobile).first() if not user or user.status != User.STATUS_ID_REQUIRED: return Response( {"message": "No ID upload is pending for this number."}, status=status.HTTP_400_BAD_REQUEST, ) token = IdUploadToken.issue(user) return Response({"token": token.key}) class IdUploadSubmitAPIView(generics.GenericAPIView): """Public: accept the ID/passport photo for a valid token.""" permission_classes = [permissions.AllowAny] parser_classes = [MultiPartParser, FormParser] def post(self, request, *args, **kwargs): key = request.data.get("token", "") token = IdUploadToken.objects.filter(key=key).select_related("user").first() if not token or not token.is_valid(): return Response( {"message": "This upload link is invalid or has expired."}, status=status.HTTP_400_BAD_REQUEST, ) photo = request.data.get("id_card_photo") if not photo: return Response( {"message": "An ID card / passport photo is required."}, status=status.HTTP_400_BAD_REQUEST, ) if photo.size > 10 * 1024 * 1024: return Response( {"message": "File size exceeds the 10 MB limit."}, status=status.HTTP_400_BAD_REQUEST, ) if getattr(photo, "content_type", "") not in ID_PHOTO_CONTENT_TYPES: return Response( { "message": "Invalid file type. Please upload a JPG, PNG or PDF file." }, status=status.HTTP_400_BAD_REQUEST, ) user = token.user ext = os.path.splitext(photo.name)[1].lower() or ".jpg" photo.name = f"{uuid.uuid4()}_{user.id}_id_card{ext}" user.id_card_photo = photo user.set_status(User.STATUS_ID_SUBMITTED, save=False) user.save() # Single-use: burn the token so the link can't be reused for another # upload. A fresh link is minted if an admin requests another upload. token.used_at = timezone.now() token.save(update_fields=["used_at"]) return Response({"message": "Your ID has been submitted for review."}) @api_view(["GET"]) def filter_user(request): id_card = request.GET.get("id_card", "").strip() or None mobile = request.GET.get("mobile", "").strip() or None if not id_card and not mobile: return Response({"ok": False}) filters = Q() if id_card and mobile: filters = Q(id_card=id_card) & Q(mobile=mobile) elif id_card: filters = Q(id_card=id_card) elif mobile: filters = Q(mobile=mobile) user = User.objects.only("id", "verified", "status").filter(filters).first() print(f"Querying with filters: {filters}") print(f"Found user: {user}") return Response( {"ok": True, "verified": user.verified, "status": user.status} if user else {"ok": False, "verified": False} ) @api_view(["GET"]) def filter_temporary_user(request): id_card = request.GET.get("id_card", "").strip() or None mobile = request.GET.get("mobile", "").strip() or None if not id_card and not mobile: return Response({"ok": False}) filters = Q() if id_card and mobile: filters |= Q(t_id_card=id_card) & Q(t_mobile=mobile) elif id_card: filters |= Q(t_id_card=id_card) elif mobile: filters |= Q(t_mobile=mobile) user = ( TemporaryUser.objects.only("t_id", "otp_verified", "t_verified") .filter(filters) .first() ) print(f"Querying with filters: {filters}") print(f"Found temporary user: {user}") return Response( {"ok": True, "otp_verified": user.otp_verified, "t_verified": user.t_verified} if user else {"ok": False, "otp_verified": False, "t_verified": False} ) class UserDetailAPIView(generics.RetrieveAPIView): permission_classes = [IsAdminOrStaffPermission] queryset = User.objects.all() serializer_class = CustomReadOnlyUserSerializer lookup_field = "pk" def retrieve(self, request, *args, **kwargs): instance = self.get_object() user = request.user if user != instance and not user_is_admin(user): 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 # Customize the response format return Response(data) class CreateAtollView(StaffEditorPermissionMixin, generics.CreateAPIView): 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 ListAtollView(generics.ListAPIView): permission_classes = (permissions.AllowAny,) serializer_class = AtollSerializer queryset = Atoll.objects.all() throttle_classes = () # override throttling 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) class PersonVerifyAPIView(generics.GenericAPIView): """ Admin-gated proxy to the external Person verification API. The SPA frontend can no longer call the external person-verify service directly (it would leak an internal infra host to the browser), so the backend owns the integration. Returns the upstream JSON as-is. This view has no model of its own, so it uses ``IsAdminOrStaffPermission`` rather than the model-based ``StaffEditorPermissionMixin`` (which would crash dereferencing ``view.queryset.model``). GET /api/auth/person// """ permission_classes = [IsAdminOrStaffPermission] def get(self, request, id_card: str, *args, **kwargs): import requests from decouple import config PERSON_VERIFY_BASE_URL = config("PERSON_VERIFY_BASE_URL", default="") # type: ignore if not PERSON_VERIFY_BASE_URL: return Response( {"detail": "PERSON_VERIFY_BASE_URL is not set."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) try: upstream = requests.get( f"{PERSON_VERIFY_BASE_URL}/api/person/{id_card}", timeout=10 ) except requests.RequestException as exc: return Response( {"detail": f"Failed to reach person verification service: {exc}"}, status=status.HTTP_502_BAD_GATEWAY, ) try: data = upstream.json() except ValueError: return Response( {"detail": "Invalid response from person verification service."}, status=status.HTTP_502_BAD_GATEWAY, ) return Response(data, status=upstream.status_code)