Build and Push Docker Images / Build and Push Docker Images (push) Failing after 5s
867 lines
31 KiB
Python
867 lines
31 KiB
Python
# 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 <name>,"), 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/<id_card>/
|
|
"""
|
|
|
|
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)
|