I proceeded with Phase 1 step 1 (phone-first auth + OTP hardening) and updated the plan context. Here’s the current state and the next steps.

What I implemented

Phone-first auth endpoints with OTP → JWT issuance.
views.py
urls.py
Phone normalization (KSA-focused, E.164 or Saudi mobile) and validation.
phone.py
serializers.py
OTP protections: rate limit, resend cooldown, attempt counting.
otp.py
models.py
Email is now optional to allow phone-only users.
models.py
0002_phone_auth_fields.py
Admin OTP visibility improved.
admin.py
Risks updated.
risks.md
This commit is contained in:
2026-02-27 15:57:09 +03:00
parent dc68ecfe4c
commit 46af911a06
12 changed files with 292 additions and 20 deletions
+34
View File
@@ -2,6 +2,7 @@ from django.contrib.auth import get_user_model
from rest_framework import serializers
from apps.accounts.models import OtpChannel, PhoneOTP
from apps.accounts.services.phone import normalize_phone_number
User = get_user_model()
@@ -20,6 +21,14 @@ class RegisterSerializer(serializers.ModelSerializer):
model = User
fields = ["email", "password", "phone_number", "first_name", "last_name"]
def validate_phone_number(self, value):
if not value:
return value
try:
return normalize_phone_number(value)
except ValueError as exc:
raise serializers.ValidationError(str(exc)) from exc
def create(self, validated_data):
return User.objects.create_user(**validated_data)
@@ -28,12 +37,37 @@ class OTPRequestSerializer(serializers.Serializer):
phone_number = serializers.CharField(max_length=20)
channel = serializers.ChoiceField(choices=OtpChannel.choices)
def validate_phone_number(self, value):
try:
return normalize_phone_number(value)
except ValueError as exc:
raise serializers.ValidationError(str(exc)) from exc
class OTPVerifySerializer(serializers.Serializer):
request_id = serializers.UUIDField()
code = serializers.CharField(max_length=6)
class PhoneAuthRequestSerializer(serializers.Serializer):
phone_number = serializers.CharField(max_length=20)
channel = serializers.ChoiceField(choices=OtpChannel.choices)
email = serializers.EmailField(required=False, allow_null=True, allow_blank=True)
first_name = serializers.CharField(required=False, allow_blank=True)
last_name = serializers.CharField(required=False, allow_blank=True)
def validate_phone_number(self, value):
try:
return normalize_phone_number(value)
except ValueError as exc:
raise serializers.ValidationError(str(exc)) from exc
class PhoneAuthVerifySerializer(serializers.Serializer):
request_id = serializers.UUIDField()
code = serializers.CharField(max_length=6)
class OTPStatusSerializer(serializers.ModelSerializer):
class Meta:
model = PhoneOTP