From 0b76356169c7b45c1dca943da5363a52ccf62783 Mon Sep 17 00:00:00 2001 From: mohd Date: Sat, 14 Mar 2026 00:47:31 +0300 Subject: [PATCH] fix: deprecate passwords, use phone auth source of truth --- .../apps/accounts/tests/test_phone_auth.py | 29 +++++++++++++++++++ .../tests/test_phone_first_enforcement.py | 14 +++++++++ backend/apps/accounts/urls.py | 5 ++-- backend/apps/accounts/views.py | 14 +++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/backend/apps/accounts/tests/test_phone_auth.py b/backend/apps/accounts/tests/test_phone_auth.py index ee7d187..6fa7f8b 100644 --- a/backend/apps/accounts/tests/test_phone_auth.py +++ b/backend/apps/accounts/tests/test_phone_auth.py @@ -47,3 +47,32 @@ def test_phone_auth_creates_user_and_issues_tokens(client): user = User.objects.filter(phone_number="+966512345678").first() assert user is not None assert user.is_phone_verified is True + + +@pytest.mark.django_db +@override_settings(OTP_PROVIDER="console") +def test_phone_auth_refresh_endpoint_still_works(client): + with patch("apps.accounts.services.otp.generate_code", return_value="123456"): + request_response = client.post( + reverse("phone_auth_request"), + {"phone_number": "0512345678", "channel": "sms"}, + content_type="application/json", + ) + request_id = request_response.json()["request_id"] + + verify_response = client.post( + reverse("phone_auth_verify"), + {"request_id": request_id, "code": "123456"}, + content_type="application/json", + ) + + assert verify_response.status_code == 200 + refresh = verify_response.json()["refresh"] + + refresh_response = client.post( + reverse("token_refresh"), + {"refresh": refresh}, + content_type="application/json", + ) + assert refresh_response.status_code == 200 + assert "access" in refresh_response.json() diff --git a/backend/apps/accounts/tests/test_phone_first_enforcement.py b/backend/apps/accounts/tests/test_phone_first_enforcement.py index c2f991e..82373e6 100644 --- a/backend/apps/accounts/tests/test_phone_first_enforcement.py +++ b/backend/apps/accounts/tests/test_phone_first_enforcement.py @@ -91,3 +91,17 @@ def test_db_rejects_duplicate_phone_number(): User.objects.create_user(phone_number="+966512345678") with pytest.raises(IntegrityError): User.objects.create_user(phone_number="+966512345678") + + +@pytest.mark.django_db +def test_password_token_endpoint_is_disabled(client): + User.objects.create_user(phone_number="+966512345678", password="StrongPass123") + + response = client.post( + reverse("token_obtain_pair"), + {"phone_number": "+966512345678", "password": "StrongPass123"}, + content_type="application/json", + ) + + assert response.status_code == 410 + assert "detail" in response.json() diff --git a/backend/apps/accounts/urls.py b/backend/apps/accounts/urls.py index 1fb541a..8bc04d3 100644 --- a/backend/apps/accounts/urls.py +++ b/backend/apps/accounts/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView +from rest_framework_simplejwt.views import TokenRefreshView from apps.accounts.views import ( MeView, @@ -7,6 +7,7 @@ from apps.accounts.views import ( OTPVerifyView, PhoneAuthRequestView, PhoneAuthVerifyView, + PasswordTokenObtainDeprecatedView, RegisterView, SocialLoginPlaceholderView, ) @@ -14,7 +15,7 @@ from apps.accounts.views import ( urlpatterns = [ path("register/", RegisterView.as_view(), name="register"), path("me/", MeView.as_view(), name="me"), - path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("token/", PasswordTokenObtainDeprecatedView.as_view(), name="token_obtain_pair"), path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), path("otp/request/", OTPRequestView.as_view(), name="otp_request"), path("otp/verify/", OTPVerifyView.as_view(), name="otp_verify"), diff --git a/backend/apps/accounts/views.py b/backend/apps/accounts/views.py index 6861403..58ec29a 100644 --- a/backend/apps/accounts/views.py +++ b/backend/apps/accounts/views.py @@ -169,3 +169,17 @@ class SocialLoginPlaceholderView(APIView): {"detail": _("Social login not configured yet. Add OAuth provider config.")}, status=status.HTTP_501_NOT_IMPLEMENTED, ) + + +class PasswordTokenObtainDeprecatedView(APIView): + permission_classes = [permissions.AllowAny] + + def post(self, request): + return Response( + { + "detail": _( + "Password login is deprecated. Use /api/auth/phone/request/ then /api/auth/phone/verify/." + ) + }, + status=status.HTTP_410_GONE, + )