feat(users): return otp expiry metadata
This commit is contained in:
@@ -125,12 +125,12 @@ class SendOTPView(APIView):
|
|||||||
serializer = SendOTPSerializer(data=request.data)
|
serializer = SendOTPSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
generate_and_send_otp(
|
payload = generate_and_send_otp(
|
||||||
mobile=serializer.validated_data["mobile"],
|
mobile=serializer.validated_data["mobile"],
|
||||||
mode=serializer.validated_data["mode"]
|
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):
|
class LoginView(APIView):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model, password_validation
|
from django.contrib.auth import get_user_model, password_validation
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
@@ -18,6 +19,7 @@ User = get_user_model()
|
|||||||
|
|
||||||
USER_ALREADY_EXISTS_MESSAGE = "User already exists."
|
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."
|
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):
|
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))
|
verification_code = "".join(random.choices(string.digits, k=5))
|
||||||
|
|
||||||
redis_conn = get_redis_connection("default")
|
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)
|
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):
|
def login_with_password(mobile, password, request=None):
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ class UserApiViewTests(APITestCase):
|
|||||||
|
|
||||||
@patch("apps.users.api.views.generate_and_send_otp")
|
@patch("apps.users.api.views.generate_and_send_otp")
|
||||||
def test_send_otp_view_validates_and_dispatches(self, 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(
|
response = self.client.post(
|
||||||
"/api/users/otp/send/",
|
"/api/users/otp/send/",
|
||||||
{"mobile": "09123330009", "mode": "login"},
|
{"mobile": "09123330009", "mode": "login"},
|
||||||
@@ -60,6 +65,7 @@ class UserApiViewTests(APITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
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(
|
generate_and_send_otp.assert_called_once_with(
|
||||||
mobile="09123330009",
|
mobile="09123330009",
|
||||||
mode="login",
|
mode="login",
|
||||||
@@ -67,6 +73,11 @@ class UserApiViewTests(APITestCase):
|
|||||||
|
|
||||||
@patch("apps.users.api.views.generate_and_send_otp")
|
@patch("apps.users.api.views.generate_and_send_otp")
|
||||||
def test_send_otp_view_supports_forget_password_mode(self, 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(
|
response = self.client.post(
|
||||||
"/api/users/otp/send/",
|
"/api/users/otp/send/",
|
||||||
{"mobile": "09123330001", "mode": "forget_password"},
|
{"mobile": "09123330001", "mode": "forget_password"},
|
||||||
|
|||||||
Reference in New Issue
Block a user