DEV Community

Dominique Megnidro
Dominique Megnidro

Posted on

Implémentation de vérification de numéro de téléphone dans un projet drf

Pour implémenter un système de vérification de numéro de téléphone avec Django REST Framework (DRF), vous pouvez suivre les étapes suivantes. Ce système permettra aux utilisateurs de fournir leur numéro de téléphone, de recevoir un code de vérification par SMS (par exemple via Twilio), et de valider ce code pour vérifier leur numéro.

Étapes Principales :

  1. Installer les dépendances nécessaires
  2. Modifier le modèle utilisateur pour inclure le numéro de téléphone
  3. Créer un modèle pour stocker les codes de vérification
  4. Configurer un service d'envoi de SMS (ex. Twilio)
  5. Créer des sérialiseurs DRF
  6. Créer des vues et des routes API
  7. Gérer la logique de vérification et la sécurité

1. Installer les Dépendances Nécessaires

Tout d'abord, assurez-vous d'avoir installé les bibliothèques nécessaires :

  • Django REST Framework : Si ce n'est pas déjà fait.
  • Twilio : Pour l'envoi de SMS.
  • django-phonenumber-field : Pour la validation et le formatage des numéros de téléphone.

Installez-les via pip :

pip install djangorestframework twilio django-phonenumber-field
Enter fullscreen mode Exit fullscreen mode

Ajoutez phonenumber_field et rest_framework à votre INSTALLED_APPS dans settings.py :

# settings.py

INSTALLED_APPS = [
    # ...
    'rest_framework',
    'phonenumber_field',
    # ...
]
Enter fullscreen mode Exit fullscreen mode

2. Modifier le Modèle Utilisateur pour Inclure le Numéro de Téléphone

Si vous utilisez un modèle utilisateur personnalisé, ajoutez un champ pour le numéro de téléphone et un indicateur de vérification.

# models.py

from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.db import models
from phonenumber_field.modelfields import PhoneNumberField

class UserManager(BaseUserManager):
    def create_user(self, email, username, phone_number, password=None):
        if not email:
            raise ValueError('Les utilisateurs doivent avoir une adresse email')
        if not phone_number:
            raise ValueError('Les utilisateurs doivent avoir un numéro de téléphone')

        user = self.model(
            email=self.normalize_email(email),
            username=username,
            phone_number=phone_number,
        )

        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, username, phone_number, password=None):
        user = self.create_user(
            email,
            username,
            phone_number,
            password=password,
        )
        user.is_admin = True
        user.save(using=self._db)
        return user

class CustomUser(AbstractBaseUser):
    email = models.EmailField(verbose_name='adresse email', max_length=255, unique=True)
    username = models.CharField(max_length=50, unique=True)
    phone_number = PhoneNumberField(unique=True, null=False, blank=False)
    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)
    is_phone_verified = models.BooleanField(default=False)

    objects = UserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username', 'phone_number']

    def __str__(self):
        return self.email

    @property
    def is_staff(self):
        return self.is_admin
Enter fullscreen mode Exit fullscreen mode

Remarque : Si vous avez déjà un modèle utilisateur, assurez-vous d'ajouter le champ phone_number et is_phone_verified de manière appropriée.

3. Créer un Modèle pour Stocker les Codes de Vérification

Ce modèle stockera les codes de vérification envoyés aux utilisateurs.

# models.py

import random
import string
from django.utils import timezone
from datetime import timedelta

class PhoneVerification(models.Model):
    user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='phone_verifications')
    code = models.CharField(max_length=6)
    created_at = models.DateTimeField(auto_now_add=True)
    is_verified = models.BooleanField(default=False)

    def is_expired(self):
        return self.created_at < timezone.now() - timedelta(minutes=10)  # Expire après 10 minutes

    def __str__(self):
        return f"Vérification de {self.user.email} - {'Validé' if self.is_verified else 'En attente'}"
Enter fullscreen mode Exit fullscreen mode

4. Configurer un Service d'Envoi de SMS (Ex. Twilio)

Vous pouvez utiliser Twilio pour envoyer des SMS. Commencez par créer un compte Twilio et obtenir les identifiants nécessaires (ACCOUNT_SID, AUTH_TOKEN, FROM_NUMBER).

Ajoutez ces configurations à votre settings.py :

# settings.py

