diff --git a/apps/users/api/views.py b/apps/users/api/views.py index d4fefb0..d9a98c2 100644 --- a/apps/users/api/views.py +++ b/apps/users/api/views.py @@ -125,12 +125,12 @@ class SendOTPView(APIView): serializer = SendOTPSerializer(data=request.data) serializer.is_valid(raise_exception=True) - generate_and_send_otp( + payload = generate_and_send_otp( mobile=serializer.validated_data["mobile"], mode=serializer.validated_data["mode"] ) - return Response({"detail": "OTP sent successfully"}, status=status.HTTP_200_OK) + return Response(payload, status=status.HTTP_200_OK) class LoginView(APIView): diff --git a/apps/users/services/auth.py b/apps/users/services/auth.py index 0be3017..b8ee019 100644 --- a/apps/users/services/auth.py +++ b/apps/users/services/auth.py @@ -1,5 +1,6 @@ import random import string +from datetime import timedelta from django.contrib.auth import get_user_model, password_validation from django.core.exceptions import ValidationError as DjangoValidationError @@ -18,6 +19,7 @@ User = get_user_model() USER_ALREADY_EXISTS_MESSAGE = "User already exists." PASSWORD_REUSE_MESSAGE = "\u0631\u0645\u0632 \u0639\u0628\u0648\u0631 \u062c\u062f\u06cc\u062f \u0646\u0628\u0627\u06cc\u062f \u0628\u0627 \u0631\u0645\u0632 \u0639\u0628\u0648\u0631 \u0642\u0628\u0644\u06cc \u06cc\u06a9\u0633\u0627\u0646 \u0628\u0627\u0634\u062f." +OTP_EXPIRY_SECONDS = 120 def _validate_new_password(password, *, user, field_name): @@ -90,9 +92,15 @@ def generate_and_send_otp(mobile, mode): verification_code = "".join(random.choices(string.digits, k=5)) redis_conn = get_redis_connection("default") - redis_conn.setex(f"verification_code:{mobile}", 120, verification_code) + redis_conn.setex(f"verification_code:{mobile}", OTP_EXPIRY_SECONDS, verification_code) send_verification_sms.delay(mobile, verification_code) + expires_at = timezone.now() + timedelta(seconds=OTP_EXPIRY_SECONDS) + return { + "detail": "OTP sent successfully", + "expires_in_seconds": OTP_EXPIRY_SECONDS, + "expires_at": expires_at.isoformat(), + } def login_with_password(mobile, password, request=None): diff --git a/apps/users/tests/test_api_views.py b/apps/users/tests/test_api_views.py index 1818c15..833fd70 100644 --- a/apps/users/tests/test_api_views.py +++ b/apps/users/tests/test_api_views.py @@ -53,6 +53,11 @@ class UserApiViewTests(APITestCase): @patch("apps.users.api.views.generate_and_send_otp") def test_send_otp_view_validates_and_dispatches(self, generate_and_send_otp): + generate_and_send_otp.return_value = { + "detail": "OTP sent successfully", + "expires_in_seconds": 120, + "expires_at": "2026-05-12T10:00:00+03:30", + } response = self.client.post( "/api/users/otp/send/", {"mobile": "09123330009", "mode": "login"}, @@ -60,6 +65,7 @@ class UserApiViewTests(APITestCase): ) self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["expires_in_seconds"], 120) generate_and_send_otp.assert_called_once_with( mobile="09123330009", mode="login", @@ -67,6 +73,11 @@ class UserApiViewTests(APITestCase): @patch("apps.users.api.views.generate_and_send_otp") def test_send_otp_view_supports_forget_password_mode(self, generate_and_send_otp): + generate_and_send_otp.return_value = { + "detail": "OTP sent successfully", + "expires_in_seconds": 120, + "expires_at": "2026-05-12T10:00:00+03:30", + } response = self.client.post( "/api/users/otp/send/", {"mobile": "09123330001", "mode": "forget_password"},