feat: DB constraints for phone auth

This commit is contained in:
2026-03-14 00:31:20 +03:00
parent 4026b94c3a
commit 5ece1036cd
4 changed files with 60 additions and 4 deletions
@@ -0,0 +1,25 @@
# Generated by Django 6.0.3 on 2026-03-13 20:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0003_preferred_language"),
]
operations = [
migrations.AlterField(
model_name="user",
name="phone_number",
field=models.CharField(max_length=20, unique=True),
),
migrations.AddConstraint(
model_name="user",
constraint=models.CheckConstraint(
condition=models.Q(("phone_number__regex", r"^\+[1-9][0-9]{7,14}$")),
name="accounts_user_phone_e164_format",
),
),
]
+10 -1
View File
@@ -4,6 +4,7 @@ from datetime import timedelta
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
from django.db import models from django.db import models
from django.db.models import Q
from django.utils import timezone from django.utils import timezone
@@ -42,7 +43,7 @@ class UserManager(BaseUserManager):
class User(AbstractBaseUser, PermissionsMixin): class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True, null=True, blank=True) email = models.EmailField(unique=True, null=True, blank=True)
phone_number = models.CharField(max_length=20, unique=True, null=True, blank=True) phone_number = models.CharField(max_length=20, unique=True)
role = models.CharField(max_length=20, choices=UserRole.choices, default=UserRole.CUSTOMER) role = models.CharField(max_length=20, choices=UserRole.choices, default=UserRole.CUSTOMER)
first_name = models.CharField(max_length=150, blank=True) first_name = models.CharField(max_length=150, blank=True)
last_name = models.CharField(max_length=150, blank=True) last_name = models.CharField(max_length=150, blank=True)
@@ -62,6 +63,14 @@ class User(AbstractBaseUser, PermissionsMixin):
USERNAME_FIELD = "phone_number" USERNAME_FIELD = "phone_number"
REQUIRED_FIELDS = [] # email is optional; phone_number is the identifier REQUIRED_FIELDS = [] # email is optional; phone_number is the identifier
class Meta:
constraints = [
models.CheckConstraint(
name="accounts_user_phone_e164_format",
condition=Q(phone_number__regex=r"^\+[1-9][0-9]{7,14}$"),
),
]
def __str__(self): def __str__(self):
return self.email or self.phone_number or str(self.id) return self.email or self.phone_number or str(self.id)
@@ -1,6 +1,7 @@
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from django.db import IntegrityError
from django.urls import reverse from django.urls import reverse
from apps.accounts.models import OtpPurpose, PhoneOTP, User from apps.accounts.models import OtpPurpose, PhoneOTP, User
@@ -69,3 +70,24 @@ def test_otp_verify_rejects_auth_purpose_otp(client):
) )
assert response.status_code == 400 assert response.status_code == 400
@pytest.mark.django_db
def test_db_rejects_null_phone_number():
# DB invariant: phone_number is mandatory.
with pytest.raises(IntegrityError):
User.objects.create(email="null-phone@example.com")
@pytest.mark.django_db
def test_db_rejects_non_e164_phone_number():
# DB invariant: store normalized E.164 only.
with pytest.raises(IntegrityError):
User.objects.create_user(phone_number="0512345678")
@pytest.mark.django_db
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")
@@ -17,18 +17,18 @@ def booking_payload():
email="owner@example.com", email="owner@example.com",
password="pass", password="pass",
role=UserRole.MANAGER, role=UserRole.MANAGER,
phone_number="0500000001", phone_number="+966500000001",
) )
customer = User.objects.create_user( customer = User.objects.create_user(
email="customer@example.com", email="customer@example.com",
password="pass", password="pass",
phone_number="0500000002", phone_number="+966500000002",
) )
staff_user = User.objects.create_user( staff_user = User.objects.create_user(
email="staff@example.com", email="staff@example.com",
password="pass", password="pass",
role=UserRole.STAFF, role=UserRole.STAFF,
phone_number="0500000003", phone_number="+966500000003",
) )
salon = Salon.objects.create( salon = Salon.objects.create(