TWILIO_ACCOUNT_SID = 'votre_account_sid'
TWILIO_AUTH_TOKEN = 'votre_auth_token'
TWILIO_FROM_NUMBER = '+1234567890'  # Numéro Twilio
Enter fullscreen mode Exit fullscreen mode

Créez un fichier utils.py pour gérer l'envoi de SMS :

# utils.py

from django.conf import settings
from twilio.rest import Client

def send_sms(to, message):
    client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)
    message = client.messages.create(
        body=message,
        from_=settings.TWILIO_FROM_NUMBER,
        to=str(to)
    )
    return message.sid
Enter fullscreen mode Exit fullscreen mode

5. Créer des Sérialiseurs DRF

Créez des sérialiseurs pour gérer la demande de vérification et la validation du code.

# serializers.py

from rest_framework import serializers
from .models import CustomUser, PhoneVerification
from phonenumber_field.serializerfields import PhoneNumberField

class PhoneVerificationRequestSerializer(serializers.Serializer):
    phone_number = PhoneNumberField()

    def validate_phone_number(self, value):
        if CustomUser.objects.filter(phone_number=value).exists():
            raise serializers.ValidationError("Ce numéro de téléphone est déjà utilisé.")
        return value

class PhoneVerificationCodeSerializer(serializers.Serializer):
    phone_number = PhoneNumberField()
    code = serializers.CharField(max_length=6)

    def validate(self, data):
        phone_number = data.get('phone_number')
        code = data.get('code')

        try:
            user = CustomUser.objects.get(phone_number=phone_number)
        except CustomUser.DoesNotExist:
            raise serializers.ValidationError("Utilisateur non trouvé avec ce numéro de téléphone.")

        try:
            verification = PhoneVerification.objects.filter(user=user, code=code, is_verified=False).latest('created_at')
        except PhoneVerification.DoesNotExist:
            raise serializers.ValidationError("Code de vérification invalide.")

        if verification.is_expired():
            raise serializers.ValidationError("Le code de vérification a expiré.")

        data['user'] = user
        data['verification'] = verification
        return data
Enter fullscreen mode Exit fullscreen mode

6. Créer des Vues et des Routes API

Créez des vues pour gérer les demandes de vérification et la validation des codes.

# views.py

from rest_framework import generics, status
from rest_framework.response import Response
from .serializers import PhoneVerificationRequestSerializer, PhoneVerificationCodeSerializer
from .models import CustomUser, PhoneVerification
from .utils import send_sms
import random
import string
from django.utils import timezone
from rest_framework.permissions import AllowAny

class PhoneVerificationRequestView(generics.GenericAPIView):
    serializer_class = PhoneVerificationRequestSerializer
    permission_classes = [AllowAny]

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        phone_number = serializer.validated_data['phone_number']

        # Générer un code de 6 chiffres
        code = ''.join(random.choices(string.digits, k=6))

        try:
            user = CustomUser.objects.get(phone_number=phone_number)
            # Si l'utilisateur existe déjà, ne pas permettre la création d'un nouveau
            return Response({"detail": "Ce numéro de téléphone est déjà associé à un utilisateur."}, status=status.HTTP_400_BAD_REQUEST)
        except CustomUser.DoesNotExist:
            pass  # Permettre la création si nécessaire

        # Créer une instance de PhoneVerification
        verification = PhoneVerification.objects.create(user=None, code=code)  # user=None pour l'instant

        # Envoyer le code par SMS
        try:
            send_sms(phone_number, f"Votre code de vérification est : {code}")
        except Exception as e:
            return Response({"detail": "Erreur lors de l'envoi du SMS."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

        return Response({"detail": "Code de vérification envoyé."}, status=status.HTTP_200_OK)

class PhoneVerificationCodeView(generics.GenericAPIView):
    serializer_class = PhoneVerificationCodeSerializer
    permission_classes = [AllowAny]

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data['user']
        verification = serializer.validated_data['verification']

        # Marquer la vérification comme validée
        verification.is_verified = True
        verification.save()

        # Mettre à jour l'utilisateur
        user.is_phone_verified = True
        user.save()

        return Response({"detail": "Numéro de téléphone vérifié avec succès."}, status=status.HTTP_200_OK)
Enter fullscreen mode Exit fullscreen mode

Remarque : Vous pourriez ajuster ces vues selon vos besoins, notamment si vous souhaitez créer un utilisateur lors de la vérification ou gérer différemment les utilisateurs existants.

7. Configurer les Routes API

Ajoutez les routes correspondantes dans votre urls.py.

# urls.py

from django.urls import path
from .views import PhoneVerificationRequestView, PhoneVerificationCodeView

urlpatterns = [
    path('api/verify-phone/request/', PhoneVerificationRequestView.as_view(), name='phone-verification-request'),
    path('api/verify-phone/verify/', PhoneVerificationCodeView.as_view(), name='phone-verification-verify'),
]
Enter fullscreen mode Exit fullscreen mode

8. Ajouter des Logiques Supplémentaires (Optionnel)

a. Génération de Code Unique par Utilisateur

Modifiez la vue de demande pour associer le code à un utilisateur spécifique ou créer un nouvel utilisateur.

b. Limiter le Nombre de Demandes

Pour éviter les abus, limitez le nombre de demandes de vérification par utilisateur ou par numéro de téléphone.

# views.py (ajout dans PhoneVerificationRequestView)

from django.utils import timezone
from datetime import timedelta

class PhoneVerificationRequestView(generics.GenericAPIView):
    # ... code existant ...

    def post(self, request, *args, **kwargs):
        # ... code existant ...

        # Vérifier le nombre de demandes récentes
        recent_verifications = PhoneVerification.objects.filter(
            phone_number=phone_number,
            created_at__gte=timezone.now() - timedelta(minutes=15)
        )
        if recent_verifications.count() >= 5:
            return Response({"detail": "Nombre maximum de demandes atteint. Réessayez plus tard."}, status=status.HTTP_429_TOO_MANY_REQUESTS)

        # ... code existant ...
Enter fullscreen mode Exit fullscreen mode

c. Gestion des Utilisateurs lors de la Vérification

Vous pouvez décider de créer un utilisateur après la vérification ou associer le numéro à un utilisateur existant.

9. Tests et Validation

Assurez-vous de tester votre système en environnement de développement avant de le déployer en production. Vérifiez que :

  • Les SMS sont correctement envoyés.
  • Les codes sont générés et stockés de manière sécurisée.
  • Les vérifications expirent après le délai défini.
  • Les erreurs sont correctement gérées et communiquées à l'utilisateur.

Exemple Complet de Implémentation

Pour vous donner une vue d'ensemble, voici un exemple complet des fichiers concernés.

models.py

from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.db import models
from phonenumber_field.modelfields import PhoneNumberField
from django.utils import timezone
from datetime import timedelta

class UserManager(BaseUserManager):
    def create_user(self, email, username, phone_number, password=None):
        if not email:
            raise ValueError('Les utilisateurs doivent avoir une adresse email')
        if not phone_number:
            raise ValueError('Les utilisateurs doivent avoir un numéro de téléphone')

        user = self.model(
            email=self.normalize_email(email),
            username=username,
            phone_number=phone_number,
        )

        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, username, phone_number, password=None):
        user = self.create_user(
            email,
            username,
            phone_number,
            password=password,
        )
        user.is_admin = True
        user.save(using=self._db)
        return user

class CustomUser(AbstractBaseUser):
    email = models.EmailField(verbose_name='adresse email', max_length=255, unique=True)
    username = models.CharField(max_length=50, unique=True)
    phone_number = PhoneNumberField(unique=True, null=False, blank=False)
    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)
    is_phone_verified = models.BooleanField(default=False)

    objects = UserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username', 'phone_number']

    def __str__(self):
        return self.email

    @property
    def is_staff(self):
        return self.is_admin

class PhoneVerification(models.Model):
    phone_number = PhoneNumberField()
    code = models.CharField(max_length=6)
    created_at = models.DateTimeField(auto_now_add=True)
    is_verified = models.BooleanField(default=False)

    def is_expired(self):
        return self.created_at < timezone.now() - timedelta(minutes=10)

    def __str__(self):
        return f"Vérification de {self.phone_number} - {'Validé' if self.is_verified else 'En attente'}"
Enter fullscreen mode Exit fullscreen mode

serializers.py

from rest_framework import serializers
from .models import CustomUser, PhoneVerification
from phonenumber_field.serializerfields import PhoneNumberField

class PhoneVerificationRequestSerializer(serializers.Serializer):
    phone_number = PhoneNumberField()

    def validate_phone_number(self, value):
        if CustomUser.objects.filter(phone_number=value).exists():
            raise serializers.ValidationError("Ce numéro de téléphone est déjà utilisé.")
        return value

class PhoneVerificationCodeSerializer(serializers.Serializer):
    phone_number = PhoneNumberField()
    code = serializers.CharField(max_length=6)

    def validate(self, data):
        phone_number = data.get('phone_number')
        code = data.get('code')

        try:
            verification = PhoneVerification.objects.filter(phone_number=phone_number, code=code, is_verified=False).latest('created_at')
        except PhoneVerification.DoesNotExist:
            raise serializers.ValidationError("Code de vérification invalide.")

        if verification.is_expired():
            raise serializers.ValidationError("Le code de vérification a expiré.")

        data['verification'] = verification
        return data
Enter fullscreen mode Exit fullscreen mode

views.py

from rest_framework import generics, status
from rest_framework.response import Response
from .serializers import PhoneVerificationRequestSerializer, PhoneVerificationCodeSerializer
from .models import CustomUser, PhoneVerification
from .utils import send_sms
import random
import string
from django.utils import timezone
from rest_framework.permissions import AllowAny
from datetime import timedelta

class PhoneVerificationRequestView(generics.GenericAPIView):
    serializer_class = PhoneVerificationRequestSerializer
    permission_classes = [AllowAny]

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        phone_number = serializer.validated_data['phone_number']

        # Vérifier le nombre de demandes récentes
        recent_verifications = PhoneVerification.objects.filter(
            phone_number=phone_number,
            created_at__gte=timezone.now() - timedelta(minutes=15)
        )
        if recent_verifications.count() >= 5:
            return Response({"detail": "Nombre maximum de demandes atteint. Réessayez plus tard."}, status=status.HTTP_429_TOO_MANY_REQUESTS)

        # Générer un code de 6 chiffres
        code = ''.join(random.choices(string.digits, k=6))

        # Créer une instance de PhoneVerification
        PhoneVerification.objects.create(phone_number=phone_number, code=code)

        # Envoyer le code par SMS
        try:
            send_sms(phone_number, f"Votre code de vérification est : {code}")
        except Exception as e:
            return Response({"detail": "Erreur lors de l'envoi du SMS."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

        return Response({"detail": "Code de vérification envoyé."}, status=status.HTTP_200_OK)

class PhoneVerificationCodeView(generics.GenericAPIView):
    serializer_class = PhoneVerificationCodeSerializer
    permission_classes = [AllowAny]

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        verification = serializer.validated_data['verification']

        # Marquer la vérification comme validée
        verification.is_verified = True
        verification.save()

        # Mettre à jour ou créer l'utilisateur
        user, created = CustomUser.objects.get_or_create(
            phone_number=verification.phone_number,
            defaults={
                'email': f"user_{verification.phone_number}@example.com",
                'username': f"user_{verification.phone_number}",
                'is_phone_verified': True
            }
        )
        if not created:
            user.is_phone_verified = True
            user.save()

        return Response({"detail": "Numéro de téléphone vérifié avec succès."}, status=status.HTTP_200_OK)
Enter fullscreen mode Exit fullscreen mode

urls.py

from django.urls import path
from .views import PhoneVerificationRequestView, PhoneVerificationCodeView

urlpatterns = [
    path('api/verify-phone/request/', PhoneVerificationRequestView.as_view(), name='phone-verification-request'),
    path('api/verify-phone/verify/', PhoneVerificationCodeView.as_view(), name='phone-verification-verify'),
]
Enter fullscreen mode Exit fullscreen mode

utils.py

from django.conf import settings
from twilio.rest import Client

def send_sms(to, message):
    client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)
    message = client.messages.create(
        body=message,
        from_=settings.TWILIO_FROM_NUMBER,
        to=str(to)
    )
    return message.sid
Enter fullscreen mode Exit fullscreen mode

10. Sécuriser et Optimiser

  • Limiter les Tentatives de Vérification : Implémentez un système pour limiter le nombre de tentatives de vérification afin d'éviter les attaques par force brute.

  • Chiffrer les Codes : Pour plus de sécurité, vous pouvez chiffrer les codes de vérification dans la base de données.

  • Utiliser des Tâches Asynchrones : Pour améliorer les performances, utilisez des tâches asynchrones (par exemple avec Celery) pour envoyer les SMS sans bloquer les requêtes API.

  • Configurer HTTPS : Assurez-vous que votre API est accessible via HTTPS pour sécuriser les communications.

Top comments (0)